Architecture
How agent-board is put together: two loops over the forge, one predictable and one judging. The pass specifies the first in full; this page is the decisions both follow. Where an older page disagrees, this one wins.
cron --> agent-board pass ---dispatch---> dispatcher ---> agent job (usage, follow PRs, epics, (agentic-job | Status, schedule) or another) | | ^ | writes | | reads | v | | forge: board, issues, PRs <--- pull requests, comments -+ | | reads pass's output and the work it audits v supervisor <------ talks with ------> operatorTwo loops
agent-board pass is the heartbeat: a cron runs it, it finishes once, and the
same facts give the same decisions. One pass refreshes usage, follows pull
requests (the standalone poll becomes this stage of pass rather than a
separate command), plans epics, derives Status and schedules new work (see
what is built).
The supervisor is an agent that answers "is the system working?". It audits individual pieces of work from time to time and is who the operator talks to. It does not schedule and has no health engine of its own: it reads pass's output and the forge.
After a deployment moves its pinned agent-board revision, the supervisor's
next audit reads what changed between the pins, exercises it with a
pass --record preview against the previous recording (and, for new writes,
one apply with --max-dispatch 1 and a small --max-writes), and reports what it
saw to the operator, recommending a rollback of the pin if it misbehaves.
Why: a scheduler should be boring and replayable, and judgment should not compete with it for the same decisions.
Writes
agent-board pass --apply makes its own writes directly; agentic-job's
independent check and apply is for what an agent produces. Guards and
caps are in order and effects.
Why: the pass's writes follow from forge facts by fixed rules, so a second checker would re-derive the same answer at the cost of another moving part.
Status
Status is derived and written each pass from live facts: Done, Needs human and In Progress are never set by hand; Todo, Backlog and Priority are a person's choice, and Blocked and Backlog are holds that only Done replaces. The rules are pass's stage 5. Any open pull request for an item makes it In Progress, draft or not, in any repository and by any author, so an upstream issue with a fork's pull request is In Progress; an issue a person marked Done that is still open goes back to Todo, since open means not done.
The live Status set is Todo, Backlog, Blocked, In Progress, Needs human and Done; the others (Triage, Draft, In Review, Cancelled) are legacy, which the derived rules override, to be removed from the board once the pass has run for a while, not now.
Why: a status someone has to remember to change goes stale, and a stale board misleads both the operator and the scheduler.
One marker for the operator
The needs-human label is the single marker that the operator must act. The
board's Needs human status comes from it. Question issues and the decisions
document fold into it: a question is an ordinary issue or pull request with
the label, and answering it means acting and taking the label off. agent-board ask create labels its question and the item it blocks, and ask resolve takes
the label off that item once no other open ask names it; neither writes Status.
Why: several ways to say "waiting on you" mean several places to check, and one of them is always forgotten.
Bookkeeping
Where the tool keeps its state depends on whose repository the work is in.
- External or upstream repositories: minimal noise. No claim or status comments. Any state the tool needs lives in one hidden comment it edits in place.
- First-party repositories: state lives on the issue or pull request itself, readable by people and machines alike: a short human line, with the machine detail alongside it. Not in a separate store, and not as scattered hidden markers.
Why: upstream maintainers did not ask for a bot's ledger in their threads, while on our own issues the record is most useful where people already look, and a person should be able to read what the machine read.
Async forge I/O
Forge I/O moves to async in slices (#300). The first is in: the GitHub board
snapshot reads through board-forge's async client, on the one
current-thread tokio runtime the CLI's main builds. That covers every
command that loads a live GitHub board (item list, project view,
reconcile plan and the others that take --owner and --project), the
pass's Status stage and the live fixture's readback. The client holds its
limits in one place: at most MAX_IN_FLIGHT requests at once, and a
request GitHub refuses under a rate limit retried at most
RATE_LIMIT_RETRIES times, waiting at most RATE_LIMIT_WAIT in all for
that request and never past its deadline (a pass's time bound). A refusal
is a 429; a 403 with Retry-After, no requests remaining or a message
naming a rate limit; or GraphQL RATE_LIMIT errors without data, which is
how GitHub answers a secondary limit, with HTTP 200 and no Retry-After.
Each retry waits what Retry-After or x-ratelimit-reset asks, or else
SECONDARY_WAIT (a minute) doubled for each retry before, as GitHub asks.
The sync paths below retry by the same policy, and one given up on is a
typed RateLimited error. The snapshot's request budget is passed per read,
SNAPSHOT_REQUEST_LIMIT (1,000) by default; running out is an error, never
a partial snapshot.
Every other forge request is still sync on ureq: the poll's and status's
own reads (the board they load is the snapshot), the project schema read
before a write, dispatch, epics, the rest of the pass, the writes, and
Forgejo. They share the snapshot's paging through an adapter
whose reads never wait. The conditional-request cache comes later, as client
middleware; a forge crate is extracted then.
Why: a pass reads many independent things, and sequential requests make its time grow with the board. Slices keep each change small, with its tests green.
The client is reqwest (rustls with ring, as ureq's, and the platform's trust store) under a thin layer of our own, not octocrab. Octocrab's GraphQL is untyped JSON, like ours, and its typed Projects v2 models are for the REST endpoints, which the snapshot does not read. Its rate-limit retry bounds the number of retries but not the total wait, and it has no cap on requests in flight. So we would write both limits either way, on top of about 40 direct dependencies (hyper, tower, jsonwebtoken, chrono and more). Its conditional-request cache layer is for REST; that slice can look at it again.
Reruns and the drop list
One automatic rerun of a run that failed for an infrastructure reason is kept (scheduling); everything else on the drop list is dropped.
Why: an infrastructure failure says nothing about the task, so one retry is cheap and usually right; retrying an agent's failure spends the budget again on the same outcome.
Open questions
These are not decided; the pass spec leaves them as they are today or open.
- Backlog as a hold? If Todo and Backlog are a person's ready/not-ready choice, should the pass refuse to start Backlog items? Today it holds on board Blocked instead.
- When a derived status stops holding. Today an item whose Needs human, In Progress or Done no longer holds goes back to Todo, not to what a person set before.
- What counts as an infrastructure failure. The rerun needs a fixed rule for telling the runner, broker or forge failing apart from the agent's own work failing, from the facts a run leaves.
- Cap numbers. A pass takes
--max-dispatchand--max-writes, unset by default; what the scheduled pass should pass is not set yet. The Status stage's first-run guard,--max-status-changes, is 50 by default. - First-party bookkeeping for PR follow-up. What poll's hidden fix-claim and round markers become in first-party repositories, where state should be a readable line with machine detail alongside.
- Retire the supervisor's own engine?
supervise health, the check-driven start insupervise triggerand the executor's dispatches are built and unchanged; should they go once the supervisor reads pass's output? - The
questionexclusion (code follow-up). The pass's candidate set still excludesquestion-labelled issues in code; with question issues folded intoneeds-human, that exclusion should go.