Conversation stages and prompt contracts

question and discussion are the two operator-applied workflow labels: nothing routes an issue into either, and neither has an agent role of its own — both reuse the decomposer's configured backend, because a question is the decomposer answering without implementing and a discussion is the decomposer reasoning about a design before anything is decomposed. Each conversation pins its own agent + session keys, so its backend, args, and session id are locked independently of any other conversation on the same issue (see roles.md#session-lifecycles and command-specs.md#in-flight-session-lock).

Label transitions, park reasons, the checks a round's branch is read by, publication and crash recovery, and terminal cleanup are the state machine's — _handle_question and _handle_discussion. What follows is what each prompt grants and forbids, where the agent is invoked, and how its session is continued.

Question stage

_handle_question runs the configured DECOMPOSE_AGENT backend in the issue's per-issue worktree (recreated from <remote>/<base> each spawn) with a read-only prompt that forbids modifying, committing, or pushing files. The agent's answer (or its own clarifying follow-up question) is posted as a comment pinging HITL_HANDLE; no PR is ever opened and no branch is ever pushed. Subsequent human replies resume the locked session (_build_question_followup_prompt), so a multi-turn Q&A keeps the same backend + args.

Violating that contract — commits, a dirty tree, a run that timed out — ends the round awaiting a human with the worktree kept for inspection rather than torn down. Closing the issue is the terminal signal. The park reason each outcome writes, and the guard a relabel to workflow:implementing has to pass, are in ../state-machine/conversation-stages.md#_handle_question-label-question.

Discussion stage

_handle_discussion runs the decomposer once per round in the issue's per-issue worktree (issue-N, on the issue's own branch) with a prompt that tells it to research the repository itself, explore the design as a tree rather than a single answer, raise architecture decisions, unconventional alternatives, and worthwhile research rather than implementation trivia, and close with a NUMBERED list of the questions answerable right now — each with the agent's own recommended answer — so a human can agree or overrule by number. Nothing is written, and nothing is implemented, until a human states on the thread that they and the agent understand the design the same way.

Where the two stages part is what a round may leave behind. question is read-only for its whole life — a commit or a dirty tree is a violation — it opens no PR, and it tears its worktree down on every safe exit. A discussion is read-only only up to the confirmation: after it the agent may commit exactly one file, the orchestrator publishes that file as a pull request, and the issue-N checkout is preserved on every round exit instead, since the conversation keeps running on it. The verdict the humans leave on that pull request is what ends the issue.

Answering by number resumes the pinned session for another round. Only issue comments past the consumed last_action_comment_id that are neither from an untrusted author nor the orchestrator's own count as an answer, so an empty or all-untrusted batch is a no-op that writes nothing and leaves the reply for the tick after the allowlist changes. The resume quotes those replies to the live session (_build_discussion_followup_prompt) and asks for the tree redrawn around what they settled and the frontier recomputed. A round with no discussion_session_id to resume gets the full prompt instead, since it reaches a fresh agent that would otherwise arrive with no design to fold an answer into; that rebuilt context keeps the orchestrator's own posted analyses even when ALLOWED_ISSUE_AUTHORS does not list the bot's account, so the fresh agent reads the human's answers together with the numbered questions they answer.

discussion_agent is written before the spawn, so the conversation's identity survives a CLI that hands back nothing and a replayed round stays on the backend that opened it rather than on whatever DECOMPOSE_AGENT says now; the run itself is recorded under agent_role="decomposer" with stage="discussion". The issue-N worktree is reused rather than rebuilt across the whole conversation, the clean response is posted as a comment pinging HITL_HANDLE, and the issue then waits for a human — a round on the thread is the humans' turn, and costs one comment read per tick until somebody answers. No developer or reviewer is ever spawned by this stage.

The plan file the confirmation earns

Both prompts also carry the one write the confirmation unlocks: plans/issue-<number>.md, holding the resolved decisions, the evidence and research behind them, the alternatives and why they lost, the risks, and the implementation plan — committed alone, with no push and no PR of the agent's own. The path is spelled by the stage's own key owner and handed to the prompt builders, so what the agent is told and what the check looks for cannot drift.

Publishing it is the orchestrator's job, not the agent's, and it is not taken on trust: no orchestrator can verify that a human agreed to anything, so a round that moved HEAD has its branch read and has to prove that what it commits is the plan file and nothing else before the stage pushes it and opens the pull request. Which probes read that branch, what each failure parks as, and how a tick that died mid-publication recovers are in ../state-machine/conversation-stages.md#_handle_discussion-label-discussion.

The plan is an artifact, not a specification anything downstream reads. Nothing in the workflow parses it: the relabel to workflow:implementing spawns the developer with the ordinary _build_implement_prompt built from the issue body and the trusted thread, on the branch the plan PR is open against, and the final-docs pass that follows is told not to inspect or modify the plans/ tree at all. So the plan file rides along on the branch and lands with the implementation as the human-readable record of what was agreed — the issue is still what the developer is briefed from and judged against.

Tracked-repository awareness in working-agent prompts

When the orchestrator drives more than one repo (REPOS) and EXPOSE_TRACKED_REPOS is on (the default), the reasoning-prompt builders prepend a compact, read-only awareness block naming the other repos this process tracks. It lets an agent implementing an issue in one repo know that a sibling repo is also monitored and where its source is checked out locally. The block is built once by _build_tracked_repos_context(current, specs) in workflow/engine/prompt_context.py from config.default_repo_specs() — no GitHub round-trip, no pinned state, no new config surface.

Shape of the block:

  • One line per other repo (- owner/name — source at <target_root> (base <base>)), excluding the current repo, with a closing Your task is on owner/name. marker. The list is capped at 20 entries with an … and N more overflow line so a host driving dozens of repos cannot blow the prompt.
  • Only the durable target_root checkout is exposed — never the ephemeral per-issue issue-N worktrees. No tokens, no remote URLs — see ../security.md for the full disclosure analysis.
  • The framing is deliberately stage-neutral: it says only that the sibling checkouts are read-only references and explicitly defers the question of whether the agent may write in its own working directory to the surrounding stage prompt. So the same block is safe in the write-granting prompts (implementer / documentation), in the read-only ones (reviewer / decomposer / question), and in the discussion prompts, whose single write a human's confirmation unlocks — none of them widens what the surrounding prompt granted.

Delivery builders live in workflow/engine/prompts.py, the reviewer's in workflow/engine/review_prompts.py, question/discussion, PR-follow-up, and human-reply resume builders in workflow/engine/conversation_prompts.py, and the decomposition builder in workflow/engine/decomposition_prompts.py. Their use of the awareness block is:

  • Embedded in _build_implement_prompt, _build_documentation_prompt, _build_review_prompt, _build_decompose_prompt, _build_question_prompt, _build_discussion_prompt, and _build_fresh_respawn_preamble. The fresh-respawn preamble matters because a transcript-less respawn (proactive DEV_SESSION_MAX_RESUMES rotation, the consecutive-silent-park fallback, poisoned-session recovery, or an operator /orchestrator continue command that drops a session-failure park's poisoned dev session before replaying the preserved PR-feedback batch) never saw the original spawn's block, so the re-grounding text must re-feed it alongside the issue body and conversation.
  • Omitted from the bare resume / followup builders (_build_fix_prompt, _build_conflict_resolution_prompt, _build_pr_comment_followup, _build_human_reply_followup, _build_question_followup_prompt, _build_discussion_followup_prompt): those text payloads resume a live session that already received the block at spawn time, so repeating it would only burn tokens.

The default single-repo deployment (or any host with EXPOSE_TRACKED_REPOS=off) gets an empty string here — zero added prompt tokens and zero behavior change. See ../configuration.md#agent-roles for the env var.

What a prompt's conversation excerpt records

Every prompt above quotes the issue thread through the trust filter in workflow/engine/prompt_context.py. A prompt that quotes the WHOLE thread is bounded to the last 4000 characters; the one that quotes a slice of it is not — the human-reply followup carries exactly the replies past a park's watermark, and resume_batch._UNBOUNDED_EXCERPT keeps that batch uncapped so the record cannot name an omission the followup never made. Two readers render the bounded text and they differ in one thing — whether anything is going to be recorded about it:

  • _recent_comments_text reads the thread and renders it, for the prompts whose stage settles nothing by them (the documentation, decomposer, question and discussion prompts).
  • _delivered_thread takes the read itself and hands back a prompt_delivery snapshot: the same text, plus the record of which comments it is made of, which ones the bound left out, and the requirements revision that read fingerprints to. The implementing fresh spawn, every requirements-drift resume and the validating reviewer round build their prompts from it. The drift resumes always settle it afterwards; the fresh spawn settles it only where a pre-session edit was handed to that spawn to answer, since an ordinary spawn's thread is the pickup's to have recorded and nothing on that tick is owed an answer; and the reviewer round settles it only where a reply bought the round — a park's retry or an operator's grant — since an ordinary round reads the thread like any other reader and answers nobody.

One prompt reads a second surface below the first. The in_review drift resume quotes the pull request's unread conversation under its own heading beneath the bounded thread, and drift_delivery._pr_drift_resume_prompt freezes the two into ONE record through _delivered_pr_conversation. That half is uncapped for the same reason the reply batch is — every comment handed over is quoted. An outsider's comment reaches neither the prompt nor the delivered half; it is kept as a refused entry, so the watermark carry behind the run may cross a comment no reader is ever owed.

What that record may be SETTLED from is narrower than what it names, and the reason is that the two surfaces are read a moment apart. pr_last_comment_id spans both, so advancing it claims every id below it on either is delivered or owed to nobody — and a comment landing on the surface read first, after that read, is in neither half while an id above it on the other surface is in one. No record built from two readings can answer that. So the settlement writes the issue thread's own cursor and the requirements revision, and the shared one is derived by the in_review park carry, which re-reads both surfaces at one moment, crosses exactly the ids the record names, and stops at the first it does not.

The rule the second shape exists for is that a prompt and the mark taken for it have to be one read. Three consequences hold wherever a prompt is settled, and they are the same on every road that does it — the frozen reply batch a parked resume delivers, the drift resume, and the fresh spawn a pre-session edit falls through to:

  • a comment a bounded prompt's excerpt dropped reaches no agent, so nothing records it as read; the issue-thread watermark stops below it and the scan that owns that surface still delivers it. The uncapped reply batch drops none, which is why it is uncapped;
  • a comment written after the read — during the minutes an agent is out — is quoted to nobody and crossed by nothing, so it is still unread input on the next tick;
  • the record is settled only once the run is back, and only for an outcome that counts the input as delivered: a shutdown kill, a live pause, and a launch the run circuit refused leave it entirely unread, while a timeout, an empty reply, an ACK:, and a question park all consume it. Delivery is not resolution — what an agent was handed is a separate question from whether it answered;
  • a settlement writes only a cursor ONE reading can vouch for. A cursor spanning two surfaces read a moment apart is derived by a merged re-read instead, never by the record;
  • a route that invokes NO developer at all — the documenting drift unwind, which changes a label and resets a worktree — settles no watermark whatever, since a relabel delivers nobody's words to anybody. Where a git step there parks, the shared HITL park's own mark is put back and kept as that unwind's retry boundary rather than left standing as a delivery cursor.

The commit-subject contract in commit-producing prompts

Every prompt whose agent may author a commit subject carries one subject contract, _COMMIT_STYLE_NOTE in workflow/engine/prompt_notes.py. It enumerates no prefix vocabulary of its own, because the orchestrator drives arbitrary configured repos and a closed list would teach the wrong style everywhere but the one it was written for: the agent reads git log --oneline -20 and mirrors whatever subject/prefix convention that repository's own recent history uses, as a single short imperative line with no body, no trailer, and one -m.

What the note carves out of that history is the reference suffix. Under PR_REF_IN_SUBJECT — the default — every commit the orchestrator publishes onto a pull request ends in (#N) naming that pull request (../configuration.md); with the switch off nothing is suffixed at all, and the history stays whatever the developers wrote. So on a repo this orchestrator has already published to with the default on, "mirror recent history" reads as an instruction to write a numeric suffix — and the only number the agent has is the issue it is implementing, which would land a subject naming the issue and the pull request both. So the note says what those suffixes are: publication metadata rather than style. The agent writes the descriptive subject alone, adds no numeric suffix of its own, never copies the tracked issue's number into one, and leaves the pull request reference to the orchestrator that appends it when configured to. Issue linkage is the pull request body's to carry, not the subject's. The note is unconditional, because the prompt is built with no reading of that switch and a repo whose history carries references from an earlier setting reads the same either way.

The note is not the only guard, and the code enforces the same rule at each of the three places an agent-written subject is read back out. PR-title selection (git/publication/titles.py) takes the tracked issue's own trailing reference off whichever line it reuses — the branch's first commit subject, or the issue title — so a commit written before this contract, one a human wrote by hand, and an issue title with the number typed onto the end each still yield a title without it. Nothing is appended in its place: a title is picked before the pull request has a number. The two publishers decide the whole of it instead, because each has a pull request to name: the approval squash reads that same first commit subject for the commit it collapses or rewrites to, and the documenting pass reads the docs: commit's own. On both, the tracked issue's reference goes and the pull request's is kept exactly once, so a subject a developer ended in the issue's number — and the <subject> (#issue) (#PR) a developer commit and an earlier publication each wrote half of — land alike as <subject> (#PR). A reference to any other number is somebody else's link and survives on every road.

Where the contract is carried:

  • Whole in the initial _build_implement_prompt, the automated-review _build_fix_prompt, the final pass's _build_documentation_prompt, the requirements-drift _build_user_content_change_prompt, the PR-feedback _build_pr_comment_followup, both discussion builders (_build_discussion_prompt and _build_discussion_followup_prompt — whichever round the confirmation lands on commits the plan file, and that subject becomes the plan pull request's title), and the late revision's _revision_prompt (decomposition/late_revision.py).
  • Whole in the bare developer resume payloads too — _build_human_reply_followup and _DEVELOPER_CONTINUE_RETRY_PROMPT — which ask for commits while resuming a transcript that may predate the contract or hold another stage's prompt. They carry it for one more reason than the report contract they restate beside it: a developer resume can rotate into a fresh session (DEV_SESSION_MAX_RESUMES, the consecutive-silent-park fallback, or poisoned-session recovery), and the only text ahead of the payload there is _build_fresh_respawn_preamble, which teaches no subject contract of its own. Without it a brand-new agent would be told to commit with nothing said about what its subject may carry.
  • Absent from _build_conflict_resolution_prompt and from the conflict stage's own bare continue, which stays on the plain _CONTINUE_RETRY_PROMPT: that agent finishes an in-progress rebase with git rebase --continue and authors no subject at all.

tests/workflow/engine/test_prompts.py sweeps the prompts listed above for both halves of the note, so removing one from a prompt already in the sweep fails the suite. A newly written commit-producing prompt has to be added to that sweep by hand — nothing enumerates the builders automatically.

The developer report contract in developer prompts

Every prompt a developer can finish work on teaches one report contract, _DEVELOPER_REPORT_NOTE in workflow/engine/prompt_notes.py. Its marker spellings come from workflow/engine/report_outcome_models.py, the vocabulary workflow/engine/report_outcomes.py reads, so what a developer is told to write and what the reader accepts cannot drift apart.

The contract settles who owns the report. The developer writes it: a concise, complete, current report for the issue. The prompt asks the developer to keep its body at or below 4,000 characters. Complete means self-contained about the final branch state, not exhaustive about the journey: it covers the final change, its rationale, and the unresolved risks or decisions a reviewer must weigh, without repeating the issue body, preserving prior report or review-round history, enumerating every touched file, or listing every individual test. Verification stays out of it — no verification or testing section, no inventory of the commands run, no summary of their results — since the reviewer verifies the head it reviews and the orchestrator publishes that evidence itself (the reviewer verification contract). That scope, _REPORT_SCOPE_NOTE in the same module, narrows what the report says and nothing the developer runs: the prompt still asks for every check the task or the repository requires, and a check that still fails or could not run is named as an unresolved risk. The parser and publication path enforce neither the writing budget nor the scope, and nothing strips a report: what the developer wrote is what is published. It is written whole rather than as the latest delta because it supersedes every earlier report. Publishing it on the pull request is routine orchestrator work, so the developer neither posts nor edits it and never asks a human whether or how to publish it. A report that needs no repository change is delivered with no commit at all: an empty commit, or any change made only to carry a report, is never asked for. Finished work ends on exactly one of two outcomes, outside any code fence and with nothing after it:

  • Report ready for publication — the complete report between a REPORT: READY line and a REPORT: END line.
  • Report already on the pull request — a single REPORT: VERIFIED <location> <revision> line, for a complete, current report the developer read on the issue's pull request during the run, such as one a human posted or edited. <location> is https://github.com/<owner>/<repo>/pull/<number> for the description, or that URL followed by #issuecomment-<id> for a comment on it, and <revision> is sha256: followed by the 64 lowercase hex digits of that text as GitHub returns it.

No other line may open on REPORT:, and an outcome never shares a message with an ACK: line, which stays the one finished reply without a report on the prompts that offer it. A question, a disagreement, or work that could not finish ends on the question with neither outcome.

The report may not contain the literal text <!--orchestrator- anywhere, not even inside inline code, a fenced code block, or a quotation; the prompt asks a developer who needs to discuss a receipt or marker to describe its format in prose instead. That text is RECEIPT_MARKER_PREFIX in github/comments.py, which opens every hidden receipt this orchestrator writes, and the note spells it from that constant. Receipts are found by searching raw comment text, so carries_reserved_marker refuses the prefix wherever it sits and Markdown that renders a copied receipt inert does not make it publishable: a report carrying one is refused and nothing is published. The restriction rides in _DEVELOPER_REPORT_NOTE and in _RESPAWN_REPORT_NOTE, so a fresh session is taught it whatever task follows the preamble.

A report the orchestrator refuses to record parks the issue under report_undeliverable with the existing report records untouched, and the notice and the log line name the refusal the record's writer gave (workflow/engine/report_refusal_notices.py) rather than guessing at one. A receipt marker is described in prose, never quoted back, and the reply is asked for the report again with every receipt it discusses described that way. A report past MAX_REPORT_TEXT is told its length and that ceiling. A pinned comment with no room names the write that measured too large: the record, the publication transaction it becomes, or that transaction's settlement. It gives the size against the ceiling and what frees the room. A rewrite helps only a REPORT: READY report whose record or transaction overflowed, since those two writes carry its text. The notice then counts the saving in the comment's own characters, not the report's: it gives the report's length beside what its text takes once escaped for JSON (two to twelve characters for a line break, a quote, or anything outside ASCII) and asks for escaped text at least the overflow smaller. A REPORT: VERIFIED record carries a location and digest, and the settlement carries where the report landed, so neither is asked for a rewrite: the notice asks for room freed on the pinned comment before the reply. Any other invalid record is worded with no claim about size and points at the pinned comment and the log. Size words appear only where a size was measured and failed. Each recovery is the same resume: the reply's session writes the corrected report with no new commit, and that report is the one published.

Where the contract is carried:

  • Whole in the initial _build_implement_prompt, the automated-review _build_fix_prompt, the requirements-drift _build_user_content_change_prompt, the PR-feedback _build_pr_comment_followup, the human-reply resume _build_human_reply_followup (_resume_developer_on_human_reply, over the replies implementing/resume_batch.py froze and recorded as delivered), the late revision's _revision_prompt (decomposition/late_revision.py), which resumes the developer against a human's guidance on an oversized candidate, _DEVELOPER_CONTINUE_RETRY_PROMPT, the retry a bare /orchestrator continue on a session-failure park resumes the developer on (implementing/continue_command.py, validating/awaiting.py), and _DEVELOPER_AGY_RECOVERY_PROMPT, the command recovery an initial AGY run that ended on an active command is resumed on (implementing/execution.py). The resumes carry it whole because the transcript they continue may predate the contract or hold another stage's prompt. The fix and PR-feedback prompts have an item that asks only for report content answered in the report, with no commit for it — in the fix prompt, a missing explanation, rationale, or risk; the PR-feedback prompt sends a developer whose comments say a human published or updated the report to read it there and, when it is complete and current, end on REPORT: VERIFIED; and the drift, late-revision, and PR-feedback prompts keep ACK: for a reply after which neither the branch nor the report has to change. The report-refresh _build_report_refresh_prompt carries it whole too, then asks for the REPORT: READY block alone and refuses REPORT: VERIFIED by name, since the report already on the pull request is about the head a rewrite replaced -- or about the approved commit an approval squash collapsed into it, which the prompt then names beside the squash; it offers no ACK:.
  • Deferred in _build_fresh_respawn_preamble, which carries _RESPAWN_REPORT_NOTE instead: the concise report covers the final state of the whole branch, the previous session's commits included; the report's scope, ownership, publication, and the receipt restriction are restated; and the outcome is the one the task below the preamble describes — that preamble also precedes tasks that close on markers of their own. Its conversation block is the caller's FROZEN, classified thread read wherever the caller holds one — the awaiting-human resumes and the explicit /orchestrator continue retries (less the commands they consume) take it from implementing/resume_batch.py, and the conflict stage's reply to a park that owes a report takes the frozen drift read its prompt is cut from, less a retry's commands too and with every reply past the watermark kept whole, its replies marked read by that read's record rather than ahead of it — so the preamble and the record of what the prompt delivered come off one reading and one filter. A caller with no frozen read gets the read _build_dev_spawn_prompt takes for itself.
  • Absent from the documentation, review, and conflict-resolution prompts, which close on markers of their own, and from the conflict stage's own reply resume and bare-continue retry, which stays on the plain _CONTINUE_RETRY_PROMPT — except over a park that left a report owed, where the reply resumes on the drift prompt whole and a retry puts _CONTINUE_RETRY_PROMPT ahead of it.

report_outcomes._report_outcome_of_run reads an outcome only out of a run that completed: a run never invoked, interrupted, timed out, refused by its provider, or exited nonzero is refused before its message is read. On a completed run's message, _parse_report_outcome answers NO_MARKER for a reply that never used the contract — a question, a disagreement, an ACK:, no-change prose — and MALFORMED for one that reached for it and missed: an unclosed or empty block, text after the outcome, a location or revision out of shape, a stray or second marker line, an ACK: beside it, or a marker line that may render as code. That last reading is made without a Markdown parser, so a doubt reads as code: a marker line four columns in or behind a tab, or on a line a code fence may enclose, whether that fence opened at the top level or in a list item (workflow/engine/report_fences.py). A REPORT: VERIFIED location and revision are parsed for shape only; completing on one is owed a fresh read of that location whose text still hashes to the revision.

The initial implementation delivery is the road that acts on an outcome: workflow/engine/report_delivery.py reads one out of the run the disposition is publishing and records it before the size gate and the push, and report_binding.py binds it to the pull request the code reaches and publishes it there (_handle_implementing). There a no-commit reply ending on a report outcome publishes the commits already on the branch where the issue still owes a report it could not deliver, and is read as any other no-commit reply everywhere else.

The requirements-drift resume on an open pull request acts on one too, on workflow:validating, in_review and workflow:resolving_conflict (user-content drift). A commit the resume made — a rebase the session ran included — has its report recorded before the size gate, under the requirements revision its own route handed it — on all three the one the prompt-delivery record fingerprints, which is the read that prompt was built from and the baseline its settlement writes — and bound and settled once the push lands and the stage's own bookkeeping is written; one with no usable report parks rather than being pushed, and stays unpublished until a reply brings the report. A no-commit reply ending on a report outcome is published onto the head the pull request already carries — over a tree proved clean, or parked on the tree with nothing recorded — and routed as an ACK: is, and the reviewer waits until the report is confirmed on the pull request. ACK: and a question keep their own roads.

The reviewer-requested fix round acts on one on the same pull request, under the workflow:fixing label it runs under. The initial CHANGES_REQUESTED run reads its result through workflow/stages/validating/fix_reports.py (_handle_validating's changes_requested arc); every round behind it is the fixing stage's own (workflow/stages/fixing/reporting.py, from _handle_fixing), holding the round to the same contract and publishing the report itself rather than handing the delivery on. A commit has its report recorded before the size gate and bound once the push lands; one with no usable report parks rather than being pushed. A no-commit reply ending on a report outcome is the handover a reviewer item naming report content earns: published onto the head the branch and the code-publication receipt are PROVED to agree on — every reading affirmative, since a reading that did not happen may be a branch carrying a commit the pull request has not got — and routed as a pushed fix is, back on workflow:validating for a fresh review of the report and requirements being handed on. What that handover costs is the SETTLEMENT's to write: the round and the automated-fix bookmarks ride the record and are closed by the write that confirms the report, so a refused post or a crash leaves the round unspent and the round to be finished again. A commit an earlier round left unpublished still passes the gate on its way there, since its own report was that round's to record. The ordinary non-actionable ACK: and a question keep their own roads, and on the human-feedback route the ACK: still returns the pull request to in_review — but only a reply that never used the report contract at all is that: one that reached for the markers and missed, an ACK: line beside a report block included, is a broken contract and parks.

The rewritten-head report refresh acts on one on workflow:validating, where a recorded developer_report_rewrite_debt says this orchestrator's own push moved the pull request onto a head no settled report is about -- a rebase, a conflict resolution, or commits an earlier tick left unpushed, and an auto-rebase replay the authorized settlement of a late adjudication published after the generation took it over (_handle_validating step 3, workflow/stages/validating/report_refresh.py). A published report of that head that already settled, still intact and written against the current requirements, pays the claim and the reviewer runs. Otherwise, before any reviewer runs, and with no human reply, the locked session is resumed for a report alone of that exact head, over requirements, a whole code-publication receipt, and a clean checkout proved to stand where the debt says, and read again once the run is back. Only a REPORT: READY from a run that left no command unfinished pays: it is recorded under the requirements revision frozen before the run, with no round or feedback riding it, bound to the receipt's publication, and settled through the reconciliation on the same tick, and the next tick drops the debt once that report still reads intact. A REPORT: VERIFIED pointing at the report already there is refused, since that report describes the head before the rewrite and verifying it again carries it forward on nobody's proof. A run that commits, leaves loose work, times out, or verifies instead parks under report_undeliverable, and so does a REPORT: READY the orchestrator refuses to record, its notice naming the refusal as above; a question or other agent failure takes its usual park; each records developer_report_owed, so the reply is read as the report it owes. That reply's resume commits nothing and needs nothing more than the report: it is published once, exactly as written, the debt is paid, and the reviewer is handed it, with no later tick posting it again or repeating its settlement. A head moved or an issue edited while the agent was out records nothing, leaving the reviewer road or the drift resume to answer it.

Every other developer road is still routed by its commits, its ACK: line, and the question parks the delivery stages describe, and a no-commit reply that ends on a report outcome is read there the way its stage reads any other no-commit reply without ACK:. The awaiting-human resumes settle the input they delivered straight into pinned state: the report transaction a run may record carries no consumed watermarks to freeze it onto. The settlement records delivery and nothing more, so the question the run came back with is still the disposition's to answer.

The PR-feedback fix round on workflow:fixing records the same thing through the same producer (workflow/engine/prompt_delivery.py): a pushed fix, an ACK:, a timeout, a dirty-tree park, and a question park all delivered the prompt, so all of them record what it carried, while a run the circuit never invoked, one a shutdown killed, and one an operator paused mid-run record nothing. Each surface of that batch settles the reader that owns it, so the issue-thread half moves last_action_comment_id and the pull-request halves do not, and an accepted /orchestrator continue settles the batch it replayed joined with the fresh rescan — including the bare command the prompt deliberately never hands the developer, on the surface that command was typed into.

That round can also end on a report outcome, since its prompt teaches the contract like every other developer prompt and asks by name for an item wanting report content only to be answered in the report with no commit for it. Such a round settles nothing on the tick: the publication it owes is not this tick's to promise, and feedback recorded as answered for a report no reviewer has is the one reading that fork exists to refuse. What carries the batch instead is the report's own record, beside the round and the pending_fix_* bookmarks, and the write that completes the publication applies all of them.

While that publication is outstanding the readers therefore say less than they usually do, and the record's frozen pairs are what the stage asks instead: a rescan with nothing above them is the prompt the outstanding report answers, so no scan resumes a second developer over it and no park clears on it. A comment that landed ABOVE those pairs is genuinely unanswered and runs as usual, and so does an accepted /orchestrator continue. An ACK: is refused for the same reason on an issue that owes a report at all: returning the pull request to in_review as needing nothing would present it while the report it owes is still on the pinned comment and nothing has gone out.

Everything a fix round derives from the issue thread comes off ONE read of it — the batch the prompt quotes, the replay an accepted /orchestrator continue rebuilds from pinned ids, the watermarks both settle, the requirements fingerprint its report is stamped with, and the conversation a retired session's fresh spawn is re-grounded on — which keeps this orchestrator's own posts by RECORDED id however the author allowlist is set, since a thread it is half of re-grounds nobody when only the answers survive.

A second read is newer, so a comment landing between them enters whichever of those answers was taken late and none taken early: an agent shown a comment no watermark records, or requirements recorded as answered by a session that never saw them.

Delivery is still not completion: an ACK: may settle a round whose comments named no actionable change, and it does not answer the automated CHANGES_REQUESTED review that asked for a concrete one — that route has no ACK fast path and parks for a human with its pending_fix_* replay anchor intact.

Publication is recoverable because the records outlive the process. The additive developer_report_delivery / developer_report_pending / developer_report_current / developer_report_handoff group (../state-machine/labels-and-state.md#pinned-state) is what carries one report from the run that wrote it across a process that dies mid-way — the complete report text included, since a transaction recovered from a text nobody kept would have to ask an agent to write it again, and a second run is not the same report. The publication that records one also tries to complete it on the tick it pushes; where that did not happen, the developer-report transaction finishes a developer_report_pending transaction ahead of every handler. Whichever of the two makes it, the settlement is one write: the report the pull request now carries, the receipt saying THIS transaction finished, the readers the run consumed, the round and bookmarks its route closes, and the debt the record left. The receipt also records the workflow label the issue was carrying as that write landed — read off an issue fetched again rather than off the copy the run began under, since the reconciliation can settle a transaction on a label its own route's stage is not watching, and a route whose bookkeeping includes a hand-back that stage has to make has no other way to tell whether the stage was ever there. Both publish through the developer-report comment owners (github/developer_reports.py, github/pull_request_reports.py, and workflow/engine/comments.py's _publish_developer_report). The reconciliation never reads an agent's message: what it acts on is the record a stage wrote, and on an issue carrying none it costs one pinned read that has already happened. No reconciliation acts on a developer_report_delivery: binding one to a publication is a stage's step — the implementing publication's, and on an open pull request the review stages' own, which is also what binds a delivery a later push carried — exchanging the delivery for its transaction in one write before anything is posted, and taken again on every tick that republishes the same commit onto the same pull request until it lands. Recording a delivery, binding it, and settling the transaction are guarded commits over the comment as it stands (workflow/engine/report_commits.py), the settlement prepared before the report is posted and committed behind the post: one GitHub took and never confirmed stops the tick, and the next tick finds that same record, transaction or settlement and carries it on with no second run, no second revision, and no second report.

The developer report lifecycle

Read end to end rather than road by road, one report passes the same stations whichever road produced it, and the division of labour between the developer and the orchestrator never changes.

  • Who owns what. The developer writes the report, and the outcome it ends on is the whole of its part: it never posts, edits, or asks permission to publish one. The orchestrator records the report, proves the world it describes, publishes or re-reads it, recovers it after an interruption, and hands it to the next reviewer. A human may publish or edit a report on the pull request themselves, and the developer's part is then to read it and verify it.
  • What identifies a report. A report is recorded with a receipt naming its publication transaction (issue-<n>-report-<revision>), a report revision minted one past every report the issue has recorded, and the requirements revision the developer run was handed. The source commit it describes joins them when the delivery is bound to the publication carrying the code. The content revision — the SHA-256 of the report's exact text — depends on the outcome: a REPORT: READY delivery and its pending transaction carry the text itself, its digest is written into the published comment's header, and the settled developer_report_current records it beside the exact location; a REPORT: VERIFIED outcome carries the location and the content revision it asserts from the start. One developer result is one revision, so several reports can stand on one commit, while a retry under the same receipt finds what an earlier attempt landed instead of posting again. The requirements revision is the one the run was given and never the one current when publication finally lands, so a delayed report is not stamped as answering an edit it never saw.
  • Where it lives. A published report is a comment of its own on the pull request, rendered under a header naming that identity and ending on the orchestrator's comment marker; the pull-request description is never rewritten to carry one. Its comment id joins orchestrator_comment_ids — including a post whose response was lost, once the retry finds it — so no feedback reader takes it for a human's, while a human commenting from the same account is still read. A verified report may sit in the description or in any comment on the pull request. developer_report_current records the exact location either one settled at, and every later reader re-reads it there.
  • Ready versus verified. REPORT: READY hands the orchestrator a report to publish. REPORT: VERIFIED names one already on the pull request, which is re-read at that location, held to a trusted author and to the digest the developer named, and settled with nothing posted; a location that changed, went missing, or could not be read is not the verified revision. Neither is read out of a run that did not complete, and neither is an ACK:, a question, or no-change prose, each of which keeps its own road.
  • Evidence before anything goes out. The report is recorded before the size gate and the push, and published only over proof: a readable, clean worktree standing on the recorded commit; the pull request the record names, open on the recorded branch of this repository and standing on that commit; a fetched branch with nothing unpushed and a remote that has not moved; a code-publication receipt naming that commit and that pull request; and requirements that still hash to the revision the run was handed. Nothing completes on an absence, and committed work nobody published still passes the size gate whatever the latest run ended on.
  • Recovery. developer_report_delivery holds what the run wrote until a publication is bound to it, developer_report_pending holds that transaction until it lands, and one write then settles the report, its handoff receipt, the readers the run consumed, and the round its route owes. Each window an interruption can leave is finished with no second developer run, no second report, and no round spent twice:
  • a post GitHub refused or never confirmed, and one it accepted while the response was lost, by the developer-report transaction ahead of every handler, which finds an accepted post by its receipt rather than posting again — and the same for a settlement refused because another road wrote the comment while the report was out, or accepted with its answer lost, which leaves nothing owed where it landed;
  • a report recorded and never bound — the process dying before the gate or before the binding, or the reviewer-requested round's relabel to workflow:validating, which it takes before the binding, never landing — by the stage that binds it: workflow:validating's review hold, or workflow:fixing's recovery, which hands the round back on the fixing_round_settled mark the record carries — the report a rewritten head's refresh recorded included, which that hold binds before it asks about the debt again;
  • a round settled on workflow:fixing and never handed back, on that same mark; and one whose hand-back was written and whose relabel never landed, by taking the relabel again while no reviewer has returned over that round's report — review_returned_subject, which only a reviewer that ran writes, rather than the launch's review_subject, which a launch the run budget refused writes too.

None of them reads feedback past the pairs the record froze, so a comment arriving in any of those windows is left for the road that ordinarily answers it. - Review-subject freshness. No reviewer spawns while a report is owed. Each is handed the report the pull request carries, re-read where it settled and quoted whole, and what it reviewed — pull request, head, requirements, and the report's revision and digest — is recorded as review_subject at the launch and as review_returned_subject once it returns, an approval's as review_approved_subject. A new report on an unchanged head is a new subject, so it reaches a fresh reviewer rather than riding an earlier approval, and the verify gate and final-docs pass still follow that approval in their usual order. - Fix-round accounting. What a handover spends depends on the road that earned it, and a replay applies the pair the record froze rather than counting again: - a requirements-drift resume on workflow:validating spends one round for a commit and nothing for a report alone; - a requirements-drift resume on in_review resets the review budget, since the approval was of requirements that are gone, and its publication spends nothing of the fresh one; - a requirements-drift resume on workflow:resolving_conflict counts one conflict_round and resets the review budget for a commit, as every pushed round of that stage does, and spends nothing for a report alone; - the automated CHANGES_REQUESTED round, direct or resumed after a park, spends one round — at the push's receipt for a commit, at the settlement for a report alone — and has no ACK: completion: an ACK: there parks for a human with the replay anchor intact; - human feedback routed from in_review (pending_fix_at set) resets review_round to 0 on its handover, since the approval was of the prior head, and its ACK: returns the pull request to in_review; - a rewritten head's report refresh spends nothing: the push that rewrote it already reset review_round for the head it published, and the report answers no feedback.

Agent runs are counted where they are spawned and nowhere else: each launch is charged once to agent_runs_used and its usage folded once into the per-issue totals, and a recovery that finishes a result spawns nobody. Review caps, /orchestrator add-review-rounds, and /orchestrator add-agent-runs bound the same counters on every road.

tests/workflow/test_report_lifecycle.py walks this whole sequence through the dispatcher — a requirements edit, a drift commit whose report publication is interrupted, a reviewer asking for the report, a report-only round on the same commit interrupted on its own accepted post, on its relabel, or on that relabel and the recovery's hand-back relabel both, with a human commenting while it runs, and the fresh review, docs pass, and in_review round behind it — and holds every combination to the same end state.

The reviewer verification contract

The reviewer prompt (workflow/engine/review_prompts.py, with its verification block in workflow/engine/review_evidence_prompts.py) hands every reviewer, beside the complete current developer report:

  • The current evidence for its subject, or the note that there is none. Only evidence recorded as current and bound to exactly the pull request, head, requirements, and report the reviewer is handed is quoted, and only once it proves current again — still the latest revision, its handoff and artifact standing, the whole binding proved (stages/validating/review_evidence.py). The artifact is re-read at its comment, held once more to the settled record, and quoted whole under the sha256: revision a reuse names: its summary, then every command with its exit status and complete output. The comment itself shows only the summary, so those commands are the ones the re-read decoded from its hidden payload — or read off the visible section of an artifact published before evidence was hidden — and a reviewer handed evidence after a restart reads exactly what one handed it before would. Evidence about an earlier report, another head, or requirements the issue has moved past is never handed over.
  • What the repository configures. The configured VERIFY_COMMANDS are listed in order, and an approval's evidence has to include each of them exactly as written; a reviewer may declare further commands beside them, which refuse an approval only where one did not exit 0. An empty VERIFY_COMMANDS is stated as such: it requires no particular command and is not evidence that any check passed, so the reviewer runs the checks the repository's own documentation requires, and an approval still needs evidence of at least one command, every one exiting 0.
  • The declaration it closes with, above its VERDICT: line, every marker alone on its line and outside any code block, spelled from workflow/engine/review_verification_models.py, the vocabulary workflow/engine/review_verification.py reads: a VERIFICATION: RUN <head> … VERIFICATION: END block naming the head it is handed and listing each COMMAND: it ran with the EXIT: status it returned and any output as plain lines, or — only where evidence was handed — VERIFICATION: REUSED sha256:<revision> naming that exact revision.
  • What is no change to request. The orchestrator publishes what the reviewer declares on the pull request itself, as a verification artifact (github/verification_artifacts.py) whose visible summary says whether the recorded checks passed, failed, or never ran, and names the artifact and evidence revisions, the witness, the tested commit, and the target head — an equivalent-tree carry where the checks never ran on it — while every command, exit status, and output stays exact in its hidden payload. So a reviewer holding valid evidence for the commit it reviews does not ask a developer or a human to copy a commit SHA, a test count, or a command into the description or the report. A failed check, verification the change still needs, a report claiming what did not happen, and evidence about another commit or subject remain changes to request.

The round (stages/validating/reviewer.py) hands every returned verdict to the disposition service (stages/validating/review_disposition.py). What the run declared is read only out of a run that completed, and never in place of its verdict: commands it ran become a reviewer-reported transaction bound to the subject it was handed and the full tree of the reviewed head, and a reuse a claim on the evidence it named (stages/validating/review_claims.py). The verdict is persisted as review_returned_verdict with that claim and transaction in one guarded commit (stages/validating/review_writes.py) — over the subject resolved again and the pinned comment read behind it, a verdict of a subject that moved while the reviewer ran being recorded and acted on by nobody, and a report, pull-request, verdict, or evidence record another road moves after that reading refusing the commit with nothing written or published — and the transaction is published through the dispatcher's own evidence reconciliation before either verdict is acted on. A publication that holds or stands down ends the tick with the verdict waiting, as does a commit GitHub never confirmed; a later tick publishes it and finishes the verdict from the record (stages/validating/review_resume.py) with no second reviewer, usage fold, run charge, or round. Evidence that can never settle — retired, superseded, or bound to a verification context that has since moved — drops the verdict for a fresh reviewer, and so does a subject that moves while it waits. One the pinned comment has no room to persist, with the transaction its commands were minted as, is acted on not at all and parks under reviewer_unrecorded.

The feedback a change request is persisted with, posted as on the pull request, and handed to its developer's fix prompt is one text: the reviewer's findings as workflow/engine/review_findings.py shows them, formatted only once the declaration has been read off the run's own message, which nothing rewrites. Every VERIFICATION: marker, COMMAND: and EXIT: line, and the output of each check shown to pass is set aside; a check not shown passing stays as a sentence naming its command and how it ended, over the output the reviewer quoted, which is the diagnostic the developer acts on. Findings that are a declaration and nothing else read as a sentence saying the reviewer stated none, never as the raw message they sit in. The transaction keeps every command, exit status, and output exactly, and the formatting decides nothing: a missing, malformed, stale, failing, or incomplete declaration is disposed of exactly as it would be unformatted, and no approval rests on what the findings show.

A request persisted before that formatting still carries its declaration raw. Its record stays exactly as written, since every comparison its recovery makes is against it, and so does any feedback post an earlier handoff made. Only the words that go out are formatted: the post a recovery makes where none was made, the fix prompt it resumes the developer on (stages/validating/review_handoffs.py), and the anchored post an accepted /orchestrator continue replays, whose findings are quoted formatted while the comment and its id stay as posted (stages/validating/feedback_posts.py).

A change request stands without evidence: its failed run is published before its feedback, and it is handed to the one developer it owes behind that feedback posted and anchored (stages/validating/review_handoffs.py), keeping its record through the relabel to workflow:fixing so a tick that stops between the relabel and the launch launches exactly that developer later rather than paying a second reviewer. The handoff is a guarded commit prepared before the feedback is posted, so a pinned comment with no room for it posts nothing, and committed behind the post over the records it was decided on, so another road's write behind that post relabels and launches nobody; a park another road records -- before the post, behind it, or at the run circuit's charge and start -- is kept for its human, with nothing posted, relabelled, or launched under it; a later tick takes a post an earlier one made in the same words, this orchestrator's own, rather than posting the feedback twice -- its opening line held to the request's round alone, whichever reviewer and round cap it names. An approval reaches the verify gate, the approval record, and the squash only on evidence that passed and is current — its own declared run with every command exiting 0, once that transaction has settled on the pull request, or the exact current evidence it reused — either way proved current again, and either way including every configured VERIFY_COMMANDS command exactly as configured (stages/validating/unverified_approvals.py). A missing, malformed, or stale declaration, any declared command that did not exit 0, a configured command the evidence leaves out or spells otherwise than as configured, or a reuse the evidence no longer vouches for parks under reviewer_unverified instead -- a further command that exited 0 beside every configured one does not, and under an empty VERIFY_COMMANDS any declared run of at least one command, every one exiting 0, covers the configuration; a bare /orchestrator continue buys a fresh reviewer, since the reviewer owes the evidence. The review caps, configured-author filtering, report freshness, and the approved versus changes-requested routing are those of every other round. tests/workflow/stages/validating/test_evidence_aware_rounds.py drives each of these through whole ticks.

Foreground execution and asynchronous command guidance

Every developer and commit-producing prompt — initial implementation, automated reviewer fixes, requirements drift, PR-feedback follow-up, human-reply resumes, conflict resolution, the documentation pass, both discussion builders, and the retry prompts — carries one shared foreground-execution note, _FOREGROUND_ONLY_NOTE in workflow/engine/prompt_notes.py. Read-only prompts (reviewer, question, and decomposition) do not carry it.

The orchestrator operates under a stateless, one-shot process execution model: an agent session terminates the moment it finishes responding, nothing continues running between turns, and any subsequent resume launches a fresh CLI invocation. A background job started during a turn (such as a long-running build, test suite, Miri, or dev server) dies when the session exits, so its result is never observed.

For agent backends that support asynchronous tool execution — such as Antigravity (agy), where commands run asynchronously when they exceed synchronous execution limits — the note instructs the agent to handle asynchronous execution explicitly within the same turn:

  • Agents must use supported wait/status tools such as manage_task to inspect and wait for command completion within the current response.
  • A RUNNING status requires continued polling or waiting within the active turn.
  • Ending the response to await an external wakeup notification terminates the AGY session process and cancels the running command.

Because _FOREGROUND_ONLY_NOTE is defined once in workflow/engine/prompt_notes.py and reused across initial implementation, fix prompts, follow-ups, resumes, and retries, the foreground and asynchronous execution contract stays authoritative in one place without duplicating instructions across prompt builders.