Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

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] prints health and daily, and writes this run's times.json for the next run, uploaded beside health.json. Every run is a health pass, so health is always true; it is kept so that steps conditioned on it work unchanged. daily, which trigger --daily and repeat --daily take, 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 --previous whenever 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] prints skip. With agent.skipUnchangedFailures true, 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] prints task: the profile and skill read from agent.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 configured operator on the locked agent.requests issue (not a pull request); any other comment is refused.
  • supervise preview --result-url URL prints 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's repo, item, idle, agent, model, budget and task. The session file is {"enabled", "repository", "issue", "idleMinutes"} and must have enabled: true; idle minutes are 1 to 60. The task is the rules, the agent.state and agent.requests issues, 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, in dispatchRepo, of the poll and the broker probe, whose default-branch runs the poll and broker checks 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 the stuck and unclosed checks 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 the idle and stuck checks read.
  • releaseRepository, wrapperRepository and wrapperPath, all three or none: the repository whose latest release the release check compares with the pin in the wrapper workflow at that path.
  • releaseTrainWorkflow: the release train's workflow file in dispatchRepo; with it, the release check also fails a due release the train has not run for. It needs releaseRepository.
  • 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::default in crates/board-forge/src/supervise.rs).
  • agent: the agent and the executor. It is off unless both agent.requests (the issue the agent posts its one comment of requests on) and agent.log (the operator's log, where the executor records every pass) are set. With either one missing it is off without an error, and trigger says "the requests and log issues are not configured". When it is on, agent.pool (the pools file's pool the agent's run uses) and agent.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, project and passConfig are described below; profile and skipUnchangedFailures under 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, with comment the ID of a comment by the configured operator on this issue whose whole text, trimmed, is /release, created after the newest labeled needs-human event. An answer, a comment that restates the hold ("waiting on legal"), or /release quoted 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 own operatorAnswers reads the operator's answer there.
  • closed: the item is a merged pull request, or closed by the operator or the bot, and its closed_at is after the newest labeled needs-human event, 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.