About the loop
The loop takes one task Issue with a frozen brief to a pull request ready for the Principal's merge, or to a pause. It runs unattended.
One pure decision function holds the whole decision logic. The controller (also called the driver) turns facts from the forge and from each dispatch into observations, calls that function, and acts on the decision it returns. The controller never re-derives a stop condition on its own.
| Role | In the loop |
|---|---|
| Developer | One vendor session, resumed across rounds. Publishes only through the seven controller-run tools. |
| Code reviewer | A fresh dispatch every round, never resumed. Has one tool, read_source, when the round has required sources. |
| Security reviewer | Same as the code reviewer |
| Controller | Prepares inputs, spawns every agent, holds the forge login, posts round markers and verdicts, decides each round |
| Principal | Rules on pauses addressed to it |
Start a run
Prepares the run (renders and freezes the brief), then runs the loop.
vinaya task run <tranche> <n> [--agent claude|codex] [--background]
vinaya task run --issue <n> [--agent …]task run wraps the loop in a watcher that waits at a pause instead of exiting (see Resume by re-execution). Without --agent, the run uses dispatch.agent.
For a walk-through, follow Deploy backlog issues or Deploy medium work.
How it works
- 1
Prepare the run
The controller fast-forwards the checkout, freezes the brief, takes the task lock and checks that enforcement is active. Round 1 creates the task worktree and reserves the task branch.
- 2
Prepare sources
Each entry in the brief's
## Documentationbecomes a manifest entry. Any source that cannot be prepared pauses the run asmissing_source. - 3
Dispatch the Developer
Round 1 gets a full context packet. Later rounds and resumes get a delta packet.
- 4
Publish through the controller's tools
The Developer commits, pushes and opens the pull request through seven tools, behind the hooks.
- 5
Accept the Developer's turn result
The Developer ends every turn with one typed result. The controller alone accepts it.
- 6
Read the mechanical gate
The controller reads CI, mergeability and the premises for the live head. A red head goes back to the Developer.
- 7
Apply the confidence rule
From round 2, missing or low confidence gets one more chance, then the run pauses.
- 8
Review
Fresh Code and Security reviewers judge the pull request on one review-input manifest.
- 9
Assess
The controller combines the verdicts and decides: publish, revise or pause.
- 10
Publish
The controller posts the verdicts and a publication comment. The pull request is ready for the Principal's merge.
Each step is described in detail below.
1. Prepare the run
- The controller fast-forwards a clean default-branch checkout that is only behind the remote tip. It refuses a checkout on another branch, with uncommitted tracked changes, or with local commits the remote lacks, and names the command that clears it.
- Preparation renders and freezes the brief. Its
**Revision:**line records the tip it was rendered from. - A one-driver-per-task lock (
driver.pid.json) refuses a second concurrent start on the same task. - A start-of-run sweep runs in the background and removes finished task folders and worktrees. If it fails, the run does not pause.
- Before every unattended Developer dispatch, the enforcement-controls check requires the local gate (tracked
pre-commitandpre-pushhooks, routed and executable) and the adapter's hook files. Readiness runs thecheck-check anddispatch- readiness verify-.dispatch - - existing- work - Round 1 creates the task worktree and pushes a commit-free reservation of the task branch. A remote branch whose head is an ancestor of the default branch counts as a fresh start. Any other existing branch enters crash recovery.
2. Prepare sources
Before every Developer dispatch, the controller turns each entry of the brief's ## Documentation into a prepared source. Each one records an identity, a URI, a digest, a delivery mode (injected, retrieval_tool or unavailable) and a freshness policy (pinned or volatile).
| Source | How it is delivered |
|---|---|
| Repository file or directory | Pinned by git object at the brief's revision, delivered injected |
| Public page | Fetched by the controller outside every sandbox. A snapshot is kept under its digest and delivered through fetch_. |
| Anything else | unavailable, with a typed reason: login_required, other_host, not_a_document, fetch_refused, unreachable, missing_ or unresolvable |
Any unavailable source pauses the run as missing_source, addressed to the Planner. The manifest and receipts live in <runtimeDir>/, outside every worker's writable folders.
3. Dispatch the Developer
- Round 1 gets a full context packet: the frozen brief and its digest, objectives and rulings, base and head, prepared sources, the capability grant and tool catalog, the terminal-result contract, budgets and stop conditions.
- Later rounds and resumes get a delta packet, built from the hand-off stored in the
loop_staterecord (unresolved findings by controller id, failing checks) and the prior session id. Conversation memory is never the only source. - A chosen model is used only for the Developer's dispatch. Reviewers use the vendor default.
4. Publish through the controller's tools
The controller hosts one MCP server in its own process for the turn, and the agent reaches it through task-. The Developer has seven tools and nothing else:
| Tool | What it does |
|---|---|
publish_changes | Commits under a Type(scope): Description header and pushes the task branch, behind the publication preconditions, the protected-path gate and the pre-push hook |
open_ | Opens the pull request (title and body), once |
update_ | Replaces the body after validation |
refresh_evidence | Rebuilds the Evidence block for the current head |
read_ | Reads state, checks, reviews, body and failed-check log tails |
run_checks | Runs vinaya check - for the current head |
fetch_ | Reads one public https page and records the delivery against the manifest |
- A change beyond the Surface is recorded, never refused. The controller rewrites the body's
AEG:BEYOND-block after every body write and every landed push. A protected administrative path passes only when the Surface'sSURFACE in:globs cover it. - A pre-push refusal returns the failing check and test lines to the Developer in the same turn.
- After the turn, the controller publishes nothing itself. It detects unpublished work and asks the same session again, at most twice.
- The after-turn confinement check re-hashes the control store, the source receipts, other roles' folders and the policy file.
5. Accept the Developer's turn result
Every Developer dispatch ends with one turn result, delivered as native structured output (Claude --json-schema, Codex --output-schema).
status | Carries |
|---|---|
completed | summary, confidence (a whole percent), confidenceExplanation, addressedFindingIds, sourceUses, and optionally reportedChecks |
blocked | A typed blocker |
needs_ruling | A rulingRequest, limited to the permitted ruling decisions |
Only the controller accepts a result. It checks that:
- finding ids belong to this round's hand-off (
R<round>-CR-<n>,R<round>-SEC-<n>), and round 1 names none; - a
completedresult cites every required source, each backed by a controller receipt.
An invalid result gets one bounded correction turn in the same session. A second failure pauses. reportedChecks is context, never evidence.
6. Read the mechanical gate
- The controller reads the live head and CI from the forge. CI is never re-run locally. The review gate's own check-run is excluded. A failed workflow run makes the head red even if every check-run passed. Queued or unreadable runs read
pending. - Mergeability (
gh pr view -) is read before reviewers.- json mergeable CONFLICTINGsends the Developer back with the conflicting files, never to a reviewer. - A red head sends the Developer back in the same round number. If the head is still red and unchanged after a turn that published nothing, the controller asks the forge to rerun its failed jobs, once per round.
- Premise re-assertion runs against the live body on each gate read.
7. Confidence rule (round 2 on)
The rule reads this round's newest accepted completed result, before reviewers run:
| Confidence | Action |
|---|---|
| Absent, first time | Ask the same session once more for its turn result; publish nothing |
| Absent, already asked | Pause: confidence |
| Below 50, extra turn unspent | One more Developer turn, with no reviewers. This extra turn is spent once, ever. |
| Below 50 again | Pause: confidence |
| 50 or over | Dispatch reviewers |
Round 1's stated confidence is recorded but the rule ignores it.
8. Fresh Code and Security reviewers on one manifest
- The controller builds one review-input manifest (protocol version 2): head, base (the merge base with the default branch), brief hash, objectives version, ruling ordinal, policy digest and
pauseBoundRuling. It is stored as amanifestrecord per round in the control store. - The controller posts the round marker comment itself:
Head:,<!-, the- aeg:developer:round- <n> - - > Pause-line andbound ruling: FINDING_IDS:. - Each round gets one read-only candidate copy, and each reviewer gets a fresh writable scratch copy per attempt. The pull request's facts are staged as files. Reviewers hold no
ghand no forge credential. - Both reviewers and the evidence report run together. Each reviewer returns one native review result; nothing is written to disk. A reviewer with no accepted result is retried once, fresh. A second miss pauses as
infrastructure. - Reviewer prompts carry facts only (objectives, rulings, head, CI, source revision) plus the role's own doctrine. They never carry Developer-written prose.
- After both return, the controller resolves the manifest again. A mismatch discards both verdicts and pauses as
objectives_,changed ruling_posted,brief_supersededorpolicy_changed. - A
NOT METobjective whose evidence names only prose is reclassifiedMET (prose note).
9. Assessment
The controller combines the verdicts. Severities block under the reviewPolicy thresholds, with these adjustments:
- A finding located in prose is capped at MINOR.
- A finding on an unchanged file outside the Surface is deferred, in any round.
- From round 2, a finding on an unchanged line is deferred, except a security finding at or above HIGH.
Deferred findings go to one vinaya/deferred backlog Issue per pull request, at publication.
| Exit | Trigger | Decision |
|---|---|---|
| Green (ready) | Both verdicts clean, every objective MET | Publish |
| Revise | Blocking findings, and none of the stops below | Findings go to the same Developer session; the round advances |
| Escalated | Either reviewer returns ESCALATE | Pause: escalation |
| Stalled | A finding marked resolved reappears | Pause: reappearance |
| Repeating a finding | Same role, same file, same description fingerprint, blocking, open in two consecutive reviewed rounds | Pause: repeat_finding |
| Repeating a failure | Two consecutive attempts end on the same normalized mechanical failure, with a finished Developer attempt between | Pause: repeat_failure |
| Capped | A non-green round reaches reviewPolicy. | Pause: max_rounds |
| Out of time | Active working time exceeds reviewPolicy. | Pause: time_budget |
| Low confidence | The confidence rule above | Pause: confidence |
When several apply in one round, the most severe wins, in a fixed order: escalation, then a Principal-owned item, then repeat, then max rounds, then infrastructure. Every applicable reason is still recorded.
Before publishing, [live] Test Plan lines run on the approved head in a fresh checkout, outside any sandbox. A failure withdraws the approval and sends the Developer back as a new round. A line the host cannot run is recorded as unverified in the AEG:LIVE block, and it holds the merge gate like an unticked [principal] item.
Mergeability is read again before publication. A conflict discards the held verdicts and resumes the Developer without advancing the round.
10. Publication
The controller posts, in order: the held code verdict, the held security verdict, and one publication comment (<!-). Each post goes through the effect executor, so a rerun never posts anything twice. Nothing is posted before green.
A run can also end in a pause (see Pauses and rulings), or by cancel through --cancel or task_cancel on a paused run. Infrastructure failures (a gate error, a dispatch error, any error thrown on the controller's path, a corrupt control record) become a decided pause, infrastructure, never a crash.
Rounds
- Rounds 1 to
maxRoundsrun. The loop pauses after the last round's review if it is not green. A green round at the cap still publishes. - The round number advances only when a reviewed round concludes
changes_. A red-gate retry and the confidence extra turn keep the same round number.requested - A resume after a ruling with a moved head restarts the counter at the ruling's newest ordinal, so the ruling acts as the round-cap override.
A finding open two rounds
A finding's identity is the reviewer role, the finding's file (line dropped) and a fingerprint of its description (lower-cased, with whitespace and punctuation collapsed). It is never the reviewer's positional id.
- Only findings treated as
blockingcount. - "Consecutive" means consecutive reviewed rounds. A red gate or a low-confidence turn between them carries the previous keys forward.
- A round that raises only new blocking findings continues.
Where state is stored
<runtimeDir>/tasks-execution/<task>/
driver.pid.json one-driver-per-task lock
control/ control store: ownership, transitions, loop_state, effects,
resolutions, escalations, manifests, pause-state.json, sources/
sessions/ vendor session and launch records per role+agent
hooks/ documentation reminder hook files (ergonomics only)
output/ raw agent output, driver.log
rounds/<n>/ held verdicts, candidate and scratch copies, developer/ turn results| Record | Holds |
|---|---|
loop_state | round, phase, pauseReason, budgets (mechanicalRetries, reviewRounds, infrastructureRetries), heldResult, deliveredFindings, repeatMemory, phaseMs, developerHandoff. Written on every transition. |
manifest | One immutable review-input manifest per task and round |
effect | Forge-write identity and status (started, verified, uncertain) |
escalation, resolution | See Pauses and rulings |
pause-state.json | The held pause (a legacy side file, still read and written) |
| Forge markers | Round markers, the publication comment and verdicts. The round journal is rebuilt from principal-authored comments. |
Resume by re-execution
The loop resumes by running again, never by replaying a checkpoint or the Log.
- The watcher.
task runholds the driver lock for its whole life. At a pause on an open pull request it polls every 30 seconds. A merged or closed pull request ends it; a recorded cancel ends it; a Principal ruling newer than the pause runs the loop again in the--resumeshape.infrastructureandstale_driverpauses resume without a ruling after a 60-second backoff, up to 5 times. --resume <pr>. ReadsCloses #N, the held pause and a fresh Principal ruling, then compares heads. If the head is unchanged, it re-enters the held round and dispatches the Developer with the ruling as context. If the head moved, the move is accepted and the loop goes straight to the gate and reviewers.- Budgets survive. Each entry seeds the round, budgets and repeat memory from
loop_state, and floors the infrastructure count againstpause-state.json. A corrupt record pauses asinfrastructureand never resets a budget. - A stale controller. If the base moved over the controller's own code, the controller pulls and restarts itself. It pauses as
stale_driveronly if that fails. - A prior launch. The controller decides what to do with a prior Developer launch:
live,finished,resume(same session),pause,freshornone. An orphaned child process is reaped first.
Gates
| Step | Gate | Run by |
|---|---|---|
| Prepare | dispatch-, verify-, the enforcement-controls check | Controller, before every Developer turn |
| Publish | Publication preconditions, protected-path gate, credential scan, then the pre-commit and pre-push hooks with the Ring 0 checks | Controller and Ring 0 |
| Pull request | closes-n, surface-scope, test-plan, evidence-fresh, body-bare-digits; Ring 0 checked again | Ring 1 |
| Gate read | CI on the current head, mergeability, premise re-assertion | Controller |
| Reviewers | Agent-config scan (securityScan), after-turn confinement check | Controller |
| Assessment | review-gate, bound to the same manifest comparison | Ring 1 |
| Before merge | principal-, which holds unticked [principal] and [live] lines | Ring 1 |
See Gates for each check.
Configuration
| Key | Effect |
|---|---|
reviewPolicy. | Round cap (default 3) |
reviewPolicy. | Active time budget (default 180; 0 turns it off) |
reviewPolicy., reviewPolicy. | Blocking severities |
dispatch.agent, dispatch., dispatch. | Runtime and time ceilings |
securityScan. | The agent-config scan |
runtimeDir | Where the controller keeps its files |
VINAYA_, VINAYA_ | Watcher poll (30 s) and infrastructure backoff (60 s) |
Fixed, not settings: the confidence threshold of 50, at most 2 publish re-asks, at most 2 stalled gate turns, at most 5 infrastructure retries, at most 2 automatic rate-limit resumes, the two-round finding rule, and the set of agent-config paths the scan watches.
What gets logged
| When | Events |
|---|---|
| Run start or resume | dev_review_loop · loop_started, resumed (by: driver or principal) |
| Each round | round_started, gate_result_read (with confidence_value, confidence_, extra_turn_spent), verdicts_read (with findings, each with policy_treatment), findings_, round_ended |
| Turns | dispatch · dispatched, outcome_received, dispatch_failed; role_attempt · attempted; usage · observed |
| Stops | stop_, paused, journal_, cancelled |
| Liveness | driver_heartbeat every five minutes; driver_exited on every exit (finished, paused, reexec, error, signal) |
| Forge writes | effect |
| Recovered hiccups | infrastructure_ |
Every event the loop emits is validated against its schema before it is written. The input versions ride on reviewer dispatch events. See Events.