@attalabs/vinaya 0.24.1 → 0.26.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 (130) hide show
  1. package/README.md +26 -3
  2. package/aeg-root/contracts/architect-planner.md +5 -5
  3. package/aeg-root/contracts/developer-reviewer.md +10 -7
  4. package/aeg-root/contracts/planner-developer.md +142 -0
  5. package/aeg-root/contracts/reviewer-archivist.md +2 -2
  6. package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
  7. package/aeg-root/enforcement.md +25 -11
  8. package/aeg-root/process.md +53 -75
  9. package/aeg-root/roles/archivist.md +3 -3
  10. package/aeg-root/roles/developer.md +30 -24
  11. package/aeg-root/roles/planner.md +100 -21
  12. package/aeg-root/roles/principal.md +14 -11
  13. package/aeg-root/roles/reviewer.md +33 -17
  14. package/aeg-root/roles/security.md +28 -13
  15. package/aeg-root/roles/tranche-archivist.md +2 -2
  16. package/aeg-root/skills/aeg/SKILL.md +5 -5
  17. package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
  18. package/aeg-root/state-machine.md +36 -35
  19. package/aeg-root/task-model.md +3 -3
  20. package/aeg-root/templates/brief-template.md +22 -6
  21. package/aeg-root/templates/issue-rationale-template.md +35 -2
  22. package/aeg-root/templates/pr-report-template.md +5 -13
  23. package/aeg-root/tranche-model.md +21 -21
  24. package/dist/checks/bin/check-body-bare-digits.js +684 -142
  25. package/dist/checks/bin/check-branch-topology.js +741 -88
  26. package/dist/checks/bin/check-brief-shape.js +708 -83
  27. package/dist/checks/bin/check-changeset-coverage.js +1445 -149
  28. package/dist/checks/bin/check-closes-n.js +741 -88
  29. package/dist/checks/bin/check-coherence.js +808 -149
  30. package/dist/checks/bin/check-dead-branch-push.js +617 -81
  31. package/dist/checks/bin/check-dispatch-readiness.js +811 -152
  32. package/dist/checks/bin/check-doc-coverage-push.js +1445 -149
  33. package/dist/checks/bin/check-doc-coverage.js +1447 -151
  34. package/dist/checks/bin/check-doctrine-no-procedures.js +684 -142
  35. package/dist/checks/bin/check-doctrine-portability.js +1445 -149
  36. package/dist/checks/bin/check-evidence-fresh.js +684 -142
  37. package/dist/checks/bin/check-exec-bits.js +1445 -149
  38. package/dist/checks/bin/check-first-push-dispatch.js +808 -149
  39. package/dist/checks/bin/check-issue-assignment.js +741 -88
  40. package/dist/checks/bin/check-main-branch-refusal.js +618 -82
  41. package/dist/checks/bin/check-no-disk-state.js +617 -81
  42. package/dist/checks/bin/check-pr-premise-reassert.js +5401 -0
  43. package/dist/checks/bin/check-pr-report-density.js +617 -81
  44. package/dist/checks/bin/check-quoted-command.js +1443 -147
  45. package/dist/checks/bin/check-reader-resolvable-prose.js +1481 -153
  46. package/dist/checks/bin/check-registry-gates.js +659 -85
  47. package/dist/checks/bin/check-retired-vocabulary.js +1443 -147
  48. package/dist/checks/bin/check-review-gate.js +758 -145
  49. package/dist/checks/bin/check-single-plan-pr.js +617 -81
  50. package/dist/checks/bin/check-surface-scope.js +5722 -0
  51. package/dist/checks/bin/check-test-plan.js +617 -81
  52. package/dist/checks/bin/check-token-collection-wired.js +617 -81
  53. package/dist/checks/bin/check-token-report.js +629 -88
  54. package/dist/checks/bin/check-workspace-escape.js +1443 -147
  55. package/dist/index.js +8548 -4335
  56. package/package.json +1 -1
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  61. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  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/api/coherence/route.js.nft.json +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  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.nft.json +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  97. 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
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1pndh3_._.js → [root-of-the-server]__0p8q38b._.js} +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_topbar_index_tsx_1h1gs1y._.js +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  116. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
  117. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
  119. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
  121. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  122. package/aeg-root/contracts/brief-developer.md +0 -141
  123. package/aeg-root/contracts/planner-brief.md +0 -130
  124. package/aeg-root/roles/brief-author.md +0 -114
  125. package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
  126. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
  127. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  128. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
  129. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
  130. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
@@ -74,6 +74,11 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
74
74
 
75
75
  **The fixed-position rule.** A gate reads its signal from a fixed position — a named anchor, an HTML-comment marker, a line-anchored field, a bounded read window — never by scanning free text for a phrase that means the right thing. Free text is caller-controlled: whoever writes the artifact can put a signal-shaped sentence anywhere in it, including inside a quotation of somebody else's, and a scanning gate cannot tell a cast verdict from a mention of one. Fixing the position is what closes that: a caller-supplied field rendered outside the window the gate reads can no longer reach a position the gate treats as structural, however the writer formats it.
76
76
 
77
+ **The two rules a gate author meets before adding a check:**
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.
81
+
77
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.
78
83
 
79
84
  **The `Audience` column, read once for all three tables below:** `product` — this row's implementation is a named check the product registers, so it ships to every adopter through `vinaya check`. `repo-own` — everything else: a mechanism specific to how this repository enforces itself on top of the product — a hand-written CI job, the check runner itself, a forge-write command, or a check this repo runs against its own doctrine tree but has deliberately not registered as a named, adopter-facing check. A row carrying no `Audience` cell at all (older doctrine, or an un-upgraded adopter copy) reads as `repo-own` — the safe default, since it makes no shipped claim to verify. G6, below, blocks CI on any `product` row whose implementation does not actually resolve in that registration.
@@ -90,7 +95,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
90
95
  | Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. **The brief itself never reaches this description** — the Developer still pastes it into the body-file's `## Reference` section, but `vinaya pr create` splits that section out before the body ever reaches `gh`, posting it as its own comment marked `<!-- aeg:brief -->`, so the description carries the report (summary, test plan, evidence, scope) only. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. **`vinaya pr create` also runs every registry check declaring `PR_BODY` over the body before it ever reaches the forge (task 12)** — `brief-shape`, `pr-report-density`, `doc-coverage`, and every other `PR_BODY`-declaring check, with `PR_NUMBER` explicitly unset (no PR exists yet) and `localOnly: true` skipping the `requiresOpenPr` set outright. The same set CI's `vinaya-checks.yml` runs against the live body, so a body that opens here is a body that passes there too. Skipped entirely when `rings.ring1_forgeWriteInterception` is `true` — the same accelerator that already skips `validateForgeWrite`'s config-driven sections. | repo-own | `packages/aeg-core/bin/open-pr.ts` |
91
96
  | Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no registry row for anything to resolve against. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | repo-own | `packages/aeg-core/bin/open-issue.ts` |
92
97
  | Merging | Ever had a broken build get merged anyway? | event | Holds the merge on the forge side while anything the gates check is still failing. | **Required review-gate check + branch protection** | No agent-side merge hook is installed in this repo. The merge gate is ring 1: the required `review-gate` check (`vinaya check review-gate`; a clean code-reviewer APPROVE and a clean security-review PASS verdict from principal-allowlisted authors AND at least one reported, green non-review check-run for the PR's current head, or a principal-applied, actor-verified `vinaya/waiver:review` label) plus branch protection marking that check required. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
93
- | Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. Optionally (task 10, `PREMISE_FILE=<brief.md> vinaya check dispatch-readiness`), also re-asserts a local brief's `Premise:` pins against current on-disk state and fails on any pin that no longer holds. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. A parsed `depends-on` edge that points back at its own task is refused as an INTERNAL parser-bug error, never reported as an unmerged dependency: a self-dependency is unsatisfiable by construction, so it can only mean the edge text or the rationale parser produced something impossible, and the ordinary unmerged-dependency wording reads as a legitimate gate state an agent is invited to route around rather than escalate. The `coherence` D1 check refuses the same shape the same way. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
98
+ | Starting the Planner's dispatch act (before dispatching a task's brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. Optionally (task 10, `PREMISE_FILE=<brief.md> vinaya check dispatch-readiness`), also re-asserts a local brief's `Premise:` pins against current on-disk state and fails on any pin that no longer holds. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `roles/planner.md` § The dispatch act; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. A parsed `depends-on` edge that points back at its own task is refused as an INTERNAL parser-bug error, never reported as an unmerged dependency: a self-dependency is unsatisfiable by construction, so it can only mean the edge text or the rationale parser produced something impossible, and the ordinary unmerged-dependency wording reads as a legitimate gate state an agent is invited to route around rather than escalate. The `coherence` D1 check refuses the same shape the same way. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
94
99
  | Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | repo-own | `packages/aeg-core/src/brief-validation.ts` |
95
100
  | Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | repo-own | `packages/aeg-core/bin/verify-task.ts` |
96
101
  | Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. Every core check in `registry.ts` carries an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@attalabs/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | repo-own | `apps/cli/src/checks/runner.ts` |
@@ -109,7 +114,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
109
114
 
110
115
  **Gate-read field recognition, specifically** (task 30): every gate-read PR-body field (`Tier:`, `Project:`, `Closes #N`, the `Premise:` block, the Test Plan section) is anchor-recognized where present — an `AEG:<FIELD>` HTML-comment pair (`packages/aeg-core/src/anchored-region.ts`) is read exclusively when it exists, so identical-looking text elsewhere in the body (a pasted reference brief, a quoted example) can never masquerade as the field — with the original prose/heading recognition as the compatibility fallback for every body that carries no anchors.
111
116
 
112
- **Issue self-assignment at first push, specifically** (task 33): the pre-push hook carries one deliberate non-gate side effect — on a `task/*/*` branch's genuinely first push (the remote ref is being created; its pre-push remote-sha is all zeros), `packages/aeg-core/bin/assign-task-issue.ts` assigns the task's Issue to the authenticated `gh` user, i.e. the actual pusher. This mechanizes the signal Studio's dispatch-visibility chip reads (task 26: `assigned = assigneesCount > 0`), which until this task was populated only when the Principal remembered to assign by hand (live-fire gap: task 28 showed no chip despite two real pushed commits). Deliberately **assignment-at-first-push, not at brief-authoring** — a written-but-undispatched brief is not "in flight", and assigning earlier would reintroduce the false positive task 26 was built to avoid. Idempotent (an already-assigned Issue is a no-op, so a deleted-and-recreated branch cannot double-assign; a force-push to an existing branch never re-triggers at all) and **fail-open by contract**: it runs last, after every blocking gate has passed, warns and exits 0 on any API failure, and the hook never gates on its exit code — a failed assignment can never block a legitimate push.
117
+ **Issue self-assignment at first push, specifically** (task 33): the pre-push hook carries one deliberate non-gate side effect — on a `task/*/*` branch's genuinely first push (the remote ref is being created; its pre-push remote-sha is all zeros), `packages/aeg-core/bin/assign-task-issue.ts` assigns the task's Issue to the authenticated `gh` user, i.e. the actual pusher. This mechanizes the signal Studio's dispatch-visibility chip reads (task 26: `assigned = assigneesCount > 0`), which until this task was populated only when the Principal remembered to assign by hand (live-fire gap: task 28 showed no chip despite two real pushed commits). Deliberately **assignment-at-first-push, not at dispatch time** — a rendered-but-unstarted brief is not "in flight", and assigning earlier would reintroduce the false positive task 26 was built to avoid. Idempotent (an already-assigned Issue is a no-op, so a deleted-and-recreated branch cannot double-assign; a force-push to an existing branch never re-triggers at all) and **fail-open by contract**: it runs last, after every blocking gate has passed, warns and exits 0 on any API failure, and the hook never gates on its exit code — a failed assignment can never block a legitimate push.
113
118
 
114
119
  ---
115
120
 
@@ -127,19 +132,19 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
127
132
  | Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Re-checks, in CI, that whatever landed on a pull request or Issue satisfies the CLI's validated-write shape rules — the actual backstop against a raw write, since no ring-0 hook can refuse one at the point it happens. | No tool-interception layer is installed in this repo (`ring1_forgeWriteInterception: false` in `vinaya.config.json`), so a raw `gh`/API write bypasses the CLI's validated forge-write commands (`vinaya issue create/edit`, `vinaya pr create/edit`) entirely rather than being refused at the tool call — the sanctioned path (the Issue-creation and PR-creation rows above, ring 0) only covers a write that goes through it. Ring-1 CI re-runs the same validators — the malformed-task-Issue rationale gate, the configured brief schema — against whatever actually landed, catching a bypassing write after the fact; that re-run, not anything at ring 0, is this row's real enforcement. | repo-own | `apps/cli/src/lib/forge-write.ts` |
128
133
  | Single-plan-PR guard | Ever had two people plan the same work at the same time, unknowingly? | ci | Re-checks that no two open pull requests are planning the same work at once. | The identical ring-0 predicate (`checkSinglePlanPr`) re-run forge-side against this PR's own touched files (via `gh pr view`) and every other open PR's touched files — closes the gap where a web-UI-opened plan PR bypasses `open-pr.ts` entirely. No-ops for an ordinary task-branch PR (never touches a topology file), and dormant altogether where a plan is a Milestone plus Issues rather than a file. | product | `packages/aeg-core/bin/verify-single-plan-pr.ts` |
129
134
  | Coherence check | Ever found a task marked "done" that was never actually merged? | ci | Re-checks every task’s recorded state against what actually merged. | Plan↔forge consistency across the whole repository: tasks closed without a merge — A1 also recognizes a task Issue hand-closed directly by a recognized Principal (`stateReason: 'completed'`) as a second, narrower "done" path alongside a merged PR, for the documented exception where a dependency's technical premise dissolved and no PR was ever going to land — archived without their audit record, orphaned Issues (T2), phantom references, unplanned placeholders in active tranches (T3), duplicate decision numbers, broken document manifests, open task Issues missing a planner-rationale field (R1 — the same grammar the ring-0 Issue gate above enforces at creation). **L5 (open-Milestone-all-closed) surfaces a tranche whose Milestone stayed open after every one of its task Issues closed — advisory (info-only), the forge-native analogue of file-based L1 for post-cutover tranches that no longer carry a topology file (drift class 2; one live incident — the forge-state tranche's Milestone left open after full archive).** **T2 and T3 are CI-scoped to the pull request's own tranche** (parsed from the branch name) — a coherence gap in an unrelated tranche's topology never blocks this pull request (task 19, closing the incident where an unrelated tranche's gap failed CI). **T2 additionally blocks CI only for a plan PR** (task 24 item 4, superseding half of task 19's placement) — the point-of-power principle: a task PR can neither cause nor cure a topology gap, so T2's findings are demoted to non-blocking for every other PR kind, `--json`/audit mode, and `daily-drift` alike (a live incident proved the old placement wrong). The full repo-wide picture (every tranche, unscoped) remains visible in `--json`/audit mode. **The job's repo-state inputs (topology files, tranche list) read from a freshly-fetched `origin/main`, not the checkout's `refs/pull/N/merge`** (task 24 item 5) — GitHub materializes that ref lazily and it can lag behind `main` (5+ false-red cycles, 2026-07-03/04); a plan PR's own topology diff still reads from its head ref. **`id`/`issue` per task now derive from the forge (`@attalabs/aeg-forge-state`, task 3b) instead of the topology file, falling back to the file if forge derivation fails; `dependsOn`/`conflictsWith` and `#TBD` rows stay file-sourced and are merged onto the forge-derived task list, but only when a topology file still exists.** As of task 7, no active tranche still carries a topology file — every one was deleted once its forge-native replacement was proven live, so `dependsOn`/`conflictsWith` is fully forge-derived for all of them, with no file fallback anywhere. | product | `packages/aeg-core/bin/verify-coherence.ts` |
130
- | Documentation gate | Ever shipped a change and forgot to update the doc explaining it? | ci | Re-checks that a change carrying real code also updates the docs explaining it. | Tier declaration and tier-appropriate documentation, including code→document ownership | product | `packages/aeg-core/bin/verify-docs.ts` |
135
+ | Documentation gate | Ever shipped a change and forgot to update the doc explaining it? | ci | Re-checks that a change carrying real code also updates the docs explaining it. | Tier declaration and tier-appropriate documentation, including code→document ownership (C5), published prose (C7), and doc-claim bindings (C8) | product | `packages/aeg-core/bin/verify-docs.ts` |
131
136
  | Test-plan state | Ever merged a PR with an unchecked "did you test this" box? | ci | Re-checks that the pull request's `[principal]` test-plan boxes are genuinely ticked. | Unticked `[principal]` test-plan boxes block merge readiness. **The `[agent]` half is no longer graded here at all** (task 12; Principal ruling: an agent never ticks a box or edits a PR body) — it is a fenced command list `vinaya pr report` runs from the PR head and writes into the `AEG:EVIDENCE` block, and `evidence-fresh` is what binds that block to the head. This check reads the body and branch alone: no comment fetch, no `PR_NUMBER`, no `pending` state — a body opened before the rollout constant may still carry a checkbox `[agent]` item (grandfathered by `brief-shape`'s own rollout), but this gate never looks at that tag either way. | product | `packages/aeg-core/bin/verify-test-plan.ts` |
132
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` |
133
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` |
134
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` |
135
- | 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. | product | `packages/aeg-core/bin/verify-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. **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` |
136
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` |
137
- | 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`the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing neither directory exists — so the live coverage is the two seeded bin sets, `packages/aeg-core/bin/*.ts` and `apps/cli/src/checks/bin/*.ts` — the latter is where most core check bins actually live.) **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` |
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` |
138
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` |
139
144
  | G4 — cited forge numbers resolve | Ever read a doc that cited a ticket number that didn't exist? | ci | Re-checks that every Issue and PR number cited in the docs resolves to a real one. | Every `#NNN` cited anywhere in this page's body is a real Issue or PR in the forge — a fabricated citation fails the build. **Currently scans nothing by design** (task 14): this page's body carries zero forge citations, since task 3 banned them from `aeg-root/**` as doctrine and the `reader-resolvable-prose` check (task 15) now enforces that ban directly. G4 is a standing guard against reintroduction, not a live proof of ongoing citation correctness — it fires the moment a `#NNN` reappears here and doesn't resolve, but has nothing to scan while the ban holds. Verified live: `packages/aeg-core/src/registry-checks.test.ts` appends a fabricated citation to this page's real content and asserts `checkG4` catches it. | product | `packages/aeg-core/bin/verify-registry.ts` |
140
145
  | G5 — role/contract integrity | Ever had a process doc reference a role that was never actually defined? | ci | Re-checks that every role and contract the doctrine references is really defined. | Every `aeg-root/contracts/*.md`'s `producer`/`consumer` names a real `role_id` from `aeg-root/roles/*.md`; every role's `performs`/`refuses_when` frontmatter is present and non-empty. | product | `packages/aeg-core/bin/verify-registry.ts` |
141
146
  | G6 — doctrine-registry parity | Ever read a doc that claimed a check ran, when nothing in the registry actually ran it? | ci | Re-checks that every row this page marks `product` actually ships as a real, adopter-runnable check, not just a documented claim. | Every row whose `Audience` cell reads `product` resolves to a real entry in `coreCheckRegistry()` (`apps/cli/src/checks/registry.ts`) — the parity a `product` claim makes but nothing checked before task 8. A `repo-own` row makes no such claim and is never scanned. Evaluated only from `apps/cli`'s `check-registry-gates.ts`, since `coreCheckRegistry()` lives there and `aeg-core`'s standalone `verify-registry.ts` cannot import it without closing a dependency cycle. | product | `packages/aeg-core/bin/verify-registry.ts` |
142
- | reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `<doctrineRoot>/**` doc and every configured reader-facing page, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes a sibling suite proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `<doctrineRoot>/glossary.md`'s own entry headings, never hard-coded. Registered in `coreCheckRegistry()` (task 7): its doctrine root, reader-facing globs, and legacy-slug corpus location are adopter-configurable via `vinaya.config.json`'s `proseGates` key, defaulting to this repo's own prior hardcoded shape (`doctrineRoot: "aeg-root"`, dormant reader-facing sweep) when unset. Part of `vinaya check --all` for every adopter; not part of the managed hooks. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
147
+ | reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `<doctrineRoot>/**` doc and every configured reader-facing page, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes a sibling suite proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `<doctrineRoot>/glossary.md`'s own entry headings, never hard-coded. Registered in `coreCheckRegistry()` (task 7): its doctrine root, reader-facing globs, and legacy-slug corpus location are adopter-configurable via `vinaya.config.json`'s `proseGates` key, defaulting to this repo's own prior hardcoded shape (`doctrineRoot: "aeg-root"`, dormant reader-facing sweep) when unset. Part of `vinaya check --all` for every adopter, including the managed hooks: `scope: 'full'`, so `--local` always runs it (the `git push` row above). **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. **A fourth class, `product`, is the one blocking exception:** a tranche-slug citation under `PRODUCT_SLUG_SCOPE` (CLI source, the CLI and sources READMEs, the workflows, `.vinaya`) fails the check outright — `severity: 'error'`, non-zero exit — where every other class here stays `warning`/exit `0`. Because this check is `scope: 'full'` and therefore already in `--local`'s sweep, `product` first fires at `git push` (ring 0) — the earliest point this row's own findings can refuse anything — and again, blocking, in CI (ring 1) over the same diff. | product | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
143
148
  | retired-vocabulary | Ever read a doc that described a mechanism as live after it was retired? | ci | Re-checks that no doctrine page claims a retired mechanism is still live. | The genuinely-retired half of a sibling suite's original scan — never the forge-number/tranche-slug citation half, which is `reader-resolvable-prose`'s own job and would double-report the identical match if duplicated here. Scoped to `<doctrineRoot>/**` only (the same `proseGates.doctrineRoot` key `reader-resolvable-prose` reads), not the whole adopter repo — this vocabulary means something only inside the shipped doctrine tree. Registered in `coreCheckRegistry()` (task 7), report-only like `reader-resolvable-prose`. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-retired-vocabulary.ts` |
144
149
  | doctrine-portability | Ever shipped doctrine that named a file only you could see? | ci | Re-checks that shipped doctrine doesn't cite a path that exists only in the authoring repository. | A cited path's top segment judged against an allow-list: the caller-supplied ships prefix itself (`<doctrineRoot>/`, `aeg-root/` by default — doctrine citing its own tree, wherever that tree actually lives for the caller, checked dynamically so a non-default `doctrineRoot` never falsely flags an adopter's own self-citations), plus a fixed set of static portable prefixes (`roles/`, `contracts/`, `skills/`, and adopter-owned `.github/`, `.vinaya/`, `.claude/`, `.git/`, `.husky/`) — an unclassified prefix is a finding, never a silent pass, so a never-seen author-repo directory (e.g. a build artifact under `apps/*/dist/`) cannot slip through the way it would under a deny-list. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrinePortability`; the bin supplies the only I/O, reading `<doctrineRoot>/**` twice — once from the working tree, once from `BASE_SHA` (`origin/main` by default) via `git show`/`git ls-tree` — and reports only citations present in the former and absent from the latter, so day-one install of an unfixed backlog never turns every adopter's CI red. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-portability.ts` |
145
150
  | doctrine-no-procedures | Ever pasted a copy-paste runbook into doctrine, and it rotted the moment the real command changed? | ci | Re-checks that doctrine (`roles/*.md`, `contracts/*.md`, …) describes commands rather than scripting them (task 9/task 10's rule). | A fenced code block anywhere under `<doctrineRoot>/**/*.md`, tagged with no language or an explicit `sh`/`bash`/`shell`/`console`, containing two or more lines that each start with a shell command word (`export`/`bun`/`gh`/`git`/`grep`/`sed`/`cat`/`diff`/`vinaya`) is a finding — a runbook, not an illustration. A block tagged with any other language (` ```ts `, ` ```json `, …) is never a candidate at all, however many command-word-shaped lines it contains — a two-line ` ```ts ` excerpt whose lines both start with `export` is TypeScript, not shell. Two further exemptions: a block sitting inside the existing `AEG:VENDOR-EXAMPLE` anchor pair (`tranche-model.md` §12's one sanctioned fenced home for a real command sequence), and any file under a `templates/` directory. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrineNoProcedures`; the bin supplies the only I/O, walking `<doctrineRoot>` once. Unlike its sibling `doctrine-portability` above, this is **blocking, not baseline** (`severity: error`, exit 1 on any finding) — a fresh rule with an expected-zero corpus after this task's own sweep-and-fix, not a 200+-finding backlog needing a report-only rollout. Registered in `coreCheckRegistry()`. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-no-procedures.ts` |
@@ -147,10 +152,15 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
147
152
  | workspace-escape | Ever deleted a package only to break a sibling's tests through a path nobody's dependency graph saw? | ci | Re-checks that no source file's constructed filesystem reference reaches outside its own workspace package, or points at a path that does not exist. | Every `readFileSync`/`readFile` call with a literal relative-path argument, and every `new URL('…', import.meta.url)`, across every `.ts`/`.tsx` file under `apps/*`/`packages/*` — resolved against the citing file's own directory and judged against its own two-segment workspace-package prefix (`apps/<name>`, `packages/<name>`), never against `import`/`require` module specifiers (the dependency graph already owns those). Zero I/O in `@attalabs/aeg-core`'s `findWorkspaceEscapes`; the bin supplies the only I/O, walking the working tree once for source content and once for the existence-check universe (files AND directories, so a reference to a directory itself resolves as existing). Registered in `coreCheckRegistry()` (task 17), report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`. `*.test.ts`/`*.test.tsx` files are excluded from the swept surface entirely (O2, found live 2026-09-04) — this repo's own corpus carried a genuine escape at `packages/sources/src/commands-router-coverage.test.ts` reaching into `apps/cli/src/index.ts`, structurally the same incident this check exists to catch, until that exclusion silenced it alongside `workspace-escape.test.ts`'s own fixture-string false positive; report-only severity stays regardless, since an ordinary (non-test) source file's constructed reference can still exist with nobody having triaged it yet. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. (this sweep's real domain is `apps`/`packages`, not `aeg-root` — named per the Issue's own list; harmless either way while `include` has no runtime effect on a `scope: 'full'` entry.) | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
148
153
  | changeset-coverage | Ever shipped a change to a published package and the release train never picked it up? | ci | Re-checks that a diff touching a published package's own shipped files also carries a changeset in the same diff. | For each member of `.changeset/config.json`'s `fixed` group, a changed path counts as SHIPPED iff it falls under that member's own `package.json` `files` allowlist, read live from every workspace member's own manifest — never a hardcoded path list, never a directory heuristic. A diff that hits at least one shipped path with no `.changeset/*.md` entry (excluding `README.md`) in the same diff is a finding, naming the shipped paths hit. The Changesets-release branch itself is exempt by construction (its diff IS the changesets being consumed) — the exemption reuses `@attalabs/aeg-core`'s own release-branch constant, the same one `body-bare-digits`'s Changesets-release exemption is keyed on, rather than inventing a second branch-name special-case; unlike that check it never live-fetches a PR author, since a report-only, always-exit-`0` check has no gate for a spoofed branch name to bypass. Written after four separate published-package pull requests, over the course of one day, each merged with no changeset and caught only by a human after the fact — the obligation existed in no enforceable place before this check: not in `roles/developer.md`'s deliverable conventions (now amended alongside this row), not in any registered check. Zero I/O in the pure predicate (`changeset-coverage-logic.ts`); the bin supplies the only I/O — reading the config, every fixed-group member's own manifest, and the diff. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape` above — graduating to a blocking check, and any waiver-label escape that would need, is a later, separately-dispatched decision once the false-positive rate is observed; v1 carries no such escape. | product | `apps/cli/src/checks/bin/check-changeset-coverage.ts` |
149
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` |
150
- | 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. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
151
- **G-checks rollout (task 3):** G1/G2 shipped report-only at first they could only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surfaced as visible debt without retroactively failing in-flight work. G3/G4/G5 have been blocking since that same tranche. **G1 flipped to blocking in task 8**, once its report-only window had let the orphan backlog get cleaned up a permanent `info` finding on every run is indistinguishable from silence, which is exactly how the gap task 8 closes (G6, below) stayed invisible for as long as it did. G2's inverse sweep (no orphan hook/CLI) stays report-only; its rollout is unchanged. **G6 — doctrine-registry parity**, also added in task 8, ships blocking from the start: it checks that every row this page marks `product` (the `Audience` column, above) actually resolves to a real `coreCheckRegistry()` entry, closing the gap where a row could claim shipped enforcement that no adopter's `vinaya check` ever actually ran.
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
+ | 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` |
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
+ | 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
+ | 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` |
161
+ **G-checks rollout (task 3):** G1/G2 shipped report-only at first — they could only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surfaced as visible debt without retroactively failing in-flight work. G3/G4/G5 have been blocking since that same tranche. **G1 flipped to blocking in task 8**, once its report-only window had let the orphan backlog get cleaned up — a permanent `info` finding on every run is indistinguishable from silence, which is exactly how the gap task 8 closes (G6, below) stayed invisible for as long as it did. **G2 flipped to blocking in this same wave**, the same way: its remaining backlog was fourteen files that were never orphans at all, only the second physical form of an already-documented mechanism G2's own string-equality test was too narrow to see (closed by the twin-form recognition described in G2's own row, above) plus two genuinely non-gate files (`NON_GATE_BINS` already named them) — a standing `info` finding on every run had become exactly the same silence-by-permanence G1's report-only window risked, which is the failure a gate that "trains its readers to ignore warnings" produces. **G6 — doctrine-registry parity**, also added in task 8, ships blocking from the start: it checks that every row this page marks `product` (the `Audience` column, above) actually resolves to a real `coreCheckRegistry()` entry, closing the gap where a row could claim shipped enforcement that no adopter's `vinaya check` ever actually ran.
152
162
 
153
- **reader-resolvable-prose / retired-vocabulary rollout (task 15; task 7 registered both in `coreCheckRegistry()`):** both ship report-only, same precedent as G1/G2 above — each can only ever print a `warning` finding, never fail CI (the check's own exit code stays `0`), so any real backlog surfaces as visible debt rather than a wave of newly-red pull requests (161 findings, all class 2, across `aeg-root` on the day `reader-resolvable-prose` first shipped). A later, separately-dispatched task flips either to blocking once its own backlog is cleared. **Exception, both checks (checkout-independent doctrine-root resolution):** the unconfigured doctrine-root default now resolves relative to the repo actually under check (its own real repository root) first, falling back to the check's own package-relative "shipped copy" resolution only when the repo under check has no local doctrine tree of its own — so the same commit sweeps identically regardless of where it happens to be checked out. When NEITHER resolves (no doctrine tree findable at all, relative to either anchor), that is not a `warning` finding and not a clean pass — it is reported as its own distinct outcome, with a non-`0`/non-`1` exit code, so a scan that never ran cannot be mistaken for a scan that ran and found nothing.
163
+ **reader-resolvable-prose / retired-vocabulary rollout (task 15; task 7 registered both in `coreCheckRegistry()`):** both ship report-only, same precedent as G1/G2 above — each can only ever print a `warning` finding, never fail CI (the check's own exit code stays `0`), so any real backlog surfaces as visible debt rather than a wave of newly-red pull requests (161 findings, all class 2, across `aeg-root` on the day `reader-resolvable-prose` first shipped). A later, separately-dispatched task flips either to blocking once its own backlog is cleared. **`reader-resolvable-prose`'s `ships`/`reader-facing`/vocabulary classes stay report-only under this rollout; `retired-vocabulary` is unaffected in full.** Only `reader-resolvable-prose`'s fourth, `product` class (see the row above) graduated to blocking, ahead of the rest: a tranche-slug citation in product code fails outright, at both `git push` and CI. **Exception, both checks (checkout-independent doctrine-root resolution):** the unconfigured doctrine-root default now resolves relative to the repo actually under check (its own real repository root) first, falling back to the check's own package-relative "shipped copy" resolution only when the repo under check has no local doctrine tree of its own — so the same commit sweeps identically regardless of where it happens to be checked out. When NEITHER resolves (no doctrine tree findable at all, relative to either anchor), that is not a `warning` finding and not a clean pass — it is reported as its own distinct outcome, with a non-`0`/non-`1` exit code, so a scan that never ran cannot be mistaken for a scan that ran and found nothing.
154
164
 
155
165
  **changeset-coverage rollout:** ships report-only, same precedent as the report-only rows above — the check's own exit code always stays `0`, so registering it cannot newly redden any existing install. Graduating to blocking, and the waiver-label escape that graduation would need, are both deliberately deferred to a later, separately-dispatched task once the check's own false-positive rate is observed in practice; v1 carries no self-serve or principal-applied escape at all.
156
166
 
@@ -179,9 +189,12 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
179
189
 
180
190
  A full audit of this page against the installed hooks and CI workflows (2026-07-04) found three gates that existed **only** at ring 0 — invisible to any writer without the local hooks installed (web UI, another tool, a human on a fresh clone, an agent harness without the wiring). This table is the durable record of that audit: every enforcement on this page, its ring coverage, its paired status, and — for anything still a gap — which of the two known gap families it belongs to.
181
191
 
182
- **Two gap families:**
192
+ **Three gap families:**
183
193
  - **(a) Local-only gates lacking a forge backstop** — a ring-0 mechanism with no ring-1 (CI) or ring-2 (audit) counterpart, so a writer that skips the local hook skips the rule entirely. This task closes three.
184
194
  - **(b) Manual CLIs lacking a forcing mechanism** — a tool that mechanizes a check (`verify-dispatch`, `verify-task`) but nothing yet *requires* an agent to run it; an agent can simply skip invoking it. **Closed by task 25**: `verify-dispatch` now runs itself on a task branch's first push (the managed `.git/hooks/pre-push` block); `verify-task` now runs itself inside `open-pr.ts` for task branches. Both land at ring 0 only (no ring-1/ring-2 backstop yet) — the same accepted, tracked shape as the branch-ID check below, not a new gap this task opened.
195
+ <!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:function renderSection9( -->
196
+ <!-- AEG:CLAIM: apps/cli/src/commands/pr-report.ts contains:extractFencedBlocks(located.section) -->
197
+ - **(c) No identity/authorship check on a task-Issue or task-PR write** — every content gate on this page (`checkIssueRationale`, `checkBlastRadiusScope`, `checkNoBriefContent`, brief-shape, and the rest) validates shape only, never who wrote it. This is safe today only because writing a task Issue requires the same access as dispatching a task in the first place — a fact about who currently holds write access to a given repo (one collaborator, here), not an enforced permission tier. A repo that adds a second collaborator, a bot, or external-contributor access without also gating who may write a task Issue inherits the exposure silently: a task Issue's `## Test plan` section renders verbatim into a brief's §9 (`brief-render.ts`), and `vinaya pr report`'s Group C step re-executes those fenced commands unattended, against the PR head — so task-Issue write access is, transitively, unattended-command-execution access. **Accepted risk, not scheduled for a gate (Principal ruling, 2026-09-06):** before adopting unattended dispatch (`runTask`, the forward-looking dispatch chokepoint) in a repo with more than one trusted writer, add an author check to the task-Issue write path, or accept the risk explicitly and say so in that repo's own doctrine.
185
198
 
186
199
  | Enforcement | Ring 0 | Ring 1 | Ring 2 | Paired status | Gap family |
187
200
  |---|---|---|---|---|---|
@@ -191,6 +204,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
191
204
  | Orphan-task coherence (T2) | — (no ring-0 form; a CI-only check) | ✅ coherence oracle, **relocated to plan-PRs-only** | ✅ `--json`/audit mode, `daily-drift` (info-only) | Paired (pre-existing); **placement corrected** (this task, item 4) | n/a — not a coverage gap, a point-of-power correction |
192
205
  | Coherence oracle repo-state reads | n/a | ✅ now reads fetched `origin/main`, not the merge-ref checkout | ✅ same fix applies to `--json`/audit mode | Paired (pre-existing); **staleness bug fixed** (this task, item 5) | n/a — not a coverage gap, a correctness fix |
193
206
  | Branch-ID must match a topology row | ✅ pre-push hook — delegates to aeg-core (`check-branch-topology.ts` → forge-derived Tranche via `deriveTrancheFromForge`, task 3a; originally `parseTranche`, task 32); formerly a hand-rolled grep fork of the same table-parse | — | — | Ring-0 only | **(a) — still a gap, not addressed by this task** |
207
+ | Task-Issue/task-PR authorship (who wrote it, not what it says) | — (no Ring-0 check; content gates validate shape only) | — | — | No gate | **(c) — accepted, not scheduled (Principal ruling)** |
194
208
  | Code→doc ownership (C5) | ✅ pre-push hook + PR-open gate | ✅ `verify-docs --pr`, now a step of the AEG gate suite job (`verify-docs.yml` deleted — task 31) | — | Paired (pre-existing) | not a gap |
195
209
  | Brief-section grammar / title convention | ✅ `open-pr.ts`/`open-issue.ts` | ✅ Brief Validation, now a step of the AEG gate suite job (moved out of `archivist.yml` — task 31) | — | Paired (pre-existing) | not a gap |
196
210
  | Planner-rationale **content** — blast radius / brief-content / docs-read | ✅ `open-issue.ts` (A/B/D refuse; C warns) | — (no continuous half yet: the checks derive collision domains live from `package.json` + built-in defaults, plus any `vinaya.config.json` entries, and read `projects.md`, which CI has, but no oracle check re-runs them over the live Issue stock) | — | Ring-0 only | **gap: no ring-1 half** (narrowed: the project-registry content check, added alongside A/B/D, does have one — it re-runs inside R1 below) |
@@ -199,7 +213,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
199
213
  | `verify-task` (pre-PR exit composite) | ✅ **new** — `open-pr.ts` runs it wholesale for task branches (`gatePlanForBranch`); on `edit <n>` the gate set now resolves from the target PR's **actual forge head branch** (`gh pr view --json headRefName`, fetch failure = hard refusal), not the local checkout/`BRANCH` env, which silently skipped `closes-n`/`verify-task` on task-PR body edits from a `main` checkout (task 36 bundled finding) | — | — | **Forced, ring-0 only (this task, item 2)**; **edit-path gate-selection bypass fixed** (task 36) | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
200
214
  | Runtime Test Plan checkbox state (`verify-test-plan`) | n/a (CI-only check) | ✅ now a step of the AEG gate suite job (`verify-test-plan.yml` deleted — task 31; still runs on PR-body `edited` events) — **reads `BRANCH`; parses both the inline `**Test Plan:**` marker and the `## N. Test Plan` heading form** (task 25, item 3) | — | Paired (pre-existing); **parser bug fixed** | n/a — not a coverage gap, a correctness fix (a live-fire gap: heading-form sections were never matched, so a task PR with an unticked `[principal]` box advisory-PASSED) |
201
215
  | Waiver-label-actor verification (task 29; **extended to `vinaya/waiver:review`, review-gate task 1**) | ✅ **new** — pre-push is warn-only (no `vinaya/waiver:docs`/`vinaya/waiver:review` label can exist before a PR does) **+ tool-layer (attalabs only)**: the attalabs reference implementation adds a session hook denying any agent-session command that mutates either label; this repo has no tool-interception layer (see the raw-API ring-0 row), so the shared-credential hole is OPEN here — a local agent using the Principal's own `gh` credential can apply a waiver label whose timeline actor then reads as the Principal, and the actor-verification step cannot distinguish it. Until an interception layer exists, waiver labels in this repo rest on credential discipline, not a gate | ✅ **new** — a GraphQL `LABELED_EVENT`/REST timeline step resolves the label's actor, verified by `isWaiverLabelActorVerified` (now parameterized by label name) and fed into `verify-docs --pr`'s C5 waiver check (`vinaya/waiver:docs`) and `verify-review-gate.ts`'s review check (`vinaya/waiver:review`) | — | **New (this task)**; **extended (review-gate task 1)** | n/a — new mechanism |
202
- | Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
216
+ | Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)**; **objectives-version binding added by a later task — implemented in `apps/cli/src/checks/bin/check-review-gate.ts`, this repo's actual live gate path, not in this row's own `verify-review-gate.ts`, which never resolves it (see the Ring 1 Review gate row above)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
203
217
 
204
218
  This table describes **installed mechanisms only** (the page's own change-discipline rule, below). The `verify-dispatch`/`verify-task` rows and the `verify-test-plan` fix landed in task 25's own change set, per that rule.
205
219