Supervisor
In the architecture the supervisor is an agent that audits individual pieces of work and talks with the operator; it does not schedule and has no health engine of its own, but reads the pass's output. The commands below are what is built today; whether their health checks, the check-driven start and the executor's dispatches are retired is an open question.
supervise health reads the poll,
probe, dispatch and scheduler runs, the followed repositories' pull requests,
the epics, the decisions, the wrapper's agentic-job pin and the usage snapshot
with a read-only token, within a request count and a deadline. It writes a
report of the checks in the CHECKS table of
crates/board-forge/src/supervise/health.rs. Each check is ok, warn or fail, and the report
holds only links, but for unset, which warns with the names, never the values,
of the poll and supervisor options agent-board knows that the caller's files
leave out (the loaders' POLL_OPTIONS, SUPERVISOR_OPTIONS and
AGENT_OPTIONS tables), so a feature merged off shows until it is switched on
or confirmed off (agent-board#141). An observation it cannot read whole is warn, and becomes
fail once --previous shows it has been unavailable for an hour. The
previous report now comes from a file the workflow downloads, not from an
artifact the binary unzips. The epics check reads each live epic's checkpoint
without the census's board read. supervise trigger decides whether the agent
runs: on a failing check, outside a cooldown, or on the daily pass. It prints
the agent's inputs for $GITHUB_OUTPUT. The task is the compiled-in playbook,
naming the configured operator as the one login whose text is instructions,
the policy and the report. supervise execute holds the bot's token. It takes
the agent's one unedited comment on the requests issue and accepts bounded
requests: dispatches (triage, research and, under an open epic, implement) and
the judgment writes below, all off by default: dispatches need their
agent.kinds entry and nonzero dispatch caps, every other write its
agent.dailyCaps entry.
A dispatch is a run on an open issue in a follow repository, with a pool from
the pools file and one of a fixed set of reasons.
The executor writes the brief itself. A batch is refused whole if any request
is invalid, if an issue holds a supervisor note, if an issue has a dispatch
run in flight, or if the batch would pass
maxDispatchesPerPass or maxDispatchesPerDay. The day's count comes from the
executor's own entries in the log issue, each written before the dispatches it
names. That count holds only if the caller runs one executor at a time, as
tracker's execute job does with its own concurrency group. Explicitly enabled
implement dispatches additionally require native membership under an open,
epic-labeled plan. Judgment writes (comments, operator-assigned tracker issues,
tracker bug issues, triage-proven closure, conflict-only poll hold removal,
evidence-bound needs-human removal, state replacement and a daily project
update) have separate trusted-config
daily caps, all off by default. The request protocol below gives
the evidence each needs, and accounting what it never
does. Its settings are the poll's agent-board.json (--config, which for the
supervisor must also name epics.repository), the supervisor's file
(--supervisor-config) and, optionally, the usage snapshot's location (--usage-config); every key is in
the configuration below. The
release train and scheduler-log settings are optional, so an organization
without them leaves those checks not configured, and unset names them. The checks
have been exercised against recorded responses only.
Workflow steps
The caller's workflow runs these around health and trigger; each prints
key=value lines for $GITHUB_OUTPUT (a multiline value under a random
delimiter) and keeps its diagnostics on stderr. All but due also take
--config and --supervisor-config, left out below.
supervise due --event EVENT [--prompt TEXT] [--previous times.json]printshealthanddaily, and writes this run'stimes.jsonfor the next run, uploaded besidehealth.json. Every run is a health pass, sohealthis alwaystrue; it is kept so that steps conditioned on it work unchanged.daily, whichtrigger --dailyandrepeat --dailytake, is true for an operator comment, a dispatch with a prompt, or a sweep due two hours after the one the previous times record. Pass--previouswhenever the previous run's artifact was looked up, even if none existed (then the sweep is due); leave it out when the lookup failed, so that unknown times make no sweep but an explicit request still does.supervise repeat --health health.json --previous OLD/health.json [--daily]printsskip. Withagent.skipUnchangedFailurestrue, a pass that is not a sweep skips the trigger when every failing check also failed in the previous report; without two readable reports it never skips. Off by default.supervise compose --task TASK --event "$GITHUB_EVENT_PATH" [--prompt TEXT]printstask: the profile and skill read fromagent.profile, then the trigger's task, then the operator request, at most 48 KiB. The request is the dispatch's prompt, or a comment's body when the event is a comment by the configuredoperatoron the lockedagent.requestsissue (not a pull request); any other comment is refused.supervise preview --result-url URLprints the agent's comment on the requests issue between::stop-commands::markers, applying nothing.supervise session --session session.json --rules session.md --pools pools.json --pool NAME [--idle-minutes N] [--prompt TEXT]prints an operator session'srepo,item,idle,agent,model,budgetandtask. The session file is{"enabled", "repository", "issue", "idleMinutes"}and must haveenabled: true; idle minutes are 1 to 60. The task is the rules, theagent.stateandagent.requestsissues, the profile and skill, and the opening prompt as task data, at most 60,000 bytes.
agent.profile is {"repository": "OWNER/agenthome", "ref": "FULL_COMMIT"}:
a 40-character commit, never a branch, so that what the agent is told
changes only with a reviewed config edit. compose and session read
.github/agentic-job/profiles/supervisor.md and skills/supervisor/SKILL.md
there through the contents API with GH_TOKEN, each at most 64 KiB.
Requests
supervise execute treats the supervisor's comment as attacker-controlled data.
It accepts one supervisor-actions fenced block containing JSON Lines, at most
ten requests and 32 KiB total. Every line has "type":"supervisor_action".
Unknown actions, unknown fields and incomplete evidence refuse the whole batch
before any write, including its reservation in the log. No request can select an
API endpoint, configuration file, credential, assignee or arbitrary label.
Configuration
Every supervise command reads two files: the poll's agent-board.json
(--config) and the supervisor's own file (--supervisor-config). health,
trigger and execute may also take --usage-config, the usage snapshot's
{"repo", "issue", "bot"} (repository may stand for repo); without it the pace check is unavailable.
From agent-board.json the supervisor requires operator, bot,
dispatchRepo, dispatchWorkflow and epics.repository: the repository whose
open epic-labeled issues are the plans, which the poll does not read. It
also reads follow, schedule.repositories (whose implement runs the dispatch
check reads), mergeAuthors, kindPools and generatedBy when they are set.
A complete supervisor file:
{ "pollWorkflow": "poll.yml", "probeWorkflow": "broker-probe.yml", "supervisorWorkflow": "supervisor.yml", "implementBranchPrefix": "dispatch/implement/agent-run-", "schedulerJob": "board", "releaseRepository": "OWNER/agentic-job", "wrapperRepository": "OWNER/actions", "wrapperPath": ".github/workflows/wrapper.yml", "releaseTrainWorkflow": "release-train.yml", "maxRequests": 500, "agent": { "pool": "large", "cooldownHours": 1, "requests": {"repository": "OWNER/tracker", "issue": 5}, "log": {"repository": "OWNER/operator", "issue": 7}, "kinds": ["triage", "research"], "maxDispatchesPerPass": 2, "maxDispatchesPerDay": 3, "dailyCaps": { "comment": 4, "create_issue": 2, "file_bug": 2, "close": 2, "remove_needs_human": 1, "clear_needs_human": 4, "replace_state": 4, "status_update": 1 }, "tracker": "OWNER/tracker", "state": {"repository": "OWNER/tracker", "issue": 123}, "project": "PVT_CONFIGURED_NODE_ID", "profile": {"repository": "OWNER/agenthome", "ref": "0123456789abcdef0123456789abcdef01234567"}, "skipUnchangedFailures": false, "passConfig": ".github/pass.json" }}Required:
pollWorkflow,probeWorkflow: the workflow files, indispatchRepo, of the poll and the broker probe, whose default-branch runs thepollandbrokerchecks read.supervisorWorkflow: the supervisor's own workflow file, whose recent agent runs the cooldown counts.implementBranchPrefix: the head-branch prefix of agentic-job's implement pull requests, which thestuckandunclosedchecks match.
Optional, each switching off only the checks that need it. Such a check
reports ok with "Not configured." and makes no request, and the unset
check names schedulerJob or releaseRepository while it is left out, so a
dropped or misspelled key is seen:
schedulerJob: the poll workflow's scheduler job, whose log theidleandstuckchecks read.releaseRepository,wrapperRepositoryandwrapperPath, all three or none: the repository whose latest release thereleasecheck compares with the pin in the wrapper workflow at that path.releaseTrainWorkflow: the release train's workflow file indispatchRepo; with it, thereleasecheck also fails a due release the train has not run for. It needsreleaseRepository.- The check thresholds and request bounds (
pageSize,maxRequests,collectionTimeoutSeconds,pollFailures,runTimeoutMinutes,probeStaleMinutes,epicStaleHours,epicFailures,approvedWaitHours,decisionWaitHours,eventLookbackHours,maxPages,maxItems,maxEvidence,maxDownloadBytes,unavailableFailHours,idlePasses,holdWaitHours,unqueuedWaitHours,unclosedWaitHours,paceMarginPercent,usageStaleMinutes,releaseIntervalHours,releaseOverdueHours), each with a working default (Limits::defaultincrates/board-forge/src/supervise.rs). agent: the agent and the executor. It is off unless bothagent.requests(the issue the agent posts its one comment of requests on) andagent.log(the operator's log, where the executor records every pass) are set. With either one missing it is off without an error, andtriggersays "the requests and log issues are not configured". When it is on,agent.pool(the pools file's pool the agent's run uses) andagent.cooldownHours(how long after an agent run a failing check may start another, a positive number) are required. The cooldown applies outside the daily pass.kinds,maxDispatchesPerPass,maxDispatchesPerDay,dailyCaps,tracker,state,projectandpassConfigare described below;profileandskipUnchangedFailuresunder workflow steps.
All new daily caps default to zero (disabled), accept integers from zero to 100,
and status_update accepts at most one. Dispatches of all admitted kinds share
the existing maxDispatchesPerPass and maxDispatchesPerDay, both defaulting to
zero; agent.kinds must explicitly enable each kind. The operator, bot,
dispatcher/workflow, follow repositories and epics repository still come from
--config; requests, log, state, tracker, project and caps come from
--supervisor-config. These files must be the caller's trusted default-branch
files, never agent-provided files or workflow inputs. Tracker and state must
belong to follow; the project is a trusted Projects v2 node ID, not a request
field. State must be distinct from requests and log.
Lines
The examples below show all fields for each action; none accepts extra fields.
{"type":"supervisor_action","action":"dispatch","repository":"OWNER/repo","item":1,"kind":"triage","pool":"small","reason":"no-plan"}{"type":"supervisor_action","action":"comment","repository":"OWNER/repo","item":1,"body":"Investigation and links."}{"type":"supervisor_action","action":"create_issue","title":"Decision needed","body":"Question and evidence."}{"type":"supervisor_action","action":"file_bug","title":"Failure class","body":"Failure and evidence."}{"type":"supervisor_action","action":"close","repository":"OWNER/repo","item":1,"triage_run":"https://github.com/OWNER/tracker/actions/runs/456","triage_comment":789}{"type":"supervisor_action","action":"remove_needs_human","repository":"OWNER/repo","item":2}{"type":"supervisor_action","action":"clear_needs_human","repository":"OWNER/repo","item":5,"evidence":"operator_comment","comment":901}{"type":"supervisor_action","action":"clear_needs_human","repository":"OWNER/repo","item":6,"evidence":"closed"}{"type":"supervisor_action","action":"replace_state","body":"Arbitrary memory, including structured state."}{"type":"supervisor_action","action":"status_update","status":"on-track","summary":"What it means.","notes":{"OWNER/repo#1":"Why it matters."}}Dispatch uses the configured pool allowlist and kind-to-pool assignments, and
the fixed reasons/brief in the playbook. Triage and research require an open,
non-held ordinary issue with no supervisor note and no run in flight. Its
authors and lock are not checked: the run reads the issue as data in its
sandbox, writes only through bounded safe outputs, and its pull request needs
review before merge (agent-board#155, operator decision on #138). Implement requires those checks and native parentage under an
open epic-labeled issue in the configured epics repository. A body link claiming
approval does not suffice. The epic label is the approval; this executor
cannot add it or create parentage, but anyone with triage access can add it
(who did is not checked here; see epic plans).
Comments target issues or pull requests in follow. New issues go only to
tracker. create_issue is a question for the operator: assigned to the
configured operator and labeled needs-human. file_bug files a failure with
neither, so it is not an operator question; its own cap keeps it off unless
configured. Both bodies carry the supervisor note below, so the supervisor
never dispatches either until a person removes it.
The executor ends every comment, new issue and status update with the
attribution poll uses (agent-board.json's generatedBy), rather than trusting
the agent to.
Titles are nonempty and at most 256 characters; public bodies are nonempty and
at most 4,000 characters. They cannot contain triple-backtick fences, HTML
comment markers or the triage-done receipt phrase below: an injected request
must not forge a bot-authored plan, poll marker or closure receipt. State alone
allows arbitrary text (including empty memory), bounded by 60,000 characters
and the batch byte limit. Its action replaces only the configured issue body.
Status accepts on-track, at-risk or off-track. The executor writes the
body itself, as project status-update does: the project's changes since its
last status update (see operator), under
the agent's summary (at most 1,000 characters, every full owner/repo#N
linked, and a short repo#N when one covered repository has that name) and
with each of its notes (at most 20, one line and 280 characters each, keyed
by ref) on that item's line, or under Also when the item is not listed.
The changes are read before the first write of the batch. A body that cannot
be read or fit refuses that request alone, with its reason in the log; the
batch's other requests, dispatches included, still go ahead.
With agent.passConfig, the path of the pass configuration (docs/pass.md)
from the caller's checkout, the body ends with the
spend split of the same window, from the changes'
start to now, in percentages only; the sections are trimmed to make room for
it under the bound. A split that cannot be read leaves out only
its line, with the reason on the update's line in the log. Absent, off.
With --usage-config, the executor also reads the pass's hold snapshot
(agent-board/holds/v1, the usage bot's comment on the usage issue starting
<!-- agent-board-holds v1 -->; see pass) and adds one line under
the summary: **Needs human:** N, how many more or fewer than the newest
status update's own such line counted, the snapshot's time, and its first
five needs-human holds by board Priority (in the order of the project's
Priority field options, as the pass ranks; those without one, or with one
the field no longer has, last), each a linked ref. The count is what the snapshot lists, which is the
items the pass considered, not a search of every repository. Without a usage
config or a snapshot there is no line, and the next update has no count to
compare with; an update by another tool, or one without the line, gives none
either. A snapshot that names no item for a hold refuses the update as an
unreadable body does. The agent's summary cannot contain **Needs human:**,
so the count the next update reads is the executor's.
A comment or new issue is the agent's text posted as the bot, so the executor appends the
fixed visible note, an agent-board-supervisor-note fenced JSON block (the
agent's text cannot hold a fence, so it cannot write one). Readers still
recognise the hidden <!-- agent-board-supervisor-note --> earlier comments
hold; the executor no longer writes it. Both the supervisor's
dispatch preflight and the scheduler's optional collaborator-only input check
refuse a conversation holding it in its body or a comment, so one agent pass cannot brief the next run
in its own words around the closed brief. A batch cannot comment on and dispatch the same item,
and a comment is refused while a dispatch run on its target is in flight.
Closing requires both a successful completed triage run of the configured
dispatch workflow targeting this exact issue and an unedited comment by the
configured bot on that issue. It must contain exactly one schedule-triage
fenced JSON block, parsed like tracker's .github/poll/schedule.cjs readTriage,
with a done verdict, for example:
Completed, with links to evidence.```schedule-triage{"verdict":"done"}```Fences are case-sensitive and start in column zero, with optional trailing
spaces or tabs. Prose and other fields (including a scheduler request) are
allowed; malformed JSON, multiple matching blocks, or any other verdict refuse.
The supervisor's triage dispatch brief asks for this answer. The named run and
issue bind it here, rather than a scheduler question's request ID.
The comment must have been created within that run's start/end timestamps.
The finding must still describe the issue: no reopen or rename event, body
edit, or comment may follow the receipt, except the bot's own comments before
the triage run ended.
Like a dispatch, a close refuses an issue that carries epic, question or
needs-human when the request runs, however long after the receipt the hold
was added.
Nothing reviews a closure, so unlike a dispatch it keeps agent-board#111's
input check: the issue must have been locked since before the triage run
started, and every author and editor of its body and comments, the bot
included, must be a user with write access. A verdict from a triage that could
have read an outsider's body or comment text does not close the issue.
triage_comment names its comment ID, not an issue number. A run link alone is
not a done finding. The old exact-body Triage found this issue done: URL
receipt is no longer accepted.
remove_needs_human is only for an open pull request of the bot's: the newest
label-add event must be the bot's, preceding its newest unedited poll stop
marker on the same head. Current review must be approved, checks successful,
the pull request not a draft, and its mergeability conflicting. Missing facts,
another head, a human's later label event, or any other failed check leave the
hold alone. The poll now retains the stopped head in its marker; older markers
without that evidence remain ineligible. Check contexts must be complete, and
the poll's required-check evaluation also reads both rulesets and classic branch
protection, including missing required checks and expected integrations.
Provenance and current facts are reread immediately before removal.
clear_needs_human removes needs-human from an issue or pull request the
operator has already settled, so that the queue holds only what still waits
on them (agent-board#350). It takes one of two kinds of evidence, which the
executor reads itself, and the log line links it:
operator_comment, withcommentthe ID of a comment by the configuredoperatoron this issue whose whole text, trimmed, is/release, created after the newestlabeled needs-humanevent. An answer, a comment that restates the hold ("waiting on legal"), or/releasequoted or among other text releases nothing. A label added again after it, an event without a time, no such event, or events past the page limit refuse. Not on a pull request: taking the poll's hold off one starts its fix rounds again, and the poll's ownoperatorAnswersreads the operator's answer there.closed: the item is a merged pull request, or closed by the operator or the bot, and itsclosed_atis after the newestlabeled needs-humanevent, so that a hold put on a closed item survives its reopening. An issue or pull request its author closed is refused, because its author could reopen it with the hold gone.
The item must still carry the label. The facts are read again immediately before removal. The agent's own reading (an answer in someone else's comment, a decision made elsewhere) is not evidence: without this rule, a prompt-injected agent could release any of the operator's holds and so get the work dispatched. The action has no text and sets no project field: it never answers a question for the operator nor writes Priority. No other label change is supported.
Accounting and limits
Epics and the requests, log and state issues are never ordinary request targets.
Only replace_state can edit state. The executor never merges, enqueues,
changes variables, reads secrets, edits other issue bodies, or closes pull
requests. The existing first-attempt gate and stop-on-first-write-failure apply
to every kind. A failed or interrupted write consumes its reserved cap, because
it may have reached GitHub.
The rolling 24-hour counts come only from bot-authored executor entries on the
configured log issue. All requests are recorded as starting before any action
and their outcomes are recorded as before. No user-supplied text is copied into
those accounting lines. Comment/evidence pagination and the log census fail
closed at their existing finite page limits. The caller must serialize executors
against the same log, as the hosted execute job already does; this is not an
atomic transaction with concurrent human edits or other controllers. Project
updates by other tools do not consume the supervisor's daily cap.