Skip to content
Alpha — private preview. Expect rough edges and breaking changes between releases.

GitHub issue automation

Write an issue, label it, and an agent picks it up: the manager watches the repositories it knows for issues carrying the right labels and starts tasks for the ones that are properly authorised. No webhook, no inbound port — the manager polls GitHub through gh, so it works from behind any router.

Label (default) Meaning
agent-task This issue is work an agent could do. Categorisation.
agent:ready Go. An execution authorisation.

They are separate on purpose: writing down what an agent could do is not the same act as telling it to run code on your machine, and the two are often done by different people at different times. Both names are configurable (github.issueTaskLabel, github.issueReadyLabel) because they are conventions in your repository, not facts about the manager.

A third, optional label says which agent engine does the work:

Label Meaning
agent:engine/claude Run this one on Claude Code.
agent:engine/codex Run this one on Codex.

Without it, github.issueWatch.defaultEngine decides — and without that, the deployment’s own agent.defaultEngine does. The label is how you steer a single issue without editing config on the Mac, and because it is a label, what GitHub shows is the whole truth about how the work will run.

Two things are refused rather than guessed, both with a note in the timeline and no task started: an engine this manager does not run (falling back would run the work on precisely the engine the issue said not to use), and two engine labels at once (GitHub’s label order is nobody’s to control, so picking one would be a coin toss).

Watching is on by default, and by default exactly one person may authorise: the account gh is signed in as on the manager machine. That person can already start agents from a paired phone, so their label counts without any configuration. To authorise anyone else, name them — and note that naming anyone replaces the fallback, so include the machine identity if you still mean it:

github:
issueWatch:
enabled: true # the pause switch, not the policy
intervalSeconds: 60
allowedActors:
- your-github-login
- a-colleague

Manage the list without editing YAML:

Terminal window
agentarivm actors # who may authorise here
agentarivm actors add <github-login>
agentarivm actors remove <github-login>

agentarivm init pre-fills it with the login gh is signed in as, since the person setting the machine up is the one entitled to authorise work on it.

  • A label is not an authorisation until an allowed person applies it. Anyone with triage access can add agent:ready; the manager checks who did against the allowlist using the issue’s timeline. Omitting the config does not open the door to strangers: the default allowlist is the manager’s own GitHub identity, and nobody else.
  • The latest application wins. A label removed and re-applied by someone else is that person’s authorisation.
  • One task per authorisation. A failed task leaves the label in place, and the manager will not start another for the same, stale authorisation — otherwise a labelled issue with a failing task would spawn a fresh agent every minute. Re-applying the label is how a person asks for another attempt, and the only way.
  • The issue body is untrusted input, even after authorisation. It is delimited in the agent’s prompt and never interpolated into commands, paths or branch names.

Issues the watcher refuses are refused visibly: the app’s Issues tab shows “not authorised” with the manager’s reason under it. A paired phone can still start that work explicitly — the person holding it has already authenticated, and a missing label should not punish them for something somebody else did or did not click.

The conversation on the issue and the pull request

Section titled “The conversation on the issue and the pull request”

The manager speaks on GitHub and reads it back — the conversation tab, and on a pull request also the bodies of submitted reviews and the notes left inline on the diff, since that is how a reviewing human usually answers. It comments on the transitions that carry a decision — the independent review’s verdict with its findings, a gate failure, an escalation — on the pull request when there is one, on the issue before that. It does not narrate lifecycle events; the app’s event log already has those, and a thread that logs everything is a thread nobody reads.

What is said there comes back where reading is turned on, at two specific moments: the thread is read when a task’s brief is written, and again before each review round. So on such a deployment a comment left on the issue before work starts reaches the agent’s brief, and a comment left at any point reaches the next review — which is what stops a finding you have answered (not fixing, and here is why) being raised cold again.

github.comments.read is off by default, and on that deployment none of the above happens: the manager speaks and does not listen. The review comment’s own footer says which deployment you are on, because a sentence telling you that replying answers a finding is worth nothing where the reply reaches no prompt. Off, it points you at the channel that does work — a message from the app, which the manager records as yours and shows to the next review. See what makes it safe to turn on.

It is not a live channel. A comment posted while an agent is mid-turn does not interrupt it and does not reach it; the running agent sees nothing until the next review round reads the thread. To steer a task that is already running, use the app.

Where to answer a review finding therefore depends on this switch. With reading on, reply on the pull request: the reviewer reads the thread and the person deciding whether to merge reads it too, so one sentence does both jobs. With reading off, a reply there is for the humans only — say it from the app as well if you want the next review round to have it.

Who gets heard is the same question as who may authorise. Only comments by a login in allowedActors reach a prompt, plus the manager’s own. Everybody else’s are dropped — not fenced, not summarised, dropped. Labelling an issue agent:ready vouches for what is there at that moment; a comment somebody else posts afterwards is input that authorisation never covered. That is also why the rule applies to comments and not to the issue body, which the act of authorising already covers.

The manager’s own comments are included whatever the allowlist says, and are attributed as Praetor’s so an agent can tell the reviewer’s words from a person’s. On a deployment with a machine account posting and a human authorising, filtering the machine account out would drop the review findings the agent is meant to read.

Which comments are the manager’s is a fact it recorded when it posted, not something read off the comment. The obvious test — does the author match the machine’s gh login — would be forgeable by exactly the party the rule defends against: the agent runs as the same user and holds the same credential, because it needs it to push its branch and open its pull request. It can therefore post a comment no author check can distinguish from the manager’s. What it cannot do is write the manager’s database, which is under stateDir — one of the trees the agent sandbox denies.

Only the manager’s review comments are read back. A gate-failure comment quotes the gate’s own output — stdout and stderr of commands run in the agent’s worktree — and an escalation quotes loop text that can carry repository-derived strings; both are posted for you to read and neither returns. What a review comment quotes is the reviewer’s own answer, which the loop already trusts to decide the verdict.

Where the gh account is also an allowed actor — the default, since an empty allowedActors falls back to that login and agentarivm init writes it explicitly — the agent holds that credential, because it needs it to push its branch and open its pull request. So it can post a comment the allowlist admits. Nothing about reading comments creates that; a credential holder can always act as itself, and it could already label an issue.

What the manager will not do is let such a comment be read as more than it is. A comment from that login that the manager did not post is marked UNVERIFIED in every prompt: shown, because on a single-account install those are your real comments, but shown as context rather than as evidence that anybody asked for anything. The reviewer is told that explicitly, and told that a commit message citing a request is not corroborated by one. The record that cannot be forged is the app-message one — it lives in the manager’s own database, which the agent sandbox denies.

The manager says which shape it is in at every start, as a log line and as an audit event on the timeline — including on the single-account default, where nothing else would ever mention it. It also raises one the first time it sees a comment it cannot attribute, and again whenever that count changes. Giving the agent its own account removes the ambiguity entirely: it pushes as the machine account, allowedActors does not name that account, and nothing the agent posts is admitted at all. See below for the exact setup.

A long thread is cut to the most recent github.comments.maxInPrompt, and the prompt says where it was cut rather than losing the oldest quietly.

The two halves have separate switches, because they are separate decisions. github.comments.post: false stops the manager commenting; the verdict still reaches the app and the event log. github.comments.read: false stops any comment reaching any prompt — that is the one that closes the untrusted-input surface, and the one to reach for if you want the manager to speak without listening.

Reading is off by default, and stays off until the agent has an account of its own. On a single-account install nothing separates a comment the operator wrote from one the agent wrote. Not the author: agentarivm init puts the gh login in allowedActors, and an empty list falls back to the same login. Not a recorded id either, because the agent can edit a comment the manager posted and the id still matches. Where such a comment appears it is shown marked UNVERIFIED and the brief says it is context rather than evidence, but the honest default is not to read at all.

What makes github.comments.read safe to turn on

Section titled “What makes github.comments.read safe to turn on”

Four steps, in this order. Nothing below is the manager’s to do for you: creating a GitHub account and minting a token is an operator’s act.

  1. Create a second GitHub account — a machine account — and give it push access to the repositories this manager works on. It must not be the account you comment from, and it must not be named in github.issueWatch.allowedActors.

  2. Mint a fine-grained token on it with contents:write and pull_requests:write, confined to the repositories this manager works on. Not a classic repo-scoped one: that is full control of every repository the account can reach, and the agent holds the token in GH_TOKEN. doctor reads X-OAuth-Scopes and raises a problem if you reach for one.

    Write the token to a file only you can read:

    Terminal window
    install -m 600 /dev/null ~/.config/agentarivm/secrets/push-token
    pbpaste > ~/.config/agentarivm/secrets/push-token # or however it reaches the machine
  3. Point the config at it, and restart the manager:

    github:
    pushAccount:
    login: my-agent-bot
    tokenFile: ~/.config/agentarivm/secrets/push-token
  4. Run agentarivm doctor and read three lines:

    sandbox agents and repository commands run under seatbelt, denied 9 path(s)
    accounts pushes as my-agent-bot, comments as you
    comment trust as far as this can be checked:
    the agent pushes as my-agent-bot and every GitHub credential store this
    manager can name is denied to it, so a comment by you is evidence that you
    wrote it. The manager's own review comments are rendered from the copy it
    recorded when it posted, so editing them on GitHub cannot reach a prompt
    whatever the push token may do. One caveat: the sandbox is a deny-list
    rather than a jail […]

    Only then set github.comments.read: true.

    If comment trust says NOT trustworthy, it names what is still open and what to do about it. The usual answer is a credential helper whose store has no path to deny: a github.com entry in the macOS login keychain, or credential.helper = libsecret on Linux. Run gh auth setup-git so git authenticates through gh, remove the github.com entry from that keyring, and read the line again.

    On Linux the affirmative answer also carries a Not checked here clause: there is no way to ask a keyring whether it holds a github.com secret without a session bus the manager may not have, so a token put there by something other than a configured git helper is not visible to doctor. That is a gap in what can be checked, not a fault it found — but it is yours to weigh before you turn reading on.

A worktree’s credential is pinned when the worktree is created, so finish or cancel the tasks that are in flight before you restart — in both directions. A worktree created before you add the key carries no pinned helper and reaches for credentials that are now denied to it; one created before you remove the key still points at the credential file the manager has just deleted. Either way the next push fails loudly rather than quietly going out as you, which is the right way round and is still a failed push.

Projects added before you set the key keep the remote they were cloned with, and nothing rewrites an existing remote. An ssh remote authenticates over your key, not the push account, so on that project the agent still pushes as you — doctor raises this as a problem naming each project, and the fix is to re-add it (or repoint the remote at https).

doctor refuses to be talked into agreeing. It asks GitHub who the token really is and raises a problem when that is not the login you named — a token minted on your own account by mistake would otherwise produce a deployment that reads as separated in every line of config and is not. The manager refuses to start in that state at all.

The sandbox line matters as much as the account lines. Two accounts separate nothing on a machine where the agent can still open ~/.config/gh and be you again — which is every machine whose sandbox is off, unavailable, or failed its own canary. With pushAccount set and the sandbox working, that directory is denied to agents outright: it is no longer on the path to anything they need, because the push goes through the pinned token and gh runs on GH_TOKEN and a config directory of its own.

Why no token scope protects the manager’s own comments

Section titled “Why no token scope protects the manager’s own comments”

It is worth spelling out, because the obvious answer is wrong and this page said it twice.

The manager’s review findings are posted as a comment and the manager records the id. On the next round it reads that comment back as its own words: unmarked, uncapped, and described to the reviewer as the authoritative record of what was decided. So the question is who can edit it.

It posts that comment on the pull request, and GitHub governs a pull request’s conversation comments through the Pull requests permission — the very permission the agent must have in order to open a pull request at all. Withholding issues does not help. There is no token that can open a pull request and not edit the comments on it.

So the manager does not ask GitHub what it said. It records the body it posted and renders its own copy — from a table under stateDir, which the agent sandbox denies. The fetched body is compared against the recorded one only in order to raise an audit event when they differ, which is a real signal: somebody with write access to that thread changed the manager’s words after it posted them. Nothing forged reaches a prompt either way.

A row with no recorded body — every row written before this landed — is not vouched for at all: it is dropped rather than trusted on the strength of its id.

The push account’s login is not in allowedActors, so anything the agent posts is dropped: conversation comments, review bodies, inline notes on the diff. That is the half a second account closes, and it is why pull_requests: write being un-withholdable costs nothing — the comments that permission lets the agent write are comments no prompt will read.

A single-account deployment keeps working exactly as it does now. Nothing above is required, nothing changes without it, and the manager says which shape it is in at every start rather than leaving you to infer it.

Messages you send from the app are not mirrored into the thread. The durable record of what was done and why belongs in the commit message and the pull-request body, which is where this repository already keeps it — so when work happens because you asked for it mid-flight, the agent is told to say so in the commit, and say who.

That attribution is for people reading the history. It is not evidence to the reviewer: a commit message is written by the agent being reviewed, so “X asked for this” in one establishes only that the agent wrote that sentence.

The checkable record is kept by the manager, in two places it can vouch for. Messages you send from the app are recorded as yours as they arrive — the loop’s own hand-backs go through the same channel and are not, so “a person asked” stays a fact rather than a role — and the reviewer is shown them under What a person asked for while this was running. Comments on the issue or pull request are the other, filtered to allowed actors. Both live in state the agent sandbox denies. Steer a task however you like; the reviewer will see it either way, and does not need the commit message to say so.

They run in parallel, up to task.maxConcurrentPerProject — agentarivm init writes 1, and the built-in default is 3. Each task gets its own worktree and its own branch, so three agents on one repository do not see each other’s work.

Issues past that limit are refused, not queued. The labels stay on, the watcher tries again on its next poll, and work starts as soon as a slot frees — so nothing is lost, but nothing waits in line either. Whichever issue GitHub lists first when a slot opens is the one that starts.

The slot is held until a task is finished with, not until the agent stops: a task at pr_open waiting for your review still counts. If you label five issues and merge nothing, work stops after the limit is reached and will not resume until you merge or clean up. That is usually what has happened when the watcher looks dead.

The limit is per repository. Two registered repositories can each run that many at once, and nothing caps the total — the real ceiling is what one machine can stand running that many agents, worktrees and verify runs at the same time.

A repository created from an idea in the app — Projects → + → New from idea — starts with a planning task rather than with code. Its agent reads the idea (IDEA.md, or the PDF or image you attached), talks it through with you in the task’s chat, writes PLAN.md in a draft pull request, and then opens one issue per task in the plan, labelled so the backlog arrives structured: an area (area: backend, area: web, area: mobile), a kind of work, and needs: human for anything only a person can do.

It writes those through the manager, with agentarivm label and agentarivm issue, not with gh — and the manager holds the rules:

  • Only a planning task may write at all, and only to its own repository. A task that changes code is refused.
  • Work an agent could do gets agent-task; nothing gets agent:ready. The manager refuses that label, and every agent:engine/… label, from a plan: applied by the manager’s own account — which is on the default allowlist — it would let a plan start its own agents. Authorising each issue stays your act, exactly as for any other issue.
  • Asking twice is answered once. An issue whose title the plan already opened comes back as the first one, so a retried command does not duplicate it.
  • The totals are capped at 100 issues and 50 labels per plan, counted in state the agent sandbox denies.

Merge the plan when you agree with it, then label the issues you want built agent:ready, a few at a time.

Sessions get better inputs from structured issues. Use an issue form that collects a goal, acceptance criteria, technical context, testing expectations and constraints; the app’s create issue screen produces the same structure, so an issue written on a phone and one written in a browser reach the agent in the same shape.

The watcher polls once per intervalSeconds per registered repository, filtered server-side by both labels — about a minute of latency against GitHub’s 5000 requests/hour budget. That is the price of needing no open port.