@attalabs/vinaya 0.20.1 → 0.22.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 (152) hide show
  1. package/README.md +76 -1
  2. package/aeg-root/enforcement.md +15 -7
  3. package/aeg-root/milestone-model.md +3 -1
  4. package/aeg-root/roles/architect.md +2 -2
  5. package/aeg-root/roles/archivist.md +2 -2
  6. package/aeg-root/roles/developer.md +3 -2
  7. package/aeg-root/roles/planner.md +2 -2
  8. package/aeg-root/roles/principal.md +1 -0
  9. package/aeg-root/roles/tranche-archivist.md +2 -2
  10. package/aeg-root/templates/issue-rationale-template.md +4 -4
  11. package/aeg-root/templates/pr-report-template.md +2 -0
  12. package/aeg-root/tranche-model.md +4 -4
  13. package/dist/checks/bin/check-body-bare-digits.js +392 -10
  14. package/dist/checks/bin/check-branch-topology.js +382 -2
  15. package/dist/checks/bin/check-brief-shape.js +357 -2
  16. package/dist/checks/bin/check-changeset-coverage.js +4262 -0
  17. package/dist/checks/bin/check-closes-n.js +382 -2
  18. package/dist/checks/bin/check-coherence.js +416 -8
  19. package/dist/checks/bin/check-dead-branch-push.js +357 -2
  20. package/dist/checks/bin/check-dispatch-readiness.js +492 -25
  21. package/dist/checks/bin/check-doc-coverage-push.js +392 -9
  22. package/dist/checks/bin/check-doc-coverage.js +392 -9
  23. package/dist/checks/bin/check-doctrine-portability.js +391 -8
  24. package/dist/checks/bin/check-evidence-fresh.js +357 -2
  25. package/dist/checks/bin/check-first-push-dispatch.js +481 -17
  26. package/dist/checks/bin/check-issue-assignment.js +382 -2
  27. package/dist/checks/bin/check-main-branch-refusal.js +357 -2
  28. package/dist/checks/bin/check-no-disk-state.js +357 -2
  29. package/dist/checks/bin/check-quoted-command.js +4514 -0
  30. package/dist/checks/bin/check-reader-resolvable-prose.js +430 -21
  31. package/dist/checks/bin/check-registry-gates.js +410 -7
  32. package/dist/checks/bin/check-retired-vocabulary.js +422 -13
  33. package/dist/checks/bin/check-review-gate.js +389 -6
  34. package/dist/checks/bin/check-single-plan-pr.js +357 -2
  35. package/dist/checks/bin/check-test-plan.js +357 -2
  36. package/dist/checks/bin/check-token-collection-wired.js +4081 -0
  37. package/dist/checks/bin/check-token-report.js +4096 -0
  38. package/dist/checks/bin/check-workspace-escape.js +359 -4
  39. package/dist/index.js +1607 -641
  40. package/package.json +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  43. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +2 -2
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +2 -2
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +2 -2
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +3 -3
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +3 -3
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +3 -3
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +3 -3
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +2 -2
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  89. 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
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__02c71kv._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__040smoa._.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__16cuydh._.js → [root-of-the-server]__069-_41._.js} +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0ykun-j._.js → [root-of-the-server]__0z6dav-._.js} +3 -3
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
  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/_0o54suy._.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.js +1 -1
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{apps_vinaya-studio_web_src_app_studio_18kqbvs._.js → apps_vinaya-studio_web_src_app_studio_06it6s-._.js} +2 -2
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  119. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
  121. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
  122. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.js +1 -1
  123. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  124. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  126. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  127. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/00pi8vhg8w6gi.js +1 -0
  128. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/03q0q9n6-xah8.js +1 -0
  129. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
  130. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/13g16cu00g570.js +1 -0
  131. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1lk7h-0buf4ab.js → 1hufpuody6vjv.js} +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1sfxfunirkcr6.js +1 -0
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 206ajg8f1a-_h.js} +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 267o-tsrnuwnv.js} +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 279qq4twinc97.js} +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 37hnluhzj9bei.js} +4 -4
  139. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +1 -0
  140. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 3zugx9esh2x51.js} +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
  143. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
  144. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0la0hhmjprdxm.css +0 -1
  145. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
  147. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
  149. /package/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  150. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_buildManifest.js +0 -0
  151. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_clientMiddlewareManifest.js +0 -0
  152. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -19,12 +19,15 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
19
19
  | `vinaya doctrine [--role <name>]` | Print the absolute path of the bundled doctrine's front door (`aeg-root/skills/aeg/SKILL.md`) on this machine. The committed root `VINAYA.md` pointer names the package, never a filesystem path — this command is the read-time resolution step it hands the reader. `--role <name>` resolves straight to a specific role's doctrine instead (`aeg-root/roles/<name>.md`, under the same resolved root), validated against the role names actually enumerated under `roles/*.md` at request time — an unknown name fails cleanly and lists the valid names, never a silent bad path. `--json` for the enveloped `{ root, entry }` form. |
20
20
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
21
21
  | `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
22
+ | `vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] [--model <id>] [--transcript <path>]` | Emit two generated, never-hand-typed blocks a PR body carries: `AEG:EVIDENCE` (the head sha, the width-invariant `git diff --numstat` against `origin/main`'s merge-base, and the result of `vinaya check --all --diff-only`) and `AEG:TOKENS` (real usage figures collected the same way `vinaya tokens` collects them, rendered into the `## Token report` heading's table). Without `--write`, prints the Evidence block to stdout. With `--write <body-file>`, replaces the Evidence block in place but APPENDS a new row to the Tokens block on every run — a re-entry after `CHANGES_REQUESTED` reports again, and a second report is a second row, never a sum or an overwrite. `--phase`/`--role` default to `<n>: develop`/`Developer` (`<n>` parsed off a `task/<tranche>/<n>` branch) but accept any role/phase pair — a Brief Author (`--role "Brief Author" --phase "<n>: brief"`) or Planner (`--role Planner --phase "<n>: plan"`) turn appends its own row into the same block alongside the Developer's, and each round-trips through `parseTokenReportEntries` into its own distinct ledger row; no role's figures ever collapse into another's. `--transcript` names a session transcript directly, skipping Stop-hook pointer-file resolution. An incapable host (no transcript resolves, or it resolves to zero usage records) gets an all-`—` row carrying the probe's reason inline in the Agent/Model cell, never a fabricated `0/0/—`. Exits non-zero when any gate in Group B fails. |
22
23
  | `vinaya review post --role code-reviewer \| security --pr <n> ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (verdict, findings, per-field text) instead of a hand-typed comment. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`Judged head:` line from validated inputs, never from caller-supplied text; refuses a contradictory verdict (a BLOCKER/CRITICAL-or-HIGH finding with a clean verdict) before posting anything; and after posting, re-fetches the comment and refuses to exit 0 unless it re-parses through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls. `--json` for the enveloped machine form. |
23
24
  | `vinaya tokens --phase <phase> --role <role> [--model <id>] [--transcript <path> \| --in <n> --out <n>]` | Print a role's `Tokens: …` report line — the portable front door over the token-report collection adapter, resolving `@attalabs/aeg-core`'s `resolveMeteringCapability`/`summarizeTranscript`/`formatTokensLine` from the installed package rather than a repo-relative path, so it works in an adopter checkout with no local `packages/`. `--transcript <path>` reads a session transcript directly; omitted, it resolves via the Stop-hook pointer file where one is installed. `--in <n> --out <n>` bypasses transcript resolution entirely for a host whose figures arrive by some other means. Refuses rather than emitting `0/0/—` when no transcript resolves, a resolved one can't be read, or it summarizes to zero usage records — the same capability probe backs a `vinaya doctor` finding when it reports incapable. |
25
+ | `vinaya archive [--merge-sha <sha>]` | Post-merge Archivist: resolves the merged PR from a merge SHA (`HEAD` by default), assembles and posts the `### AEG provenance` comment via `@attalabs/aeg-core`'s `buildProvenanceBlock`, then closes the PR's `Closes #N` Issue. Idempotent — a PR that already carries the block (`hasProvenance`) is skipped, posting nothing new. Appends its own one-line `Tokens: …` report to that same comment, collected via the same metering adapter `vinaya tokens` uses: the sanctioned all-`—` line on an incapable host, or — when the probe reports capable but summarized to zero tokens — the line is omitted and flagged `DANGLING (tokens): …` instead, since a capable host's blank is never licensed to read as `—`. Provenance and Issue-closure always proceed regardless of that outcome; a missing token row never withholds either. `vinaya archive tranche <slug> [--yes]` closes a tranche's legacy Milestone once every `vinaya/tranche:<slug>`-labeled Issue is closed. `rings.ring2_asyncAudits: true` skips this command's work entirely (see Config below). |
24
26
  | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. `--port <n>` binds an exact port — see below. |
25
27
  | `vinaya milestone create --title <title> --body-file <path>` | Create a GitHub Milestone from a validated body. The title is free text, never parsed for a version — the description's optional `Release:` field is the sole authority for one. Refuses before any `gh` write when the goal is absent, `Release:` is present but malformed, or an `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
26
28
  | `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]` | Move one or more existing tranches into a target Milestone: reattaches every Issue carrying each `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact is gathered and refused-or-passed as ONE batch before any write — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone refuses the whole invocation, not just its own slug. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
27
29
  | `vinaya milestone edit <n> --body-file <path>` | Edit an existing Milestone's description after the same `checkMilestoneShape` gate `create` runs — the gated replacement for a raw `gh api PATCH` against a Milestone. Only the description changes; the title is untouched. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
30
+ | `vinaya milestone close --slug <slug>` | Close a tranche's Milestone — the gated replacement for the raw `gh api .../milestones/<n> -X PATCH -f state=closed` recipe the Tranche Archivist used to run on faith. Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge. `--validate-only` verifies attachment without closing; `--json` for the enveloped machine form. |
28
31
  | `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
29
32
 
30
33
  ## Config
@@ -73,6 +76,58 @@ To declare a domain beyond those two — a `migrations/` folder, a codegen outpu
73
76
 
74
77
  `checkBlastRadiusScope` is one of three content checks `vinaya issue create`/`vinaya issue edit` run automatically on a task Issue (any `vinaya/tranche:*` label) — unconditional, not something `vinaya.config.json`'s `briefSchema` opts into or out of. The other two: `checkNoBriefContent` refuses an Issue body carrying a brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan` — those belong in the brief, not the Issue); `checkRationaleNamesDocs` refuses a rationale whose "Docs to keep coherent"/"Traps" fields name no concrete doc/skill path, unless it carries the explicit `no-doc-surface` sentinel. All three grade what the eight-field Planner rationale (`checkIssueRationale`) *says*, once that gate has confirmed the fields exist.
75
78
 
79
+ ### Config-native project metadata (`projects`)
80
+
81
+ `vinaya init product <name>` has always appended a row to `.vinaya/projects.md` (the project registry). It now *also* appends an entry to `vinaya.config.json`'s `projects` array — a second, config-native home for the same declared fact, alongside the registry file rather than instead of it:
82
+
83
+ ```json
84
+ {
85
+ "projects": [
86
+ { "name": "mobile", "path": "apps/mobile", "description": "The mobile client" }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ `name` is required (the dedup key, matching the registry row's own `Project` column); `description` and `path` are optional. This key is display metadata only — no gate or resolver reads it, and single-project repos rightly have none. `vinaya doctor` reports, at `info` severity, when a registry row and a `projects` entry name the same project but only one of the two exists — never an error, since keeping only the registry file is a fully supported shape.
92
+
93
+ ### Token-usage collection for non-Claude-Code hosts (`tokens.collect`)
94
+
95
+ `vinaya tokens` ships one collection adapter, for Claude Code — it reads that host's own session transcript. A repo whose coding-agent host is something else (Codex, Grok build, or any other harness) has no route to a real `Tokens:` line without declaring one:
96
+
97
+ ```json
98
+ {
99
+ "tokens": {
100
+ "collect": "node scripts/collect-usage.js"
101
+ }
102
+ }
103
+ ```
104
+
105
+ **`tokens.collect` must be exactly `"<interpreter> <repo-relative-script-path>"`** — two whitespace-separated tokens, nothing else: no flags, no extra arguments, no shell syntax (`&&`/`|`/`;`), no quoting. `vinaya tokens` spawns the interpreter directly with the script as its one argument — never through a shell — whenever it is declared and `--in`/`--out` are not given, and parses its stdout as a JSON object shaped:
106
+
107
+ ```json
108
+ {
109
+ "inputTokens": 0,
110
+ "outputTokens": 0,
111
+ "cacheCreationInputTokens": 0,
112
+ "cacheReadInputTokens": 0,
113
+ "model": "your-model-id-or-null"
114
+ }
115
+ ```
116
+
117
+ Absent, `vinaya tokens` falls back to the shipped Claude Code transcript adapter unchanged — this key only adds a second route, never removes the first, and a Claude Code adopter continues to get working enforcement on `vinaya upgrade` having declared nothing. When declared, a command that exits non-zero or prints output that doesn't parse into that shape fails loudly rather than silently falling back to the transcript route or emitting a plausible `0/0/—`.
118
+
119
+ This is an opt-in collection route, not a capability declaration: whether a host is treated as capable of metering itself is always *probed*, never read from this key — there is no `tokens.metering` field.
120
+
121
+ Read from the repo-root config only, same trust class as `checks`/`principals`/`releaseActor`: a value that decides what command runs on this turn must come from the reviewed, committed per-repo file, never a machine-wide personal config — a global `~/.vinaya/config.json`'s `tokens` key is stripped at load time with a loud stderr warning, never resolved.
122
+
123
+ Unlike `ci.setup` — which only ever executes inside a generated, reviewed CI workflow step, under the runner's own isolation — a declared `tokens.collect` script executes IN-PROCESS, unsandboxed, on whatever machine runs the ordinary `vinaya tokens` command. Three layers close that gap:
124
+
125
+ - **Rigid grammar.** The `"<interpreter> <script>"` restriction above is not a style preference — it is what makes the content pin below rigorous rather than heuristic. Because a declaration can only ever mean one file, `vinaya tokens` never has to guess which token of an open-ended shell string "looks like a path".
126
+ - **Trust gate, content-pinned.** `vinaya tokens` refuses to run `tokens.collect` at all until a human has explicitly approved this exact interpreter/script declaration, AT the script's exact current content, for this repo, on this machine: run `vinaya tokens --trust-collect` once. Approval is keyed to this repo's git common directory, not to any one worktree, so it survives a fresh `git worktree add` of the same repo; a different interpreter, a different script path, or so much as one byte of script content changing — committed or not — needs its own fresh approval. Approvals live in `~/.vinaya/tokens-collect-trust.json`, machine-local and never read from any committed file, so a pull request can no more grant itself trust than it can add itself to `principals`.
127
+ - **Printed audit trail.** Once trusted, `vinaya tokens` still prints the exact interpreter/script to stderr immediately before every run, so nothing executes invisibly even after approval.
128
+
129
+ This is a trust-then-verify design, not a blocking interactive prompt — the unattended-agent path this key exists for keeps working once a human has approved the script's content a single time. Editing only the script, never `vinaya.config.json`, requires that same fresh approval again — this is the specific gap two earlier, less rigid designs left open (security review, PR #303), and the reason the grammar above is fixed rather than an arbitrary shell string.
130
+
76
131
  ## Where the git hooks live
77
132
 
78
133
  `vinaya init` installs the ring-0 hooks (`pre-commit`, `pre-push`, `commit-msg`) into a **tracked** `.vinaya/hooks/` directory and points git at it with `git config core.hooksPath .vinaya/hooks` — commit that directory. Raw `.git/hooks` is never versioned by git, so hooks installed there exist only on the installing machine; tracked hooks travel with the repo into every clone and every linked worktree checkout.
@@ -85,7 +140,7 @@ On that legacy `.git/hooks` layout, note where the hook actually lives: hooks ar
85
140
 
86
141
  ## Claude Code Stop hook (transcript pointer)
87
142
 
88
- When `--agents` includes `claude` (the default), `vinaya init`/`vinaya upgrade` also install a Claude Code `Stop` hook — `.claude/hooks/track-transcript.sh`, registered in `.claude/settings.json` — that records each session's transcript pointer (session id and transcript path, tab-separated) to `${TMPDIR:-/tmp}/claude-transcript-<sanitized-project-dir>.txt`. This is the pointer `packages/aeg-core/bin/report-tokens.ts`'s token-report adapter reads, so a fresh Claude Code adopter's token-report obligation resolves without an operator naming a transcript by hand, and without falling back to scanning `~/.claude/projects/` for the newest file (which grabs the wrong session's transcript when two worktrees run concurrently).
143
+ When `--agents` includes `claude` (the default), `vinaya init`/`vinaya upgrade` also install a Claude Code `Stop` hook — `.claude/hooks/track-transcript.sh`, registered in `.claude/settings.json` — that records each session's transcript pointer (session id and transcript path, tab-separated) to `${TMPDIR:-/tmp}/claude-transcript-<sanitized-project-dir>-<sha256-of-project-dir>.txt`. Appending a full `SHA-256` digest of the (uncollapsed) project directory keeps two project directories whose paths differ only in non-alphanumeric characters (e.g. `/a/b` vs `/a-b`) from sharing one pointer file — the sanitized prefix alone would collapse both to the same name (`#315`). The reader falls back to the pre-`#315`, digest-less filename when the new one is absent, so a pointer an older hook already wrote stays readable. This is the pointer `packages/aeg-core/bin/report-tokens.ts`'s token-report adapter reads, so a fresh Claude Code adopter's token-report obligation resolves without an operator naming a transcript by hand, and without falling back to scanning `~/.claude/projects/` for the newest file (which grabs the wrong session's transcript when two worktrees run concurrently). The adapter reads both the primary and legacy pointer names through the same hardened, symlink-and-FIFO-safe I/O every other metering call site uses (`hardenedMeteringDeps()`, `packages/aeg-core/src/metering-io-guard.ts`), never a hand-rolled `existsSync`/`readFileSync` pair.
89
144
 
90
145
  The script follows the same never-clobber discipline as the git hooks above: it is a marker-delimited managed block, appended onto an adopter's existing file at that path rather than overwritten. `.claude/settings.json`, by contrast, is refuse-if-foreign (created only when absent) — strict JSON has no comment syntax the marker convention could use, so an adopter's existing `settings.json` is left untouched; wire the hook in by hand (see the install diff's REFUSE guidance) if you already have one.
91
146
 
@@ -156,6 +211,8 @@ Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.att
156
211
  - `agent_recovery_prompt` is a corrective **instruction**, not a restated diagnosis — it tells the model what to do, not what is wrong (that's `message`'s job).
157
212
  - Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
158
213
  - Never reach the network unless explicitly declared as an exception (today: none of the custom-check surface; the core `coherence`/`dispatch-readiness` checks are the only declared exceptions).
214
+ - `token-collection-wired` (ring 0, part of `vinaya check --all --local`'s managed hooks) is a worked example of this contract's narrowest shape: it consults only local `fs`/`process.env` — no PR body, no network — and refuses a commit only when the host's token-metering probe (`resolveMeteringCapability`, `@attalabs/aeg-core`) found a transcript pointer that is this session's on one of two grounds — its recorded id matches ours and the transcript it named could not be reached, or its id could not be read at all yet the file sits at this project's own pointer path owned by this user, which is broken wiring whoever wrote it. A host never wired to meter passes, and so does one holding only a pointer that provably belongs to another session — a plain human terminal carrying an earlier session's leftover, or a second agent session whose Stop hook has not fired yet. Both are the sanctioned operator-metered case, not a wiring defect.
215
+ - `token-report` (ring 1, `requiresOpenPr`) is a worked example of the CI-only shape: it reads `PR_BODY` and re-runs the same `resolveMeteringCapability` probe fresh in its own process, then fails only when the host is metering-capable AND the PR body's "Token report" section is missing, or carries a blank/non-numeric Tokens in/out cell — an incapable host, or an empty `PR_BODY` (no PR yet), both pass silently. It proves presence and shape only, never that the reported figures are true — a check has no transcript of its own to recompute them against. A probe that itself throws (as opposed to cleanly reporting incapable) is never caught into a false pass: left uncaught, it surfaces as `status: 'error'` through the runner's own malformed-stderr path, never a silent `status: 'pass'`.
159
216
 
160
217
  `vinaya new check <yourname>/<id>` scaffolds a worked, self-contained example that honors this contract out of the box, and prints the exact — namespaced — registration to paste.
161
218
 
@@ -186,6 +243,24 @@ The generated review-authority workflows run only default-branch code: the requi
186
243
 
187
244
  `.vinaya/doc-owners` binds code globs to the docs that must change with them; `vinaya check`'s C5 gate enforces that binding against each pull request's own diff — it fires only when a changed file matches a bound glob. That leaves a gap C5 cannot close on its own: a binding whose code was deleted or renamed wholesale matches nothing on any later diff, ever again, and reads as healthy forever. `vinaya doctor` closes it separately — it never fails `vinaya check` — by walking every binding against the repo's full tracked-file list and reporting any whose code glob matches zero tracked files anywhere in the repo, or whose in-repo doc pointer doesn't exist on disk. Report-only, like every other `vinaya doctor` diagnostic: it repoints or removes nothing itself.
188
245
 
246
+ ## Changeset coverage
247
+
248
+ The `changeset-coverage` core check couples a diff touching a published package's shipped files to a `.changeset/*.md` entry 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. The Changesets-release branch itself is exempt by construction. Report-only (`scope: diff`, ring 0): findings print at `warning` severity and the check's own exit code always stays `0`, so installing it cannot newly redden an existing repo's CI. Dormant when the repo carries no `.changeset/config.json`, or none of its `fixed`-group members resolve.
249
+
250
+ ## Quoted-command staleness
251
+
252
+ A doc that quotes a command or config line verbatim, in backticks, as a statement of present fact ("what runs today: `X`") goes stale silently once the thing it quotes changes — every other gate can pass while the quotation is simply wrong. The `quoted-command` core check catches this, but **only for spans an author explicitly marks** — it never infers from a command-looking span, because an adopter-facing instruction (a README's `npx @attalabs/vinaya init`) is not a claim and flagging it is exactly the false-positive shape that gets a check disabled.
253
+
254
+ Opt a span in by wrapping it in an `AEG:QUOTES-FILE` marker pair naming the file it quotes:
255
+
256
+ ```markdown
257
+ one job — <!-- AEG:QUOTES-FILE:START:.github/workflows/ci.yml -->`npm test`<!-- AEG:QUOTES-FILE:END --> in CI
258
+ ```
259
+
260
+ Both markers are HTML comments — invisible on render — and may sit inline within a sentence or on their own lines around a fenced block. The check reads the text between them (an inline `` `span` ``, a fenced block, or bare text — one layer of wrapping is stripped either way) and asserts it still appears verbatim in the named file's current content; a marker inside a fenced/inline code example (e.g. one demonstrating this very syntax) is ignored, never mistaken for a real annotation. A finding names both sides: what the doc claims, and which file no longer contains it.
261
+
262
+ Swept corpus: the same `ships`/`reader-facing` governed-doc classes `reader-resolvable-prose` sweeps (`<doctrineRoot>/**` by default, plus any configured reader-facing pages) — never `apps/*/specs/**` or a `CLAUDE.md`. The cited file itself can be anywhere in the repo. Report-only (`scope: diff`, ring 0): findings print at `warning` severity and the check's own exit code always stays `0`, so installing it cannot newly redden an existing repo's CI. Dormant everywhere no doc carries the marker.
263
+
189
264
  ## Brief-schema divergence
190
265
 
191
266
  `briefSchema` in `vinaya.config.json` is yours: `vinaya upgrade` preserves it wholesale and never rewrites it. On its own that ownership has a silent cost — nothing else reads it either, so a builtin deleted to work around a defect stays deleted, with no later upgrade to repair it and nothing to surface it.
@@ -16,11 +16,11 @@ provenance: >
16
16
  they are not in the surface map of a task that only meant to add a gate:
17
17
 
18
18
  - packages/aeg-core/src/registry-parse.test.ts asserts the TOTAL row count
19
- across all three rings (currently 42).
19
+ across all three rings (currently 47).
20
20
  - packages/aeg-core/src/markdown-table.test.ts asserts per-ring counts
21
- (currently ring 0 = 16, ring 1 = 19, ring 2 = 7).
21
+ (currently ring 0 = 17, ring 1 = 22, ring 2 = 8).
22
22
  - packages/aeg-core/src/docs/node-route.test.ts asserts the ring-0 count
23
- (currently 16) alongside the actions/roles/contracts counts.
23
+ (currently 17) alongside the actions/roles/contracts counts.
24
24
 
25
25
  Bumping those numbers is a mechanical consequence of a correct edit, not a
26
26
  design change — fix them in the same commit and say so in the PR body.
@@ -97,6 +97,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
97
97
  | A task branch's first push (dispatch re-check) | Ever found out mid-push that your task was never dispatchable? | hook | Re-runs the dispatch-readiness gate once, on a task branch's first push, before its pull request exists. | **`check-first-push-dispatch` standalone shim** (wraps the unchanged `verify-dispatch` gate mode — see the dedicated `verify-dispatch` row above; the registered `first-push-dispatch` check classifies the same readiness through the shared evaluator) | On a `task/*/*` branch with no pull request yet, `verify-dispatch`'s `dispatch-readiness:` line reads `READY`. Only that line is read — never the combined exit code, which also folds in leftover-detection and would false-block every push after the first on a task legitimately mid-flight. Fail-open on infrastructure, loudly: a failed `gh auth status` probe (or `verify-dispatch`'s own infra marker) classifies as `UNKNOWN` and the push is allowed; unparseable output refuses — fail loud, never silently allow. Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/check-first-push-dispatch.ts` |
98
98
  | A task branch's first push (Issue self-assignment) | Ever had no idea who was actually working a task? | hook | Assigns the task's Issue to the authenticated pusher on the branch's genuinely first push — visibility automation, deliberately not a gate. | **`assign-task-issue` standalone shim** (the registered `issue-assignment` check mirrors it; the Issue-self-assignment note below this table is the same mechanism's prose record) | Nothing — this row names a mechanism, not a refusal. Fail-open by contract: it runs after every blocking gate has passed, degrades every failed forge call to a skip, warns and exits 0 on any API failure, and nothing gates on its exit code — a failed assignment can never block a legitimate push. Idempotent: an already-assigned Issue is a no-op, and a push to an already-existing branch never re-triggers it. | repo-own | `packages/aeg-core/bin/assign-task-issue.ts` |
99
99
  | Committing or pushing while checked out on the default branch | Ever committed straight onto main because you forgot to cut a worktree first? | hook | Refuses a commit or push whose current branch IS the repo's default branch — mechanizing the worktree-plus-PR rule at ring 0 for every adopter, not just this repository's own hand-written pre-push script (the `git push` row above, `repo-own`, predates this check and covers a different class: a push whose destination *ref* is the default branch, not a local checkout parked on it). Registered in `coreCheckRegistry()` (task 9), so `vinaya init` ships it to every adopter through the generated `check --all --local` hooks — closing the gap where this rule previously reached only this repo's own maintainers. | **`main-branch-refusal` check** (`vinaya check main-branch-refusal`, part of `vinaya check --all --local`) | The discriminator is the SYMBOLIC current branch, not any derived name: `git symbolic-ref --short HEAD` equaling the repo's default branch refuses; a detached HEAD (every CI checkout, or a deliberate `git checkout --detach`) has no symbolic branch at all and always passes; any other named branch passes. A plain `vinaya check --all` run while parked on the default branch locally WILL refuse — intended, since work belongs in a worktree, never a bug. The default branch is derived from the already-fetched local `refs/remotes/origin/HEAD`, never hardcoded as `main`; when that ref cannot be resolved (never fetched, or a remote other than `origin`) the check fails OPEN with a `warning` finding naming why, rather than risk a false refusal. Otherwise a real failure (`error`, exit 1) — this is an action refusal, not a doctrine-parity report, so warn-only would be the check refusing to do its one job; it cannot redden existing CI, since CI always runs on a detached HEAD. Local-only: both facts come from `git` alone, no forge call. The ring-2 `direct-push` audit (`checkDirectMainPush`, `direct-main-push.ts`) stays the untouched, unrelated detection backstop for a commit that lands on the default branch some other way this hook cannot reach at all (a repo-admin action, or an adopter who never ran `init`). | product | `apps/cli/src/checks/bin/check-main-branch-refusal.ts` |
100
+ | Committing when the token-metering adapter is wired but unreachable | Ever had a host that could genuinely meter itself silently report `—` because one specific path broke? | hook | Refuses a commit when the token-metering probe finds a wiring point resolved — a transcript pointer naming a path — but cannot reach what it names. A host never wired to meter at all still passes unchanged. | **`token-collection-wired` check** (`vinaya check token-collection-wired`, part of `vinaya check --all --local`) | `resolveMeteringCapability`'s verdict is `capable: true`, or `capable: false` with reason `no-transcript-resolved` — no pointer at all, or one that provably belongs to another session. That is the sanctioned operator-metered case (**Operator-metered** — a role whose host exposes no usage figure to the agent, leaving a human the only source), and it covers a plain human terminal and a second agent session whose own Stop hook has not fired yet. Refusal needs the pointer to be THIS session's on one of two grounds, and only one of them involves a session id: either its recorded id matches ours (`transcript-unreadable`, `transcript-empty` — we followed our own pointer and could not reach figures), or the id could not be read at all and the file nonetheless sits at this project's own pointer path owned by this user (`pointer-unusable` — the pointer itself is unreadable or malformed, which is broken wiring whoever wrote it, cleared by removing the file). Local and offline only: no PR body is read (none exists yet at pre-commit) and no network call is made. | product | `apps/cli/src/checks/bin/check-token-collection-wired.ts` |
100
101
 
101
102
  **Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
102
103
 
@@ -112,7 +113,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
112
113
 
113
114
  Every pull request, on open and on every push, re-runs the same checks in CI:
114
115
 
115
- > **Packaging (this repo):** every deterministic row below runs inside **one job — `vinaya check --all --diff-only`** in `.github/workflows/vinaya-checks.yml` (the generated, vinaya-managed workflow), not as its own job: one checkout/build, with each check reporting its own pass/fail line in that job's log. The two exceptions are `Typecheck + unit tests` (the hand-written `.github/workflows/ci.yml`) and the Review gate (its own required check in `.github/workflows/vinaya-review.yml`, re-triggered by `vinaya-review-verdict.yml` when a verdict comment lands). Same checks, same strictness, at ring 0 and ring 1 — the runner is one codebase invoked from both.
116
+ > **Packaging (this repo):** every deterministic row below runs inside **one job — <!-- AEG:QUOTES-FILE:START:.github/workflows/vinaya-checks.yml -->`vinaya check --all --diff-only`<!-- AEG:QUOTES-FILE:END -->** in `.github/workflows/vinaya-checks.yml` (the generated, vinaya-managed workflow), not as its own job: one checkout/build, with each check reporting its own pass/fail line in that job's log. The two exceptions are `Typecheck + unit tests` (the hand-written `.github/workflows/ci.yml`) and the Review gate (its own required check in `.github/workflows/vinaya-review.yml`, re-triggered by `vinaya-review-verdict.yml` when a verdict comment lands). Same checks, same strictness, at ring 0 and ring 1 — the runner is one codebase invoked from both.
116
117
 
117
118
  | CI check | Summary | Category | Description | Re-verifies | Audience | implementation |
118
119
  | --- | --- | --- | --- | --- | --- | --- |
@@ -127,7 +128,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
127
128
  | 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` |
128
129
  | 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. | product | `packages/aeg-core/bin/verify-review-gate.ts` |
129
130
  | 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` |
130
- | 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` 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 seeded `packages/aeg-core/bin/*.ts` set.) **Scaffold-writer** (task 22): `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), or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered aeg-core bin) 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 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` |
131
+ | 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` |
131
132
  | 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-0 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
132
133
  | 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` |
133
134
  | 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` |
@@ -137,10 +138,17 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
137
138
  | 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`. | product | `apps/cli/src/checks/bin/check-doctrine-portability.ts` |
138
139
  | No new on-disk state | Ever had a file and the forge disagree about the same fact? | ci | Blocks a diff that creates a new on-disk state file duplicating what the forge already derives. | No live per-tranche topology file appears under `aeg-root/tranches/` (a top-level file there fails on add or edit; under `completed/**` only genuinely NEW files are refused, so the legacy archives stay editable), and no new `*.tokens.md` appears anywhere — token figures live in the PR body, and the ledger row is appended post-merge. Status-aware (`git diff --name-status`), never fired by a deletion; a rename counts as an addition at its new path. The registered `no-disk-state` check runs the same `isNewDiskStateFile` predicate at ring 0 (the managed hooks) and here; this row names the standalone CLI shim of that gate. | product | `packages/aeg-core/bin/check-no-disk-state.ts` |
139
140
  | 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` — this repo's own corpus carries one pre-existing genuine escape (`packages/sources/src/commands-router-coverage.test.ts` reaching into `apps/cli/src/index.ts`, structurally the same incident this check exists to catch), and a blocking check on day one would fail this repo's own CI on that finding before a separate task has triaged fixing it. | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
141
+ | 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` |
142
+ | 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 six 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 six 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` |
143
+ | 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` |
140
144
 
141
145
  **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.
142
146
 
143
- **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 always 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.
147
+ **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.
148
+
149
+ **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.
150
+
151
+ **quoted-command rollout:** ships report-only, same precedent as every row above — findings print at `warning` severity and the check's own exit code always stays `0`, so registering it cannot newly redden any existing install, and coverage starts at exactly one annotated quote (this page's own CI-invocation callout, above) rather than a retroactive sweep. Graduating to blocking, and any waiver-label escape that graduation would need, are both deliberately deferred to a later, separately-dispatched task once the marker's real-world false-positive rate is observed; v1 carries no such escape.
144
152
 
145
153
  Red CI is now unmergeable by any actor — the repository ruleset's `required_status_checks` rule carries `bypass_actors: []`, so the Principal has no ambient override either. The Principal's override paths are explicit and auditable instead: edit the ruleset itself, or apply an actor-verified `vinaya/waiver:review` label to skip the review gate specifically.
146
154
 
@@ -156,7 +164,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
156
164
  | **Staleness audits** | Ever had documentation flatly contradict a decision everyone agreed to? | event | Flags documentation that has fallen behind the decisions it is meant to follow. | Dispatched periodically | Documentation whose claims contradict recorded decisions; each contradiction becomes a tracked fix Issue | repo-own | |
157
165
  | Direct-main-push detection | Ever had someone push straight to main and nobody noticed for a while? | event | Catches pushes that reached main anyway, including from writers the hooks cannot reach. | Every push to `main`, or manual `workflow_dispatch` (task 31) | Whether the pushed commit is introduced by a merged pull request, via the commits→pulls association API. No associated merged PR → opens an incident Issue (`aeg:direct-main-push` label, idempotent per SHA) and **fails the run loudly** — the one ring-2 check in this table that IS allowed to go red, because nothing it does blocks a pull request or a merge; it only makes an already-irreversible direct push visible. **Detection only, never mutation** — no revert, no force-push; permissions are read-only plus `issues: write`, including the `pull-requests: read` the association API requires — as shipped by task 24 that scope was missing, so the job 403'd (`Resource not accessible by integration`) on every real `main` push instead of detecting anything; fixed by task 31 (item 2). | repo-own | `packages/aeg-core/bin/check-direct-main-push.ts` |
158
166
  | Dead-branch-push audit | Ever kept committing to a branch after its PR had already merged? | event | Catches commits still landing on a branch whose pull request already resolved. | Scheduled (`0 2 * * *`) or manual `workflow_dispatch` | Every remote `task/*` branch whose most recent PR already resolved (`MERGED`/`CLOSED`): flags it when the branch's current tip commit is dated strictly after that resolution — commits kept landing on a branch after the forge considered its work done (the exact "six topology rows landed on a merged plan PR's branch" incident class, 2026-07-03, now caught within a day instead of never). `aeg:dead-branch-push` label plus one idempotent tracking comment on the branch's own PR. **Notification only, never a gate** — `continue-on-error: true`. (2026-07-13: this job previously also ran a stuck row-adjacent-blocker check, `stale-blocker.ts`, retired along with the dispatch-gate predicate it watched for.) | repo-own | `packages/aeg-core/bin/dead-branch-audit.ts` |
159
- | Token self-report (Claude Code collection adapter) | Ever seen a cost figure nobody could trace to a source? | event | Collects a role's exact token usage on **one** host, by reading that host's own session transcript, and emits the line the PR-body token report is built from. The obligation to report tokens is host-agnostic doctrine; this row is only the adapter that satisfies its collection step on Claude Code, never the requirement itself — an adopter on another harness collects by their own means and ships no equivalent of this row. | By each role that is **self-metering** — its host exposes the session's own usage to the agent — at turn-end, before its pull request opens or re-pushes; mandated by the role docs, not hook-fired | Reporting, not enforcement — it refuses no repo action; its refusals guard only its own output. A stale transcript pointer (written by a previous session in a reused worktree) or a transcript yielding zero usage data throws loudly instead of emitting a plausible-looking wrong or zero figure — the misattribution and fabrication classes that would otherwise flow into the PR body and, from there, the post-merge token ledger. | repo-own | `packages/aeg-core/bin/report-tokens.ts` |
167
+ | Token self-report (Claude Code collection adapter) | Ever seen a cost figure nobody could trace to a source? | event | Collects a role's exact token usage on **one** host, by reading that host's own session transcript, and emits the line the PR-body token report is built from. The obligation to report tokens is host-agnostic doctrine; this row is only the adapter that satisfies its collection step on Claude Code, never the requirement itself — an adopter on another harness collects by their own means and ships no equivalent of this row. | By each role that is **self-metering** — its host exposes the session's own usage to the agent — at turn-end, before its pull request opens or re-pushes; mandated by the role docs, not hook-fired | Reporting, not enforcement — it refuses no repo action; its refusals guard only its own output. A stale transcript pointer (written by a previous session in a reused worktree) or a transcript yielding zero usage data throws loudly instead of emitting a plausible-looking wrong or zero figure — the misattribution and fabrication classes that would otherwise flow into the PR body and, from there, the post-merge token ledger. | repo-own | `apps/cli/src/commands/tokens.ts` |
160
168
  | Published lifecycle audit | Ever wondered if the artifact people actually install still works? | event | Runs the full shipped-command lifecycle against the real published `@attalabs/vinaya` npm artifact — never this workspace's own source. | Weekly (`schedule`), or manual `workflow_dispatch` | Whether the published artifact still passes the shipped-command lifecycle. The window between merging a Version Packages PR and running `changeset publish` by hand names a version the registry does not have yet — a guard step checks the registry first and skips with a loud `::notice::` (exit `0`) in that window, so a red here always means the published artifact is broken, never "not published yet." No secrets — installs a public package. | repo-own | `.github/workflows/published-lifecycle.yml` |
161
169
 
162
170
  ---
@@ -42,7 +42,9 @@ A Milestone stores no status. Its lifecycle (§4) is computed from its tranches,
42
42
 
43
43
  The **Architect** (`roles/architect.md`) is the only role that creates a Milestone, via `vinaya milestone create --title <title> --body-file <path>`. The Planner does not create Milestones — a tranche it plans needs no Milestone at all, and most don't (`tranche-model.md` §6). `checkMilestoneShape` refuses a malformed body — missing goal, a `Release:` field present but not a version, an unparseable `Tranche intents` section — before any write reaches the forge, the same discipline `checkIssueRationale` applies to a task Issue.
44
44
 
45
- Moving an **existing** tranche into a Milestone is a separate act: `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]`, never done by hand. It reattaches every Issue carrying each named tranche label to the target Milestone's native `milestone` field (GitHub-view hygiene — no reader in this model consults that field, only the label — the same reason `open-issue.ts` auto-attaches a new task Issue to its tranche's Milestone at creation time), then closes (never deletes) each slug's old legacy tranche-Milestone. `checkAdoptable` gathers every fact for every named slug and refuses the whole invocation before any write — an unknown slug, a slug whose label carries no Issues, a target that doesn't exist or is closed, a slug already adopted elsewhere — so one bad slug blocks the whole call, never a partial move.
45
+ Moving an **existing** tranche into a Milestone is a separate act: `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]`, never done by hand. It reattaches every Issue carrying each named tranche label to the target Milestone's native `milestone` field (GitHub-view hygiene — no reader in this model consults that field, only the label — the same reason `vinaya issue create` auto-attaches a brand-new task Issue to its tranche's open Milestone at creation time, resolving `resolveMilestoneAttachTarget`'s legacy-title-or-intent-declared match to the Milestone's own title before handing it to `gh`), then closes (never deletes) each slug's old legacy tranche-Milestone. `checkAdoptable` gathers every fact for every named slug and refuses the whole invocation before any write — an unknown slug, a slug whose label carries no Issues, a target that doesn't exist or is closed, a slug already adopted elsewhere — so one bad slug blocks the whole call, never a partial move.
46
+
47
+ Correcting an already-written Milestone's goal or `Release:` field — `vinaya milestone edit <n> --body-file <path>`, gated by the same `checkMilestoneShape` check `create` uses — belongs to neither role above. Revising a stated goal is the same product call declaring it was (§5, stage 1: "the goal and its scope are a product call, not a derivable fact"), so it is the Principal's (`roles/principal.md` "What the Principal owns"), never the Architect's (its one write is create-once, `roles/architect.md` "What you own") and never the Planner's (its altitude is tranche shape, not Milestone-body grammar).
46
48
 
47
49
  ---
48
50
 
@@ -62,7 +62,7 @@ If any of these isn't true, refuse rather than write a Milestone `checkMilestone
62
62
 
63
63
  ## What you own
64
64
 
65
- **The Milestone, once.** `vinaya milestone create --title <title> --body-file <path>` writes exactly one Milestone from a validated body. There is no `edit` — a Milestone's title and goal, once written, are corrected by whoever owns Milestone editing next (out of this task's surface; see `milestone-model.md` §3 for what `adopt` does and does not cover). You do not maintain a Milestone across its life; you declare it once.
65
+ **The Milestone, once.** `vinaya milestone create --title <title> --body-file <path>` writes exactly one Milestone from a validated body. `vinaya milestone edit <n> --body-file <path>` exists and is wired — it runs the identical `checkMilestoneShape` gate `create` does but you do not invoke it. Correcting a Milestone's goal or `Release:` field after it is written is the same class of call as declaring it in the first place, not a bug fix on your own output: `milestone-model.md` §5 already calls the goal and its scope "a product call, not a derivable fact," and a correction to that goal is that same call made again. That call is the Principal's (`roles/principal.md` "What the Principal owns"), not yours. You do not maintain a Milestone across its life; you declare it once.
66
66
 
67
67
  **The `Release:` grammar.** Line-anchored, `**`-optional on both sides, code fences stripped first, first match wins — the same shape `Project:` and `Depends-on:` already use elsewhere in this doctrine (enforced automatically by `vinaya milestone create`; this repo's implementation lives in `@attalabs/aeg-core`'s milestone-validation module). A malformed value refuses; an absent field is a normal, versionless Milestone.
68
68
 
@@ -75,7 +75,7 @@ If any of these isn't true, refuse rather than write a Milestone `checkMilestone
75
75
  - **Cut a task Issue.** That's the Planner's canonical plan act (`roles/planner.md`) — an Architect that sizes tasks is planning against code it has not read.
76
76
  - **Size a task, or decide a tranche's dependency/conflict edges.** Not your altitude.
77
77
  - **Parse a version out of the title.** The title is free text for humans. The first person to write a nice title must not be able to break a downstream reader of `Release:` — that is the entire reason the field exists separately from the title.
78
- - **Move, close, or edit an existing Milestone.** Adoption and movement are a later tranche's job (`milestone-model.md` §3, `vinaya milestone adopt`) — you create, once, and stop.
78
+ - **Move, close, or edit an existing Milestone.** `vinaya milestone edit` exists and is shipped, but editing a Milestone's already-declared goal or `Release:` field is a product call, not a maintenance task on your own artifact — that's the Principal's (`roles/principal.md`). Adoption and movement are a later tranche's job (`milestone-model.md` §3, `vinaya milestone adopt`) — you create, once, and stop.
79
79
  - **Write status anywhere.** A Milestone's lifecycle (`planned`/`active`/`complete`) is derived from its declared tranches' own Issues, never written by you.
80
80
 
81
81
  ---
@@ -93,7 +93,7 @@ Work through this checklist for the merged task. Confirm each against reality
93
93
  2. **Docs updated.** The tier-required docs the brief listed actually moved. (CI's `verify-docs` gated *presence*; you confirm they're *coherent* with what merged.)
94
94
  3. **Non-derivable facts recorded as Issues — never as a state document.** The per-project state layer (`state.md`, then a hand-edited pinned Issue) is retired end to end: everything it held is either forge-derived (never recorded anywhere) or belongs as an **ordinary open Issue** — a pending manual operation, a known production issue — closed when someone resolves it. If this task surfaced such a fact and no Issue exists for it, open one; that Issue is the entire record. (`now.md` no longer exists either.)
95
95
  4. **`docs-index.md`** updated if files were added, removed, or renamed.
96
- 5. **Token ledger rows recorded.** No role appends its own row on a task branch. **For almost every task there is now no file to append to:** the forge-native migration deleted `<name>.tokens.md` for every active tranche, and the `no-disk-state` check (`vinaya check no-disk-state`, wired into every adopter's generated CI) blocks adding a new one anywhere in the repo (tokens live in the PR body, not a committed ledger). Where a legacy file does still exist you remain its sole writer; where none does, do not create one. Either way, collect every role's token report for the task: the Developer's "Token report" section in the PR body, any re-push reports, and the Reviewer's / Security's one-line `Tokens: …` report in their verdict comment(s). For each report found, append one row (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`) — one row per role-turn, including re-entry rows (a second Developer turn, a re-review), and including your own turn (`Phase: <task-id>: archive`, `Role: Archivist`). Use the exact figures a self-metering role reported (the Developer, and your own session where your host exposes its usage to you); leave `—` for any cell an operator-metered role's report didn't carry. If a role's report is missing entirely (e.g. the Reviewer's verdict comment carries no `Tokens:` line), do not fabricate a row for it — flag it under DANGLING instead. **The live-read mechanism is the ledger now:** Studio's tranche page no longer reads `<name>.tokens.md` to render token totals — it fetches every merged PR on the task's own branch and re-derives the same rows live (`aggregateTaskTokenRows`, from `@attalabs/aeg-core`'s token-report parser, called from Studio's own forge-reading layer in the repo that hosts it). Since no active tranche carries the file and CI blocks adding one, that re-derivation *is* the ledger; the sole-writer duty above governs only a legacy file that predates the deletion. One real, load-bearing gap in the live mechanism, discovered building it: it can only recover rows from a PR's own body/comments, so it cannot see your own `Phase: <task-id>: archive` row (you have no PR to report it through) or the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task without false-positive cross-task matches — see that file's own docstring). Those two sources therefore have **no durable home today** — that is a known, open gap, not a licence to create a `.tokens.md` for them.
96
+ 5. **Token ledger rows recorded.** No role appends its own row on a task branch. **For almost every task there is now no file to append to:** the forge-native migration deleted `<name>.tokens.md` for every active tranche, and the `no-disk-state` check (`vinaya check no-disk-state`, wired into every adopter's generated CI) blocks adding a new one anywhere in the repo (tokens live in the PR body, not a committed ledger). Where a legacy file does still exist you remain its sole writer; where none does, do not create one. Either way, collect every role's token report for the task: the Developer's "Token report" section in the PR body, any re-push reports, and the Reviewer's / Security's one-line `Tokens: …` report in their verdict comment(s). For each report found, append one row (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`) — one row per role-turn, including re-entry rows (a second Developer turn, a re-review), and including your own turn (`Phase: <task-id>: archive`, `Role: Archivist`). Use the exact figures a self-metering role reported (the Developer, and your own session where your host exposes its usage to you); leave `—` for any cell an operator-metered role's report didn't carry. If a role's report is missing entirely (e.g. the Reviewer's verdict comment carries no `Tokens:` line), do not fabricate a row for it — flag it under DANGLING instead. **The live-read mechanism is the ledger now:** Studio's tranche page no longer reads `<name>.tokens.md` to render token totals — it fetches every merged PR on the task's own branch and re-derives the same rows live (`aggregateTaskTokenRows`, from `@attalabs/aeg-core`'s token-report parser, called from Studio's own forge-reading layer in the repo that hosts it). Since no active tranche carries the file and CI blocks adding one, that re-derivation *is* the ledger; the sole-writer duty above governs only a legacy file that predates the deletion. One real, load-bearing gap in the live mechanism: it can only recover rows from a PR's own body/comments, so it cannot see the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task without false-positive cross-task matches — see that file's own docstring). Your own `Phase: <task-id>: archive` row is not part of that gap: `vinaya archive` folds it into the same provenance comment it posts on the merged task PR, so the live mechanism recovers it the same way it recovers every other role's row on that PR. That leaves the Planner's tranche-wide report as the one source with **no durable home today** — a known, open gap, not a licence to create a `.tokens.md` for it.
97
97
  6. **Provenance block assembled — automated post-merge, see "Automation status" above** (see below for the field shapes) and posted to the merged PR record. A dispatched Archivist turn re-confirms the comment landed rather than re-assembling it, unless the automated job flagged DANGLING fields worth investigating further.
98
98
 
99
99
  ## The provenance block
@@ -163,4 +163,4 @@ You are the last step of the flow (`process.md`) — the close-out that sits und
163
163
 
164
164
  ## Turn-end: record the ledger rows for every role that turned on this task
165
165
 
166
- You are the **sole writer** of `aeg-root/tranches/<name>.tokens.md` for a task branch — no other role appends its own row. **In practice there is usually no such file:** it is deleted for every active tranche and CI blocks creating a new one (item 6 above), so for those tasks the rows live in the forge — the Developer's PR-body "Token report" entries and the reviewing roles' `Tokens: …` verdict lines — and are re-derived on read. Do not create the file to have somewhere to write. Only when a legacy file survives do you append to it; then append one row per role-turn you collected in item 7 above (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`), then append your own turn's row last (`Phase: <task-id>: archive`, `Role: Archivist`). When you run as automation on a host that exposes your session's own usage to you, you are **self-metering** — a role that can read its own figures directly — for your own row: collect them by that host's mechanism and fill your numeric cells with the exact values (on this repo's toolchain that is `bun packages/aeg-core/bin/report-tokens.ts --phase "<task-id>: archive" --role Archivist`, run against your own session; another host is satisfied by its own means — and if this specific script is unreachable on a self-metering host, read the transcript/log yourself rather than writing `—`, since `—` is sanctioned only when the host itself has no usage figure to expose, not when one adapter for reading it is missing). When your host exposes nothing, you are **operator-metered** — the host offers no usage figure to the agent — and you leave those cells `—`, the same rule every other role follows (`tranche-model.md` §12). Every row you record for another role carries whatever that role reported — `—` where the report itself had no numeric figure; you never estimate, and you never fill in another role's cell yourself. Drift cron: as part of the close-out checks, flag any merged task in this tranche that has **no Developer row** for `<task-id>: develop` (the role obligation was missed), and any inline `## Token ledger` section that violates the append-only rule (an existing row was edited rather than a new one appended). The ledger is append-only.
166
+ You are the **sole writer** of `aeg-root/tranches/<name>.tokens.md` for a task branch — no other role appends its own row. **In practice there is usually no such file:** it is deleted for every active tranche and CI blocks creating a new one (item 6 above), so for those tasks the rows live in the forge — the Developer's PR-body "Token report" entries and the reviewing roles' `Tokens: …` verdict lines — and are re-derived on read. Do not create the file to have somewhere to write. Only when a legacy file survives do you append to it; then append one row per role-turn you collected in item 7 above (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`), then append your own turn's row last (`Phase: <task-id>: archive`, `Role: Archivist`). When you run as automation on a host that exposes your session's own usage to you, you are **self-metering** — a role that can read its own figures directly — for your own row: collect them by that host's mechanism and fill your numeric cells with the exact values (that is `vinaya tokens --phase "<task-id>: archive" --role Archivist`, run against your own session; another host is satisfied by its own means — and if this specific command is unreachable on a self-metering host, read the transcript/log yourself rather than writing `—`, since `—` is sanctioned only when the host itself has no usage figure to expose, not when one adapter for reading it is missing). When your host exposes nothing, you are **operator-metered** — the host offers no usage figure to the agent — and you leave those cells `—`, the same rule every other role follows (`tranche-model.md` §12). Every row you record for another role carries whatever that role reported — `—` where the report itself had no numeric figure; you never estimate, and you never fill in another role's cell yourself. Drift cron: as part of the close-out checks, flag any merged task in this tranche that has **no Developer row** for `<task-id>: develop` (the role obligation was missed), and any inline `## Token ledger` section that violates the append-only rule (an existing row was edited rather than a new one appended). The ledger is append-only.
@@ -111,7 +111,7 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
111
111
 
112
112
  *How* you obtain the figures is host-specific and is the one part of this obligation that differs by toolchain (`tranche-model.md` §12 calls this layer 2). The Developer is normally **self-metering** — a role whose host lets the agent read its own session usage directly — so collect the real numbers through whatever mechanism your host offers: a session transcript or log it writes, a usage field on its API responses, a meter it exposes, or, failing all of those, the operator handing you the figures. Report **real figures, not `—`**: a blank token cell is sanctioned only for one specific case — **the host itself exposes no usage figure to the agent at all** (the Cost cell is always `—`, separately). **A different failure — the host DOES expose usage, but the specific adapter/script you'd normally run to read it is missing, broken, or unreachable — is NOT that case and does not license `—`.** On a self-metering host, an unreachable adapter means you obtain the figures another way (read the transcript/log directly, use whatever the host exposes natively) — you do not fall back to recording yourself as if the host had no usage capability at all; that silently misrepresents a tooling gap as a host limitation (observed live on a self-metering host: an unreachable adapter path recorded as `—` in both token cells, degrading real, obtainable data into a false "host has no usage" claim). Never estimate. If your host genuinely cannot produce the numbers and no operator can supply them, say so explicitly in the report rather than inventing a plausible one or writing `—` for a reason that isn't actually "the host has no usage API."
113
113
 
114
- > **On this repo's toolchain (Claude Code), one command does it:** `bun packages/aeg-core/bin/report-tokens.ts --phase "<task-id>: develop" --role Developer`, which reads the session transcript and emits the line to paste. Pass `--transcript <path>` when you already know which transcript is yours. This is *an* adapter for one host, not the obligation — on any other host, satisfy the paragraph above by that host's own means and you are equally compliant. If this specific script is unreachable on this repo's own toolchain, that is the adapter-unreachable case above, not the host-has-no-usage case: read the transcript yourself rather than writing `—`.
114
+ > **On a Claude Code host, one command does it:** `vinaya tokens --phase "<task-id>: develop" --role Developer`, which reads the session transcript and emits the line to paste. Pass `--transcript <path>` when you already know which transcript is yours. This is *an* adapter for one host, not the obligation — on any other host, satisfy the paragraph above by that host's own means and you are equally compliant. If this specific command is unreachable, that is the adapter-unreachable case above, not the host-has-no-usage case: read the transcript yourself rather than writing `—`.
115
115
 
116
116
  The per-task Archivist reads this report at close-out and appends the ledger row post-merge — see `roles/archivist.md`. Re-entry (a second turn after `CHANGES_REQUESTED`) adds a **new** "Token report" entry to the PR body — never edits the first.
117
117
 
@@ -205,7 +205,7 @@ All of the following must pass before the PR is opened:
205
205
  - [ ] Code passes lint/format (this repo: `bun run format-and-lint`)
206
206
  - [ ] Tests pass if applicable (this repo: `bun test`)
207
207
  - [ ] PR description follows the template, carries the brief, and declares `Tier: 0`
208
- - [ ] "Token report" section in the PR body carrying your turn's real token figures, collected by whatever means your host offers (see the token-reporting section above; on this repo's toolchain, `bin/report-tokens.ts`) — and again on each re-push after `CHANGES_REQUESTED`; the Archivist appends the ledger row post-merge, you do not
208
+ - [ ] "Token report" section in the PR body carrying your turn's real token figures, collected by whatever means your host offers (see the token-reporting section above; on a Claude Code host, `vinaya tokens`) — and again on each re-push after `CHANGES_REQUESTED`; the Archivist appends the ledger row post-merge, you do not
209
209
 
210
210
  ### Tier 1 checklist
211
211
 
@@ -330,6 +330,7 @@ After every commit: `git log --oneline -3` to confirm the new commit is a direct
330
330
  - Reference the task's Issue in the PR body (`Closes #N`), not necessarily in every commit message
331
331
  - Do not include agent self-attribution / "generated by" trailers in commit messages
332
332
  - Never skip verification hooks on commits unless the brief explicitly authorizes it
333
+ - A change to a published package's shipped files carries its `.changeset/*.md` entry in the same PR — the `changeset-coverage` check (`aeg-root/enforcement.md`) reports, but does not yet block, a diff that misses this
333
334
 
334
335
  ---
335
336
 
@@ -88,7 +88,7 @@ Before planning, you MUST verify every one of these and explicitly confirm them
88
88
  5. **The relevant prior decisions are known.** You've read the specs and skills that bear on this work, so you don't plan a task that re-litigates a settled call. A past choice's reasoning lives in the spec it governs and in the pull request that made it. If you cannot reach them, STOP.
89
89
  6. **The registry resolves every project you'll assign** (`.vinaya/projects.md`) — see the entry gate. "Resolves" is a claim about a file, so back it with the file: for each `Project:` name you will declare, quote that name's exact registry row text in your readiness check. A bare "all projects resolve" with no quoted rows is a self-attestation, not a check — the unregistered name it hides surfaces later, at Issue creation (`checkProjectsRegistered`), after the whole plan is already built on it.
90
90
  7. **Open ambiguities are surfaced, not assumed.** If, after the above, real decisions remain unmade (which DB owns this? is structured output required on all vendors?), collect them and put them to the Principal BEFORE planning — do not pick an answer and plan on top of a guess. A plan built on an unstated assumption is a plan that ships the wrong thing.
91
- 8. **Previous tranches on each in-scope product are archived.** For every product in scope, confirm the previous tranche on that product is in `aeg-root/tranches/completed/`. If any prior tranche on an in-scope product exists in `aeg-root/tranches/` but NOT in `completed/`, the Tranche Archivist has not run STOP: *"The previous tranche `<name>` on `<product>` has not been archived. Dispatch the Tranche Archivist for it before planning proceeds."* The contract governing this gate is `aeg-root/contracts/tranche-archivist-planner.md`. **Supersession carve-out:** this gate does NOT apply to a prior tranche that *this* plan is superseding — i.e. absorbing `todo`/backlog tasks from. For that one source tranche you refactor it in-place during this plan (see "Tranche refactor & cross-tranche task-movement" below) and the Tranche Archivist archives it *after* the plan lands; the order is refactor-and-plan → then archive, never archive-then-plan. The gate still fully applies to every *unrelated* prior tranche.
91
+ 8. **A finished-but-unarchived prior tranche on each in-scope product is caught.** For every product in scope, check each other active tranche that touches it: if it has **zero open task Issues** (`vinaya/tranche:<slug>`-labeled, `--state open`) i.e. it is finished but the Tranche Archivist has not run for it, STOP: *"The previous tranche `<name>` on `<product>` is finished but not archived. Dispatch the Tranche Archivist for it before planning proceeds."* A prior tranche with genuinely open task work is **not** a blocker here — it is an active sibling, not an unarchived one; the correct instrument for it is a declared `conflicts-with` edge wherever the two tranches' surfaces actually overlap (file granularity — strictly better information than product granularity). **Legacy topology file:** for a tranche still carrying a pre-cutover file, the same finished-but-unarchived signal is: the file still sits at `aeg-root/tranches/<name>.md` (not yet moved to `aeg-root/tranches/completed/<name>.md`) *and* it has zero open task Issues. This is the legacy case only — the forge-native default (see the top of this doc) carries no such file to check at all. The contract governing this gate is `aeg-root/contracts/tranche-archivist-planner.md`. **Supersession carve-out:** this gate does NOT apply to a prior tranche that *this* plan is superseding — i.e. absorbing `todo`/backlog tasks from. For that one source tranche you refactor it in-place during this plan (see "Tranche refactor & cross-tranche task-movement" below) and the Tranche Archivist archives it *after* the plan lands; the order is refactor-and-plan → then archive, never archive-then-plan. The gate still fully applies to every *unrelated* prior tranche.
92
92
 
93
93
  **State the readiness check explicitly at the top of your planning pass** — a short "Readiness: I have X, Y, Z; I verified I can read A, B; the following are unresolved and I need answers before I proceed: …". This makes it visible that the gate was run, not skipped. A plan emitted without a passed readiness check is malformed. (This is also conversational-protocol step 6 — announce the gate's result before moving on.)
94
94
 
@@ -294,4 +294,4 @@ AEG defines **task close-out** (the per-task Archivist) and **tranche close-out*
294
294
 
295
295
  ## Turn-end: report your tokens, don't append them
296
296
 
297
- You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — self-append was retired for every role. When the planning session closes, report your tokens instead: `Tokens: planning — Planner — <model> — in/out/cost or — if unknown`, in the plan PR body if one exists, or in your planning report to the Principal otherwise. Planning normally runs **operator-metered** — on a host that exposes no usage figure to the agent — so report `—` for the numeric cells; that host capability is the one sanctioned reason for a blank token cell (`tranche-model.md` §12), never inconvenience, and you never estimate. If your host does expose your own usage to you, report the real figures instead. Re-planning a wave reports again, never edits the prior report. The ledger is append-only.
297
+ You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — self-append was retired for every role. When the planning session closes, report your tokens instead: `Tokens: planning — Planner — <model> — in/out/cost or — if unknown`, in the plan PR body when one exists; when it doesn't, in a comment on the pinned lessons Issue — the existing forge object, never a new file or forge object of your own (your new-file hard gate forbids that regardless). Planning is normally **self-metering**, the same as the Developer, on any host that exposes the session's own usage to the agent (a transcript it writes, a usage field on its responses, a meter it exposes) — collect the real figures through whatever mechanism your host offers and report them; a blank token cell is not the default outcome. **Operator-metered** — a host that exposes no usage figure to the agent at all is the fallback case, not the norm: only then does `—` for the numeric cells become the sanctioned outcome (`tranche-model.md` §12), never inconvenience, and you never estimate. Re-planning a wave reports again, never edits the prior report. The ledger is append-only.
@@ -53,6 +53,7 @@ The Principal holds final authority over:
53
53
  - **The merge button.** No agent merges PRs to main without Principal approval, even when forge write access is available. Merge authority is Principal-only unless the brief explicitly delegates it for a specific PR and the brief was authored by the Brief Author.
54
54
  - **Right to reject.** The Principal can reject a direction at any phase — idea, brief, PR, or post-merge — and the system accepts that without pushback. Agents can surface concerns but not override.
55
55
  - **Ratification.** Type 1 decisions are not ratified until the Principal explicitly says so. PENDING decisions wait; they do not auto-promote.
56
+ - **Editing an already-declared Milestone.** `vinaya milestone edit <n> --body-file <path>` corrects a Milestone's goal or `Release:` field after the Architect's one-time write — the same product call declaring it was (`milestone-model.md` §5), never the Architect's or Planner's (`roles/architect.md` "What you do NOT do").
56
57
 
57
58
  ---
58
59
 
@@ -106,8 +106,8 @@ If you don't have the information to fill a field, write "unknown — Principal
106
106
 
107
107
  ### 3. Close the Milestone
108
108
 
109
- - `gh` has no built-in `milestone` subcommand resolve the Milestone's number by title, then close it via the REST API directly: `gh api "repos/{owner}/{repo}/milestones?state=open" --jq '.[] | select(.title=="<slug>") | .number'`, then `gh api repos/{owner}/{repo}/milestones/<number> -X PATCH -f state=closed`. Closing the Milestone IS the tranche's lifecycle transition to `complete`. This is a forge action, not a repo commit.
110
- - The Issues themselves are already closed (verified in step 1) and stay attached to the closed Milestone — that attachment is the durable historical record; nothing needs to be moved or archived as a file.
109
+ - `vinaya milestone close --slug <slug>` — never the raw `gh api` recipe this step used to carry. It resolves the tranche's Milestone (legacy exact-title match, or an intent-declared match), verifies every Issue carrying `vinaya/tranche:<slug>` is actually attached to it via GitHub's native `milestone` field, and refuses naming each unattached or foreign Issue and its repair path — before closing anything. Closing the Milestone IS the tranche's lifecycle transition to `complete`. This is a forge action, not a repo commit.
110
+ - The Issues themselves are already closed (verified in step 1) and, once `milestone close` has run, are verified attached to the closed Milestone — not merely assumed to be which is what makes that attachment the durable historical record.
111
111
  - **Legacy exception:** if this tranche still has a pre-cutover topology file at `aeg-root/tranches/<name>.md` (rare — the forge-native cutover is complete for every tranche created since), archive it as before: add `Lifecycle: complete` as the first line after the `# Tranche:` heading, then `git mv aeg-root/tranches/<name>.md aeg-root/tranches/completed/<name>.md`. Do NOT delete it — the rationale is durable history. Confirm the move landed and the source path no longer exists.
112
112
 
113
113
  > **Step 4 (a pinned per-project state Issue, hand-edited in place) is retired.** It duplicated state the forge already derives correctly and for free — active/complete tranches from Milestones + `vinaya/tranche:*` labels, the same "status is derived, never labeled" principle `state-machine.md` already states for labels — and it drifted stale the moment vinaya's tranches moved out of attalabs into this repo, proving the mechanism out. "Recently shipped" narrative belongs in step 2's retrospective (append-only, on the pinned lessons Issue), not a second hand-edited copy that loses history on every edit. Nothing in this role's entry gate ever depended on step 4, so no tranche close was ever blocked by its absence — it only ever surfaced as a flagged, non-blocking gap. **`now.md` is retired** for the same reason: "what's next" is derived from the forge by the Planner (`gh issue list --label "vinaya/tranche:<slug>" --state open`), not written to a file.
@@ -9,6 +9,10 @@ sidebar_title: "Template: Issue rationale"
9
9
 
10
10
  [tranche-slug] [n] — [task title, repeated from the Issue title]
11
11
 
12
+ **Tier:** [0 | 1 | 3]
13
+ **Project:** [project(s), comma-separated, matching the blast radius stated in the "Project(s) + blast radius" field below]
14
+ **Type:** [build | chore | docs | feat | fix | perf | refactor | revert | style | test — the commit-type word this task belongs to; free-text metadata, not a forge label]
15
+
12
16
  ## Planner's rationale
13
17
 
14
18
  **Boundary** — [BOUNDARY — what this task is and, crucially, what it is NOT: what was deliberately split out, where the edges sit. Make the in/out sets concrete enough that the Brief Author can bound a file surface from them.]
@@ -30,7 +34,3 @@ sidebar_title: "Template: Issue rationale"
30
34
  ## Origin
31
35
 
32
36
  [ORIGIN — where this task came from: Principal-directed, backlog item, incident follow-up — with dates and the Issue/PR references that motivated it.]
33
-
34
- **Tier:** [0 | 1 | 3]
35
- **Project:** [project(s), comma-separated, matching the blast radius above]
36
- **Type:** [build | chore | docs | feat | fix | perf | refactor | revert | style | test — one, matching a `vinaya/type:*` label]
@@ -64,9 +64,11 @@ The block opens with `Head:` and a `Summary:` line — a file and line count der
64
64
 
65
65
  ## Token report
66
66
 
67
+ <!-- AEG:TOKENS:START -->
67
68
  | Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |
68
69
  |---|---|---|---|---|---|---|
69
70
  | [task-id]: develop | Developer | [model] | [exact in] | [exact out] | [cost] | [YYYY-MM-DD] |
71
+ <!-- AEG:TOKENS:END -->
70
72
 
71
73
  ## Reference — the dispatched brief
72
74
 
@@ -293,7 +293,7 @@ Token reporting is three separable things, and only one of them depends on the a
293
293
  2. **Collection — host-specific.** *How* a role obtains true figures for its own turn. This is the only layer an adapter belongs in, and the only layer that may name a host, a transcript format, or a script path.
294
294
  3. **The destination and the grammar — portable.** The `Tokens: …` line, in the artifact the role's turn already produced: the PR body's "Token report" section for a role that holds a branch, the verdict comment for a reviewing role, the plan PR or planning report for the Planner. The ledger stays append-only, and the Archivist transcribes what each role reported rather than re-deriving it.
295
295
 
296
- The destination was never the coupled part — no role writes a shared ledger file on a task branch, so layer 3 is satisfied through the forge for every task-branch role. Two sources remain the known open gap recorded in `roles/archivist.md`: the Archivist's own row, and the Planner's tranche-wide report. **An adopter implements layer 2 for their own harness and inherits layers 1 and 3 unchanged.** A doctrine sentence that names a tool belongs in layer 2 or nowhere.
296
+ The destination was never the coupled part — no role writes a shared ledger file on a task branch, so layer 3 is satisfied through the forge for every task-branch role. One source remains the known open gap recorded in `roles/archivist.md`: the Planner's tranche-wide report — the Archivist's own row is not part of it (see the "Known gap" paragraph below). **An adopter implements layer 2 for their own harness and inherits layers 1 and 3 unchanged.** A doctrine sentence that names a tool belongs in layer 2 or nowhere.
297
297
 
298
298
  ### Two collection capabilities (the design constraint, not a bug)
299
299
 
@@ -308,11 +308,11 @@ The split is **operator vs. agent, not terminal vs. chat** — a figure a human
308
308
 
309
309
  Which capability applies is a fact about the **host**, not a fixed property of the role: the same role is self-metering on a harness that exposes usage and operator-metered on one that does not. Read the role names above as today's common case, not as an allocation.
310
310
 
311
- Self-metering capture is real, not estimated: the adapter sums the session's own usage records, so a Developer or automated Archivist turn's numbers are exact rather than approximated. Whether it also runs with *no* operator step depends on the host's wiring — where a Stop hook writes a transcript pointer — configured in the repo or in the operator's own host settings — the adapter finds it unaided; with no pointer written, the caller names the transcript with `--transcript <path>`. Either way the figures are read, never guessed. The remaining manual seam is operator-metered roles only — closing it depends on the host giving a session a way to read its own usage, which conversational surfaces generally do not today. **Known gap (flagged, not solved):** tranche-wide operator-metered turns with no task PR to report into a Planner session outside a plan PR, a Brief Author session — have no established recording path; see `roles/planner.md` "Plan-PR close-out."
311
+ Self-metering capture is real, not estimated: the adapter sums the session's own usage records, so a Developer or automated Archivist turn's numbers are exact rather than approximated. Whether it also runs with *no* operator step depends on the host's wiring — where a Stop hook writes a transcript pointer — configured in the repo or in the operator's own host settings — the adapter finds it unaided; with no pointer written, the caller names the transcript with `--transcript <path>`. Either way the figures are read, never guessed. The remaining manual seam is operator-metered roles only — closing it depends on the host giving a session a way to read its own usage, which conversational surfaces generally do not today. **Known gap now partly closed:** tranche-wide operator-metered turns with no task PR to report into no longer lack a destination across the board. A Planner session outside a plan PR now reports into a comment on the pinned lessons Issue — the existing forge object, never a new one (`roles/planner.md` "Turn-end: report your tokens, don't append them"). A Brief Author session already has a destination when a plan PR exists the same plan PR the Planner reports into (`roles/brief-author.md`) — unchanged by this fix. The Archivist's own `<task-id>: archive` row was never actually homeless either: `vinaya archive` folds the Archivist's `Tokens: …` line into the same provenance comment it posts on the merged task PR (`roles/archivist.md` "The provenance block") — a different fact from the narrower Studio live-read caveat that same file still carries, which is about re-deriving ledger totals from a PR's own body/comments, not about where the row is written. What remains genuinely open: a Brief Author session that ends with no PR of any kind yet to write into — mid-authoring, before a plan PR exists — still has nowhere durable to report; it falls back to an ephemeral report to the Principal, and that fallback is not fixed here.
312
312
 
313
313
  ### The collection adapter AEG ships (one layer-2 instance, not the requirement)
314
314
 
315
- AEG ships exactly one collection adapter, for the Claude Code host: `packages/aeg-core/bin/report-tokens.ts`. It reads that host's session transcript — a JSONL file carrying a `usage` object per assistant message — sums it, and emits a layer-3 `Tokens: …` line. Every doctrine citation of it is an **example of one way to satisfy layer 2**, never the requirement; a role on another host satisfies the same obligation by other means and is equally compliant.
315
+ AEG ships exactly one collection adapter, for the Claude Code host, exposed as `vinaya tokens`. It reads that host's session transcript — a JSONL file carrying a `usage` object per assistant message — sums it, and emits a layer-3 `Tokens: …` line. Every doctrine citation of it is an **example of one way to satisfy layer 2**, never the requirement; a role on another host satisfies the same obligation by other means and is equally compliant.
316
316
 
317
317
  **The seam is the summary shape, not the file tree.** An adapter's whole job is to produce a `TranscriptSummary` — four integers (fresh input, cache creation, cache read, output) plus a model id. Everything downstream of that shape is portable and already shipped: `formatTokensLine` renders the grammar, `parse-token-report.ts` reads it back. Two things sit adapter-side because both know host-specific facts: `summarizeTranscript` (`@attalabs/aeg-core`'s Claude Code transcript module), which knows Claude Code's transcript JSONL and its `usage` field names, and the bin itself, which knows how that host points a session at its own transcript. The `bin/` vs `src/` split does **not** mark this seam and never did.
318
318
 
@@ -322,7 +322,7 @@ AEG ships exactly one collection adapter, for the Claude Code host: `packages/ae
322
322
 
323
323
  Vinaya Studio's tranche page no longer reads `<name>.tokens.md` off disk to render token totals — it re-derives the same row shape live off the forge: every MERGED PR on a task's own branch (`task/<tranche>/<id>`), parsing the Developer's "Token report" entries from the PR body (every one, including re-push entries) and the Reviewer's/Security's `Tokens: …` lines from that PR's comments (`@attalabs/aeg-core`'s token-report parser, `aggregateTaskTokenRows`, fetched by Studio's own forge-reading layer in the repo that hosts it). Same row shape, same `sumLedger` totals math (`parse-ledger.ts`) — different source.
324
324
 
325
- This is deliberately **narrower** than what the Archivist collects into the file: it cannot recover the Archivist's own `<task-id>: archive` row (no PR carries it — the file itself is that row's only record) or the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task from the forge alone without false-positive cross-task matches, confirmed live during 4b's build — a task Issue's cross-reference timeline picks up ANY PR that merely mentions its number in passing prose, not just its own plan PR). Both remain recoverable only from `.tokens.md`, which is why the file is not deleted here (task 7's job, once the live mechanism is proven in wider use). A task's report that's missing or malformed (e.g. a "Token report" heading with no table and no parseable text after it) yields no row for that report, never a fabricated one — same discipline as the Archivist's own DANGLING convention.
325
+ This is deliberately **narrower** than what the Archivist collects into the file: it cannot recover the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task from the forge alone without false-positive cross-task matches, confirmed live during 4b's build — a task Issue's cross-reference timeline picks up ANY PR that merely mentions its number in passing prose, not just its own plan PR). The Archivist's own `<task-id>: archive` row is not part of that gap: `vinaya archive` posts it inside the same provenance comment it posts on the merged task PR, so this mechanism recovers it exactly as it recovers every other role's row on that PR. The Planner's report remains recoverable only from `.tokens.md`, which is why the file is not deleted here (task 7's job, once the live mechanism is proven in wider use). A task's report that's missing or malformed (e.g. a "Token report" heading with no table and no parseable text after it) yields no row for that report, never a fabricated one — same discipline as the Archivist's own DANGLING convention.
326
326
 
327
327
  ### Anti-regression
328
328