@attalabs/vinaya 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +5 -4
  2. package/aeg-root/contracts/planner-developer.md +4 -1
  3. package/aeg-root/contracts/planner-operator.md +86 -0
  4. package/aeg-root/contracts/principal-operator.md +88 -0
  5. package/aeg-root/enforcement.md +4 -4
  6. package/aeg-root/roles/developer.md +25 -18
  7. package/aeg-root/roles/operator.md +133 -0
  8. package/aeg-root/roles/planner.md +6 -0
  9. package/aeg-root/roles/principal.md +4 -0
  10. package/aeg-root/roles/reviewer.md +3 -3
  11. package/aeg-root/roles/security.md +3 -3
  12. package/aeg-root/skills/aeg-context-packets/SKILL.md +123 -0
  13. package/aeg-root/skills/aeg-context-packets/examples/operator-start.md +30 -0
  14. package/aeg-root/templates/brief-template.md +6 -2
  15. package/dist/checks/bin/check-body-bare-digits.js +1275 -240
  16. package/dist/checks/bin/check-branch-topology.js +1207 -180
  17. package/dist/checks/bin/check-brief-shape.js +1279 -243
  18. package/dist/checks/bin/check-changeset-coverage.js +2176 -264
  19. package/dist/checks/bin/check-closes-n.js +1211 -184
  20. package/dist/checks/bin/check-coherence.js +1378 -265
  21. package/dist/checks/bin/check-dead-branch-push.js +1158 -176
  22. package/dist/checks/bin/check-dispatch-readiness.js +1440 -276
  23. package/dist/checks/bin/check-doc-coverage-push.js +2171 -259
  24. package/dist/checks/bin/check-doc-coverage.js +2173 -261
  25. package/dist/checks/bin/check-doctrine-no-procedures.js +1275 -240
  26. package/dist/checks/bin/check-doctrine-portability.js +2171 -259
  27. package/dist/checks/bin/check-evidence-fresh.js +2613 -347
  28. package/dist/checks/bin/check-exec-bits.js +2169 -257
  29. package/dist/checks/bin/check-first-push-dispatch.js +1326 -246
  30. package/dist/checks/bin/check-issue-assignment.js +1209 -182
  31. package/dist/checks/bin/check-issue-milestone-attach.js +6087 -0
  32. package/dist/checks/bin/check-issue-objectives-numbering.js +6089 -0
  33. package/dist/checks/bin/check-issue-parts-coverage.js +6089 -0
  34. package/dist/checks/bin/check-issue-surface-globs.js +7304 -0
  35. package/dist/checks/bin/check-issue-title-grammar.js +6089 -0
  36. package/dist/checks/bin/check-issue-tranche-label.js +6089 -0
  37. package/dist/checks/bin/check-main-branch-refusal.js +1158 -176
  38. package/dist/checks/bin/check-no-disk-state.js +1158 -176
  39. package/dist/checks/bin/check-pr-premise-reassert.js +1275 -240
  40. package/dist/checks/bin/check-pr-report-density.js +1158 -176
  41. package/dist/checks/bin/check-quoted-command.js +2150 -257
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +2153 -260
  43. package/dist/checks/bin/check-registry-gates.js +1245 -176
  44. package/dist/checks/bin/check-retired-vocabulary.js +2148 -255
  45. package/dist/checks/bin/check-review-gate.js +1375 -245
  46. package/dist/checks/bin/check-single-plan-pr.js +1158 -176
  47. package/dist/checks/bin/check-surface-scope.js +1217 -182
  48. package/dist/checks/bin/check-test-plan.js +1187 -189
  49. package/dist/checks/bin/check-token-collection-wired.js +1158 -176
  50. package/dist/checks/bin/check-token-report.js +1171 -179
  51. package/dist/checks/bin/check-workspace-escape.js +2167 -255
  52. package/dist/index.js +17322 -8709
  53. package/dist/lib/pre-push-changed-files.js +57 -0
  54. package/dist/lib/pre-push-select-tests.js +1616 -0
  55. package/package.json +4 -2
  56. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  58. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error/page.js +2 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +8 -7
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +8 -7
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +9 -8
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +9 -8
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +8 -7
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +16 -4
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +10 -8
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +8 -7
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +8 -7
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +9 -8
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__08f5pba._.js +3 -0
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1o_0tjy._.js → [root-of-the-server]__0h9yhwx._.js} +2 -2
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0053k9k._.js → [root-of-the-server]__0ls23oh._.js} +3 -2
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0puovz5._.js → [root-of-the-server]__0nye8tb._.js} +3 -2
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__18kqy4q._.js → [root-of-the-server]__0tfykr-._.js} +2 -2
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0o771t1._.js → [root-of-the-server]__0th3gyp._.js} +3 -2
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1-zcufa._.js +51 -0
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0p8q38b._.js → [root-of-the-server]__11svemm._.js} +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__02c71kv._.js → [root-of-the-server]__140m1i_._.js} +2 -2
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1bh2awk._.js +3 -0
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0112h-k._.js → [root-of-the-server]__1gjqmhq._.js} +3 -2
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1qmxez3._.js +4 -0
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1r47dr7._.js +28 -0
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  119. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01fv4fj._.js +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0gvm3og._.js → _096f46i._.js} +3 -2
  121. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0lwxg63._.js → _0af2one._.js} +3 -2
  122. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1n0cnq-._.js → _0zf4d27._.js} +45 -2
  123. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_03x_w6q._.js → _1-wo68i._.js} +3 -2
  124. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +1 -12
  125. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1wdusod._.js +3 -0
  126. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{node_modules_1vo08dj._.js → node_modules_@clerk_nextjs_dist_esm_app-router_server-actions_1x7vfhn.js} +3 -4
  127. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_next_1khj-jh._.js +3 -0
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +17 -5
  132. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3_9xytdmiv1xi.js → 25f3-jtiv8p5n.js} +4 -4
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0bn8c8v5q429o.js → 27-6ym7lp2v8o.js} +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3-6a3tinbdb-7.js → 3daxns3o--9me.js} +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/tsconfig.json +12 -0
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0z6dav-._.js +0 -51
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__13rrmhr._.js +0 -3
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__17h4sy8._.js +0 -45
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +0 -3
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_next_17sz44y._.js +0 -3
  141. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_buildManifest.js +0 -0
  142. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_clientMiddlewareManifest.js +0 -0
  143. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -26,9 +26,10 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
26
26
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
27
27
  | `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
28
28
  | `vinaya brief render <tranche> <n> --surfaces <glob,...> [--out <path>]` | Emit the twelve-section brief skeleton from the task Issue and the tree, with every mechanically-derivable section filled: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses, naming the missing section, when a section cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder. Never writes under `aeg-root/` or to the Issue — stdout, or `--out <path>`, only; a brief is pasted to the Developer, never committed. |
29
- | `vinaya task brief <tranche> <n>` | Preparation only: render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. Starts no agent under any circumstances — there is no `--agent` flag. Principal-only: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. The successor to `task dispatch` for the preparation step; `task run` is the full unattended run. |
29
+ | `vinaya task brief <tranche> <n>` \| `vinaya task brief --issue <n>` | Preparation only: render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. `--issue <n>` names a backlog Issue directly — one carrying no `vinaya/tranche:*` label — and renders its brief from that Issue's own `## Objectives`/`## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` sections, no tranche or Milestone involved. Starts no agent under any circumstances — there is no `--agent` flag. Principal-only: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. The successor to `task dispatch` for the preparation step; `task run` is the full unattended run. |
30
30
  | `vinaya task dispatch <tranche> <n> [--agent claude \| codex \| gemini] [--model <name>]` | **Deprecated** — prefer `task brief` (preparation only) or `task run` (the full unattended loop). Renders the same brief `brief render` renders, then posts it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. With `--agent`, starts the Developer through `dispatchRole` (`apps/cli/src/lib/dispatch.ts`) when that function is available; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block. `--model <name>` names the model to run, passed through to `dispatchRole` and always taking precedence over any class-derived resolution; without it, the task's own Issue rationale is read for its "Suggested agent-class" (`high`/`mid`/`fast`) and resolved through the chosen vendor's own class-to-model table, falling through to that vendor's default when neither resolves. Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. |
31
- | `vinaya task run <tranche> <n> --agent <claude \| codex \| gemini>` | One command from a planned Issue to a reviewed pull request, exactly one developer started. Composes `task brief`'s own preparation (starts no agent) with `dev-review-loop` (whose own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task) — nothing else. A brief already frozen on the Issue is reused, not re-posted; a task whose Issue refuses preparation is refused before any agent starts, with nothing posted; a frozen brief whose developer branch already has an open pull request refuses a second start. Exit `0` with the PR URL on a published, reviewed pull request; exit `1` with the exact `vinaya dev-review-loop --resume <pr>` command on a pause; exit `2` on a usage/argv error (an unrecognized flag, a missing `--agent`); exit `3` on any other failure (a refused preparation, an open-PR refusal) — never sharing `1` with a pause, so an unattended host tells the two apart from the exit code alone. The loop's own existing `--resume` flag is how a paused run continues, never a flag on this command. |
31
+ | `vinaya task run <tranche> <n> --agent <claude \| codex \| gemini>` \| `vinaya task run --issue <n> --agent …` | One command from a planned Issue to a reviewed pull request, exactly one developer started. `--issue <n>` runs a backlog Issue — no `vinaya/tranche:*` label, no Milestone — through the identical unattended path on branch `task/issue-<n>`, same brief, same loop, same gate. Composes `task brief`'s own preparation (starts no agent) with `dev-review-loop` (whose own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task) — nothing else. A brief already frozen on the Issue is reused, not re-posted; a task whose Issue refuses preparation is refused before any agent starts, with nothing posted; a frozen brief whose developer branch already has an open pull request refuses a second start. Exit `0` with the PR URL on a published, reviewed pull request; exit `1` with the exact `vinaya dev-review-loop --resume <pr>` command on a pause; exit `2` on a usage/argv error (an unrecognized flag, a missing `--agent`); exit `3` on any other failure (a refused preparation, an open-PR refusal) — never sharing `1` with a pause, so an unattended host tells the two apart from the exit code alone. The loop's own existing `--resume` flag is how a paused run continues, never a flag on this command. **Known gap:** a backlog Issue's run state (`running`/`paused`/`published`) does not yet appear on Vinaya Studio's backlog page — that reader lives in the separate `apps/vinaya-studio` checkout (attalabs monorepo, out of this repository's own Surface) and has not been updated for the `task/issue-<n>` shape; `vinaya task status --issue <n>` is the reader in the meantime. |
32
+ | `vinaya task status [<tranche> <n>] [--json]` | Every open task Issue carrying a frozen brief, its pull request, and whether its dev-review-loop is `running` (naming the driver pid, `review-validity-v1` task 7's pid record), `paused` (naming the reason from the pause record), `published` (the newest round's reviewer and security verdict effect markers both read `posted`), or has `no driver` — read from the outbox (`<outboxRoot>/dev-review-loop/<task>/`) and the forge, never from a `ps` scan and never by re-parsing posted verdict comments. Read-only: no write, no change to any loop. `vinaya task status <tranche> <n>` narrows to one task and adds the last round's held or published verdict lines plus the exact `vinaya dev-review-loop --resume <pr>` command when paused. `--json` for the enveloped machine form (schema: 1). `vinaya task status <tranche> <n> --follow` (and `--issue <n> --follow` for a backlog Issue) tails that task's per-driver log (`~/.vinaya/loops/<owner>-<repo>/<issue>.log`) live, `tail -f` style — every driver (`task run`, `dev-review-loop`) tees its own role-prefixed stream there regardless of where it was launched, so the state of any run is one command away with no path to remember. |
32
33
  | `vinaya pr create --body-file <path> --title <title> [--label ...] [--validate-only] [--json]` | Open a pull request after full brief-schema validation of the body against `vinaya.config.json`'s configured sections — never a body containing the retired `<!-- aeg:brief:start -->`/`<!-- aeg:brief:end -->` markers, refused outright: the brief lives on the task Issue's `aeg:brief:v1` comment now (`vinaya task dispatch`), never split out of or pasted into the PR body. `--validate-only` runs every gate and reports PASS without opening anything; `--json` for the enveloped machine form. |
33
34
  | `vinaya issue objectives edit <n> --add "<sentence>" \| --drop O<k> \| --replace O<k> "<sentence>" --reason "<text>"` | Rewrite a task Issue's `## Objectives` section by command — the Principal's way to change a task's scope mid-flight, findable and versioned rather than a silent hand-edit. Exactly one of `--add`/`--drop`/`--replace` plus a non-empty `--reason` is required; the edit runs through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself. `--drop` never renumbers the survivors — a drop that leaves the list non-contiguous from `O1` is refused with `objectivesOf`'s own parser message, since task 1's contiguous-from-O1 grammar and this command's never-renumber rule can genuinely contradict on a real drop, and that contradiction is reported rather than papered over. Splices the rendered section back in place, leaving every other byte of the body untouched, then posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new version hash — `k` counted on the forge at post time, never from a local file. |
34
35
  | `vinaya log flush --issue <n> \| --pr <n>` | Post a target Issue or PR's outbox (`~/.vinaya/outbox/<owner>-<repo>/<issue-or-none>.ndjson`, written by `log()`) as one or more comments, each opening with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` followed by a fenced `ndjson` block. Splits first at `run_id` boundaries — a maximal run of consecutive lines sharing one `run_id`, so an interleaved outbox never produces a range spanning a gap — then at `FORGE_COMMENT_MAX_CHARS` (65536); a single line too large to fit alone is refused by its seq, never split. `--pr <n>` resolves the Issue from that PR's body `Closes #N` line and posts on the PR instead of the Issue; a body with none is refused. Logs its own `forge_write` line (`validated` before posting, `written` with every comment id after the last post succeeds, or `refused` with gh's error) through `log()` into the same outbox before truncating, so that line rides to the next flush; the outbox is truncated only to the lines confirmed posted, and a gh failure leaves the rest untouched. `--json` for the enveloped machine form. |
@@ -46,9 +47,9 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
46
47
  | `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
47
48
  | `vinaya release [--dry-run] [--allow-any-commit]` | Run this repo's own publish sequence in one command (`apps/cli/specs/self-hosting.md`, "How the published version is produced"). Refuses unless HEAD is the default branch, the tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix. Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and a real `git push origin --tags`, so the repo's own generated pre-push hook sees the push exactly as any other push would. Afterward prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version. `--dry-run` stops after the preconditions and prints the plan; publishes nothing. |
48
49
  | `vinaya dispatch <role> --agent claude \| codex \| gemini --prompt-file <path> [--task <n> \| --pr <n>] [--round <n>] [--resume <id>] [--model <name>]` | Start a role's headless agent session, attributed: `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` are set on the CHILD's environment only, never on this process's own — a role started this way is what makes its later `vinaya` calls (the Stop hook, a nested dispatch) attributable instead of `unattributed`. The prompt is read from `--prompt-file` and sent on the child's stdin, never argv. `--model <name>` is passed to the chosen vendor through that vendor's own model flag, and the dispatch log's `model` field never records the vendor name; omitted, no model flag reaches the vendor and its own default decides, exactly as before this flag existed. A model shaped as another vendor's own (e.g. a Claude alias passed to `codex`) is refused by name before any spawn, naming the vendor and what it accepts. Before the vendor produces any output, and for any dispatch that never completes, the log records the requested value as a marked `requested:<name>` label (or `default` when none was named) — never presented as a confirmed observation. Once the vendor's own headless output actually reports which model ran (Claude's `modelUsage` object key, Gemini's `stats.models` object key(s) — Codex emits no such receipt in any event), that bare, unprefixed model name is recorded instead, even when it differs from what was requested or when no model was requested at all. Refuses by name, before any spawn, when the named vendor binary is absent from `PATH` or present but not executable. Every line this command and the loop it can drive print to the terminal is prefixed with its role (e.g. `[developer]`) and colour-coded per role, with the coordinator/loop's own lines in a distinct colour from every role — colour is applied only on a live TTY and is disabled entirely, prefix kept, when `NO_COLOR` (https://no-color.org) is set to any value. While the child runs, its raw stdout/stderr bytes are teed (never taken from the same stream the outcome parser reads) to a machine-local file under `~/.vinaya/dispatch-output/<effect-id>.log` — never inside the repo tree — whose path is printed once to this process's own stderr so an operator can `tail -f` it; a heartbeat line reporting elapsed time prints there every minute so a working agent is distinguishable from a hung one without inspecting processes by hand. A wall-time ceiling (`dispatch.timeoutMs` in config, default four hours) sends `SIGTERM` then, after a grace window, `SIGKILL`; a warning prints before the ceiling fires (at most 5 minutes ahead, sooner for a shorter configured ceiling) so a timeout is an expected, visible event rather than a silent disappearance. Records `dispatched`/`outcome_received`/`dispatch_failed` through the Vinaya Log's `dispatch` family (`apps/cli/specs/log.md`); when `--task`/`--pr` is given, flushes that outbox via `vinaya log flush` immediately after. A successful dispatch's own vendor session/thread identifier is returned as `resumeId` (printed alongside the other fields); passing that value back as `--resume <id>` on a later call swaps in that vendor's own resume invocation instead of starting a fresh session. That id is also durably recorded — never only printed — at `~/.vinaya/dispatch-resume/<owner>-<repo>|unresolved/<role>-<agent>-<issue<n>\|pr<n>\|unscoped>.json` (the repo segment keeps two different repos' own same-numbered task from overwriting each other's record on this one machine-wide home), overwritten by each dispatch's own outcome; a run that stops with a question is answered by reading the id from that file (or from the run's own printed output, if that terminal is still open) and running `vinaya dispatch <role> --agent <vendor> --resume <id> --prompt-file <answer-file>` — the same session continues from where it stopped, rather than being killed and re-dispatched fresh. There is no live channel: an unattended dispatch never blocks on a read waiting for an answer, so a stop condition always degrades to record-and-exit, and the resume path above is how it is answered afterward. |
49
- | `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` / `vinaya dev-review-loop --resume <pr> --agent … [--json]` | Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`) before `publish`. At `publish`, the two held verdicts and a `renderSummary` comment are posted, in order, each re-read back through the same extractors the merge gate uses, each idempotent across a rerun. At `pause`, one comment marked `<!-- aeg:loop:paused:<reason> -->` is posted (carrying the exact `--resume <pr>` command) and the process exits non-zero; `--resume <pr>` reads that held state and a since-posted Principal ruling off the same PR and continues. Full account: `apps/cli/specs/loop.md`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
50
+ | `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` / `vinaya dev-review-loop --resume <pr> --agent … [--json]` | `--issue <n>` is `--task <n>`'s exact synonym — the loop's own `task` field is already the Issue number, tranche or not, so a backlog Issue (no `vinaya/tranche:*` label) needs no separate flag, only the naming that matches `task run --issue <n>`. Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`) before `publish`. At `publish`, the two held verdicts and a `renderSummary` comment are posted, in order, each re-read back through the same extractors the merge gate uses, each idempotent across a rerun. At `pause`, one comment marked `<!-- aeg:loop:paused:<reason> -->` is posted (carrying the exact `--resume <pr>` command) and the process exits non-zero; `--resume <pr>` reads that held state and a since-posted Principal ruling off the same PR and continues. Full account: `apps/cli/specs/loop.md`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
50
51
 
51
- Which one lib function backs each command, and today's dated exemptions where it calls more than that one: `apps/cli/specs/surface.md`, enforced by `apps/cli/tests/surface-index.test.ts`.
52
+ The one-lib-function-per-command rule, and where a command's own dated exemption lives when it calls more than that one today: `apps/cli/specs/surface.md`. Each exempt command declares its own exemption as a `SURFACE_EXEMPTIONS` export in its own file (`apps/cli/src/lib/surface-exemption.ts`), enforced against the real source tree by `apps/cli/tests/surface-index.test.ts` — there is no central per-command listing to read instead.
52
53
 
53
54
  The typed event log's header, families and outbox: `apps/cli/specs/log.md`.
54
55
 
@@ -51,11 +51,12 @@ The task **Issue's `aeg:brief:v1` comment** — posted once, frozen, by `vinaya
51
51
 
52
52
  ## The contract — field-by-field mapping
53
53
 
54
- Every field below has exactly one named obligation for the Developer (right column). The left column names where the field lives before dispatch: either a section the Planner wrote directly onto the task Issue at plan time (the eight-field rationale, the `## Objectives` block, and the four judgment sections `## Surface` / `## Parts` / `## Test plan` / `## Stop conditions` — all Issue sections, not brief prose), or a section the dispatch act (`vinaya task dispatch`, via `renderBrief`) fills mechanically from the forge and the tree at dispatch time. A brief missing any right-column obligation is malformed — the dispatch act's own render refuses on a gap rather than emitting an incomplete brief. A Developer who drops a right-column obligation is executing incorrectly.
54
+ Every field below has exactly one named obligation for the Developer (right column). The left column names where the field lives before dispatch: either a section the Planner wrote directly onto the task Issue at plan time (the eight-field rationale, the `## Objectives` block, and the five judgment sections `## Surface` / `## Parts` / `## Test plan` / `## Stop conditions` / `## Documentation` — all Issue sections, not brief prose), or a section the dispatch act (`vinaya task dispatch`, via `renderBrief`) fills mechanically from the forge and the tree at dispatch time. A brief missing any right-column obligation is malformed — the dispatch act's own render refuses on a gap rather than emitting an incomplete brief. A Developer who drops a right-column obligation is executing incorrectly.
55
55
 
56
56
  | Issue section / dispatch-act render | Developer consumes at | What the consumption means |
57
57
  |---|---|---|
58
58
  | **`## Objectives`** (the Issue's section: numbered `O<n>. <sentence>` lines, one observable outcome each) | The brief's own rendered `## Objectives` section, cited by at least one numbered Part each | The dispatch act's render copies the Issue's `## Objectives` section into the brief byte-for-byte and cites each `O<n>` from at least one numbered Part. `checkObjectivesCopy`/`checkObjectivesCoverage` (`@attalabs/aeg-core`) enforce both directions at `verify-brief`/`brief-shape` — a copy that drifts from the Issue's section, an `O<n>` no Part cites, or a Part citing an `O<n>` that doesn't exist all refuse. |
59
+ | **`## Documentation`** (the Issue's section: `- <source> — <mechanism it governs> (O<n>)` bullets, or the `None` sentinel) | Fetched/read before Step 0 (entry-gate item 8, `roles/developer.md`); mechanism and supported runtime/protocol version recorded in the PR body's Decisions section | Copied into the rendered brief verbatim, right after Objectives, so it is read before the Developer holds a complete plan rather than skimmed as appendix evidence after the fact. At least one source, when any are real, must cite a defined `O<n>` — `checkDocumentationCitesObjective` (`@attalabs/aeg-core`) refuses a brief where none does, so the Reviewer's ordinary Objectives grading covers whether the cited source's mechanism/version was actually incorporated — never left to compete on salience alone with nothing checking it. For a URL-shaped source, a `PostToolUse`/`Stop` hook pair enforces that it was fetched at all; this is a mechanical gate the Developer cannot talk past, never the Developer's own attestation. |
59
60
  | **Worktree Step 0** (rendered verbatim `git worktree add` command, from the task's forge-derived branch id) | First action before any other command | The Developer must execute this exact command first. No exceptions. Never assume the right branch exists. Before executing it, the Developer independently re-verifies the branch-name suffix literal-matches the task's forge-derived id — the same check the dispatch act already ran before rendering the command. |
60
61
  | **`Tier:` field** (rendered from the Issue's tier declaration) | PR-open checklist + `vinaya/tier:*` label | The Developer sets the matching `vinaya/tier:*` label on the Issue at PR open. The field is binding; the label is the scannable projection. |
61
62
  | **`Project:` field** (rendered from the Issue's `Project(s) + blast radius`) | PR description + `verify-docs` | The Developer confirms the project resolves against `.vinaya/projects.md`. |
@@ -83,6 +84,7 @@ Every field below has exactly one named obligation for the Developer (right colu
83
84
  - The surface map must be bounded and named. "Wherever else turns out to need it" is not a surface map.
84
85
  - **The documentation-update list must be populated from reading, not memory.** The Planner's Dig (`roles/planner.md`) must identify and read any relevant specs/skills/docs before cutting the Issue. The list for Tier 1+ must be non-empty unless the surface map genuinely touches no documented surface (state "No doc updates required" explicitly in that case).
85
86
  - **A brief with a real code surface must carry a `Premise:` block pinning at least one checkable fact inside that surface.** A Tier 0 brief with zero code/runtime surface has nothing to pin.
87
+ - **`## Documentation` names normative sources, never internal doc-owners pointers.** Distinct from the documentation-update list above (which names this repo's OWN doctrine to keep coherent): `## Documentation` names the OUTSIDE sources the implementation must match — an SDK/protocol page, a certification guide — against the mechanism each governs, sourced from the same Dig, never invented at render time. A task with no such source states the explicit `None` sentinel; a task that does have one names at least one Objective it feeds (a trailing `(O<n>)` citation) so the obligation is graded by the Reviewer's ordinary Objectives pass rather than left as prose nobody checks — `checkDocumentationCitesObjective` refuses a real source with no citation.
86
88
 
87
89
  ## Task-status coherence precondition — hard STOP before authoring or executing any task
88
90
 
@@ -119,6 +121,7 @@ The dispatch act's enforcement is the same forge-derived check the Developer run
119
121
  - **Branch-ID verification precondition (hard STOP before step 0).** Before executing step 0, confirm via the forge that the Step 0 branch-name suffix literal-matches the task's forge-derived id — character for character, no added prefix, no case change, no truncation. If it doesn't, STOP: do not create the worktree/branch, report the mismatch to the Planner/Principal rather than silently using either name. This is the same check the dispatch act already ran before rendering the command — the Developer re-runs it independently rather than trusting the render was correct. This gate is enforced in `aeg-root/roles/developer.md` (entry gate, item 6).
120
122
  - **Mechanized precondition check.** The three preceding preconditions (row-existence, Issue-existence, and the prior-tranche-archival check in `roles/developer.md` entry gate item 5) are all re-derivable in one run: `vinaya check dispatch-readiness`, run from the task branch. A `NOT READY` result names the exact failing precondition and is the same STOP described above. **Known gap:** the shipped check's prior-tranche-archival predicate always reports empty — confirm item 5 yourself rather than trusting its pass. (The prior-archival/row-adjacency precondition previously listed here was removed from this composed check.) **On this repo's toolchain**, the unabridged derivation (including the real prior-tranche-archival predicate) is `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>`.
121
123
  - **Premise re-check (hard STOP before step 0).** If the brief carries a `Premise:` block, re-assert it before step 0 by confirming its stated facts still hold against the live forge/codebase. A failed premise means the surface moved since the Issue's rationale was written — STOP and re-dig, do not proceed on a stale mental model. **On this repo's toolchain**, `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --premise <body-file>` (the body-file being the dispatched brief text) automates that re-assertion.
124
+ - **Documentation sources, read before step 0 (entry-gate item 8, `roles/developer.md`).** Fetch/read every source the brief's `## Documentation` section names, before step 0. Record the mechanism confirmed and the specific supported runtime/protocol version in the PR body's Decisions section — a real value read off the source, or an explicit `unverifiable: <why>` marker when the host genuinely cannot confirm one, never an invented number. For a URL-shaped source this is not self-attested: a `PostToolUse` hook records every `WebFetch` call and a `Stop` hook refuses to end the turn while one remains unfetched.
122
125
  - Read the full brief before opening the worktree. Not a skim — every section.
123
126
  - Execute step 0 first, always. Never branch from `HEAD` of the current local checkout.
124
127
  - Verify all dependencies are merged before the first line of code.
@@ -0,0 +1,86 @@
1
+ ---
2
+ sidebar_title: Planner → Operator
3
+ title: Planner → Operator
4
+ order: 8.6
5
+ contract_id: planner-operator
6
+ description: The Operator runs what the Planner cut — the Planner makes a task dispatchable, the Operator operates it, and every scope or strategy change routes back to the Planner, never through the Operator.
7
+ status: active
8
+ producer: planner
9
+ consumer: operator
10
+ carrier: dispatchable-issue, escalation-redirect
11
+ summary: Ever let the seat that runs the work quietly become the seat that re-scopes it?
12
+ ---
13
+ # Contract: Planner → Operator
14
+
15
+ ## The short version
16
+
17
+ This seam sits between the seat that **decides what a task is** (the Planner, across its plan and dispatch acts) and the seat that **runs it** (the Operator). It exists because running a task and re-scoping one look adjacent from the run button, and they are not: the Operator operates what the Planner already made ready, and the moment the work needs its scope, criteria, or approach changed, that is the Planner's again — never the Operator's to edit in flight.
18
+
19
+ **What crosses, downward** — a task the Planner has made dispatchable: its Issue cut, its dependencies merged, no conflicting sibling open, its brief renderable. The Planner's dispatch act is what turns a planned Issue into a startable run; the Operator picks it up from there and operates it through the controller.
20
+
21
+ **What crosses, upward** — a scope or strategy question. When a run reveals that the plan is wrong, the boundary is wrong, or the approach must change, the Operator does not edit the Issue or improvise a new plan. It routes the question back to the Planner (`severity: strategy` for approach, `severity: execution` for a missing detail), and waits for a re-plan or a redispatch.
22
+
23
+ **The hand-off is malformed when** — the Operator is handed a task that is *not* dispatchable (no Issue, an unmerged dependency, an open conflicting sibling) and asked to run it anyway, or when the Operator edits the plan, the Issue, or the criteria instead of routing the change back. Either way the run seat has taken on planning authority it does not hold.
24
+
25
+ **What it does not carry** — the power to move a task between tranches, re-size it, or rewrite its criteria. Those are scoping decisions, and they belong to planning. The Operator surfaces the need; the Planner exercises the authority.
26
+
27
+ **How it physically runs** — downward, the carrier is the dispatchable Issue itself, made ready by the Planner's dispatch act and confirmed at the Operator's own entry gate. Upward, the carrier is an escalation whose severity routes it to the Planner. Starting the run is a controller call against an already-ready task, not a status write.
28
+
29
+ ---
30
+
31
+ ## Reference
32
+
33
+ **Status:** active
34
+ **Seam:** the hand-off between the Planner (producer of dispatchable tasks and re-plans) and the Operator (consumer, running them).
35
+ **Single source of truth for this seam.** `roles/planner.md`'s delegation section and `roles/operator.md` point to this file; neither redefines the boundary alone. AEG terms (seam, tranche, brief, dispatch, gate) are defined in the [glossary](../glossary.md).
36
+
37
+ ---
38
+
39
+ ## Why this file exists
40
+
41
+ The Operator holds the run button, and from there re-scoping a task feels like part of running it. It is not. If the Operator could edit an Issue's criteria or reshape its plan mid-run, the plan would stop being a durable, reviewed decision and become whatever the run seat found convenient — the exact drift the plan/execute split exists to prevent. This contract draws the line: the Planner makes a task ready and owns every change to what it is; the Operator runs what it was handed and routes every scope change back.
42
+
43
+ ---
44
+
45
+ ## The hand-off carrier
46
+
47
+ Two carriers, one per direction:
48
+
49
+ 1. **Downward — the dispatchable Issue.** The Planner's dispatch act confirms the task's gates (Issue exists, dependencies merged, no open conflicting sibling, render complete) and makes the run startable. The Operator confirms the same gates at its own entry gate before starting — the readiness is a fact to verify, never an assumption to make.
50
+ 2. **Upward — the severity-routed escalation.** A scope or approach change the run reveals is raised as an escalation the Planner receives, not an edit the Operator performs.
51
+
52
+ ---
53
+
54
+ ## The contract — what each may ask of the other
55
+
56
+ | The Planner may ask the Operator to… | The Operator may ask the Planner to… |
57
+ |---|---|
58
+ | Run a task the dispatch act has made dispatchable | Re-plan or re-scope a task whose plan a run proved wrong (`severity: strategy`) |
59
+ | Operate the run it cut — start, follow status, present escalations, request continuation or cancellation | Supply a missing execution detail the brief did not anticipate (`severity: execution`) |
60
+ | Stop or cancel a run the plan has superseded | Cut or make dispatchable a task that is not yet ready, rather than running a task that is not |
61
+ | — (the Planner never asks the Operator to author the plan) | Edit the Issue or its criteria on the Operator's behalf — the Operator never edits it directly |
62
+
63
+ **The Planner may NOT ask the Operator to** author or amend a plan, write the brief, or make a task dispatchable — cutting the Issue and rendering the brief are the Planner's own acts.
64
+
65
+ **The Operator may NOT ask the Planner to** bless it editing the Issue itself — the Operator surfaces the change; the Planner makes it.
66
+
67
+ ---
68
+
69
+ ## Producer obligations (the Planner)
70
+
71
+ - Hand the Operator only a **dispatchable** task — Issue cut, dependencies merged, no open conflicting sibling, brief renderable. Name the Operator as the seat that runs what the dispatch act cut (see `roles/planner.md`'s delegation section).
72
+ - Receive a scope or strategy escalation and re-plan or redispatch, rather than expecting the Operator to work around a wrong plan.
73
+
74
+ ## Consumer obligations (the Operator)
75
+
76
+ - Confirm dispatch readiness at the entry gate before starting; refuse to run a task that is not yet ready.
77
+ - Route every scope, criteria, or approach change back to the Planner as an escalation — never edit the Issue or the plan directly.
78
+ - Operate the run the Planner cut; do not author a parallel plan, re-size the task, or move it between tranches.
79
+
80
+ ---
81
+
82
+ ## Changing this contract
83
+
84
+ This file is the seam. Change it and both role docs' references as one unit; `roles/planner.md`'s delegation section and `roles/operator.md` point here rather than each restating the boundary.
85
+
86
+ *This contract is the seam. The Planner fills the left column with dispatchable work; the Operator drains it and raises scope changes on the right. One source of truth, changed as a unit.*
@@ -0,0 +1,88 @@
1
+ ---
2
+ sidebar_title: Principal → Operator
3
+ title: Principal → Operator
4
+ order: 8.5
5
+ contract_id: principal-operator
6
+ description: Carries process authority down and content decisions up — the Principal tells the Operator which planned task to run, pause, or stop; the Operator brings the Principal the escalations only the Principal may rule.
7
+ status: active
8
+ producer: principal
9
+ consumer: operator
10
+ carrier: task-tool-grant, escalation-packet
11
+ summary: Ever handed someone the run button and then wondered who was supposed to make the call it stopped for?
12
+ ---
13
+ # Contract: Principal → Operator
14
+
15
+ ## The short version
16
+
17
+ This seam sits between the seat that decides **what is true and what is allowed** (the Principal) and the seat that **runs a selected task through the controller** (the Operator). It exists because the Operator has real process authority — start, pause, resume, cancel — and no content authority at all, so the two directions of the ask must be named or the Operator improvises the decisions it was never granted.
18
+
19
+ **What crosses, downward** — the Principal names which already-planned task the Operator should run, and may ask it to pause, resume, or cancel one. That is the whole of what the Principal delegates: process, over an already-dispatchable task. The Principal never asks the Operator to plan it, size it, edit its Issue, or change its criteria — none of those are the Operator's to do.
20
+
21
+ **What crosses, upward** — the Operator brings the Principal every escalation packet the controller addressed to the Principal: an escalation, a round cap reached, repeated findings with no progress, an unresolved confidence question, a reappeared finding. The Operator presents the packet as recorded and asks for a ruling, an approval, or a merge. It never supplies the decision itself.
22
+
23
+ **The hand-off is malformed when** — the Operator is asked to exercise content or ratification authority (rule, approve, publish a review, merge, edit an Issue, re-scope), or when a Principal-addressed escalation is cleared by the Operator rather than presented. Either way the boundary between process and content authority has been crossed, and the seam's whole purpose is to make that crossing visible and refused.
24
+
25
+ **What it does not carry** — a duration. The Principal may ask "what state is it in?"; the answer is derived and durationless. "When will it be done?" has no grounded answer on this seam, and the Operator supplies none.
26
+
27
+ **How it physically runs** — downward, the carrier is the Operator's tool grant: the five task tools plus the status-follow read, and nothing that could rule, approve, or merge. Upward, the carrier is the persisted escalation packet, whose `requestedAuthority` field names the Principal as the seat that must decide. Neither direction is a status write — the run's branch, pull request, and pause record are the status, read rather than restated.
28
+
29
+ ---
30
+
31
+ ## Reference
32
+
33
+ **Status:** active
34
+ **Seam:** the hand-off between the Principal (producer of process delegation and content rulings) and the Operator (consumer, running the selected task).
35
+ **Single source of truth for this seam.** `roles/principal.md` and `roles/operator.md` do not redefine what crosses here — they point to this file. AEG terms (seam, ratification, escalation, forge) are defined in the [glossary](../glossary.md).
36
+
37
+ ---
38
+
39
+ ## Why this file exists
40
+
41
+ The Operator seat was, for a period, a set of tools with no role text: any session that found the task tools improvised the authority around them. The failure mode is specific — a seat with the run button and no instructions decides, on its own, the calls it was only ever meant to *stop for*. This contract names the two directions so the improvisation has nowhere to hide: what the Principal may delegate downward is bounded to process over an already-planned task, and what must come back upward is every decision the packet addresses to the Principal.
42
+
43
+ ---
44
+
45
+ ## The hand-off carrier
46
+
47
+ Two carriers, one per direction:
48
+
49
+ 1. **Downward — the tool grant.** The Operator holds `task_start`, `task_status`, `task_escalation_read`, `task_resume`, `task_cancel`, and the status-follow read. The grant is the delegation: it is exactly the process authority the Principal hands down, and it contains no tool that could rule, approve, publish a review, merge, or edit an Issue. The router refuses any call outside it, so the delegation cannot silently widen.
50
+ 2. **Upward — the escalation packet.** A paused run's persisted packet carries a `requestedAuthority` field. When it names the Principal, the Operator presents that packet — reason, inputs, held evidence, attempted recovery, and permitted next actions, verbatim — and waits for the Principal's ruling, approval, or merge.
51
+
52
+ ---
53
+
54
+ ## The contract — what each may ask of the other
55
+
56
+ | The Principal may ask the Operator to… | The Operator may ask the Principal to… |
57
+ |---|---|
58
+ | Run a specific already-planned, dispatchable task (`task_start`, or the `task run` composition) | Rule on an escalation the packet addresses to the Principal |
59
+ | Read a task's grounded status, or follow it (`task_status`, status-follow read) | Approve or merge — the ratification acts the Operator structurally cannot perform |
60
+ | Present a paused run's escalation packet (`task_escalation_read`) | Resolve a Principal-authority pause (round cap, no-progress, confidence, reappearance) with a decision, not a retry |
61
+ | Request continuation or cancellation of a run (`task_resume`, `task_cancel`) | Address a scope or criteria change to the Planner — the Principal redirects it there, as the Operator cannot edit the Issue |
62
+
63
+ **The Principal may NOT ask the Operator to** plan or size a task, edit its Issue or criteria, write code, approve or publish a review, merge, or state how long a run will take — the Operator has no grant for any of these, and asking does not create one.
64
+
65
+ **The Operator may NOT ask the Principal to** hand it a tool outside its grant, or to bless it ruling on a packet itself — the Operator presents; the Principal decides.
66
+
67
+ ---
68
+
69
+ ## Producer obligations (the Principal)
70
+
71
+ - Delegate only process authority over an **already-planned, dispatchable** task — never ask the Operator to bring a task into existence.
72
+ - Make the ruling, approval, or merge when an escalation packet is presented — the packet is a decision request, and the Operator cannot make it.
73
+ - Redirect a scope or criteria change to the Planner rather than asking the Operator to edit the Issue.
74
+
75
+ ## Consumer obligations (the Operator)
76
+
77
+ - Confirm the task is already dispatchable before running it; refuse to plan it into readiness.
78
+ - Present every Principal-addressed escalation packet as recorded, `requestedAuthority` intact, and wait — never rule, approve, or merge to clear it.
79
+ - Keep every request inside the grant; when a task needs authority the grant lacks, name the seat that holds it rather than improvising past the refusal.
80
+ - Attach no duration to any status.
81
+
82
+ ---
83
+
84
+ ## Changing this contract
85
+
86
+ This file is the seam. Change it and both role docs' references as one unit; neither `roles/principal.md` nor `roles/operator.md` may redefine the boundary on its own.
87
+
88
+ *This contract is the seam. The Principal delegates the left column and decides the right; the Operator drains the left and raises the right. One source of truth, changed as a unit.*
@@ -76,8 +76,8 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
76
76
 
77
77
  **The two rules a gate author meets before adding a check:**
78
78
 
79
- 1. **Decide from parsed structure, never from prose.** A gate that must decide whether a task touches a domain, resolves a path, or crosses a boundary decides from a field a writer fills in a fixed grammar — a glob list, a numbered citation, a table row — never from scanning prose for a phrase that would mean the right thing if read charitably. Prose can name a thing to include it or to exclude it, and a scanning gate cannot tell the two apart; a heuristic built to guess which one a sentence meant is wrong in both directions, and a blocking gate that is wrong in either direction is worse than one that runs less often. Where the parsed structure does not yet exist for an artifact (an Issue below the cutover that first mandates a `## Surface`), the gate degrades to the old prose scan for that artifact only — it neither invents structure nobody asked for, nor extends the prose scan's blast radius to cover stock that predates it. `issue-validation.ts`'s `checkBlastRadiusScope` is the worked example: cutover-gated, it decides from `## Surface`'s `in:` glob list once one is mandatory, never again from `Boundary`/`Project(s) + blast radius` prose, whatever that prose says — naming a shared package to explicitly *exclude* it no longer trips a gate that used to read prose span-blind to intent.
80
- 2. **One fact, one implementation, resolved once and passed down.** A fact two gates must agree on — whether a glob resolves to a real tracked file, whether a domain falls under a path — is computed by one function and passed to every caller that needs the answer; it is never re-derived by a second hand-written implementation that merely happens to agree with the first today. Two implementations of the same predicate are a race the moment either one changes: the day they diverge is invisible until an artifact one side accepts and the other refuses reaches the seam between them, and by then neither implementation is provably the bug. Inject the shared function into whichever side cannot compute it itself — a pure, `fs`-free module (`issue-validation.ts`) takes the answer as an injected parameter (a `(glob: string) => boolean`, a resolved list); the impure caller that already computes it (`forge-write.ts`, injecting `brief-assembly.ts`'s own `expandGlob`) is the one and only place that does, so the authoring gate and the brief renderer read the identical resolution and can never disagree about whether a Surface resolves.
79
+ 1. **Decide from parsed structure, never from prose.** A gate that must decide whether a task touches a domain, resolves a path, or crosses a boundary decides from a field a writer fills in a fixed grammar — a glob list, a numbered citation, a table row — never from scanning prose for a phrase that would mean the right thing if read charitably. Prose can name a thing to include it or to exclude it, and a scanning gate cannot tell the two apart; a heuristic built to guess which one a sentence meant is wrong in both directions, and a blocking gate that is wrong in either direction is worse than one that runs less often. Where the parsed structure does not yet exist for an artifact (an Issue below the cutover that first mandates a `## Surface`), the gate degrades to the old prose scan for that artifact only — it neither invents structure nobody asked for, nor extends the prose scan's blast radius to cover stock that predates it. `checkBlastRadiusScope` is the worked example: cutover-gated, it decides from `## Surface`'s `in:` glob list once one is mandatory, never again from `Boundary`/`Project(s) + blast radius` prose, whatever that prose says — naming a shared package to explicitly *exclude* it no longer trips a gate that used to read prose span-blind to intent.
80
+ 2. **One fact, one implementation, resolved once and passed down.** A fact two gates must agree on — whether a glob resolves to a real tracked file, whether a domain falls under a path — is computed by one function and passed to every caller that needs the answer; it is never re-derived by a second hand-written implementation that merely happens to agree with the first today. Two implementations of the same predicate are a race the moment either one changes: the day they diverge is invisible until an artifact one side accepts and the other refuses reaches the seam between them, and by then neither implementation is provably the bug. Inject the shared function into whichever side cannot compute it itself — a pure, filesystem-free module takes the answer as an injected parameter (a `(glob: string) => boolean`, a resolved list); the impure caller that already computes it (the forge-write path, injecting the brief renderer's own glob expansion) is the one and only place that does, so the authoring gate and the brief renderer read the identical resolution and can never disagree about whether a Surface resolves.
81
81
 
82
82
  **The own-PR fixture rule.** A pull request that adds a check which reads a PR body ships a fixture test running that check over **that PR's own body**. A body-reading check is the one class whose real input exists at the moment the PR opens and is never exercised by a synthetic fixture the author also wrote: the author's fixture agrees with the author's mental model by construction, and the first real body it meets is the one it was supposed to grade. Running it over the body in hand costs one test and converts "it should work on a real body" from a belief into a passing assertion.
83
83
 
@@ -137,7 +137,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
137
137
  | Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | repo-own | `.github/workflows/ci.yml` |
138
138
  | Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | States where formatting/naming conventions stand in this repo: currently unenforced. | **Nothing re-checks lint/format or commit-message grammar in this repo today** — the managed hooks run only the `vinaya check` suite, and the one CI workflow that builds the code (`ci.yml`, the implementation named here; its typecheck + unit-test substance belongs to the `Typecheck + unit tests` row) carries no lint or commitlint step. Those conventions are discipline, not a gate, until a step is added. | repo-own | `.github/workflows/ci.yml` |
139
139
  | AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Requires independent review verdicts to exist on every pull request before merge. | No automated reviewer workflow is installed in this repo — review passes are separate, fresh-context dispatched agent sessions that post verdict comments on the PR. `vinaya-review-verdict.yml` evaluates a landed verdict comment (only principal-allowlisted authors count — the same verdict-author verification rule the Review gate applies) and re-runs the required review-gate check so it can go green natively. Until clean verdicts exist, the required check stays red by design. | repo-own | `.github/workflows/vinaya-review-verdict.yml` |
140
- | Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **A verdict is also bound to the newest PRINCIPAL RULING on the PR** (`review-validity-v1` task 3): the winning comment's `Ruling ordinal: <k>` line (`0` when the PR carried no ruling at cast time — this line renders UNCONDITIONALLY, unlike the objectives-version line) must equal `input.rulingOrdinal`, the newest ruling ordinal `check-review-gate.ts` counts off the PR's own principal-authored `<!-- aeg:principal:ruling:<pr>-<k> -->` comments — a ruling posted after a verdict was cast no longer counts as clean, even when its head and objectives bindings still hold, and the gate names the newer ruling in its failure message. Unlike `objectivesVersion`, there is no `null`-means-skip input value here — a PR's ruling count is never ambiguous — but a verdict predating this feature (no `Ruling ordinal:` line at all) still binds when the PR's newest ruling ordinal is `0`, the same "nothing to bind against" case expressed as an equality rather than a skip. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
140
+ | Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **A verdict is also bound to the newest PRINCIPAL RULING on the PR** (`review-validity-v1` task 3): the winning comment's `Ruling ordinal: <k>` line (`0` when the PR carried no ruling at cast time — this line renders UNCONDITIONALLY, unlike the objectives-version line) must equal `input.rulingOrdinal`, the newest ruling ordinal `check-review-gate.ts` counts off the PR's own principal-authored `<!-- aeg:principal:ruling:<pr>-<k> -->` comments — a ruling posted after a verdict was cast no longer counts as clean, even when its head and objectives bindings still hold, and the gate names the newer ruling in its failure message. Unlike `objectivesVersion`, there is no `null`-means-skip input value here — a PR's ruling count is never ambiguous — but a verdict predating this feature (no `Ruling ordinal:` line at all) still binds when the PR's newest ruling ordinal is `0`, the same "nothing to bind against" case expressed as an equality rather than a skip. **Every binding above is now ONE typed manifest, compared by ONE function** (`review-validity-v1` task 4): one `ReviewInputManifest` — head, the frozen brief's own hash, objectives version, newest ruling ordinal, and the effective review policy's digest — is built by the driver before reviewers are dispatched and rendered into every verdict/escalation comment; `compareManifest` is the SAME comparison `checkReviewGate` and the dev-review-loop's own publication self-check both call, so no field binds on one side and is ignored on the other. Two more bindings ride this manifest: **a verdict is also bound to the FROZEN BRIEF it was judged against** — the winning comment's `Brief hash:` line (a `sha256` of the brief's posted content) must equal `input.briefHash` (optional, defaulting to `null` — skip — for every caller predating this field); a Planner superseding the frozen brief after a verdict was cast voids it the same way an objectives edit does. **A verdict is also bound to the REVIEW POLICY in effect when it was cast** — the winning comment's `Policy digest:` line (rendered unconditionally, right after `Ruling ordinal:`) must equal the current policy's digest; unlike the other bindings above, a `null` echo (a comment predating this line, or one stripped of it) is never grandfathered — a policy is always resolvable, so there is no genuine "nothing to bind against" case on the current side the way an unset objectives list or a ruling-free PR has, and a blanket forever-exempt reading of a missing line would let it silently outlive a real threshold change (found live during `review-validity-v1` task 4's own review). A digest that no longer matches a since-changed policy, missing or present, invalidates the verdict the same way a newer ruling does. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
141
141
  | G1 — implementation exists | Ever read about a safeguard that turned out not to actually exist? | ci | Re-checks that every gate the doctrine describes has real code behind it. | Every ring's non-empty `implementation` cell on this very page resolves to a real path on disk — makes this page's registry columns load-bearing instead of decorative. | product | `packages/aeg-core/bin/verify-registry.ts` |
142
142
  | G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, `apps/cli/src/checks/bin/*.ts` is named by some row's `implementation`, OR — **twin-form recognition (O14)** — resolves, through the same `claimedCheckNames` derivation G6 uses, to a `coreCheckRegistry()` name some OTHER row's `implementation` already claims: the same enforcement mechanism routinely ships as both a `packages/aeg-core/bin/*.ts` standalone form and an `apps/cli/src/checks/bin/check-*.ts` CLI-registered form, and a row's single `implementation` cell can only name one — the other is not a second undocumented fact, it is the first fact read through its other physical shape. A `NON_GATE_BINS`-listed aeg-core bin (a forge writer, a one-shot reporter, an eval harness — `report-tokens.ts`, `eval-agent-compliance.ts`, …) and a listed non-gate hook script (`.claude/hooks/track-transcript.sh`, real Stop-hook plumbing feeding two already-documented mechanisms' own inputs, with no pass/fail decision of its own) implement no enforcement mechanism to be named for — the honest fix for those is that this gate stops asking, never an invented row (`.husky/*` matches nothing in this repo today — that directory does not exist here). **Blocking as of this task** (re-graded from report-only: the twin-form gap above was G2's entire non-scaffold-placeholder backlog, and a 0-finding G2 run means something again now that it can fail). **Scaffold-writer** (task 22, later widened to cover that second location): `verify-registry.ts --scaffold` auto-inserts a stub row for an orphan candidate whose ring is mechanically derivable — a `packages/aeg-core/bin/*.ts` file resolving through `GATE_AUDIENCE` to a registered check (its ring comes from that check's own `apps/cli/src/checks/registry.ts` entry), an `apps/cli/src/checks/bin/*.ts` file whose `check-`-stripped name resolves through the analogous `CLI_CHECK_RING` mirror table, or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered bin under either directory) gets no stub and stays a plain finding — the writer never guesses. A second, independent half of G2 scans every row's `summary`/`description`/`spec` cells for the literal placeholder marker `[undocumented — fill in why]` the scaffold writes into every non-mechanical cell: a stub row that filled only `implementation` would otherwise read as "documented" to the orphan half above and silence G2 outright, which is the exact trap this second half exists to close. The writer only ever appends a new row after a ring table's last existing row — it never edits, reorders, or removes one, so a hand-authored row (including `workspace-escape`'s and `main-branch-refusal`'s own) is untouched. Repo-side only: the writer lives on `verify-registry.ts` (aeg-core), never on the shipped `check-registry-gates.ts` (apps/cli) — an adopter's checks stay read-only over doctrine. | product | `packages/aeg-core/bin/verify-registry.ts` |
143
143
  | G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-1 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
@@ -154,7 +154,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
154
154
  | quoted-command | Ever read a doc that quoted a command as "what runs today", and the command changed underneath it? | ci | Re-checks that a doc's explicitly marked quote of a command or config line still matches, verbatim, the file it names as its source. | Marker-based only, by explicit Principal decision, not inferred from command-looking spans: a doc opts a span in with `<!-- AEG:QUOTES-FILE:START:<path> -->…<!-- AEG:QUOTES-FILE:END -->` (the same invisible-on-render, code-blind HTML-comment idiom `anchored-region.ts` uses for its own seven PR/Issue-body fields, reusing that file's masking primitive rather than a second parser — but a distinct marker kind, since the cited path is free text, not one of that file's seven closed field names). An adopter-facing command in a README with no marker is instruction, not a claim, and is never flagged — inference over command-looking spans was explicitly rejected as the false-positive shape that gets a gate disabled. `findCitedQuotes` sweeps the identical `ships`/`reader-facing` corpus `reader-resolvable-prose` sweeps (never a second notion of "governed doc"); `evaluateCitedQuotes` then checks each marked span against its cited file's live content, wherever that file lives in the repo, naming both sides in a finding — what the doc claims, and which file no longer contains it verbatim. Zero I/O in `@attalabs/aeg-core`'s pure predicate (`quoted-command.ts`), corpus-tested against the real `aeg-root/**` tree; the bin supplies the only I/O. Ships with one real, live annotation (this very paragraph's own sibling callout above, quoting the CI invocation this page names) rather than only synthetic fixtures. Written after a measured incident: a CI invocation was pinned to a version in one pull request; this page quoted the pre-pin form verbatim as current fact; every registered check passed, `doctor` reported healthy, security passed, and a code-review agent found the drift only after independent re-derivation. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape`/`changeset-coverage` above — graduating to a blocking check is a later, separately-dispatched decision once the false-positive rate is observed against real adopter corpora; v1 carries no waiver escape. | product | `apps/cli/src/checks/bin/check-quoted-command.ts` |
155
155
  | token-report | Ever seen a cost figure nobody could trace to a source? | ci | Re-checks that a PR's "Token report" section actually carries real numeric Tokens in/out figures, on any host this repo can prove is metering-capable. | The presence and shape of the `## Token report` block `roles/developer.md` requires every self-metering role to paste at turn-end — never whether the reported figures are TRUE, which CI structurally cannot recompute (the same bounded honesty this page's `evidence-fresh` row states for its own Group B). Runs `resolveMeteringCapability` (task 1's probe, `@attalabs/aeg-core`) fresh in THIS process; an incapable verdict (any reason) passes silently — the sanctioned operator-metered case, same discipline `token-collection-wired` above already applies. **Ring derived mechanically as `1`, not chosen** (a 2026-08-29 amendment correcting the original brief rationale's now-retracted ring-0/1 instruction): `requiresOpenPr: true` makes this check CI-only by `CoreCheckRing`'s own derivation rule, since the PR body it reads does not exist before a PR does. A probe that itself fails to run (an unexpected `exists`/`readFile` throw the probe does not catch) is never read as a clean incapable verdict: the bin lets it propagate uncaught, which the runner surfaces as `status: 'error'`, distinct from the `status: 'pass'` a real incapable verdict produces. **A ledger row must exist, not merely be well-formed when present** (O13): before this task, `!capability.capable` returned a silent pass BEFORE checking whether the body carried a "Token report" row at all, so a merged task on an incapable host could leave the ledger with no row at all for its own spend — a silent hole, never a stated `—`. The row-existence check now runs unconditionally (any capability), and only the numeric-cell shape check stays capability-gated — `tranche-model.md` §12's operator-metered case writes `—` in a row; it never omits the section. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
156
156
  | Bare code-fact digits in a PR body | Ever had a PR body cite a line number that drifted the moment the file changed? | ci | Re-checks that a pull request's narrative prose carries no bare `<path>.<ext>:<digits>` code-fact pointer outside a fenced code span or a `Premise:` pin. | A bare digit anywhere in the body outside a fenced/indented code block or a documented anchor (`Closes`/`Project`/`Tier`/`Evidence`) — an Issue/PR ref, a date, a version, a path, a section number, a pass count, a duration, all need their own backticks or a fenced block, never bare prose. Reports every violation the body carries in one pass, not only the first (O12) — a mechanically-fixable body is corrected in one round, not one round-trip per digit. Its own workflow (`ownWorkflow: true`), `pull_request_target`-triggered only, never `pull_request` — a live-fetched author/branch check a `pull_request`-triggered run of this same file could otherwise spoof via the PR's own workflow YAML. | product | `apps/cli/src/checks/bin/check-body-bare-digits.ts` |
157
- | Evidence-block freshness | Ever seen a PR's own "tests pass" claim go stale the moment someone pushed again? | ci | Re-checks that a PR's `AEG:EVIDENCE` block still matches a fresh recompute at the PR's current head. | Group A (the diff stat) is recomputed exactly and byte-compared — a hand-typed or stale diff stat cannot survive this. Group B (the real gate run) and Group C (the `[agent]` fenced command list `vinaya pr report` runs) are checked for staleness against the current head, never re-run here — this closes fabrication for the two facts a checker can cheaply recompute, never for the Decisions section's prose. Resolves the real PR head via `gh pr view --json headRefOid`, never `HEAD` (the merge commit in CI). | product | `apps/cli/src/checks/bin/check-evidence-fresh.ts` |
157
+ | Evidence-block freshness | Ever seen a PR's own "tests pass" claim go stale the moment someone pushed again? | ci | Re-checks that a PR's Evidence block still matches a fresh recompute at the PR's current head. | Group A (the diff stat) is recomputed exactly and byte-compared — a hand-typed or stale diff stat cannot survive this. Group B (the real gate run) and Group C (the `[agent]` fenced command list `vinaya pr report` runs) are checked for staleness against the current head, never re-run here — this closes fabrication for the two facts a checker can cheaply recompute, never for the Decisions section's prose. Resolves the real PR head via `gh pr view --json headRefOid`, never `HEAD` (the merge commit in CI). | product | `apps/cli/src/checks/bin/check-evidence-fresh.ts` |
158
158
  | Documentation gate (PR open/edit) | Ever opened a PR, watched the doc-coverage gate pass, then pushed a second commit that broke it? | ci | Re-checks C5 doc-coverage — the SAME code→doc binding the push-time row above enforces — again at PR create/edit time, not only on push. | Every code change matching a `.vinaya/doc-owners` binding carries its owning doc in the same diff, a `Doc-ack:`/`Doc-neutral:` declaration, or an actor-verified `vinaya/waiver:docs` label, resolved live via `gh` from `PR_NUMBER` (fixing a prior silent-unreachable label lookup in an adopter's CI, where the label was never actually forwarded to this check). `roles/developer.md` names this as one of two chokepoints "at every push... and again at pull-request creation and editing" — this row is the second, previously undocumented despite already shipping and already running from `vinaya pr create`'s own pre-flight. | product | `apps/cli/src/checks/bin/check-doc-coverage.ts` |
159
159
  | Surface-scope (out-of-boundary file) | Ever had a "small" task quietly touch a file its own plan said it wouldn't? | ci | Re-checks that a task branch's changed files stay inside its own Issue's declared `## Surface` — never inside a declared `out:` glob. | Every file in the branch's diff against `origin/main` is checked against the task's own Issue (resolved from the branch name → forge topology → Issue number → `## Surface` `out:` list) with the same `globCoversPath` predicate the Issue-authoring gate and the blast-radius check (O4) both already use — an undeclared boundary crossing is caught mechanically, naming the file and the glob it crosses, rather than depending on a reviewer noticing. Same check runs at the `git push` hook (ring 0) and again here — same runner, one codebase, two enforcement points. Dormant (never blocks) when the branch is not a task branch, the forge/repo cannot be resolved or reached, the task's row carries no Issue yet, the Issue's `## Surface` doesn't parse (below the brief-sections cutover, or malformed — caught at authoring time instead), or the Issue declares no `out:` globs at all. | product | `apps/cli/src/checks/bin/check-surface-scope.ts` |
160
160
  | PR-body premise reassertion | Ever had a brief's pinned fact quietly go stale, and the merge never noticed? | ci | Re-checks, in CI, that a pull request body's `Premise:` pins still hold against the PR's own current tree — not only at Step 0, authoring time. | Every `contains`/`absent`/`sha256` pin `parsePremiseBlock` extracts from the PR body, re-asserted via the same frozen `checkPremises`/`reassertPremiseFile` (`apps/cli/src/checks/premise-reassert-logic.ts`) `verify-dispatch --premise` already uses at Step 0 — a second caller, not a second implementation, so a pin the PR's own diff falsifies fails CI instead of merging as decoration. Trigger is the block's presence alone: a body with no `Premise:` header parses to zero pins and this check is silent, on every branch — there is no branch-name condition anywhere in it. | product | `apps/cli/src/checks/bin/check-pr-premise-reassert.ts` |
@@ -88,12 +88,13 @@ Before writing any code, validate the following — and refuse if any fails:
88
88
  5. **Prior-tranche-archival precondition.** Before opening a PR against any product, confirm each product named in the brief's `Project:` field has its previous tranche archived. For each product, check whether a prior tranche for that product has an open Milestone (forge-native) — or, for a tranche still carrying a pre-cutover topology file, exists in `aeg-root/tranches/` but NOT in `aeg-root/tranches/completed/` (legacy exception; a forge-native tranche carries no such file to check). If any such unarchived tranche exists and all its task PRs are merged, the Tranche Archivist has not run. STOP: *"Product `<X>`'s previous tranche `<name>` is complete but not archived — the Tranche Archivist must run before new work on this product. Dispatch it first."* If there is no prior tranche on a product, this gate passes trivially. The contract governing this gate is `aeg-root/contracts/tranche-archivist-planner.md`.
89
89
  6. **Branch-ID verification (hard STOP before step 0).** Before executing step 0, confirm via the forge (`vinaya/tranche:<slug>`-labeled Issue titled `[<slug>] <n> — …`, and its Milestone) — not `aeg-root/tranches/<name>.md` — that the branch-name suffix in the Step 0 command you were just handed literal-matches this task's forge-derived id `<n>` — character for character: no added prefix, no case change, no truncation. If it doesn't: *"The Step 0 branch name `task/<tranche>/<X>` doesn't match this task's topology ID `<Y>` — STOP, do not create the worktree/branch; report the mismatch to the Planner/Principal rather than silently using either name."* Do not begin work.
90
90
  7. **Row-existence precondition (hard STOP before step 0).** Before executing step 0, confirm via the forge (`vinaya/tranche:<slug>`-labeled Issue titled `[<slug>] <n> — …`, and its Milestone) — not `aeg-root/tranches/<name>.md` — that this task's row exists **at all**. This is distinct from and prior to item 3's `#TBD`/blank check: a missing row means the plan/Issue for this task has not merged/opened yet, and there is nothing to inspect — no Issue, no dependencies, no `Project(s)` value. If the row is absent: STOP: *"Task <id> is not present in tranche `<name>`'s forge-derived task list (no `vinaya/tranche:<name>`-labeled Issue with this task id yet) — the plan/Issue for this task hasn't merged/opened. Not dispatchable until it does."* Do not begin work.
91
+ 8. **Documentation sources, read before step 0.** Before executing step 0, fetch every source named in the brief's own `## Documentation` section — each one fetched (a URL) or read (an in-repo path) in full, not skimmed from its own summary. For each, record two facts (in the PR body's Decisions section at open — see [§ PR body — canonical form](#pr-body--canonical-form)): the mechanism you confirmed it governs, and the specific supported runtime/protocol version that source states — a real value read off the page (as `[task-operator-v1] 2` recorded `2.1.197`, verified live), never copied from training-data memory and never invented. When the host you're running on genuinely cannot confirm a version (the source doesn't state one, or nothing on this host can check it live), record that explicitly — `unverifiable: <why>` — rather than a guessed number; an explicit unverifiable marker is honest, a plausible-looking invented one is not. This is never your own judgement call to skip: for every URL-shaped source, the driver mechanically records whether your session actually fetched it, and refuses to let your turn end while one remains unfetched — an honest miss is always caught, and you never get to decide you read "enough." **What this does not do:** it is a mechanical backstop against an inattentive skip, not a sandboxed guarantee against a session that deliberately tampers with the record files it reads (round 2 security review, CRITICAL) — this dispatch's own Bash access reaches the same files the hooks trust, the same trust model every other self-reported artifact in this contract already carries (your confidence line, your token report, your test output: reviewed and re-verified independently, never sandboxed against you). Fetching the source honestly is always the faster path; do that, and the gate clears itself. A `## Documentation` section carrying only the `None` sentinel, or only in-repo paths, owes nothing to this gate — an in-repo path's own read is never mechanically observed this way, so read it anyway; the obligation is the same, only the enforcement differs. **On this repo's toolchain**, this is a `PostToolUse` hook (matcher `WebFetch`) plus a `Stop` hook wired into the dispatched session's own settings (`apps/cli/src/lib/dispatch.ts`'s `writeDispatchSettings`) — the Stop hook exits 2, which on Claude Code "prevents Claude from stopping, continues the conversation," naming every unfetched source; see this repo's own `apps/cli/specs/loop.md`, "The Documentation read-gate," for the mechanism's wiring. On another host, satisfy the paragraph above by whatever means that host offers for observing a fetch and blocking a stop — reusing this exact hook shape is not required.
91
92
 
92
93
  **Mechanized version of items 3, 5, and 7.** Items 3, 5, and 7 above (Issue-existence, prior-tranche-archival, row-existence) are all re-derivable in one run: `vinaya check dispatch-readiness`, run from the task branch against a freshly-fetched `origin/main` and the live forge, before step 0. A `NOT READY` result names the exact failing predicate and is the same STOP each item above describes — read the printed blocker rather than re-deriving the fact by hand. **Known gap:** the shipped check's prior-tranche-archival predicate always reports empty (a narrower parity gap than the full derivation below) — do not treat its pass as covering item 5; confirm item 5 yourself. The prose above remains the *why* (what each precondition means, and the manual `gh`/`jq` fallback if the tool is ever unavailable); item 6 (branch-ID verification) is a static check against the brief's own Step 0 text, not a mechanized command, and stays manual. Item 4 is superseded and no longer part of this composed check. **This gate now also runs mechanically** (task 25) — the `first-push-dispatch` check, wired into every adopter's generated CI and managed `.git/hooks/pre-push`, invokes the same derivation on a task branch's first push, before its PR exists — but running it yourself before step 0 remains the cheaper, earlier catch: the hook fires only at push time, after you've already done the work. **On this repo's toolchain**, the underlying, unabridged derivation (including the real prior-tranche-archival predicate) is `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>` — prefer it here over the shipped check's narrower parity.
93
94
 
94
95
  If the brief carries a `Premise:` block, also re-assert it before step 0: confirm by hand that the brief's stated facts still hold against the live forge/codebase (a stale premise means the surface moved since the brief was rendered — STOP and re-dig; see `aeg-root/contracts/planner-developer.md`). **On this repo's toolchain**, `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --premise <body-file>` (the body-file being the dispatched brief text) automates that re-assertion.
95
96
 
96
- Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 text against that same forge-derived id. You never write status anywhere — opening your branch and PR *is* the status.
97
+ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 text against that same forge-derived id. Item 8 is enforced mechanically by the driver's own hooks, never by your own attestation — see that item's own text. You never write status anywhere — opening your branch and PR *is* the status.
97
98
 
98
99
  ---
99
100
 
@@ -109,6 +110,8 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
109
110
 
110
111
  **Commit per Part, push once.** Commit after each numbered Part in the brief — small, one logical change per commit, so the history reads as a narrative of how you approached the problem. Push exactly once, immediately before `pr create` — not after every commit. The pre-push hook itself runs the affected test suite (`bunx turbo test --affected`) on that one push and refuses it on failure; you do not additionally run it yourself per Part. The affected suite covers only the packages `turbo`'s own dependency graph marks affected by this diff — a rule about one package's files that lives in another package's test never runs on a push that only touches the first package (found live: a CLI-only diff never marked `aeg-core` affected, so a rule about CLI files, asserted only in an `aeg-core` test, never ran at the push hook at all). A rule meant to bind a package's own files belongs in a `vinaya check`, not in a sibling package's test suite. A regeneration or evidence run (`vinaya pr report --write`/`--push`) passes `--force` to its own test invocation. A verdict binds to the head it judged; a push landing after the newest verdict's judged head voids it and is named as such: `vinaya review status` prints `push after verdict — re-review required`, and merge waits on a fresh review round.
111
112
 
113
+ **Push and PR-open are foreground, never backgrounded.** A dispatched turn cannot run a shell command in the background at all: `vinaya dispatch` wires a permission deny rule into the session's settings for exactly this (Bash's own `run_in_background: true`), so the tool call itself is refused, with the refusal text naming the foreground alternative — this is enforcement, not a convention you are trusted to follow unprompted. The push and the `pr create` (or `pr edit`) call that opens or updates the pull request are foreground steps, not something you fire and end your turn on. Run each to completion and confirm it: your turn does not end until `gh pr view` on that branch shows the pull request. A turn that ends with the push or the PR-open still running in the background — or with commits made but never pushed — is a contract violation, not a completed turn; the driver treats it as incomplete and resumes you, once, with the exact commands to finish (found live, twice: a backgrounded `git push` that reported "running in the background" died with the session, leaving only local commits, while the loop polled for a pull request that no process was ever going to open). If the branch is behind the base in a way that conflicts, merge or rebase and resolve before pushing — never push, or end a turn, with an unresolved conflict against the base; a resume that finds you here names the conflicting files.
114
+
112
115
  **Opening the PR with a complete description.** The PR description must (1) **carry the report only** — the brief itself never rides in this body at all; it is already posted, frozen, as the task Issue's `aeg:brief:v1` comment by `vinaya task dispatch`, before your worktree even exists, and that comment is the brief's permanent, durable home — the Reviewer and Archivist read it there; (2) follow the canonical form in [§ PR body — canonical form](#pr-body--canonical-form) below — that section holds the verbatim copy-pasteable template, including the **exact `Tier:` field syntax** the `verify-docs` gate requires; (3) reference the task's Issue (`Closes #N`) so the merge auto-closes it. The description is not optional — the reviews depend on it. Opening the PR is itself the `in-flight → in-review` transition; you write no status field. **The body is authored once, at open.** After the PR is open, you never hand-edit it again — not to append a response to a review round, not to record a decision, not for any reason. Two writes are sanctioned after open, both machine-regenerated, never typed: the Evidence block, and one appended row in the Token report for a re-entry turn (see [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed)). Everything else a review round produces — your response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment.
113
116
 
114
117
  **Reporting exact tokens in the PR body at turn-end.** You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — no role writes its own ledger row on a task branch, and parallel Developer sessions on different tasks have collided appending to the same shared file. Instead, before opening the PR (and again before each `changes-requested → in-review` re-push), report your exact tokens in the PR body under a **"Token report"** heading: `Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date` with `Phase: <task-id>: develop` and `Role: Developer`. **That destination and that grammar are the requirement, and they are the same on every agent host.**
@@ -201,15 +204,11 @@ The `AEG:EVIDENCE` block is populated by running `vinaya pr report --write <body
201
204
 
202
205
  `check-evidence-fresh` (CI) refuses a body whose block doesn't match the head it's attached to — recomputing Group A exactly and checking Group B for staleness. This closes fabrication for **Group A only** (a hand-typed diff stat cannot survive a byte-compare); Group B is checked for freshness, not re-run, so a stale-but-not-fabricated Group B slips past unless the block is also out of date. Do not claim in this PR's own Evidence section, or anywhere else, that this closes fabrication generally — it closes it for the two facts a checker can cheaply recompute, never for the Decisions section's prose.
203
206
 
204
- **Regeneration is one command, run last, after every other change.** The Developer never hand-edits the live PR body after open there is no local body file to keep in sync with the forge. When a push forces the Evidence block to go stale or a re-entry turn needs its one appended Token report row — run, from the repo root, after every commit for that round is already pushed:
205
-
206
- `vinaya pr report --push <n>`
207
+ **After open, regenerating `AEG:EVIDENCE` is the driver's job, not yours.** Before this fix, a push-forced-stale Evidence block was the Developer's own turn to re-close: run `vinaya pr report --push <n>` and wait for it, a re-run of the real gate suite that could take past ten minutes and stall the whole loop on the Developer's single tool call. That command still exists the underlying engine module both `vinaya pr report --push` and the driver's own in-process call now share, a command never calling a command but you no longer run it. Once your turn ends at the push (below), the driver runs the SAME engine function itself, in-process, the moment the head's CI turns green, and posts the round marker comment in your place too. You never see a stale Evidence block to fix, because you never reach for the command that used to fix it.
207
208
 
208
- **On this repo's toolchain**, substitute `bun apps/cli/src/index.ts` for `vinaya` in that command.
209
+ **`--push <n> --body-file <path>` still exists as a narrower, separate mode** for the one case a routine splice cannot cover: a section outside the two generated blocks (a Decisions bullet, most often) that only ever existed in a local draft, never yet posted. It does not relax "the body is authored once, at open" above — reaching for it to restate the routine splice's own job is scope creep, not a shortcut. This mode is still yours to run by hand if you ever need it; the driver's own automatic call never uses `--body-file`.
209
210
 
210
- It fetches the PR's live body itself, splices the fresh `AEG:EVIDENCE`/`AEG:TOKENS` content into it through the same anchor resolver `--write` uses, pushes the result via the forge's own PR-edit, then re-reads the live body and refusesrestoring the pre-edit body unless the two agree outside those two anchored regions. A `[principal]` tick, if one landed since this turn started, is a live-body write only this command's own fetch-then-splice sequence carries forward correctly; it refuses rather than appending when the live body carries no real `AEG:EVIDENCE` pair at all.
211
-
212
- After open the Developer changes nothing outside the `AEG:EVIDENCE` anchor and one appended `AEG:TOKENS` row. The Principal's `[principal]` ticks are the Principal's writes and must survive every Developer edit. Everything else a review round produces — the response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment, never a body edit.
211
+ After open, the Developer changes nothing in the PR body at all the driver's own automatic call regenerates `AEG:EVIDENCE` every round; the Principal's `[principal]` ticks are the Principal's writes and must survive every edit. **`AEG:TOKENS` is deliberately untouched by the driver's automatic call:** the driver runs in its own session, not yours, so a token row it collected would misattribute the driver's own usage to your `<task-id>: develop` phase. Token reporting for a re-entry round therefore has no automatic mechanism right now a known, accepted gap this fix does not close, not a silent one: if you need a re-entry round's tokens recorded, run `vinaya pr report --write <body-file>` yourself and hand-splice the result, or ask the Principal to record it, rather than treating the missing row as this fix's oversight. Everything else a review round produces the response to findings, any disclosure the brief didn't anticipate is a PR comment, never a body edit.
213
212
 
214
213
  ---
215
214
 
@@ -265,16 +264,18 @@ A spike is exploratory, not a permanent excuse to skip documentation. The pull r
265
264
 
266
265
  ---
267
266
 
268
- ## After you open the PR — the post-open sequence
267
+ ## After you open the PR — your turn ends here
268
+
269
+ Opening the PR is the end of your turn — for round 1, and for every later round too: after you push a fix in response to review findings, your turn ends at that push, on the same branch, no new PR. You do not run `vinaya pr report --push`, you do not post a round comment, and you do not tick anything.
269
270
 
270
- Opening the PR is not the end of your turn; it is the point at which the rest of your turn becomes runnable. The `[agent]` half of your Test Plan is no longer something you run by hand and paste it is a fenced command list in §9 that `vinaya pr report` itself executes from the PR head and writes into the `AEG:EVIDENCE` block (task 12; Principal ruling: an agent never ticks a box or edits a PR body). What remains for you to do runs now, in this order, and the order is load-bearing:
271
+ This is a change from before. It used to take four more steps merge main if behind, regenerate the Evidence block, post a `Head: <sha>` comment carrying the round marker, tick nothing and the second of those, a full re-run of the real gate suite, could take past ten minutes on a real Test Plan and stall the whole loop waiting on your one tool call to finish. None of those four steps are yours any more:
271
272
 
272
- 1. **Merge the main branch first if you are behind it.** A branch behind its base is judged against a base nobody will merge into. `vinaya review status <pr>` prints that distance as a second line reading `behind main by <n> — merge first`, and exits non-zero unless the loop is converging at a branch that is not behind. Merge, push, and only then continue — the head your evidence names must be the head your reviewer will read.
273
- 2. **Regenerate the Evidence block** — one command, `vinaya pr report --push <n>`, never a hand edit. This runs the real gates (Group B) AND every `[agent]` command in your §9 fenced list (Group C), writing each command's actual output into `AEG:EVIDENCE`. This is the only sanctioned write into the body after open.
274
- 3. **Post one comment, headed `Head: <sha>`, carrying the round marker `<!-- aeg:developer:round-<n> -->` and your `Tokens:` line.** One comment per round, never an edit to the one already there. No Test Plan output belongs in this comment any more it lives in the Evidence block `pr report` just wrote. The marker is what makes the round machine-readable for the round-derivation logic that still reads it.
275
- 4. **Tick nothing.** There is no `[agent]` checkbox left to tick the fenced command list has no box at all. **Never tick a `[principal]` box.** You structurally cannot satisfy one, and the asymmetry is the point.
273
+ - **The `AEG:EVIDENCE` block** is regenerated by the driver itself, in-process, the moment your head's required CI turns green the same engine module `vinaya pr report --push` always called, now also called directly by the loop's own driver rather than shelled out to as a subprocess.
274
+ - **The round marker comment** — `Head: <sha>`, `<!-- aeg:developer:round-<n> -->`, and (starting from round 2) the ids of the findings you addressed is composed and posted by the driver too, from a small side-channel file you write before your turn ends: see the next paragraph.
275
+ - **A branch behind its base** is caught by the driver's own mergeability check before it ever dispatches a reviewer or runs the report you never need to check this yourself; a conflicting head is sent back to you with the conflicting files named, same as before.
276
+ - **Ticking `[agent]`/`[principal]` boxes** was never yours to begin with (task 12; Principal ruling: an agent never ticks a box or edits a PR body) nothing changes there.
276
277
 
277
- Step 1 is not optional and not reorderable: there is no path through this sequence that reaches a `Head:` comment while the branch is behind. The evidence in that comment is a claim about a head, and a head that is about to be superseded by a merge you have not done yet is the wrong head to make it about.
278
+ **Citing which findings you addressed, from round 2 on.** When your resumed turn is sent back to fix review findings, write a `FINDING_IDS:` line the same comma-separated grammar the reviewer's own `report.txt` already uses — to `.vinaya-round-response` at the root of your worktree, naming the ids (e.g. `F1`, `F2`) you addressed this round, before your turn ends. The driver reads and clears this file itself once your push lands and the head goes green, and folds it into the round comment it composes. This file is best-effort by design: if you forget it, or your turn crashes before writing it, the driver still posts the round comment — with no citation line — and the round is never judged stalled for want of one. You are never resumed a second time just to write it.
278
279
 
279
280
  Then stop. Review is a separate invocation.
280
281
 
@@ -309,6 +310,8 @@ Escalate with the appropriate severity — a manual escalation note, or, if you
309
310
 
310
311
  The brief's stop conditions tell you when to STOP and ask. Honor them. If the stop conditions say "STOP if you discover X" and you discover X, you stop. You do not improvise a workaround.
311
312
 
313
+ **Refusing or escalating before you have ever pushed.** The entry-gate refusals in this doc, and a stop condition hit before your first commit, happen before a branch or pull request exists — there is no PR yet to comment on, and an unattended loop has nothing else to read but the task Issue. In that case only, post your refusal or escalation as a comment on the task Issue itself, with `<!-- aeg:developer:stop -->` as the comment's own first line, followed by your reason. This is what lets an automation layer end the run at once rather than wait out a full poll budget for a pull request you were never going to open. Once you have pushed at least once, escalate normally — a PR exists, and every later escalation goes there per your automation layer's own request-input mechanism, never this marker.
314
+
312
315
  ---
313
316
 
314
317
  ## Stop conditions
@@ -411,7 +414,7 @@ Items 1–4 are also composed into one command, `bun packages/aeg-core/bin/verif
411
414
 
412
415
  The checks above are **static**: they prove the change compiles, lints, types and matches its declared surface. They do not prove the feature works. Verification is the separate, mandatory phase that runs the brief's Test Plan against a booted app, after the review passes and before the Principal merges.
413
416
 
414
- **It is a phase, not an actor.** There is no Verifier to dispatch. The plan splits by who can structurally execute it: `vinaya pr report` runs the `[agent]` half's fenced command list from the PR head and writes it into `AEG:EVIDENCE`; the Principal runs the `[principal]` half in a real signed-in browser and ticks its boxes. Both halves must be satisfied before a merge is allowed — the `[agent]` half by the Evidence block existing and matching a fresh recompute (`evidence-fresh`), the `[principal]` half by every unticked `[principal]` box in the PR body, which `test-plan` refuses a merge while any remain unticked.
417
+ **It is a phase, not an actor.** There is no Verifier to dispatch. The plan splits by who can structurally execute it: `vinaya pr report` runs the `[agent]` half's fenced command list from the PR head and writes it into `AEG:EVIDENCE`; the Principal runs the `[principal]` half in a real signed-in browser and ticks its boxes. Both halves must be satisfied before a merge is allowed — the `[agent]` half by the Evidence block existing and matching a fresh recompute (`evidence-fresh`), the `[principal]` half by every unticked `[principal]` box in the PR body, which `review-gate` refuses a merge while any remain unticked — `test-plan` grades the `[agent]` half and the plan's structure only; it is `principalOwed`, so its own `pending` failure never blocks the loop's mechanical gate, and enforcement of the unticked box lives at `review-gate` instead.
415
418
 
416
419
  **Why it exists:** four consecutive features once merged with green CI and were broken at runtime — a missing migration, a missing environment variable, a missing provider, an unexecuted test plan. The static gates ran and passed; the reviews read the diff; nobody booted the app. Verification is the phase that closes that gap.
417
420
 
@@ -424,13 +427,17 @@ The checks above are **static**: they prove the change compiles, lints, types an
424
427
 
425
428
  If the brief declares `unit-tests-only` and the diff really is pure logic, the phase is satisfied by the unit-test gate; record that as the outcome.
426
429
 
427
- ### The `[agent]` half — yours
430
+ ### The `[agent]` half — under the loop, the driver's; standalone, yours
431
+
432
+ **Under the automated dev-review loop, this already ran.** The driver's own per-round evidence report (see [§ After you open the PR — your turn ends here](#after-you-open-the-pr--your-turn-ends-here)) executes the SAME §9 fenced command list, from the SAME PR head, into the SAME `AEG:EVIDENCE` block, the moment your head's CI turns green — every round, automatically. You do not separately run this phase; by the time review finishes, it has already run.
433
+
434
+ **If you are working outside the loop** — dispatched by hand, with no driver watching this PR — the phase is still yours to run explicitly:
428
435
 
429
436
  1. **Boot the app(s)** named in the brief from the worktree, and wait until each is reachable, if your §9 fenced commands need one running. If it does not boot, that is the failure — the plan never gets a chance to run.
430
437
  2. **Run `vinaya pr report --push <n>`.** It executes every line in your §9 fenced command list from the PR head and writes each command plus its actual output into `AEG:EVIDENCE` — never a hand-pasted comment, never a checkbox tick. Round-tripping through prose is how falsely-passing claims slip through; a command this tool did not run is not evidence. **Accepted risk, Principal default:** `pr report --push` executes the PR's own §9 commands on the machine running it, with no check of who is running it — only the PR's author runs it; nothing enforces that today.
431
438
  3. **Stop there.** Do not execute `[principal]` items; you structurally cannot. Mark them as awaiting the Principal.
432
439
 
433
- A failed `[agent]` command makes the PR unmergeable (`vinaya pr report`'s own exit code reflects it, and `evidence-fresh` binds the recorded output to the PR head). Fix on the same branch and re-run `vinaya pr report --push <n>` — it overwrites the block with fresh output, never appends a second copy.
440
+ A failed `[agent]` command makes the PR unmergeable (`vinaya pr report`'s own exit code reflects it, and `evidence-fresh` binds the recorded output to the PR head). Fix on the same branch under the loop, the next round's own automatic report overwrites the block with fresh output; standalone, re-run `vinaya pr report --push <n>` yourself either way it overwrites, never appends a second copy.
434
441
 
435
442
  ### The `[principal]` half — not yours
436
443