@attalabs/vinaya 0.22.0 → 0.23.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.
- package/README.md +13 -3
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +26 -20
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
- 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
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -19,8 +19,10 @@ 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
|
|
23
|
-
| `vinaya
|
|
22
|
+
| `vinaya brief render <tranche> <n> --surfaces <glob,...> [--out <path>]` | Emit the twelve-section brief skeleton from the task Issue and the tree, with every mechanically-derivable section filled: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file), §7 from the `.vinaya/doc-owners` derivation, and every remaining section from the Issue's eight-field Planner rationale. Refuses, naming the missing fact, when a derived section cannot be derived: no Issue, the dispatch gate not clear, or a `--surfaces` glob matching no tracked file. Never writes under `aeg-root/` or to the Issue — stdout, or `--out <path>`, only; a brief is pasted to the Developer, never committed. |
|
|
23
|
+
| `vinaya pr report [--write <body-file> \| --push <pr>] [--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, the result of `vinaya check --all --diff-only`, and — Group C, task 12, #387 — every command in the body's §9 fenced Test Plan list, re-run from the PR head with its actual output) and `AEG:TOKENS` (real usage figures collected the same way `vinaya tokens` collects them, rendered into the `## Token report` heading's table). Without `--write`/`--push`, 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. With `--push <pr>`, does the same splice directly against the PR's LIVE body — fetched from the forge, never a local file — and pushes it via `gh pr edit`, then re-reads the live body and refuses (restoring the pre-edit body) unless it agrees with the pre-push body outside the two anchored regions; refuses before writing anything on a live body with no real `AEG:EVIDENCE` pair (never appends one, unlike `--write`), a divergent anchor resolution, or a `<pr>` value that isn't a bare number. When the live body has no real `AEG:TOKENS` pair, the token splice is skipped rather than creating one (same reasoning, softer outcome — `AEG:EVIDENCE` alone is still pushed). `--write` and `--push` are mutually exclusive. `--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. The two incapable classes are kept apart, and never fabricate a `0/0/—`: a session whose own wiring — a pointer whose id matches, or one at this project's own pointer path — was reached and the figures still could not be (`pointer-unusable`, the pointer cannot be read or parsed; `transcript-unreadable`; or `transcript-empty`, it resolves to zero usage records) gets an all-`—` row carrying the probe's reason inline in the Agent/Model cell; a session that resolved no transcript at all — no Stop-hook pointer, or one that cannot be corroborated as this session's — gets **no row and a refusal on stderr** naming `--transcript` and `vinaya tokens --in/--out`, because nothing there established that the host cannot meter, and a blank row would claim it did. The `AEG:EVIDENCE` block is still written/pushed in that case; only the row is withheld. Exits non-zero when any gate in Group B fails, or when the token row was refused. |
|
|
24
|
+
| `vinaya review status <pr>` | Print the review loop's own state for a PR. Line one is `CONTINUE`, `PAUSE: <reason>[ <id>]` for `reappearance` (an id a round marked `resolved` came back `reproduced`), `zero-deaths` (a round resolved nothing it inherited and still raised something new) or `max-rounds` — or, for `stale` (the newest verdict judged a superseded head and no Developer round comment followed), the actionable fact itself: `push after verdict — re-review required`. Line two reads `behind main by <n> — merge first` when the branch is behind its base, or `behind main: unknown — fetch origin/<base> first` when git cannot measure the distance at all; it is absent only when the branch is measurably not behind. Rounds are derived from the PR's own verdict comments — one round per `Judged head:` value, read through the same extractors `review-gate` blocks merges with, and only from comments an allowlisted principal authored; a Developer round comment is recognised by its `<!-- aeg:developer:round-<n> -->` marker at that fixed position, never by scanning its prose. Exit `0` only when the state is `CONTINUE` and the branch is not behind; `1` otherwise, so a script can gate on the exit code without parsing the text. |
|
|
25
|
+
| `vinaya review post --role code-reviewer \| security --pr <n> [--verdict <v>] [--escalate <class> --summary <text>] ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (findings, per-field text) instead of a hand-typed comment. The verdict is DERIVED from the findings file (REQUEST_CHANGES/FAIL iff a BLOCKER/CRITICAL-or-HIGH finding is present) — `--verdict` is optional and refused before posting anything when it disagrees with the derivation, naming the derived value. `--escalate authority \| strategy \| product --summary <text>` posts an `ESCALATE:` comment instead of a verdict — its own outcome, refused together with `--verdict` or with a blocking finding present. When the PR already carries a same-role verdict comment, a new findings file must carry every prior `F<n>` id with a state (`open`/`fix-claimed`/`reproduced`/`resolved` — a `resolved` finding keeps its severity for the record but no longer drives the verdict), and a non-blocking finding outside the diff since that comment's `Judged head:` is refused. `--scope-evidence-file <path>` (code-reviewer only) renders the file's contents as a fenced block directly below the verdict block, backing the `SCOPE:` claim. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`ESCALATE:`/`Judged head:` line from validated inputs, never from caller-supplied text; before posting, runs the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls over its own rendered text and refuses (exit `2`) unless exactly the intended verdict extracts and the other role extracts none (an escalation: both extract none) — those extractors read only a comment's first three lines, so no caller-supplied field can ever reach either marker; and after posting, re-fetches the comment and refuses to exit 0 unless the same re-parse holds against the live forge state. `--print-only` renders and self-checks the comment exactly like a real post, then prints it and returns — never calling `gh pr comment`, never re-fetching to self-verify a write that never happened (closes atta-labs/vinaya#184). `--json` for the enveloped machine form. |
|
|
24
26
|
| `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
27
|
| `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). |
|
|
26
28
|
| `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. |
|
|
@@ -207,12 +209,14 @@ Run `vinaya check --plan` to see the resolved role registry before anything rend
|
|
|
207
209
|
Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.attalabs.dev/docs/cli). The short version — what an executable must do to be a valid check:
|
|
208
210
|
|
|
209
211
|
- Exit `0` to pass, `1` to report findings. Any other exit code reads as `status: 'error'` to the runner.
|
|
210
|
-
- Emit findings as JSON lines on stderr, one per line: `{ schema: 1, check, severity: 'error' | 'warning', message, agent_recovery_prompt, file?, line? }`.
|
|
212
|
+
- Emit findings as JSON lines on stderr, one per line: `{ schema: 1, check, severity: 'error' | 'warning', message, agent_recovery_prompt, file?, line?, pending? }`.
|
|
213
|
+
- `pending: true` marks a finding that is "has not happened yet", not "is wrong" — a gate reading an artefact a later step of the same turn produces. A report renders it as a wait rather than an error, instead of guessing that distinction back out of the message text.
|
|
211
214
|
- `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).
|
|
212
215
|
- Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
|
|
213
216
|
- 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
217
|
- `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
218
|
- `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'`.
|
|
219
|
+
- `exec-bits` (ring 0, part of `vinaya check --all --local`'s managed hooks) is a worked example of a diff-scoped local check: for every changed file that lives under a `checks/bin/` directory or begins with a `#!` line, it reads the mode git has in the INDEX (`git ls-files -s`) and fails on anything but `100755`. The index, never the working tree — a `chmod +x` on disk changes nothing git ships, so the exec bit is the one property that can be right locally and wrong for everyone who checks the repo out. A check bin is spawned directly, never through a shell, so a bin committed `100644` cannot start at all.
|
|
216
220
|
|
|
217
221
|
`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.
|
|
218
222
|
|
|
@@ -261,6 +265,12 @@ Both markers are HTML comments — invisible on render — and may sit inline wi
|
|
|
261
265
|
|
|
262
266
|
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
267
|
|
|
268
|
+
## Doctrine-no-procedures
|
|
269
|
+
|
|
270
|
+
Doctrine explains what a command does and why; it is not a runbook a reader executes verbatim, and a copy-pasted sequence rots the moment the real command changes underneath it. The `doctrine-no-procedures` core check refuses a fenced code block anywhere under `<doctrineRoot>/**/*.md` that contains two or more lines each starting with a shell command word (`export`/`bun`/`gh`/`git`/`grep`/`sed`/`cat`/`diff`/`vinaya`) — that shape is a sequence to run, not an illustration.
|
|
271
|
+
|
|
272
|
+
Two exemptions: a block sitting inside the `AEG:VENDOR-EXAMPLE` anchor pair (this repo's own one sanctioned fenced home for a real command sequence, `tranche-model.md` §12), and any file under a `templates/` directory (a worked-example template legitimately shows the full shape a real PR/Issue body carries). Unlike `doctrine-portability`/`reader-resolvable-prose`/`retired-vocabulary` above, this check is **blocking, not report-only** (`severity: error`, exit `1` on any finding) — it ships with an expected-zero corpus rather than an unfixed backlog, so day-one install does not need a baseline rollout.
|
|
273
|
+
|
|
264
274
|
## Brief-schema divergence
|
|
265
275
|
|
|
266
276
|
`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.
|
|
@@ -56,7 +56,7 @@ Every output the per-task Archivist produces (left) has exactly one obligation f
|
|
|
56
56
|
| per-task Archivist produces (per task) | Tranche Archivist consumes at | What the consumption means |
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| **Provenance block comment** on each merged PR | Entry gate verification | The Tranche Archivist verifies every task PR has a provenance block comment before starting close-out. A missing provenance block means that task's per-task Archivist close-out was incomplete — stop and flag: *"Task N's PR has no provenance block — per-task Archivist did not run for this task. Flag for Principal before proceeding."* |
|
|
59
|
-
| **Lessons Issue comments** for any `BLOCKER`/`MAJOR` findings that merged | Retrospective assembly | The Tranche Archivist reads the pinned lessons Issue's comments since the tranche started and includes the patterns they identify in the retrospective's "What stalled or caused rework" and "Carry-forward lessons" sections. |
|
|
59
|
+
| **Lessons Issue comments** for any `BLOCKER`/`MAJOR` findings that merged | Retrospective assembly | A merged `BLOCKER` means a deviation was approved; a merged `MAJOR` means the Principal accepted a surfaced finding at the go. The Tranche Archivist reads the pinned lessons Issue's comments since the tranche started and includes the patterns they identify in the retrospective's "What stalled or caused rework" and "Carry-forward lessons" sections. |
|
|
60
60
|
| **Follow-up Issues** opened for `STALE-SPEC` findings | Close-out report | The Tranche Archivist lists open follow-up Issues in the output report as DANGLING items — the follow-up Issue itself IS the durable record, open until resolved. A `STALE-SPEC` finding with no follow-up Issue is a DANGLING item — flag it for the Principal. (`now.md` and the pinned state Issue are both retired.) |
|
|
61
61
|
|
|
62
62
|
**Reading the table:** left is the producer obligation (per-task Archivist role doc and this contract enforce it), right is the consumer obligation (Tranche Archivist role doc and this contract enforce it). The two role docs must not contradict this table.
|
|
@@ -66,7 +66,7 @@ Every output the per-task Archivist produces (left) has exactly one obligation f
|
|
|
66
66
|
## Producer obligations (the per-task Archivist)
|
|
67
67
|
|
|
68
68
|
- Post a provenance block comment on every merged task PR — no exceptions. This is the single most critical output: without it, the Tranche Archivist's entry gate fails and close-out cannot proceed.
|
|
69
|
-
- Post a new comment on the pinned lessons Issue for every `BLOCKER` or `MAJOR` finding that was present in the Reviewer's verdict and merged anyway
|
|
69
|
+
- Post a new comment on the pinned lessons Issue for every `BLOCKER` finding (a deviation was approved) or `MAJOR` finding (the Principal accepted a surfaced finding at the go) that was present in the Reviewer's verdict and merged anyway. Either case without a lessons entry is a missed learning.
|
|
70
70
|
- Open a follow-up Issue for every `STALE-SPEC` finding identified by the Reviewer. If the Developer already opened one, confirm it exists; do not open a duplicate.
|
|
71
71
|
- Append one row to the tranche's token ledger at close-out.
|
|
72
72
|
|
|
@@ -62,6 +62,7 @@ Every field the Brief Author emits (left) has exactly one named obligation for t
|
|
|
62
62
|
| **Technical Dependencies** | Verify all depends-on are merged | The Developer confirms every named dependency is on `main` before starting. A depends-on not yet merged is a hard stop. |
|
|
63
63
|
| **Technical Surface Map** | Bounds the diff | The Developer touches only files in the surface map. Files outside it are a stop-and-escalate. |
|
|
64
64
|
| **Premise pins (`Premise:` block, mandatory when the surface map names a real code surface)** | Re-asserted before Step 0, via `verify-dispatch --premise <body-file>`; re-asserted again pre-PR via `verify-task` | A failed premise means the surface moved since the brief was authored — the Developer stops and re-digs rather than executing against a stale mental model. This is a stop condition, not a silent re-guess. `checkPremiseCoverage` (Brief Validation) fails a brief with a real code surface and zero premise coverage. |
|
|
65
|
+
| **A fenced command with its executed output pasted beneath it (the only other form a behavioural fact about code may take, per the rule in `aeg-root/skills/brief-authoring/SKILL.md` §2)** | Re-run before the Part that depends on it; output pasted in that round's PR comment | The Developer re-runs the command and compares its own output against what the brief pasted. A mismatch is a brief defect, not a fact to transcribe — the Developer stops, `severity: strategy`, and never writes the brief's sentence into doctrine or code. |
|
|
65
66
|
| **Documentation-update list** | Self-check before opening PR + Reviewer doc check | The Developer updates every doc named in the list before claiming done. The list is a DoD obligation, not a recommendation — a named doc not updated is a BLOCKER at review. `verify-docs --pr` gates structural presence; the Reviewer gates content correctness. |
|
|
66
67
|
| **Task Done checklist** | Self-check before opening PR | The Developer runs every item before opening the PR. An unchecked item means the PR is not ready. |
|
|
67
68
|
| **Test Plan** tagged `[agent]` / `[principal]` | Runs `[agent]` items; leaves `[principal]` for Principal | The Developer runs every `[agent]` item and posts evidence. Does not tick `[principal]` boxes. |
|
|
@@ -64,7 +64,7 @@ Every item the Developer produces in the open PR (left) has exactly one obligati
|
|
|
64
64
|
| **Documentation-update list honored** (every named doc updated in the diff) | Doc coupling check — first diff step after surface-map check | A named doc absent from the diff or present but incorrect is a BLOCKER (it is a DoD obligation, not guidance). `verify-docs` gates structural presence; the Reviewer gates content correctness. |
|
|
65
65
|
| **`.vinaya/doc-owners` coverage (C5) satisfied** — every binding fired by the diff is bound (in-diff doc), URL-acked (`Doc-ack: <pointer> — <note>`), or waived by an actor-verified `vinaya/waiver:docs` label applied by a principal. | Coverage check — *mechanical* via CI; the Reviewer reads it as already-true and moves on. | Coverage presence is mechanical (`verify-docs` C5 fails CI if missing). The Reviewer's job shrinks to **judging correctness of the covered doc**: a passing C5 plus an incorrect / no-op / misleading doc update is still a BLOCKER. A waiver is no longer the author's to grant — it is an actor-verified `vinaya/waiver:docs` label applied by a principal, so there is no deferral line for the Reviewer to judge. The seam is dormant when `.vinaya/doc-owners` is absent or no binding matches; in that case there is nothing for either side to do. |
|
|
66
66
|
|
|
67
|
-
**Reading the table:** left is the producer obligation (Developer role doc and this contract enforce it), right is the consumer obligation (Reviewer role doc and this contract enforce it). The two role docs must not contradict this table.
|
|
67
|
+
**Reading the table:** left is the producer obligation (Developer role doc and this contract enforce it), right is the consumer obligation (Reviewer role doc and this contract enforce it). The two role docs must not contradict this table. A MAJOR finding in this table is a surfaced finding — it is listed and shown to the Principal at the go, and never by itself produces REQUEST CHANGES; only a BLOCKER row does.
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -74,10 +74,10 @@ Every item the Developer produces in the open PR (left) has exactly one obligati
|
|
|
74
74
|
- **Documentation-update list honored.** Every doc named in the list must be updated in the diff before opening the PR. A PR with list items outstanding is not ready for review; do not open it and expect the Reviewer to discover the gap.
|
|
75
75
|
- **`.vinaya/doc-owners` coverage satisfied.** For every binding fired by the diff: update the bound doc in this PR; or, for URL bindings, add `Doc-ack: <pointer> — <note>`; or have a principal apply the actor-verified `vinaya/waiver:docs` label — you cannot self-serve it, and there is no body-field waiver grammar anymore. `verify-docs` C5 enforces this mechanically — if it fails CI, do not request review.
|
|
76
76
|
- CI must be green before requesting review. Do not request review with a red CI and expect the Reviewer to begin.
|
|
77
|
-
- The brief must be in the PR body, unmodified — pasted verbatim, not summarized or paraphrased.
|
|
77
|
+
- The brief must be in the PR body, unmodified — pasted verbatim, not summarized or paraphrased. The whole body is authored once, at open. After open the Developer changes nothing outside the `AEG:EVIDENCE` anchor and one appended `AEG:TOKENS` row. The Principal's `[principal]` ticks are the Principal's writes and must survive every Developer edit. A review round's response and any re-run evidence are PR comments, not body edits.
|
|
78
78
|
- The diff must touch only files in the brief's Technical Surface Map. Files outside it are a stop-and-escalate before opening the PR, not a finding for the Reviewer to catch.
|
|
79
79
|
- The Task Done checklist must be ticked — all items, with actual verification evidence for each.
|
|
80
|
-
- Every `[agent]` Test Plan item must have an evidence comment posted on the PR — the actual command output, not a paraphrase.
|
|
80
|
+
- Every `[agent]` Test Plan item must have an evidence comment posted on the PR — the actual command output, not a paraphrase. A re-run after fixes posts a new comment; it never edits the one already there.
|
|
81
81
|
- One row appended to the tranche's token ledger before opening the PR.
|
|
82
82
|
|
|
83
83
|
## Consumer obligations (the Reviewer)
|
|
@@ -56,7 +56,7 @@ Every item the Reviewer produces in the verdict (left) has exactly one obligatio
|
|
|
56
56
|
| Reviewer produces | per-task Archivist consumes at | What the consumption means |
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| **Verdict** (`APPROVE` or `REQUEST CHANGES`) | Entry gate | The Archivist only runs close-out on `APPROVE` + merged PRs. A `REQUEST CHANGES` verdict means the task is not done — close-out does not run until the Reviewer posts `APPROVE` and the PR is subsequently merged. |
|
|
59
|
-
| **Finding list** with severity tags (`BLOCKER` / `MAJOR` / `MINOR`
|
|
59
|
+
| **Finding list** with severity tags (`BLOCKER` / `MAJOR` / `MINOR`) | Provenance block assembly | The Archivist includes the verdict and finding count in the provenance block. A `BLOCKER` finding that merged despite being raised means a deviation was approved; a `MAJOR` finding that merged means the Principal accepted a surfaced finding at the go. Both are logged in the provenance block under DANGLING. |
|
|
60
60
|
| **Spec-conformance result** (`CONFORMS` / `DRIFTS` / `CONTRADICTS` / `STALE-SPEC`) | Provenance block + pinned lessons Issue | `CONTRADICTS` that merged is a `severity:strategy` flag in the provenance block. `STALE-SPEC` triggers a follow-up Issue (the Archivist opens it if the Developer did not) — it is not a reason to block merge, but it must not disappear. |
|
|
61
61
|
|
|
62
62
|
**Reading the table:** left is the producer obligation (Reviewer role doc and this contract enforce it), right is the consumer obligation (Archivist role doc and this contract enforce it). The two role docs must not contradict this table.
|
|
@@ -65,7 +65,7 @@ Every item the Reviewer produces in the verdict (left) has exactly one obligatio
|
|
|
65
65
|
|
|
66
66
|
## Producer obligations (the Reviewer)
|
|
67
67
|
|
|
68
|
-
- Every finding must carry a severity tag (`BLOCKER`, `MAJOR`,
|
|
68
|
+
- Every finding must carry a severity tag (`BLOCKER`, `MAJOR`, or `MINOR`). A finding without a tag is malformed — the Reviewer revises the comment before the verdict is considered valid.
|
|
69
69
|
- The verdict line must be the **first line** of the verdict comment, in the exact format specified by `roles/reviewer.md`: `VERDICT: APPROVE | REQUEST CHANGES`.
|
|
70
70
|
- The spec-conformance result must be stated explicitly — `CONFORMS`, `DRIFTS`, `CONTRADICTS`, or `STALE-SPEC`. "Not checked" is not acceptable for Tier 1+ tasks with a named Project.
|
|
71
71
|
- A verdict comment missing any of these three elements is malformed. The Reviewer does not post it.
|
|
@@ -77,7 +77,7 @@ Every item the Reviewer produces in the verdict (left) has exactly one obligatio
|
|
|
77
77
|
- Assemble the provenance block from frozen PR facts (brief in PR body, verdict comment, merge metadata) — never fabricate a field whose source fact is absent. A missing source fact is a DANGLING item, not an opportunity to infer.
|
|
78
78
|
- Post the provenance block as a comment on the merged PR (the PR is a frozen truth domain once merged; the provenance block is its permanent record).
|
|
79
79
|
- **The provenance block comment is the forge-derived coherence signal that downstream roles depend on.** The next Developer to start a task in this tranche checks whether the most-recently-merged task PR carries a provenance block before executing step 0 — its absence is a hard STOP that blocks the next task from starting. Post it completely; a partial or absent block does not satisfy the Developer's entry gate (see `aeg-root/roles/developer.md` and `aeg-root/contracts/brief-developer.md`).
|
|
80
|
-
- A `BLOCKER`
|
|
80
|
+
- A `BLOCKER` finding present in the verdict of a merged PR means a deviation was approved. A `MAJOR` finding present in the verdict of a merged PR means the Principal accepted a surfaced finding at the go. Both are logged in the provenance block under DANGLING and both get a new comment on the pinned lessons Issue.
|
|
81
81
|
- A `STALE-SPEC` finding in a merged PR must produce a follow-up Issue if the Developer did not already open one. This is the Archivist's responsibility to ensure it happens.
|
|
82
82
|
- Append one row to the tranche's token ledger at close-out.
|
|
83
83
|
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -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
|
|
19
|
+
across all three rings (currently 54).
|
|
20
20
|
- packages/aeg-core/src/markdown-table.test.ts asserts per-ring counts
|
|
21
|
-
(currently ring 0 =
|
|
21
|
+
(currently ring 0 = 19, ring 1 = 27, ring 2 = 8).
|
|
22
22
|
- packages/aeg-core/src/docs/node-route.test.ts asserts the ring-0 count
|
|
23
|
-
(currently
|
|
23
|
+
(currently 19) 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.
|
|
@@ -72,6 +72,10 @@ This page answers two questions: **what prevents an invalid artifact from ever b
|
|
|
72
72
|
|
|
73
73
|
The same check implementations run at ring 0 and ring 1 — one codebase, two enforcement points, so the local gates and CI can never disagree.
|
|
74
74
|
|
|
75
|
+
**The fixed-position rule.** A gate reads its signal from a fixed position — a named anchor, an HTML-comment marker, a line-anchored field, a bounded read window — never by scanning free text for a phrase that means the right thing. Free text is caller-controlled: whoever writes the artifact can put a signal-shaped sentence anywhere in it, including inside a quotation of somebody else's, and a scanning gate cannot tell a cast verdict from a mention of one. Fixing the position is what closes that: a caller-supplied field rendered outside the window the gate reads can no longer reach a position the gate treats as structural, however the writer formats it.
|
|
76
|
+
|
|
77
|
+
**The own-PR fixture rule.** A pull request that adds a check which reads a PR body ships a fixture test running that check over **that PR's own body**. A body-reading check is the one class whose real input exists at the moment the PR opens and is never exercised by a synthetic fixture the author also wrote: the author's fixture agrees with the author's mental model by construction, and the first real body it meets is the one it was supposed to grade. Running it over the body in hand costs one test and converts "it should work on a real body" from a belief into a passing assertion.
|
|
78
|
+
|
|
75
79
|
**The `Audience` column, read once for all three tables below:** `product` — this row's implementation is a named check the product registers, so it ships to every adopter through `vinaya check`. `repo-own` — everything else: a mechanism specific to how this repository enforces itself on top of the product — a hand-written CI job, the check runner itself, a forge-write command, or a check this repo runs against its own doctrine tree but has deliberately not registered as a named, adopter-facing check. A row carrying no `Audience` cell at all (older doctrine, or an un-upgraded adopter copy) reads as `repo-own` — the safe default, since it makes no shipped claim to verify. G6, below, blocks CI on any `product` row whose implementation does not actually resolve in that registration.
|
|
76
80
|
|
|
77
81
|
---
|
|
@@ -81,13 +85,12 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
81
85
|
| Action | Summary | Category | Description | Gate | What must be true before the action is allowed | Audience | implementation |
|
|
82
86
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
83
87
|
| Editing a governed file | Ever had a teammate change code they clearly never read the docs for? | hook | Couples an edit to a governed code surface to its owning document, so the two cannot drift apart in one change. | **C5 doc-coverage (update-or-waive)** | No session read-gate hook is installed in this repo. The nearest enforcement is C5 doc-coverage: a changed code file matching a `.vinaya/doc-owners` binding must have its owning doc in the same diff (or a `Doc-ack:`/principal-applied `vinaya/waiver:docs` escape) — warn-only at the ring-0 hooks, blocking at ring 1. Dormant until `.vinaya/doc-owners` carries a binding for the surface. | product | `apps/cli/src/checks/bin/check-doc-coverage-push.ts` |
|
|
84
|
-
| `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Managed `.git/hooks/pre-commit` block** (written by `vinaya init`/`upgrade` through the common-dir hook resolver, so every linked worktree shares it) | The ring-0 check suite passes: the hook runs `vinaya check --all --local` (in this vendoring repo, the built `node apps/cli/dist/index.js` invocation, guarded by a build-missing refusal). Build, lint and unit tests are NOT run by this hook here — they run in the repo-own `ci.yml` (ring 1). | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
85
|
-
| `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Managed `.git/hooks/pre-push` block** (same generator and common-dir resolver as pre-commit) | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2
|
|
86
|
-
| Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. | repo-own | `packages/aeg-core/bin/open-pr.ts` |
|
|
88
|
+
| `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Managed `.git/hooks/pre-commit` block** (written by `vinaya init`/`upgrade` through the common-dir hook resolver, so every linked worktree shares it) | The ring-0 check suite passes: the hook runs `vinaya check --all --diff-only --local` (in this vendoring repo, the built `node apps/cli/dist/index.js` invocation, guarded by a build-missing refusal). `--diff-only` skips a `scope: 'diff'` check whose `include` globs match none of the changed files, so pre-commit runs a genuinely narrower set than pre-push/CI for those checks — but a `scope: 'full'` check always runs regardless (an `include` declared on a `scope: 'full'` entry, `apps/cli/src/checks/registry.ts`, is presently pinning/documentation only: `runner.ts`'s skip decision returns before ever consulting it for a non-`'diff'` scope). Build, lint and unit tests are NOT run by this hook here — they run in the repo-own `ci.yml` (ring 1). | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
89
|
+
| `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Managed `.git/hooks/pre-push` block** (same generator and common-dir resolver as pre-commit) | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2 report-only audit watching for it (see below, task 24 item 2) — notification only, never a gate, matching that audit's own non-gate classification; nothing at ring 2 actually backstops (blocks) this hook's own fail-open gap. This check is deliberately **fail-open**: if the forge cannot be reached (network flake, auth issue, rate limit) it is treated the same as "no pull request exists yet" and the push is allowed — a transient reachability issue must never block every push. On a task branch's first push (before its PR exists), `verify-dispatch` also runs itself here — see the dedicated row below (task 25). Any changed code that has a designated owning document must have that document updated in the same branch, OR (task 29) carry an actor-verified `vinaya/waiver:docs` label on the open pull request, OR (task 4) carry a `Doc-neutral: <pointer> — <note>` PR-body declaration whose matched-file diff is verified comment/whitespace-only (`isMechanicallyNeutralDiff`) — a waiver is a forge-authenticated human act, never a parseable string, and a Doc-neutral declaration is never sufficient by itself; only diff-confirmed evidence satisfies it. Before a PR exists (first push) no waiver label can exist yet, so an owned-doc violation here is **warn-with-declared-intent, not a hard block**: the push always succeeds, and the printed message states plainly that ring 1 stays red until a principal applies the label, the doc is updated, or a Doc-neutral declaration with confirming evidence is added — this replaces the earlier first-push `Doc-waiver:` commit-trailer self-service, which is now dead code (that path is superseded). `Doc-ack:` (URL-pointer acknowledgment) is unaffected by the waiver change — still read from the branch's own last commit message before a PR exists, or — for a pre-authoring dry run via `verify-dispatch --simulate`, before any commit exists — from a local drafted-PR-body file. | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
90
|
+
| Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. **The brief itself never reaches this description** — the Developer still pastes it into the body-file's `## Reference` section, but `vinaya pr create` splits that section out before the body ever reaches `gh`, posting it as its own comment marked `<!-- aeg:brief -->`, so the description carries the report (summary, test plan, evidence, scope) only. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. **`vinaya pr create` also runs every registry check declaring `PR_BODY` over the body before it ever reaches the forge (task 12)** — `brief-shape`, `pr-report-density`, `doc-coverage`, and every other `PR_BODY`-declaring check, with `PR_NUMBER` explicitly unset (no PR exists yet) and `localOnly: true` skipping the `requiresOpenPr` set outright. The same set CI's `vinaya-checks.yml` runs against the live body, so a body that opens here is a body that passes there too. Skipped entirely when `rings.ring1_forgeWriteInterception` is `true` — the same accelerator that already skips `validateForgeWrite`'s config-driven sections. | repo-own | `packages/aeg-core/bin/open-pr.ts` |
|
|
87
91
|
| Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no registry row for anything to resolve against. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | repo-own | `packages/aeg-core/bin/open-issue.ts` |
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. Optionally (task 10, `PREMISE_FILE=<brief.md> vinaya check dispatch-readiness`), also re-asserts a local brief's `Premise:` pins against current on-disk state and fails on any pin that no longer holds. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
92
|
+
| Merging | Ever had a broken build get merged anyway? | event | Holds the merge on the forge side while anything the gates check is still failing. | **Required review-gate check + branch protection** | No agent-side merge hook is installed in this repo. The merge gate is ring 1: the required `review-gate` check (`vinaya check review-gate`; a clean code-reviewer APPROVE and a clean security-review PASS verdict from principal-allowlisted authors AND at least one reported, green non-review check-run for the PR's current head, or a principal-applied, actor-verified `vinaya/waiver:review` label) plus branch protection marking that check required. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
93
|
+
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. Optionally (task 10, `PREMISE_FILE=<brief.md> vinaya check dispatch-readiness`), also re-asserts a local brief's `Premise:` pins against current on-disk state and fails on any pin that no longer holds. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. A parsed `depends-on` edge that points back at its own task is refused as an INTERNAL parser-bug error, never reported as an unmerged dependency: a self-dependency is unsatisfiable by construction, so it can only mean the edge text or the rationale parser produced something impossible, and the ordinary unmerged-dependency wording reads as a legitimate gate state an agent is invited to route around rather than escalate. The `coherence` D1 check refuses the same shape the same way. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
91
94
|
| Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | repo-own | `packages/aeg-core/src/brief-validation.ts` |
|
|
92
95
|
| Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | repo-own | `packages/aeg-core/bin/verify-task.ts` |
|
|
93
96
|
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. Every core check in `registry.ts` carries an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@attalabs/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
@@ -98,6 +101,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
98
101
|
| 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
102
|
| 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
103
|
| 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` |
|
|
104
|
+
| Committing a check bin that nobody can execute | Ever had a script fail with a permission error on somebody else's machine, never your own? | hook | Refuses a staged executable whose index mode is not `100755`, on the machine that staged it. | **`exec-bits` check** (`vinaya check exec-bits`, part of `vinaya check --all --local`) | Every changed file that either lives under a `checks/bin/` directory or begins with a `#!` line is staged `100755`. The mode read is the INDEX's (`git ls-files -s`), never the working tree's: a `chmod +x` on disk changes nothing git ships, so the one property that can be right locally and wrong for everyone else is exactly the one being checked. Two signals, either sufficient, because neither covers the real cases alone — the path (what the runner spawns) and the shebang (anything meant to be executed directly, wherever it lives). A symlink stages as `120000` and fails: intended, since the runner spawns the path it is given and a symlink there is not an executable check bin. A real failure (`error`, exit 1), not a report: the runner spawns a check bin directly, never through a shell, so a bin committed `100644` does not misbehave — it cannot start at all, and surfaces as an opaque permission error on a runner long after the commit that caused it. No diff boundary resolvable means nothing staged to judge, and is never a refusal. | product | `apps/cli/src/checks/bin/check-exec-bits.ts` |
|
|
101
105
|
|
|
102
106
|
**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.
|
|
103
107
|
|
|
@@ -117,31 +121,33 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
117
121
|
|
|
118
122
|
| CI check | Summary | Category | Description | Re-verifies | Audience | implementation |
|
|
119
123
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
120
|
-
| Brief validation | Ever seen a PR title that told you nothing about what changed? | ci | Re-checks in CI that a pull request’s title and brief sections are properly formed. | Title convention (all branches) and every required brief section — on a task branch **or any PR whose body is a brief** (`isBriefShaped`); a PR carrying no brief stays exempt, and `Closes #N` is required only on task branches. Also runnable at authoring time as the `brief-shape` check: `PR_BODY="$(cat brief.md)" vinaya check brief-shape` (on this repo's toolchain, also `verify-brief.ts --body-file <brief.md>`). | product | `packages/aeg-core/bin/verify-brief.ts` |
|
|
124
|
+
| Brief validation | Ever seen a PR title that told you nothing about what changed? | ci | Re-checks in CI that a pull request’s title and brief sections are properly formed. | Title convention (all branches) and every required brief section — on a task branch **or any PR whose body is a brief** (`isBriefShaped`); a PR carrying no brief stays exempt, and `Closes #N` is required only on task branches. **Four further shape rules (task 10), each firing on the whole body, `<details>`-wrapped reference copy included:** (1) a bare `<path>.<ext>:<digits>` code-fact pointer outside a `Premise:` pin and outside a fenced code block fails — pin it or show the command that produced it; (2) in `§5`/`§6`, a fenced block opening with a shell command (`export`/`bun`/`gh`/`git`/`grep`/`sed`/`cat`/`diff`/`vinaya`) must be immediately followed by another fenced block that does NOT itself open with a shell command — that very next fence is the output claim; a later fence, or another command block sitting between it and the real output, never satisfies it — the Step `0` `git worktree add` block is exempt; (3) a `§4` naming a path under `packages/<pkg>/` must also name, for every workspace member (every `apps/*`/`packages/*` glob in the root manifest, `apps/cli` included — not `packages/*` alone) depending on `@attalabs/<pkg>`, a consumer test path or the `consumer-tests: none — <reason>` sentinel, found in `§4` itself — a quotation of that grammar elsewhere in the body (e.g. prose describing the rule) does not count; (4) a `§4` naming a check (a `check-<slug>.ts` bin, or its registry entry) or a forge-writing command (`gh pr create`, `git push`, `vinaya pr create`, …) must carry a `Defeat cases:` line in `§6` — the inputs that would defeat the check, or that the command must not misfire on. **A fifth rule (task 12), on its own rollout constant `AGENT_BOXES_REFUSED_SINCE_PR`, separate from the first four's `BRIEF_RULES_SINCE_PR`:** (5) a `- [ ] **[agent]**`/`- [x] **[agent]**` checkbox item anywhere in the Test Plan section fails — an agent never ticks a box or edits a PR body (Principal ruling); the `[agent]` half is a fenced command list `vinaya pr report` runs and grades against `AEG:EVIDENCE` instead. All five are presence/shape checks, same philosophy as the rest of this row: they judge whether the pin/output/test-path/defeat-case/fenced-list shape exists, never whether it is the *right* one. Both entry points run all five identically: `packages/aeg-core/bin/verify-brief.ts` (authoring time, pre-dispatch) and CI's `brief-shape` check compose the same `checkBriefSections` with the same consumer enumeration (`@attalabs/aeg-core`'s `buildConsumersOf`) — a brief that passes one passes the other. Also runnable at authoring time as the `brief-shape` check: `PR_BODY="$(cat brief.md)" vinaya check brief-shape` (on this repo's toolchain, also `verify-brief.ts --body-file <brief.md>`). | product | `packages/aeg-core/bin/verify-brief.ts` |
|
|
125
|
+
| PR-report density | Ever read a PR body whose Summary quietly grew into three paragraphs nobody actually reads? | ci | Re-checks that a pull request's `## Summary` and `## Scope` sections are each exactly one paragraph, per the canonical PR-body form. | One blank-line-delimited text block per section — code-blind (a fenced example inside a section is not a second paragraph) and anchor-blind (Scope's trailing `AEG:TIER` block is a field, never a second paragraph). A multi-line blockquote's own `\n>\n` continuation lines are never blank, so it counts as one block too, but only when the blockquote is the section's entire content — prose before or after it is still a separate block, so an intro sentence plus a blockquote aside plus a closing sentence is three blocks and still fails, the same as any other multi-paragraph shape. Structured content that needs more than a single whole-section blockquote (a subsection, a list, a table, or a blockquote alongside other prose) redirects to a section of the Developer's own below the canonical four (`developer.md`: "Add anything you want beneath the four sections"), never crammed into Summary/Scope and never hand-typed into the AEG:EVIDENCE block, which is emitted only. Also runnable at authoring time, before a PR exists: `PR_BODY="$(cat draft.md)" vinaya check pr-report-density`. Confirmed live against real merged history: several already-merged pull requests shipped multi-paragraph Summary/Scope sections with nothing catching it before this check existed. | product | `apps/cli/src/checks/bin/check-pr-report-density.ts` |
|
|
121
126
|
| Closes linkage | Ever had a PR merge and it wasn't clear which ticket it actually closed? | ci | Re-checks that a pull request names the Issue it closes. | A task pull request names, and will close, exactly its planned Issue | product | `packages/aeg-core/bin/verify-coherence.ts` |
|
|
127
|
+
| Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Re-checks, in CI, that whatever landed on a pull request or Issue satisfies the CLI's validated-write shape rules — the actual backstop against a raw write, since no ring-0 hook can refuse one at the point it happens. | No tool-interception layer is installed in this repo (`ring1_forgeWriteInterception: false` in `vinaya.config.json`), so a raw `gh`/API write bypasses the CLI's validated forge-write commands (`vinaya issue create/edit`, `vinaya pr create/edit`) entirely rather than being refused at the tool call — the sanctioned path (the Issue-creation and PR-creation rows above, ring 0) only covers a write that goes through it. Ring-1 CI re-runs the same validators — the malformed-task-Issue rationale gate, the configured brief schema — against whatever actually landed, catching a bypassing write after the fact; that re-run, not anything at ring 0, is this row's real enforcement. | repo-own | `apps/cli/src/lib/forge-write.ts` |
|
|
122
128
|
| Single-plan-PR guard | Ever had two people plan the same work at the same time, unknowingly? | ci | Re-checks that no two open pull requests are planning the same work at once. | The identical ring-0 predicate (`checkSinglePlanPr`) re-run forge-side against this PR's own touched files (via `gh pr view`) and every other open PR's touched files — closes the gap where a web-UI-opened plan PR bypasses `open-pr.ts` entirely. No-ops for an ordinary task-branch PR (never touches a topology file), and dormant altogether where a plan is a Milestone plus Issues rather than a file. | product | `packages/aeg-core/bin/verify-single-plan-pr.ts` |
|
|
123
129
|
| Coherence check | Ever found a task marked "done" that was never actually merged? | ci | Re-checks every task’s recorded state against what actually merged. | Plan↔forge consistency across the whole repository: tasks closed without a merge — A1 also recognizes a task Issue hand-closed directly by a recognized Principal (`stateReason: 'completed'`) as a second, narrower "done" path alongside a merged PR, for the documented exception where a dependency's technical premise dissolved and no PR was ever going to land — archived without their audit record, orphaned Issues (T2), phantom references, unplanned placeholders in active tranches (T3), duplicate decision numbers, broken document manifests, open task Issues missing a planner-rationale field (R1 — the same grammar the ring-0 Issue gate above enforces at creation). **L5 (open-Milestone-all-closed) surfaces a tranche whose Milestone stayed open after every one of its task Issues closed — advisory (info-only), the forge-native analogue of file-based L1 for post-cutover tranches that no longer carry a topology file (drift class 2; one live incident — the forge-state tranche's Milestone left open after full archive).** **T2 and T3 are CI-scoped to the pull request's own tranche** (parsed from the branch name) — a coherence gap in an unrelated tranche's topology never blocks this pull request (task 19, closing the incident where an unrelated tranche's gap failed CI). **T2 additionally blocks CI only for a plan PR** (task 24 item 4, superseding half of task 19's placement) — the point-of-power principle: a task PR can neither cause nor cure a topology gap, so T2's findings are demoted to non-blocking for every other PR kind, `--json`/audit mode, and `daily-drift` alike (a live incident proved the old placement wrong). The full repo-wide picture (every tranche, unscoped) remains visible in `--json`/audit mode. **The job's repo-state inputs (topology files, tranche list) read from a freshly-fetched `origin/main`, not the checkout's `refs/pull/N/merge`** (task 24 item 5) — GitHub materializes that ref lazily and it can lag behind `main` (5+ false-red cycles, 2026-07-03/04); a plan PR's own topology diff still reads from its head ref. **`id`/`issue` per task now derive from the forge (`@attalabs/aeg-forge-state`, task 3b) instead of the topology file, falling back to the file if forge derivation fails; `dependsOn`/`conflictsWith` and `#TBD` rows stay file-sourced and are merged onto the forge-derived task list, but only when a topology file still exists.** As of task 7, no active tranche still carries a topology file — every one was deleted once its forge-native replacement was proven live, so `dependsOn`/`conflictsWith` is fully forge-derived for all of them, with no file fallback anywhere. | product | `packages/aeg-core/bin/verify-coherence.ts` |
|
|
124
130
|
| Documentation gate | Ever shipped a change and forgot to update the doc explaining it? | ci | Re-checks that a change carrying real code also updates the docs explaining it. | Tier declaration and tier-appropriate documentation, including code→document ownership | product | `packages/aeg-core/bin/verify-docs.ts` |
|
|
125
|
-
| Test-plan state | Ever merged a PR with an unchecked "did you test this" box? | ci | Re-checks that the pull request
|
|
131
|
+
| Test-plan state | Ever merged a PR with an unchecked "did you test this" box? | ci | Re-checks that the pull request's `[principal]` test-plan boxes are genuinely ticked. | Unticked `[principal]` test-plan boxes block merge readiness. **The `[agent]` half is no longer graded here at all** (task 12; Principal ruling: an agent never ticks a box or edits a PR body) — it is a fenced command list `vinaya pr report` runs from the PR head and writes into the `AEG:EVIDENCE` block, and `evidence-fresh` is what binds that block to the head. This check reads the body and branch alone: no comment fetch, no `PR_NUMBER`, no `pending` state — a body opened before the rollout constant may still carry a checkbox `[agent]` item (grandfathered by `brief-shape`'s own rollout), but this gate never looks at that tag either way. | product | `packages/aeg-core/bin/verify-test-plan.ts` |
|
|
126
132
|
| Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | repo-own | `.github/workflows/ci.yml` |
|
|
127
133
|
| Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | States where formatting/naming conventions stand in this repo: currently unenforced. | **Nothing re-checks lint/format or commit-message grammar in this repo today** — the managed hooks run only the `vinaya check` suite, and the one CI workflow that builds the code (`ci.yml`, the implementation named here; its typecheck + unit-test substance belongs to the `Typecheck + unit tests` row) carries no lint or commitlint step. Those conventions are discipline, not a gate, until a step is added. | repo-own | `.github/workflows/ci.yml` |
|
|
128
134
|
| 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` |
|
|
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` |
|
|
135
|
+
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. | product | `packages/aeg-core/bin/verify-review-gate.ts` |
|
|
130
136
|
| 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` |
|
|
131
137
|
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, `apps/cli/src/checks/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing — neither directory exists — so the live coverage is the two seeded bin sets, `packages/aeg-core/bin/*.ts` and `apps/cli/src/checks/bin/*.ts` — the latter is where most core check bins actually live.) **Scaffold-writer** (task 22, later widened to cover that second location): `verify-registry.ts --scaffold` auto-inserts a stub row for an orphan candidate whose ring is mechanically derivable — a `packages/aeg-core/bin/*.ts` file resolving through `GATE_AUDIENCE` to a registered check (its ring comes from that check's own `apps/cli/src/checks/registry.ts` entry), an `apps/cli/src/checks/bin/*.ts` file whose `check-`-stripped name resolves through the analogous `CLI_CHECK_RING` mirror table, or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered bin under either directory) gets no stub and stays a plain finding — the writer never guesses. A second, independent half of G2 scans every row's `summary`/`description`/`spec` cells for the literal placeholder marker `[undocumented — fill in why]` the scaffold writes into every non-mechanical cell: a stub row that filled only `implementation` would otherwise read as "documented" to the orphan half above and silence G2 outright, which is the exact trap this second half exists to close. The writer only ever appends a new row after a ring table's last existing row — it never edits, reorders, or removes one, so a hand-authored row (including `workspace-escape`'s and `main-branch-refusal`'s own) is untouched. Repo-side only: the writer lives on `verify-registry.ts` (aeg-core), never on the shipped `check-registry-gates.ts` (apps/cli) — an adopter's checks stay read-only over doctrine. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
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-
|
|
138
|
+
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-1 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
133
139
|
| 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` |
|
|
134
140
|
| 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` |
|
|
135
141
|
| G6 — doctrine-registry parity | Ever read a doc that claimed a check ran, when nothing in the registry actually ran it? | ci | Re-checks that every row this page marks `product` actually ships as a real, adopter-runnable check, not just a documented claim. | Every row whose `Audience` cell reads `product` resolves to a real entry in `coreCheckRegistry()` (`apps/cli/src/checks/registry.ts`) — the parity a `product` claim makes but nothing checked before task 8. A `repo-own` row makes no such claim and is never scanned. Evaluated only from `apps/cli`'s `check-registry-gates.ts`, since `coreCheckRegistry()` lives there and `aeg-core`'s standalone `verify-registry.ts` cannot import it without closing a dependency cycle. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
136
|
-
| reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `<doctrineRoot>/**` doc and every configured reader-facing page, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes a sibling suite proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `<doctrineRoot>/glossary.md`'s own entry headings, never hard-coded. Registered in `coreCheckRegistry()` (task 7): its doctrine root, reader-facing globs, and legacy-slug corpus location are adopter-configurable via `vinaya.config.json`'s `proseGates` key, defaulting to this repo's own prior hardcoded shape (`doctrineRoot: "aeg-root"`, dormant reader-facing sweep) when unset. Part of `vinaya check --all` for every adopter; not part of the managed hooks. | product | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
|
|
137
|
-
| retired-vocabulary | Ever read a doc that described a mechanism as live after it was retired? | ci | Re-checks that no doctrine page claims a retired mechanism is still live. | The genuinely-retired half of a sibling suite's original scan — never the forge-number/tranche-slug citation half, which is `reader-resolvable-prose`'s own job and would double-report the identical match if duplicated here. Scoped to `<doctrineRoot>/**` only (the same `proseGates.doctrineRoot` key `reader-resolvable-prose` reads), not the whole adopter repo — this vocabulary means something only inside the shipped doctrine tree. Registered in `coreCheckRegistry()` (task 7), report-only like `reader-resolvable-prose`. | product | `apps/cli/src/checks/bin/check-retired-vocabulary.ts` |
|
|
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` |
|
|
142
|
+
| reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `<doctrineRoot>/**` doc and every configured reader-facing page, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes a sibling suite proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `<doctrineRoot>/glossary.md`'s own entry headings, never hard-coded. Registered in `coreCheckRegistry()` (task 7): its doctrine root, reader-facing globs, and legacy-slug corpus location are adopter-configurable via `vinaya.config.json`'s `proseGates` key, defaulting to this repo's own prior hardcoded shape (`doctrineRoot: "aeg-root"`, dormant reader-facing sweep) when unset. Part of `vinaya check --all` for every adopter; not part of the managed hooks. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
|
|
143
|
+
| retired-vocabulary | Ever read a doc that described a mechanism as live after it was retired? | ci | Re-checks that no doctrine page claims a retired mechanism is still live. | The genuinely-retired half of a sibling suite's original scan — never the forge-number/tranche-slug citation half, which is `reader-resolvable-prose`'s own job and would double-report the identical match if duplicated here. Scoped to `<doctrineRoot>/**` only (the same `proseGates.doctrineRoot` key `reader-resolvable-prose` reads), not the whole adopter repo — this vocabulary means something only inside the shipped doctrine tree. Registered in `coreCheckRegistry()` (task 7), report-only like `reader-resolvable-prose`. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-retired-vocabulary.ts` |
|
|
144
|
+
| doctrine-portability | Ever shipped doctrine that named a file only you could see? | ci | Re-checks that shipped doctrine doesn't cite a path that exists only in the authoring repository. | A cited path's top segment judged against an allow-list: the caller-supplied ships prefix itself (`<doctrineRoot>/`, `aeg-root/` by default — doctrine citing its own tree, wherever that tree actually lives for the caller, checked dynamically so a non-default `doctrineRoot` never falsely flags an adopter's own self-citations), plus a fixed set of static portable prefixes (`roles/`, `contracts/`, `skills/`, and adopter-owned `.github/`, `.vinaya/`, `.claude/`, `.git/`, `.husky/`) — an unclassified prefix is a finding, never a silent pass, so a never-seen author-repo directory (e.g. a build artifact under `apps/*/dist/`) cannot slip through the way it would under a deny-list. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrinePortability`; the bin supplies the only I/O, reading `<doctrineRoot>/**` twice — once from the working tree, once from `BASE_SHA` (`origin/main` by default) via `git show`/`git ls-tree` — and reports only citations present in the former and absent from the latter, so day-one install of an unfixed backlog never turns every adopter's CI red. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-portability.ts` |
|
|
145
|
+
| doctrine-no-procedures | Ever pasted a copy-paste runbook into doctrine, and it rotted the moment the real command changed? | ci | Re-checks that doctrine (`roles/*.md`, `contracts/*.md`, …) describes commands rather than scripting them (task 9/task 10's rule). | A fenced code block anywhere under `<doctrineRoot>/**/*.md`, tagged with no language or an explicit `sh`/`bash`/`shell`/`console`, containing two or more lines that each start with a shell command word (`export`/`bun`/`gh`/`git`/`grep`/`sed`/`cat`/`diff`/`vinaya`) is a finding — a runbook, not an illustration. A block tagged with any other language (` ```ts `, ` ```json `, …) is never a candidate at all, however many command-word-shaped lines it contains — a two-line ` ```ts ` excerpt whose lines both start with `export` is TypeScript, not shell. Two further exemptions: a block sitting inside the existing `AEG:VENDOR-EXAMPLE` anchor pair (`tranche-model.md` §12's one sanctioned fenced home for a real command sequence), and any file under a `templates/` directory. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrineNoProcedures`; the bin supplies the only I/O, walking `<doctrineRoot>` once. Unlike its sibling `doctrine-portability` above, this is **blocking, not baseline** (`severity: error`, exit 1 on any finding) — a fresh rule with an expected-zero corpus after this task's own sweep-and-fix, not a 200+-finding backlog needing a report-only rollout. Registered in `coreCheckRegistry()`. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-no-procedures.ts` |
|
|
139
146
|
| 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` |
|
|
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` |
|
|
147
|
+
| 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. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. (this sweep's real domain is `apps`/`packages`, not `aeg-root` — named per the Issue's own list; harmless either way while `include` has no runtime effect on a `scope: 'full'` entry.) | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
|
|
141
148
|
| 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
|
|
149
|
+
| quoted-command | Ever read a doc that quoted a command as "what runs today", and the command changed underneath it? | ci | Re-checks that a doc's explicitly marked quote of a command or config line still matches, verbatim, the file it names as its source. | Marker-based only, by explicit Principal decision, not inferred from command-looking spans: a doc opts a span in with `<!-- AEG:QUOTES-FILE:START:<path> -->…<!-- AEG:QUOTES-FILE:END -->` (the same invisible-on-render, code-blind HTML-comment idiom `anchored-region.ts` uses for its own seven PR/Issue-body fields, reusing that file's masking primitive rather than a second parser — but a distinct marker kind, since the cited path is free text, not one of that file's seven closed field names). An adopter-facing command in a README with no marker is instruction, not a claim, and is never flagged — inference over command-looking spans was explicitly rejected as the false-positive shape that gets a gate disabled. `findCitedQuotes` sweeps the identical `ships`/`reader-facing` corpus `reader-resolvable-prose` sweeps (never a second notion of "governed doc"); `evaluateCitedQuotes` then checks each marked span against its cited file's live content, wherever that file lives in the repo, naming both sides in a finding — what the doc claims, and which file no longer contains it verbatim. Zero I/O in `@attalabs/aeg-core`'s pure predicate (`quoted-command.ts`), corpus-tested against the real `aeg-root/**` tree; the bin supplies the only I/O. Ships with one real, live annotation (this very paragraph's own sibling callout above, quoting the CI invocation this page names) rather than only synthetic fixtures. Written after a measured incident: a CI invocation was pinned to a version in one pull request; this page quoted the pre-pin form verbatim as current fact; every registered check passed, `doctor` reported healthy, security passed, and a code-review agent found the drift only after independent re-derivation. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape`/`changeset-coverage` above — graduating to a blocking check is a later, separately-dispatched decision once the false-positive rate is observed against real adopter corpora; v1 carries no waiver escape. | product | `apps/cli/src/checks/bin/check-quoted-command.ts` |
|
|
143
150
|
| token-report | Ever seen a cost figure nobody could trace to a source? | ci | Re-checks that a PR's "Token report" section actually carries real numeric Tokens in/out figures, on any host this repo can prove is metering-capable. | The presence and shape of the `## Token report` block `roles/developer.md` requires every self-metering role to paste at turn-end — never whether the reported figures are TRUE, which CI structurally cannot recompute (the same bounded honesty this page's `evidence-fresh` row states for its own Group B). Runs `resolveMeteringCapability` (task 1's probe, `@attalabs/aeg-core`) fresh in THIS process; an incapable verdict (any reason) passes silently — the sanctioned operator-metered case, same discipline `token-collection-wired` above already applies. **Ring derived mechanically as `1`, not chosen** (a 2026-08-29 amendment correcting the original brief rationale's now-retracted ring-0/1 instruction): `requiresOpenPr: true` makes this check CI-only by `CoreCheckRing`'s own derivation rule, since the PR body it reads does not exist before a PR does. A probe that itself fails to run (an unexpected `exists`/`readFile` throw the probe does not catch) is never read as a clean incapable verdict: the bin lets it propagate uncaught, which the runner surfaces as `status: 'error'`, distinct from the `status: 'pass'` a real incapable verdict produces. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
|
|
144
|
-
|
|
145
151
|
**G-checks rollout (task 3):** G1/G2 shipped report-only at first — they could only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surfaced as visible debt without retroactively failing in-flight work. G3/G4/G5 have been blocking since that same tranche. **G1 flipped to blocking in task 8**, once its report-only window had let the orphan backlog get cleaned up — a permanent `info` finding on every run is indistinguishable from silence, which is exactly how the gap task 8 closes (G6, below) stayed invisible for as long as it did. G2's inverse sweep (no orphan hook/CLI) stays report-only; its rollout is unchanged. **G6 — doctrine-registry parity**, also added in task 8, ships blocking from the start: it checks that every row this page marks `product` (the `Audience` column, above) actually resolves to a real `coreCheckRegistry()` entry, closing the gap where a row could claim shipped enforcement that no adopter's `vinaya check` ever actually ran.
|
|
146
152
|
|
|
147
153
|
**reader-resolvable-prose / retired-vocabulary rollout (task 15; task 7 registered both in `coreCheckRegistry()`):** both ship report-only, same precedent as G1/G2 above — each can only ever print a `warning` finding, never fail CI (the check's own exit code stays `0`), so any real backlog surfaces as visible debt rather than a wave of newly-red pull requests (161 findings, all class 2, across `aeg-root` on the day `reader-resolvable-prose` first shipped). A later, separately-dispatched task flips either to blocking once its own backlog is cleared. **Exception, both checks (checkout-independent doctrine-root resolution):** the unconfigured doctrine-root default now resolves relative to the repo actually under check (its own real repository root) first, falling back to the check's own package-relative "shipped copy" resolution only when the repo under check has no local doctrine tree of its own — so the same commit sweeps identically regardless of where it happens to be checked out. When NEITHER resolves (no doctrine tree findable at all, relative to either anchor), that is not a `warning` finding and not a clean pass — it is reported as its own distinct outcome, with a non-`0`/non-`1` exit code, so a scan that never ran cannot be mistaken for a scan that ran and found nothing.
|
|
@@ -164,7 +170,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
164
170
|
| **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 | |
|
|
165
171
|
| 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` |
|
|
166
172
|
| 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` |
|
|
167
|
-
| Token self-report (
|
|
173
|
+
| Token self-report (shipped 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 today's shipped reference host, 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` |
|
|
168
174
|
| 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` |
|
|
169
175
|
|
|
170
176
|
---
|
package/aeg-root/process.md
CHANGED
|
@@ -67,7 +67,7 @@ If already locked or specced, the conversation ends here. If genuinely new, it p
|
|
|
67
67
|
|
|
68
68
|
**When:** high-stakes only — architectural locks, project-direction shifts, decisions blocking weeks of downstream work, or when the Principal's instinct and the Brief Author's read disagree. **Not** for tactical decisions, naming, or style. The Principal may waive it and ratify in-session (the pull request notes the skip, for audit honesty).
|
|
69
69
|
|
|
70
|
-
**Who:** Brief Author orchestrates; external AI reviewers (vendor-diverse —
|
|
70
|
+
**Who:** Brief Author orchestrates; external AI reviewers (vendor-diverse — independently hosted, each from a provider other than the one authoring the brief) participate via pasted briefs.
|
|
71
71
|
|
|
72
72
|
The Brief Author writes a brainstorming brief (idea, sketch, alternatives, what to pushback on), pairs it with `reviewer-prompt.md`, and pastes to each reviewer. The Brief Author synthesizes. Converge on a flaw → back to Phase 1. Validate → proceed. **Max two rounds** — if two don't converge, the issue is framing, not a third round.
|
|
73
73
|
|
|
@@ -98,7 +98,9 @@ A brief is self-contained and executable without further conversation. If it nee
|
|
|
98
98
|
|
|
99
99
|
**Artifacts:** the brief (a markdown block, not a committed file). The task's Issue already exists from tranche planning.
|
|
100
100
|
|
|
101
|
-
**
|
|
101
|
+
Before dispatch, a fresh-context Reviewer runs **Brief review mode** (`roles/reviewer.md`) on the brief itself — a separate, time-boxed pass from the post-dispatch code review, under five minutes, returning one line, `BRIEF: READY` or `BRIEF: NOT READY`, with findings limited to two classes: a contradiction between two of the brief's own sentences, or a design the brief specifies that the party it constrains can defeat or that fails on an input the brief never named. `BRIEF: NOT READY` returns the brief to its author rather than letting it reach dispatch.
|
|
102
|
+
|
|
103
|
+
**Exit:** the brief is well-formed, has passed Brief review mode, and is ready to dispatch.
|
|
102
104
|
|
|
103
105
|
---
|
|
104
106
|
|
|
@@ -219,7 +221,9 @@ Each pass is a **separate fresh-context invocation** with no memory of writing t
|
|
|
219
221
|
1. **Code-reviewer pass** — `roles/reviewer.md`. Brief conformance, scope violations, test honesty, code quality, doc coupling, lock awareness, multi-project reach. Emits `VERDICT: APPROVE | REQUEST CHANGES` (BLOCKER / MAJOR / MINOR).
|
|
220
222
|
2. **Security pass** — `roles/security.md`. Secret leakage, BYOK/crypto, auth/permissions, MCP/agent-tooling exposure, injection surfaces, dependency risk. Runs a config-security scan over the agent/MCP/hook config when that config is touched. Emits `VERDICT: PASS | FAIL` (CRITICAL / HIGH / MEDIUM / LOW).
|
|
221
223
|
|
|
222
|
-
A BLOCKER (code) or CRITICAL/HIGH (security) returns the PR to the Developer, who fixes on the **same branch**; the pass re-runs. (Pushing fixes returns the PR's review decision to open — the `changes-requested → in-review` transition, derived.)
|
|
224
|
+
A BLOCKER (code) or CRITICAL/HIGH (security) returns the PR to the Developer, who fixes on the **same branch**; the pass re-runs. (Pushing fixes returns the PR's review decision to open — the `changes-requested → in-review` transition, derived.) A verdict binds to the head it judged; a push landing after the newest verdict's judged head voids it and is named as such: `vinaya review status` prints `push after verdict — re-review required`, and merge waits on a fresh review round. An escalation (`--escalate authority | strategy | product`) is its own review outcome, never a finding — it routes to Brief Author (`strategy`) or Principal (`authority`/`product`).
|
|
225
|
+
|
|
226
|
+
**A fix commit adds no mechanism beyond what the finding names.** The finding bounds the fix: a flag, a gate, a window, a config knob or a second code path that the finding did not ask for is new design, arriving inside a round that exists to close a defect and reviewed by nobody as design. A finding that genuinely cannot be answered without new mechanism is not a fix at all — it is escalated (`--escalate strategy`) and waits, and the mechanism it needs enters the work the way every other design does: as a constraint amended into the next brief. This is the round-count rule's twin. Rounds multiply when each one both closes something and opens something; a round that only closes is a round the loop can converge out of.
|
|
223
227
|
|
|
224
228
|
### Stage B — Human reviews
|
|
225
229
|
|
|
@@ -227,7 +231,7 @@ A BLOCKER (code) or CRITICAL/HIGH (security) returns the PR to the Developer, wh
|
|
|
227
231
|
|
|
228
232
|
**Spec review (Brief Author).** Do the specs describe what was built? Is the pull request's stated reasoning honest about what changed? Coherence, not technical correctness (that's the Principal's code review).
|
|
229
233
|
|
|
230
|
-
If both pass (and agent verdicts are APPROVE
|
|
234
|
+
If both pass (and agent verdicts are APPROVE and PASS, and surfaced findings have been shown to the Principal at the go) → merge. If issues are found → back to the Developer with specific feedback. The loop ends by finding identity, never by round count — per the Review Cycle specification, round two judges only the lines changed since the previously judged head, for every non-blocking severity: a finding outside that delta is surfaced for the Principal's go rather than driving the verdict. A BLOCKER, CRITICAL or HIGH outside the delta still drives the verdict on any round; only MAJOR, MINOR, MEDIUM and LOW outside the delta are surfaced for the Principal's go. After round two the Principal decides, and there is no round three unless the Principal orders it. The loop pauses when a resolved finding reappears, when two consecutive rounds resolve no prior finding, or when one finding stays open three consecutive rounds while others resolve — every trigger is measured from the finding id sets, never from anyone's narrative of progress, and round five is a backstop, not a trigger. On a pause the Principal applies the `vinaya/needs:principal-input` label and works the stall menu cheapest first: a different role in the seat, resume with the trigger overridden, reseed the developer, abandon — a ruling re-enters the work as a constraint amended into the brief.
|
|
231
235
|
|
|
232
236
|
**Enforcement note:** the agent passes are **trusted discipline** today — Phase 10 requires them, but no CI bot dispatches them automatically yet. The mechanical CI gate is `verify-docs` (Phase 9). Automating review-agent dispatch is future work.
|
|
233
237
|
|