@attalabs/vinaya 0.24.0 → 0.25.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 +25 -2
- package/aeg-root/contracts/developer-reviewer.md +3 -0
- package/aeg-root/contracts/planner-brief.md +13 -0
- package/aeg-root/enforcement.md +11 -7
- package/aeg-root/process.md +4 -4
- package/aeg-root/roles/brief-author.md +4 -2
- package/aeg-root/roles/developer.md +20 -14
- package/aeg-root/roles/planner.md +14 -0
- package/aeg-root/roles/principal.md +3 -0
- package/aeg-root/roles/reviewer.md +23 -7
- package/aeg-root/roles/security.md +21 -6
- package/aeg-root/skills/brief-authoring/SKILL.md +2 -2
- package/aeg-root/state-machine.md +2 -2
- package/aeg-root/templates/brief-template.md +20 -4
- package/aeg-root/templates/issue-rationale-template.md +34 -1
- package/aeg-root/templates/pr-report-template.md +5 -13
- package/dist/checks/bin/check-body-bare-digits.js +572 -127
- package/dist/checks/bin/check-branch-topology.js +629 -73
- package/dist/checks/bin/check-brief-shape.js +1675 -70
- package/dist/checks/bin/check-changeset-coverage.js +584 -127
- package/dist/checks/bin/check-closes-n.js +629 -73
- package/dist/checks/bin/check-coherence.js +696 -134
- package/dist/checks/bin/check-dead-branch-push.js +505 -66
- package/dist/checks/bin/check-dispatch-readiness.js +696 -134
- package/dist/checks/bin/check-doc-coverage-push.js +572 -127
- package/dist/checks/bin/check-doc-coverage.js +572 -127
- package/dist/checks/bin/check-doctrine-no-procedures.js +572 -127
- package/dist/checks/bin/check-doctrine-portability.js +572 -127
- package/dist/checks/bin/check-evidence-fresh.js +572 -127
- package/dist/checks/bin/check-exec-bits.js +572 -127
- package/dist/checks/bin/check-first-push-dispatch.js +696 -134
- package/dist/checks/bin/check-issue-assignment.js +629 -73
- package/dist/checks/bin/check-main-branch-refusal.js +505 -66
- package/dist/checks/bin/check-no-disk-state.js +505 -66
- package/dist/checks/bin/check-pr-report-density.js +505 -66
- package/dist/checks/bin/check-quoted-command.js +572 -127
- package/dist/checks/bin/check-reader-resolvable-prose.js +610 -133
- package/dist/checks/bin/check-registry-gates.js +517 -69
- package/dist/checks/bin/check-retired-vocabulary.js +572 -127
- package/dist/checks/bin/check-review-gate.js +646 -130
- package/dist/checks/bin/check-single-plan-pr.js +505 -66
- package/dist/checks/bin/check-test-plan.js +505 -66
- package/dist/checks/bin/check-token-collection-wired.js +505 -66
- package/dist/checks/bin/check-token-report.js +505 -66
- package/dist/checks/bin/check-workspace-escape.js +582 -129
- package/dist/index.js +7740 -4283
- package/package.json +2 -2
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -1
- 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/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]__1pndh3_._.js → [root-of-the-server]__0p8q38b._.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]__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/_0wxycau._.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/chunks/ssr/packages_ui_topbar_index_tsx_1h1gs1y._.js +1 -1
- 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/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -12,6 +12,12 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
12
12
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/verdict-extraction.ts contains:function firstFiveLines(comment: string): string { -->
|
|
16
|
+
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function renderEscalationComment(input: EscalationInput): string { -->
|
|
17
|
+
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function checkRenderedComment(body: string, expectation: RenderExpectation): RenderCheckResult { -->
|
|
18
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:export function renderBrief(facts: BriefFacts, template: string): RenderResult { -->
|
|
19
|
+
<!-- AEG:CLAIM: apps/cli/src/commands/brief.ts contains:refuse(`cannot render — missing fact(s): -->
|
|
20
|
+
|
|
15
21
|
| Command | Description |
|
|
16
22
|
|---------|-------------|
|
|
17
23
|
| `vinaya help` | Usage text |
|
|
@@ -19,10 +25,15 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
19
25
|
| `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
26
|
| `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
|
|
21
27
|
| `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
|
|
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
|
|
28
|
+
| `vinaya brief render <tranche> <n> --surfaces <glob,...> [--out <path>]` | Emit the twelve-section brief skeleton from the task Issue and the tree, with every mechanically-derivable section filled: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses, naming the missing section, when a section cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder. Never writes under `aeg-root/` or to the Issue — stdout, or `--out <path>`, only; a brief is pasted to the Developer, never committed. |
|
|
29
|
+
| `vinaya task dispatch <tranche> <n> [--agent claude \| codex \| gemini]` | Render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. With `--agent`, starts the Developer through `dispatchRole` (`apps/cli/src/lib/dispatch.ts`) when that function is available; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block. Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. |
|
|
30
|
+
| `vinaya pr create --body-file <path> --title <title> [--label ...] [--validate-only] [--json]` | Open a pull request after full brief-schema validation of the body against `vinaya.config.json`'s configured sections — never a body containing the retired `<!-- aeg:brief:start -->`/`<!-- aeg:brief:end -->` markers, refused outright: the brief lives on the task Issue's `aeg:brief:v1` comment now (`vinaya task dispatch`), never split out of or pasted into the PR body. `--validate-only` runs every gate and reports PASS without opening anything; `--json` for the enveloped machine form. |
|
|
31
|
+
| `vinaya issue objectives edit <n> --add "<sentence>" \| --drop O<k> \| --replace O<k> "<sentence>" --reason "<text>"` | Rewrite a task Issue's `## Objectives` section by command — the Principal's way to change a task's scope mid-flight, findable and versioned rather than a silent hand-edit. Exactly one of `--add`/`--drop`/`--replace` plus a non-empty `--reason` is required; the edit runs through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself. `--drop` never renumbers the survivors — a drop that leaves the list non-contiguous from `O1` is refused with `objectivesOf`'s own parser message, since task 1's contiguous-from-O1 grammar and this command's never-renumber rule can genuinely contradict on a real drop, and that contradiction is reported rather than papered over. Splices the rendered section back in place, leaving every other byte of the body untouched, then posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new version hash — `k` counted on the forge at post time, never from a local file. |
|
|
32
|
+
| `vinaya log flush --issue <n> \| --pr <n>` | Post a target Issue or PR's outbox (`~/.vinaya/outbox/<owner>-<repo>/<issue-or-none>.ndjson`, written by `log()`) as one or more comments, each opening with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` followed by a fenced `ndjson` block. Splits first at `run_id` boundaries — a maximal run of consecutive lines sharing one `run_id`, so an interleaved outbox never produces a range spanning a gap — then at `FORGE_COMMENT_MAX_CHARS` (65536); a single line too large to fit alone is refused by its seq, never split. `--pr <n>` resolves the Issue from that PR's body `Closes #N` line and posts on the PR instead of the Issue; a body with none is refused. Logs its own `forge_write` line (`validated` before posting, `written` with every comment id after the last post succeeds, or `refused` with gh's error) through `log()` into the same outbox before truncating, so that line rides to the next flush; the outbox is truncated only to the lines confirmed posted, and a gh failure leaves the rest untouched. `--json` for the enveloped machine form. |
|
|
23
33
|
| `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
34
|
| `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
|
|
35
|
+
| `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. `--objectives-file <path>` (`O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` per line) renders an `OBJECTIVES:` block after `SPEC CONFORMANCE:`/before `CONFIG SCAN:` and an `Objectives version:` line at line 5 — resolved from the closed Issue's `## Objectives` list, or the PR body's own section when it closes none; required whenever that resolution finds a list to judge, its ids must cover the list exactly, a clean verdict is refused alongside any `NOT MET`, and a re-review must restate every prior objective; an Issue below the objectives cutover renders neither line at all (never together with `--escalate`). Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`ESCALATE:`/`Judged head:`/`Objectives version:` 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 five lines, and a code-review/security render's caller-supplied fields never open one of those lines, but an escalation's `--summary` can (pre-cutover, it becomes line 5 unprefixed); this pre-post re-parse, not the render's construction, is what catches that case before anything is posted; and after posting, re-fetches the comment and refuses to exit 0 unless the same re-parse holds against the live forge state, including the objectives version. A `doc-correctness` finding is refused before posting unless its description ends with a `Search: <pattern>` carrying no path filter (`--`), so the claim, not the anchored line, is the unit of the finding. `--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. |
|
|
36
|
+
| `vinaya pr rule <pr> --file <ruling.md>` | Post the Principal's ruling on a PR as its own marked, versioned comment (`<!-- aeg:principal:ruling:<pr>-<k> -->`) — never mistaken for a code-review or security verdict. Refuses before posting when the file's first line reads as an escalation (`ESCALATE:`), or when the file carries verdict grammar anywhere `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` would treat as a candidate — a `VERDICT:` line sitting past the extractor's own first-five-line read window still counts, since it's the whole-body candidate test that disqualifies the file, not merely a clean read; a blockquoted mention of `VERDICT:` is unaffected, since the extractors ignore it too. `k` is counted on the forge at post time from `gh pr view --json comments`, never derived from a local file. |
|
|
26
37
|
| `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. |
|
|
27
38
|
| `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). |
|
|
28
39
|
| `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. |
|
|
@@ -32,6 +43,12 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
32
43
|
| `vinaya milestone close --slug <slug>` | Close a tranche's Milestone — the gated replacement for the raw `gh api .../milestones/<n> -X PATCH -f state=closed` recipe the Tranche Archivist used to run on faith. Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge. `--validate-only` verifies attachment without closing; `--json` for the enveloped machine form. |
|
|
33
44
|
| `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
|
|
34
45
|
| `vinaya release [--dry-run] [--allow-any-commit]` | Run this repo's own publish sequence in one command (`apps/cli/specs/self-hosting.md`, "How the published version is produced"). Refuses unless HEAD is the default branch, the tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix. Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and a real `git push origin --tags`, so the repo's own generated pre-push hook sees the push exactly as any other push would. Afterward prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version. `--dry-run` stops after the preconditions and prints the plan; publishes nothing. |
|
|
46
|
+
| `vinaya dispatch <role> --agent claude \| codex \| gemini --prompt-file <path> [--task <n> \| --pr <n>] [--round <n>] [--resume <id>]` | Start a role's headless agent session, attributed: `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` are set on the CHILD's environment only, never on this process's own — a role started this way is what makes its later `vinaya` calls (the Stop hook, a nested dispatch) attributable instead of `unattributed`. The prompt is read from `--prompt-file` and sent on the child's stdin, never argv. Refuses by name, before any spawn, when the named vendor binary is absent from `PATH` or present but not executable. A wall-time ceiling (`dispatch.timeoutMs` in config, default one hour) sends `SIGTERM` then, after a grace window, `SIGKILL`. Records `dispatched`/`outcome_received`/`dispatch_failed` through the Vinaya Log's `dispatch` family (`apps/cli/specs/log.md`); when `--task`/`--pr` is given, flushes that outbox via `vinaya log flush` immediately after. A successful dispatch's own vendor session/thread identifier is returned as `resumeId` (printed alongside the other fields); passing that value back as `--resume <id>` on a later call swaps in that vendor's own resume invocation instead of starting a fresh session. |
|
|
47
|
+
| `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` | Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`/`gh pr review`) before `publish`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
|
|
48
|
+
|
|
49
|
+
Which one lib function backs each command, and today's dated exemptions where it calls more than that one: `apps/cli/specs/surface.md`, enforced by `apps/cli/tests/surface-index.test.ts`.
|
|
50
|
+
|
|
51
|
+
The typed event log's header, families and outbox: `apps/cli/specs/log.md`.
|
|
35
52
|
|
|
36
53
|
## Config
|
|
37
54
|
|
|
@@ -272,6 +289,12 @@ Doctrine explains what a command does and why; it is not a runbook a reader exec
|
|
|
272
289
|
|
|
273
290
|
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.
|
|
274
291
|
|
|
292
|
+
## Objectives gate
|
|
293
|
+
|
|
294
|
+
A task Issue must carry a `## Objectives` section — numbered `O<n>. <sentence>` lines, one observable outcome each, contiguous from `O1`, never a file path. `vinaya issue create`/`vinaya issue edit` refuse a task Issue without one via the `objectives` `briefSchema.issue` builtin, and `vinaya check coherence`'s R1 grades the same rule continuously against the live stock — both for Issues numbered `OBJECTIVES_SINCE_ISSUE` (404) and above; an Issue below that number passes unconditionally, so the pre-gate stock stays green.
|
|
295
|
+
|
|
296
|
+
The brief side has two more gates, run at `verify-brief`/`brief-shape` time: the brief's own `## Objectives` section must match the Closes-linked Issue's (compared normalised — whitespace never fails it, one changed word does), and every numbered Part in §6 must cite at least one `O<n>` while every `O<n>` is cited by at least one Part. A standalone brief with no linked Issue is exempt unless it opts in with its own `## Objectives` section, in which case it is compared against itself.
|
|
297
|
+
|
|
275
298
|
## Brief-schema divergence
|
|
276
299
|
|
|
277
300
|
`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.
|
|
@@ -53,6 +53,8 @@ The **open PR** — the diff plus the PR body, which carries the brief verbatim.
|
|
|
53
53
|
|
|
54
54
|
Every item the Developer produces in the open PR (left) has exactly one obligation for the Reviewer (right). A PR missing any left-column item is not ready for review — the Reviewer refuses to start and sends it back to the Developer.
|
|
55
55
|
|
|
56
|
+
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function checkDocCorrectnessSearch(description: string, lineNumber: number): void { -->
|
|
57
|
+
|
|
56
58
|
| Developer produces in the PR | Reviewer consumes at | What the consumption means |
|
|
57
59
|
|---|---|---|
|
|
58
60
|
| **Brief in PR body** (the frozen brief, pasted verbatim) | Entry — read before looking at the diff | The Reviewer reads the brief first to understand intent, boundary, surface map, and traps. Reviewing a diff without the brief is not a valid review pass. |
|
|
@@ -62,6 +64,7 @@ Every item the Developer produces in the open PR (left) has exactly one obligati
|
|
|
62
64
|
| **Task Done checklist ticked** | Confirms Developer self-checked | The Reviewer verifies the checklist is present and ticked. An unticked item that the Reviewer then finds broken is a MAJOR finding. |
|
|
63
65
|
| **`[agent]` Test Plan items run with evidence comment** | Confirms runtime verification | The Reviewer checks that actual command output was posted for every `[agent]` item. Missing evidence = unticked item = MAJOR finding. |
|
|
64
66
|
| **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. |
|
|
67
|
+
| **Every copy of a false doc claim fixed** | Re-review of that finding id | A `doc-correctness` finding carries a `Search:` pattern — a repo-wide `git grep -n -iE` pattern, with no path filter — and is resolved only when every hit it returns at the new head is a true statement. `vinaya review post` refuses a missing or path-filtered pattern mechanically; the rest is the Reviewer's. A missing pasted search output, a still-false hit, or a corrected claim about code left unbound by `AEG:CLAIM` keeps the id open — a BLOCKER id keeps driving the verdict. |
|
|
65
68
|
| **`.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
69
|
|
|
67
70
|
**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.
|
|
@@ -65,9 +65,22 @@ Every field the Planner emits in the rationale (left) has exactly one named home
|
|
|
65
65
|
| **Suggested agent-class** (high/mid/fast + one-line reason) | `For:` + `Reason:` header | The Brief Author confirms or deviates (with stated reason) and makes the **final** model pick. Class is the planner's; pick is the brief's. |
|
|
66
66
|
| **Stop-and-escalate** (when the agent must stop, not improvise) | Stop conditions | The planner's stop conditions are copied into the brief's stop-condition list verbatim in substance. |
|
|
67
67
|
| **Docs to keep coherent** (which specs/skills/docs this task will make incoherent) | the documentation-update list | The Brief Author turns the Planner's named list into the explicit doc-update items. Conditional: if the Planner stated "No docs touched," the list is "No doc updates required (Tier 0)." If the Planner named docs, they are all in the list. Presence-**and-correctness** of the list is the Planner's mechanical obligation, not naming docs from memory: the Planner names intended surfaces and the Brief Author derives the floor at brief-authoring time by matching those surfaces against `.vinaya/doc-owners` (`deriveSection7`), then supplements from the read obligation. Any override of the derived floor (added doc, or a derived pointer marked out of scope) must carry a one-line reason in the brief — silent overrides are a regression. |
|
|
68
|
+
| **Objectives** (the Issue's `## Objectives` section: numbered `O<n>. <sentence>` lines, one observable outcome each) | the brief's own `## Objectives` section | The Brief Author copies the Issue's `## Objectives` section into the brief; every numbered Part in §6 cites at least one `O<n>` and every `O<n>` is cited by at least one Part. `checkObjectivesCopy`/`checkObjectivesCoverage` (`@attalabs/aeg-core`) enforce both directions at `verify-brief`/`brief-shape`. |
|
|
69
|
+
| **Surface** (the Issue's `## Surface` `in:`/`out:` directory-level glob lists) | §4 Technical surface map's Create/Modify split and Out of surface line | `vinaya brief render` renders §4's Out of surface verbatim from the Issue's `out:` list, and matches concrete surface files against the `in:` globs — never a hand-authored placeholder. |
|
|
70
|
+
| **Parts** (the Issue's `## Parts` section: numbered `Part <k> (O<n>[, O<m>]) — <outcome>` lines) | §6 Numbered parts | Each `IssuePart` renders as one numbered Part in §6, its citation reconstructed verbatim from the parsed objective refs. |
|
|
71
|
+
| **Test plan** (the Issue's `## Test plan` section: the `unit-tests-only` sentinel, or a fenced command list plus `[principal]` items) | §9 Test Plan | Copied into §9 verbatim — `vinaya brief render` never re-derives a Test Plan from the surface file list. |
|
|
72
|
+
| **Stop conditions** (the Issue's `## Stop conditions` bullet list) | §10 Stop conditions | Rendered alongside the Stop-and-escalate field, never in place of it. |
|
|
68
73
|
|
|
69
74
|
**Reading the table:** left is the producer obligation (Planner role doc enforces it), right is the consumer obligation (brief-authoring skill enforces it). The two role docs must not contradict this table; if either needs to change what it emits or consumes, it changes *here*, and both sides update together.
|
|
70
75
|
|
|
76
|
+
Bindings for the four `Surface`/`Parts`/`Test plan`/`Stop conditions` rows above — each names what `packages/aeg-core/src/brief-render.ts` actually does, not what it is planned to do:
|
|
77
|
+
|
|
78
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:'**Out of surface:** ' + -->
|
|
79
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:function renderPartCitation(part: IssuePart): string { -->
|
|
80
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:...facts.testPlan.lines, -->
|
|
81
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:`${facts.rationale.stopAndEscalate}` -->
|
|
82
|
+
Confirmed, in order: the Out-of-surface line is rendered from `facts.surface.out`; `renderPartCitation` is the one function that reconstructs a Part's citation; `facts.testPlan.lines` is spliced into §9 verbatim, never re-derived; `facts.rationale.stopAndEscalate` renders alongside `facts.stopConditions`, never in its place.
|
|
83
|
+
|
|
71
84
|
**Premise pinning is deliberately NOT a field in this table.** `verify-dispatch`/`verify-task`'s `Premise:` block is file-content-level, perishable detail — current signatures, current constants — squarely inside the Brief Author's half of the division of labor this contract already describes, not a durable conclusion the Planner should seed as a rationale field. It is governed entirely by the Brief Author → Developer seam (`contracts/brief-developer.md`), not this one.
|
|
72
85
|
|
|
73
86
|
---
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -127,30 +127,30 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
127
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` |
|
|
128
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` |
|
|
129
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` |
|
|
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` |
|
|
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 (C5), published prose (C7), and doc-claim bindings (C8) | product | `packages/aeg-core/bin/verify-docs.ts` |
|
|
131
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` |
|
|
132
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` |
|
|
133
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` |
|
|
134
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` |
|
|
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.
|
|
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. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
136
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` |
|
|
137
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` |
|
|
138
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` |
|
|
139
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` |
|
|
140
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` |
|
|
141
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` |
|
|
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
|
|
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, including the managed hooks: `scope: 'full'`, so `--local` always runs it (the `git push` row above). **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. **A fourth class, `product`, is the one blocking exception:** a tranche-slug citation under `PRODUCT_SLUG_SCOPE` (CLI source, the CLI and sources READMEs, the workflows, `.vinaya`) fails the check outright — `severity: 'error'`, non-zero exit — where every other class here stays `warning`/exit `0`. Because this check is `scope: 'full'` and therefore already in `--local`'s sweep, `product` first fires at `git push` (ring 0) — the earliest point this row's own findings can refuse anything — and again, blocking, in CI (ring 1) over the same diff. | product | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
|
|
143
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
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
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` |
|
|
146
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` |
|
|
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
|
|
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`. `*.test.ts`/`*.test.tsx` files are excluded from the swept surface entirely (O2, found live 2026-09-04) — this repo's own corpus carried a genuine escape at `packages/sources/src/commands-router-coverage.test.ts` reaching into `apps/cli/src/index.ts`, structurally the same incident this check exists to catch, until that exclusion silenced it alongside `workspace-escape.test.ts`'s own fixture-string false positive; report-only severity stays regardless, since an ordinary (non-test) source file's constructed reference can still exist with nobody having triaged it yet. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. (this sweep's real domain is `apps`/`packages`, not `aeg-root` — named per the Issue's own list; harmless either way while `include` has no runtime effect on a `scope: 'full'` entry.) | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
|
|
148
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` |
|
|
149
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` |
|
|
150
150
|
| token-report | Ever seen a cost figure nobody could trace to a source? | ci | Re-checks that a PR's "Token report" section actually carries real numeric Tokens in/out figures, on any host this repo can prove is metering-capable. | The presence and shape of the `## Token report` block `roles/developer.md` requires every self-metering role to paste at turn-end — never whether the reported figures are TRUE, which CI structurally cannot recompute (the same bounded honesty this page's `evidence-fresh` row states for its own Group B). Runs `resolveMeteringCapability` (task 1's probe, `@attalabs/aeg-core`) fresh in THIS process; an incapable verdict (any reason) passes silently — the sanctioned operator-metered case, same discipline `token-collection-wired` above already applies. **Ring derived mechanically as `1`, not chosen** (a 2026-08-29 amendment correcting the original brief rationale's now-retracted ring-0/1 instruction): `requiresOpenPr: true` makes this check CI-only by `CoreCheckRing`'s own derivation rule, since the PR body it reads does not exist before a PR does. A probe that itself fails to run (an unexpected `exists`/`readFile` throw the probe does not catch) is never read as a clean incapable verdict: the bin lets it propagate uncaught, which the runner surfaces as `status: 'error'`, distinct from the `status: 'pass'` a real incapable verdict produces. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
|
|
151
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.
|
|
152
152
|
|
|
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.
|
|
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. **`reader-resolvable-prose`'s `ships`/`reader-facing`/vocabulary classes stay report-only under this rollout; `retired-vocabulary` is unaffected in full.** Only `reader-resolvable-prose`'s fourth, `product` class (see the row above) graduated to blocking, ahead of the rest: a tranche-slug citation in product code fails outright, at both `git push` and CI. **Exception, both checks (checkout-independent doctrine-root resolution):** the unconfigured doctrine-root default now resolves relative to the repo actually under check (its own real repository root) first, falling back to the check's own package-relative "shipped copy" resolution only when the repo under check has no local doctrine tree of its own — so the same commit sweeps identically regardless of where it happens to be checked out. When NEITHER resolves (no doctrine tree findable at all, relative to either anchor), that is not a `warning` finding and not a clean pass — it is reported as its own distinct outcome, with a non-`0`/non-`1` exit code, so a scan that never ran cannot be mistaken for a scan that ran and found nothing.
|
|
154
154
|
|
|
155
155
|
**changeset-coverage rollout:** ships report-only, same precedent as the report-only rows above — the check's own exit code always stays `0`, so registering it cannot newly redden any existing install. Graduating to blocking, and the waiver-label escape that graduation would need, are both deliberately deferred to a later, separately-dispatched task once the check's own false-positive rate is observed in practice; v1 carries no self-serve or principal-applied escape at all.
|
|
156
156
|
|
|
@@ -179,9 +179,12 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
179
179
|
|
|
180
180
|
A full audit of this page against the installed hooks and CI workflows (2026-07-04) found three gates that existed **only** at ring 0 — invisible to any writer without the local hooks installed (web UI, another tool, a human on a fresh clone, an agent harness without the wiring). This table is the durable record of that audit: every enforcement on this page, its ring coverage, its paired status, and — for anything still a gap — which of the two known gap families it belongs to.
|
|
181
181
|
|
|
182
|
-
**
|
|
182
|
+
**Three gap families:**
|
|
183
183
|
- **(a) Local-only gates lacking a forge backstop** — a ring-0 mechanism with no ring-1 (CI) or ring-2 (audit) counterpart, so a writer that skips the local hook skips the rule entirely. This task closes three.
|
|
184
184
|
- **(b) Manual CLIs lacking a forcing mechanism** — a tool that mechanizes a check (`verify-dispatch`, `verify-task`) but nothing yet *requires* an agent to run it; an agent can simply skip invoking it. **Closed by task 25**: `verify-dispatch` now runs itself on a task branch's first push (the managed `.git/hooks/pre-push` block); `verify-task` now runs itself inside `open-pr.ts` for task branches. Both land at ring 0 only (no ring-1/ring-2 backstop yet) — the same accepted, tracked shape as the branch-ID check below, not a new gap this task opened.
|
|
185
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/brief-render.ts contains:function renderSection9( -->
|
|
186
|
+
<!-- AEG:CLAIM: apps/cli/src/commands/pr-report.ts contains:extractFencedBlocks(located.section) -->
|
|
187
|
+
- **(c) No identity/authorship check on a task-Issue or task-PR write** — every content gate on this page (`checkIssueRationale`, `checkBlastRadiusScope`, `checkNoBriefContent`, brief-shape, and the rest) validates shape only, never who wrote it. This is safe today only because writing a task Issue requires the same access as dispatching a task in the first place — a fact about who currently holds write access to a given repo (one collaborator, here), not an enforced permission tier. A repo that adds a second collaborator, a bot, or external-contributor access without also gating who may write a task Issue inherits the exposure silently: a task Issue's `## Test plan` section renders verbatim into a brief's §9 (`brief-render.ts`), and `vinaya pr report`'s Group C step re-executes those fenced commands unattended, against the PR head — so task-Issue write access is, transitively, unattended-command-execution access. **Accepted risk, not scheduled for a gate (Principal ruling, 2026-09-06):** before adopting unattended dispatch (`runTask`, the forward-looking dispatch chokepoint) in a repo with more than one trusted writer, add an author check to the task-Issue write path, or accept the risk explicitly and say so in that repo's own doctrine.
|
|
185
188
|
|
|
186
189
|
| Enforcement | Ring 0 | Ring 1 | Ring 2 | Paired status | Gap family |
|
|
187
190
|
|---|---|---|---|---|---|
|
|
@@ -191,6 +194,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
|
|
|
191
194
|
| Orphan-task coherence (T2) | — (no ring-0 form; a CI-only check) | ✅ coherence oracle, **relocated to plan-PRs-only** | ✅ `--json`/audit mode, `daily-drift` (info-only) | Paired (pre-existing); **placement corrected** (this task, item 4) | n/a — not a coverage gap, a point-of-power correction |
|
|
192
195
|
| Coherence oracle repo-state reads | n/a | ✅ now reads fetched `origin/main`, not the merge-ref checkout | ✅ same fix applies to `--json`/audit mode | Paired (pre-existing); **staleness bug fixed** (this task, item 5) | n/a — not a coverage gap, a correctness fix |
|
|
193
196
|
| Branch-ID must match a topology row | ✅ pre-push hook — delegates to aeg-core (`check-branch-topology.ts` → forge-derived Tranche via `deriveTrancheFromForge`, task 3a; originally `parseTranche`, task 32); formerly a hand-rolled grep fork of the same table-parse | — | — | Ring-0 only | **(a) — still a gap, not addressed by this task** |
|
|
197
|
+
| Task-Issue/task-PR authorship (who wrote it, not what it says) | — (no Ring-0 check; content gates validate shape only) | — | — | No gate | **(c) — accepted, not scheduled (Principal ruling)** |
|
|
194
198
|
| Code→doc ownership (C5) | ✅ pre-push hook + PR-open gate | ✅ `verify-docs --pr`, now a step of the AEG gate suite job (`verify-docs.yml` deleted — task 31) | — | Paired (pre-existing) | not a gap |
|
|
195
199
|
| Brief-section grammar / title convention | ✅ `open-pr.ts`/`open-issue.ts` | ✅ Brief Validation, now a step of the AEG gate suite job (moved out of `archivist.yml` — task 31) | — | Paired (pre-existing) | not a gap |
|
|
196
200
|
| Planner-rationale **content** — blast radius / brief-content / docs-read | ✅ `open-issue.ts` (A/B/D refuse; C warns) | — (no continuous half yet: the checks derive collision domains live from `package.json` + built-in defaults, plus any `vinaya.config.json` entries, and read `projects.md`, which CI has, but no oracle check re-runs them over the live Issue stock) | — | Ring-0 only | **gap: no ring-1 half** (narrowed: the project-registry content check, added alongside A/B/D, does have one — it re-runs inside R1 below) |
|
|
@@ -199,7 +203,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
|
|
|
199
203
|
| `verify-task` (pre-PR exit composite) | ✅ **new** — `open-pr.ts` runs it wholesale for task branches (`gatePlanForBranch`); on `edit <n>` the gate set now resolves from the target PR's **actual forge head branch** (`gh pr view --json headRefName`, fetch failure = hard refusal), not the local checkout/`BRANCH` env, which silently skipped `closes-n`/`verify-task` on task-PR body edits from a `main` checkout (task 36 bundled finding) | — | — | **Forced, ring-0 only (this task, item 2)**; **edit-path gate-selection bypass fixed** (task 36) | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
|
|
200
204
|
| Runtime Test Plan checkbox state (`verify-test-plan`) | n/a (CI-only check) | ✅ now a step of the AEG gate suite job (`verify-test-plan.yml` deleted — task 31; still runs on PR-body `edited` events) — **reads `BRANCH`; parses both the inline `**Test Plan:**` marker and the `## N. Test Plan` heading form** (task 25, item 3) | — | Paired (pre-existing); **parser bug fixed** | n/a — not a coverage gap, a correctness fix (a live-fire gap: heading-form sections were never matched, so a task PR with an unticked `[principal]` box advisory-PASSED) |
|
|
201
205
|
| Waiver-label-actor verification (task 29; **extended to `vinaya/waiver:review`, review-gate task 1**) | ✅ **new** — pre-push is warn-only (no `vinaya/waiver:docs`/`vinaya/waiver:review` label can exist before a PR does) **+ tool-layer (attalabs only)**: the attalabs reference implementation adds a session hook denying any agent-session command that mutates either label; this repo has no tool-interception layer (see the raw-API ring-0 row), so the shared-credential hole is OPEN here — a local agent using the Principal's own `gh` credential can apply a waiver label whose timeline actor then reads as the Principal, and the actor-verification step cannot distinguish it. Until an interception layer exists, waiver labels in this repo rest on credential discipline, not a gate | ✅ **new** — a GraphQL `LABELED_EVENT`/REST timeline step resolves the label's actor, verified by `isWaiverLabelActorVerified` (now parameterized by label name) and fed into `verify-docs --pr`'s C5 waiver check (`vinaya/waiver:docs`) and `verify-review-gate.ts`'s review check (`vinaya/waiver:review`) | — | **New (this task)**; **extended (review-gate task 1)** | n/a — new mechanism |
|
|
202
|
-
| Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
|
|
206
|
+
| Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)**; **objectives-version binding added by a later task — implemented in `apps/cli/src/checks/bin/check-review-gate.ts`, this repo's actual live gate path, not in this row's own `verify-review-gate.ts`, which never resolves it (see the Ring 1 Review gate row above)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
|
|
203
207
|
|
|
204
208
|
This table describes **installed mechanisms only** (the page's own change-discipline rule, below). The `verify-dispatch`/`verify-task` rows and the `verify-test-plan` fix landed in task 25's own change set, per that rule.
|
|
205
209
|
|
package/aeg-root/process.md
CHANGED
|
@@ -94,7 +94,7 @@ When a task is picked up for execution, the Brief Author writes its brief — **
|
|
|
94
94
|
|
|
95
95
|
A brief is self-contained and executable without further conversation. If it needs clarifying questions, it's incomplete.
|
|
96
96
|
|
|
97
|
-
**Where the brief lives:** it
|
|
97
|
+
**Where the brief lives:** `vinaya task dispatch` posts it once, frozen, as the task Issue's own `aeg:brief:v1` comment, before the Developer starts — never in the PR body, which carries only the Developer's report. It is **not** committed and **not** put in the Issue's own body — the Issue body (created by the Planner) holds task identity + metadata only; the brief is a comment on it, not a field of it.
|
|
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
|
|
|
@@ -127,7 +127,7 @@ If it fails, it's not dispatchable until fixed. The Developer also re-checks wel
|
|
|
127
127
|
|
|
128
128
|
Dispatch starts the task. There are two equivalent routes:
|
|
129
129
|
|
|
130
|
-
- **Manual:** the Principal pastes
|
|
130
|
+
- **Manual:** `vinaya task dispatch <tranche> <n>` posts the frozen `aeg:brief:v1` Issue comment, then the Principal pastes that comment's content into the coding agent. The brief's worktree-first Step 0 makes the Developer create its own worktree (`.worktrees/task/<tranche>/<n>/`, branch `task/<tranche>/<n>`, from `origin/main`) as its first action.
|
|
131
131
|
- **Automated:** an automation layer creates the worktree, generates the agent's config, spawns the Developer in it, and streams progress. This is a convenience; the semantics are identical to manual.
|
|
132
132
|
|
|
133
133
|
Either way: **before starting, the Developer checks the dispatch gates against the forge** — every `depends-on` task's PR merged, no `conflicts-with` sibling's PR open. If a gate isn't satisfied, it does not start (the task serializes). Opening the branch *is* the `todo → in-flight` transition; nobody writes a status label.
|
|
@@ -193,7 +193,7 @@ Before opening the PR, the Developer runs the tier-appropriate Task Done checkli
|
|
|
193
193
|
|
|
194
194
|
The Developer opens a PR with:
|
|
195
195
|
- Title in commitlint format (`Type: Subject`)
|
|
196
|
-
- **
|
|
196
|
+
- **No brief in the body at all** — its permanent home is the task Issue's frozen `aeg:brief:v1` comment (posted at Phase 5, before the Developer ever started), which is where the Reviewer and Archivist read it
|
|
197
197
|
- A `Tier:` declaration (`Tier: 0|1|3`) so verify-docs reads the correct tier
|
|
198
198
|
- `Closes #N` linking the task's Issue (so the merge auto-closes it)
|
|
199
199
|
- Body following the PR template (what shipped, validated mechanism, what's not in scope, next steps)
|
|
@@ -221,7 +221,7 @@ Each pass is a **separate fresh-context invocation** with no memory of writing t
|
|
|
221
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).
|
|
222
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).
|
|
223
223
|
|
|
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
|
|
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 and the objectives list it judged against; a push or an objectives edit — including a mid-PR scope change made through `vinaya issue objectives edit` — voids the verdict, and a body edit alone changes nothing the gate reads. A voided verdict is named as such: `vinaya review status` prints `push after verdict — re-review required` for the head case, `objectives moved — re-review required` for the objectives case, and merge waits on a fresh review round either way. 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
225
|
|
|
226
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.
|
|
227
227
|
|
|
@@ -59,7 +59,7 @@ Item 1 governs every brief, whatever the branch: a real Issue is required either
|
|
|
59
59
|
|
|
60
60
|
1. **The task has a real issue.** A task with no forge issue is not briefable. Cutting the issue is the Planner's act and cannot be delegated to you: *"Task <id> has no issue yet — it is backlog, not dispatchable. The Planner cuts it first."*
|
|
61
61
|
2. **Its dispatch gates are clear.** Every `depends-on` task's pull request is merged, and no `conflicts-with` sibling has an open one. If not, the task serializes behind it and briefing it now produces a brief that goes stale while it waits.
|
|
62
|
-
3. **The rationale is complete.** All eight planner fields present. A rationale missing one is malformed — send it back rather than inventing the missing half; the whole point of the seam is that you do not re-derive the planner's work cold.
|
|
62
|
+
3. **The rationale is complete.** All eight planner fields present, and a `## Objectives` section above them (numbered `O<n>. <sentence>` lines, from `OBJECTIVES_SINCE_ISSUE` (404) onward). A rationale missing one is malformed — send it back rather than inventing the missing half; the whole point of the seam is that you do not re-derive the planner's work cold.
|
|
63
63
|
4. **You have read the surfaces.** The specs, skills and docs for every surface this task touches, read now — not recalled. **First, render the mechanical skeleton:** `vinaya brief render <tranche> <n> --surfaces <glob,...>` (task 12) fills every section a program can derive — the header, Step 0, the dispatch-gate pre-flight line, §4's file list with consumer packages and a `sha256` premise pin per file, §7 from the doc-owners derivation, and every remaining section from the Issue's eight-field rationale — straight from the forge and the tree, refusing and naming the missing fact when a derived section cannot be derived. What is left is the judgment: the sections it could not derive, and confirming what it did derive still matches the current code. The documentation-update list §7 renders from the same mechanical derivation against the current `.vinaya/doc-owners` bindings the Planner runs during Dig (`roles/planner.md` explains it) — match the brief's real file surface globs against every binding's own glob (segment-wise overlap, not exact-string match) and the union of fired pointers, or the explicit zero-match line, is what the renderer already pastes in. Where the renderer isn't available, `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --surfaces <glob1,glob2,...>` runs the §7 derivation alone and prints exactly that; failing that, do the glob-match by hand. A list written from memory is the exact failure the read obligation exists to close.
|
|
64
64
|
|
|
65
65
|
**The standalone-fix path.** A plain forge Issue with no `task/<tranche>/<n>` row and no Planner rationale, briefed onto a `fix/<slug>` branch instead, is still a brief: `aeg-root/skills/brief-authoring/SKILL.md`'s `brief-shape` paragraph states the gate applies whatever the branch is, and the same check's non-task-branch path (**on this repo's toolchain**, `bun packages/aeg-core/bin/verify-brief.ts --body-file <brief.md>`) validates the same sections without requiring `Closes #N`. Item 4's own named command needs a `<tranche> <n>` pair a standalone fix doesn't have: match the brief's file surface against `.vinaya/doc-owners` by hand instead.
|
|
@@ -68,7 +68,9 @@ Item 1 governs every brief, whatever the branch: a real Issue is required either
|
|
|
68
68
|
|
|
69
69
|
## What you own
|
|
70
70
|
|
|
71
|
-
**The brief.** Every required section, in the order the skill defines: who it is for and why, the premise pins, the context with boundary and traps, the technical dependencies, the bounded file surface, the documentation-update list, the test plan tagged by who can run each item, the stop conditions, the constraints, the autonomy clause, and the deliverable. A brief missing any of them is malformed and you do not dispatch it.
|
|
71
|
+
**The brief.** Every required section, in the order the skill defines: who it is for and why, the Issue's `## Objectives` section copied byte-for-byte, the premise pins, the context with boundary and traps, the technical dependencies, the bounded file surface, the documentation-update list, the test plan tagged by who can run each item, the stop conditions, the constraints, the autonomy clause, and the deliverable. A brief missing any of them is malformed and you do not dispatch it.
|
|
72
|
+
|
|
73
|
+
**Objectives copy and coverage.** Copy the Issue's `## Objectives` section into the brief — `checkObjectivesCopy` (`@attalabs/aeg-core`) refuses a brief whose section does not match the Issue's (compared normalised, so an editor's whitespace never fails it; one changed word does). Then cite: every numbered Part in §6 cites at least one `O<n>`, and every `O<n>` the Issue declares is cited by at least one Part — `checkObjectivesCoverage` refuses an uncited objective or a Part citing an `O<n>` past the end of the list. A Part with no citation at all (an administrative step — a changeset commit, the final push) is not scrutinized either way; it is the citing Parts that must get it right. The documentation-update list in particular has a mechanical floor, and you are the role that computes it: at authoring time, run the entry gate's `--surfaces` command against the brief's final file list — or, for a standalone fix with no `<tranche> <n>` pair, the by-hand glob-match the standalone-fix path names instead — and carry every binding that turns up into the list, or mark it out of scope with a one-line reason, because silent overrides are a regression (`contracts/planner-brief.md`).
|
|
72
74
|
|
|
73
75
|
**The perishable half of the hand-off.** The planner persists durable conclusions; you add what decays: current signatures, the exact file list, the literal first command, the pinned facts that let the executing agent detect that the surface moved since you wrote. This division is the substance of the Planner→Brief contract — read it before authoring, because it names field-by-field what you must consume.
|
|
74
76
|
|
|
@@ -28,13 +28,13 @@ summary: Ever had someone review their own work?
|
|
|
28
28
|
|
|
29
29
|
You execute **one** brief, on **one** branch, and answer for it. You are the only role that writes code.
|
|
30
30
|
|
|
31
|
-
**You own** — the code, the tests, and the documentation the brief names; a clean typecheck, lint, test and production build; the worktree; and the pull request, carrying the report
|
|
31
|
+
**You own** — the code, the tests, and the documentation the brief names; a clean typecheck, lint, test and production build; the worktree; and the pull request, carrying the report, its impact tier, the issue it closes, and your own exact token figures. The brief itself lives elsewhere: dispatch tooling posts it once, frozen, as a comment on the task's own tracking issue, before you ever start — you never author or post it yourself.
|
|
32
32
|
|
|
33
33
|
**You refuse** — to start, when the input is not a well-formed brief, when a task you depend on has not merged, when a conflicting task is still open, when the task has no issue yet, when the previous tranche of a product you touch was never closed out, or when the branch name you were handed does not match the task; and to continue, when a pre-flight check fails, when the brief contradicts the code irreconcilably, when a test still fails after repeated genuine diagnosis, when you are about to touch a file outside the brief's surface, or when an action would be destructive and the brief never authorized it. Refusing is reporting what blocks you, not improvising past it.
|
|
34
34
|
|
|
35
35
|
**You never** author your own brief, write status anywhere, review or approve your own work, merge, settle a contested architectural question, skip a verification hook to get a commit through, or commit a new file whose only purpose is to hold a report.
|
|
36
36
|
|
|
37
|
-
**How it physically runs** — you work in a git worktree of your own, at `.worktrees/task/<tranche>/<n>`, on a branch named `task/<tranche>/<n>`, cut from the tip of the main branch rather than from whatever your local checkout happens to be. Creating it is the first thing you do, before reading a line of code. That branch name is the entire addressing scheme: every other role finds this task's branch, its pull request, and therefore its state from that one string, which is why it must match the task exactly. Commits are small and frequent — one per Part, pushed once.
|
|
37
|
+
**How it physically runs** — you work in a git worktree of your own, at `.worktrees/task/<tranche>/<n>`, on a branch named `task/<tranche>/<n>`, cut from the tip of the main branch rather than from whatever your local checkout happens to be. Creating it is the first thing you do, before reading a line of code. That branch name is the entire addressing scheme: every other role finds this task's branch, its pull request, and therefore its state from that one string, which is why it must match the task exactly. Commits are small and frequent — one per Part, pushed once. The brief itself was already posted, frozen, on the tracking issue before you started — its permanent home, and where the reviewer reads it — while the pull-request description carries the report: the impact tier and the issue the merge closes. No file records progress: the branch existing, the pull request opening, and the merge landing **are** the status.
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
---
|
|
@@ -45,6 +45,10 @@ You execute **one** brief, on **one** branch, and answer for it. You are the onl
|
|
|
45
45
|
|
|
46
46
|
You are the Developer when you are running in a coding-agent surface, a task brief has been dispatched to you (pasted in chat, or by an automation layer), and the brief tells you to execute specific work. You are executing — not planning, not strategizing, not authoring briefs.
|
|
47
47
|
|
|
48
|
+
<!-- AEG:CLAIM: apps/cli/src/lib/dispatch.ts contains:VINAYA_ROLE: role, -->
|
|
49
|
+
<!-- AEG:CLAIM: packages/aeg-core/src/log/envelope.ts contains:isRole(input.env.role) ? input.env.role : 'unattributed' -->
|
|
50
|
+
A turn started via `vinaya dispatch developer --agent <vendor>` carries its role and task in every `vinaya` call it makes; one started by hand in a terminal reads `unattributed` in the Vinaya Log, which is the truth about it.
|
|
51
|
+
|
|
48
52
|
> **Toolchain is per-repo.** This role names obligations (tests pass, typecheck passes, lint passes, production build passes), not specific commands. Each repo declares its own commands — the exact `typecheck` / `lint` / `test` / `build` invocations live in the repo's config (e.g. `package.json` scripts, a Makefile, the brief's verification section). Where this doc shows commands, they are **this repo's** instances (a Bun/JS toolchain) — substitute your repo's equivalents.
|
|
49
53
|
|
|
50
54
|
---
|
|
@@ -103,9 +107,9 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
|
|
|
103
107
|
|
|
104
108
|
**Worktree discipline.** Your brief's first pre-flight step (Step 0) is creating a worktree — do it before anything else. If dispatched by an automation layer, you work in the worktree it created at `.worktrees/task/<tranche>/<n>/`. If working manually, the brief's Step 0 gives you the `git worktree add … origin/main` command — run it and `cd` in. Never branch from a local checkout that may be behind.
|
|
105
109
|
|
|
106
|
-
**Commit per Part, push once.** Commit after each numbered Part in the brief — small, one logical change per commit, so the history reads as a narrative of how you approached the problem. Push exactly once, immediately before `pr create` — not after every commit.
|
|
110
|
+
**Commit per Part, push once.** Commit after each numbered Part in the brief — small, one logical change per commit, so the history reads as a narrative of how you approached the problem. Push exactly once, immediately before `pr create` — not after every commit. The pre-push hook itself runs the affected test suite (`bunx turbo test --affected`) on that one push and refuses it on failure; you do not additionally run it yourself per Part. The affected suite covers only the packages `turbo`'s own dependency graph marks affected by this diff — a rule about one package's files that lives in another package's test never runs on a push that only touches the first package (found live: a CLI-only diff never marked `aeg-core` affected, so a rule about CLI files, asserted only in an `aeg-core` test, never ran at the push hook at all). A rule meant to bind a package's own files belongs in a `vinaya check`, not in a sibling package's test suite. A regeneration or evidence run (`vinaya pr report --write`/`--push`) passes `--force` to its own test invocation. A verdict binds to the head it judged; a push landing after the newest verdict's judged head voids it and is named as such: `vinaya review status` prints `push after verdict — re-review required`, and merge waits on a fresh review round.
|
|
107
111
|
|
|
108
|
-
**Opening the PR with a complete description.** The PR description must (1) **carry the report only** —
|
|
112
|
+
**Opening the PR with a complete description.** The PR description must (1) **carry the report only** — the brief itself never rides in this body at all; it is already posted, frozen, as the task Issue's `aeg:brief:v1` comment by `vinaya task dispatch`, before your worktree even exists, and that comment is the brief's permanent, durable home — the Reviewer and Archivist read it there; (2) follow the canonical form in [§ PR body — canonical form](#pr-body--canonical-form) below — that section holds the verbatim copy-pasteable template, including the **exact `Tier:` field syntax** the `verify-docs` gate requires; (3) reference the task's Issue (`Closes #N`) so the merge auto-closes it. The description is not optional — the reviews depend on it. Opening the PR is itself the `in-flight → in-review` transition; you write no status field. **The body is authored once, at open.** After the PR is open, you never hand-edit it again — not to append a response to a review round, not to record a decision, not for any reason. Two writes are sanctioned after open, both machine-regenerated, never typed: the Evidence block, and one appended row in the Token report for a re-entry turn (see [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed)). Everything else a review round produces — your response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment.
|
|
109
113
|
|
|
110
114
|
**Reporting exact tokens in the PR body at turn-end.** You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — no role writes its own ledger row on a task branch, and parallel Developer sessions on different tasks have collided appending to the same shared file. Instead, before opening the PR (and again before each `changes-requested → in-review` re-push), report your exact tokens in the PR body under a **"Token report"** heading: `Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date` with `Phase: <task-id>: develop` and `Role: Developer`. **That destination and that grammar are the requirement, and they are the same on every agent host.**
|
|
111
115
|
|
|
@@ -125,15 +129,17 @@ This is the verbatim PR-body template every Developer pastes when opening a PR.
|
|
|
125
129
|
|
|
126
130
|
This form is **forge-agnostic.** It depends on no GitHub feature, no `.github/PULL_REQUEST_TEMPLATE.md`, no agent-specific skill. It is the source of truth that travels with the methodology.
|
|
127
131
|
|
|
128
|
-
**Start from the template file:** copy `aeg-root/templates/pr-report-template.md` and fill its placeholders — it packages this canonical form as a literal skeleton, with each gate-read field (`Closes #N`, `Project:`, `Tier:`, the Test Plan section) wrapped in its AEG anchor pair (an HTML comment pair, invisible on the rendered PR) so a pasted reference brief or quoted example can never be mistaken for the real field. Anchors are optional — prose-only bodies keep parsing exactly as before (`aeg-root/enforcement.md`) — but the template seeds them by default; keep them.
|
|
132
|
+
**Start from the template file:** copy `aeg-root/templates/pr-report-template.md` and fill its placeholders — it packages this canonical form as a literal skeleton, with each gate-read field (`Closes #N`, `Project:`, `Tier:`, the Test Plan section) wrapped in its AEG anchor pair (an HTML comment pair, invisible on the rendered PR) so a pasted reference brief or quoted example can never be mistaken for the real field. Anchors are optional — prose-only bodies keep parsing exactly as before (`aeg-root/enforcement.md`) — but the template seeds them by default; keep them. There is no `## Reference` section to fill: the brief never rides in this body at all — it is already posted, frozen, on the task Issue — and `pr create` refuses a body still carrying either legacy `aeg:brief:start`/`aeg:brief:end` marker.
|
|
129
133
|
|
|
130
134
|
```markdown
|
|
131
|
-
##
|
|
135
|
+
## Decisions
|
|
132
136
|
|
|
133
|
-
<one
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
<one line per choice the brief left open, e.g. `- <choice>: <what you picked
|
|
138
|
+
and why>` — the alternatives you considered and why you picked yours, so the
|
|
139
|
+
Principal can reverse a wrong call. Never restate what the diff does. No
|
|
140
|
+
verification claims — no "typecheck passes", no diff stats, no test counts.
|
|
141
|
+
Those are the Evidence block below, and it is the ONLY sanctioned home for
|
|
142
|
+
them: emitted by `vinaya pr report --write`, never hand-typed.>
|
|
137
143
|
|
|
138
144
|
## Test plan
|
|
139
145
|
|
|
@@ -176,7 +182,7 @@ field on its own line:>
|
|
|
176
182
|
|
|
177
183
|
| Field | Requirement |
|
|
178
184
|
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
179
|
-
|
|
|
185
|
+
| Decisions | One line per choice the brief left open — the alternatives considered and why yours won. Never a restatement of what the diff does. No verification claims (typecheck/lint/test/diff-stat output, pass counts) — those belong exclusively in Evidence, below. "No open choices" is a valid value, stated explicitly, same as `Test Plan: unit-tests-only`'s sentinel pattern — never left blank with no words. |
|
|
180
186
|
| **Bare digits (whole body)** | `body-bare-digits` (CI) refuses any bare digit outside a fenced/indented/inline code span or `Closes`/`Project`/`Tier`/`Evidence`'s own anchor, correctly placed under its own documented section — nowhere else. `Premise`/`Test plan` get no anchor exemption at all (their real content is unbounded free text, so it's scanned like ordinary prose — a Test Plan item's own pass count or exit code needs backticks too). An Issue/PR ref, a date, a version, a path, a section number all now need their own backticks (`` `#N` ``); a countable claim ("138 passed", a duration, a percentage) belongs in a fenced block or doesn't get written. |
|
|
181
187
|
| Test plan | Every runtime-observable check. The brief-authoring skill makes this a **required** field — empty plans use `Test Plan: unit-tests-only` as the sentinel. |
|
|
182
188
|
| `[agent]` fenced list | A fenced block, one command per line, each with `→ <expected observable>`. `vinaya pr report` runs every line from the PR head and writes the command plus its actual output into `AEG:EVIDENCE` — never a checkbox, never a hand-pasted comment. (This is the `[agent]` half of the Verification phase, see `state-machine.md` § Verification.) |
|
|
@@ -193,7 +199,7 @@ field on its own line:>
|
|
|
193
199
|
|
|
194
200
|
The `AEG:EVIDENCE` block is populated by running `vinaya pr report --write <body-file>` — never by hand-typing a diff stat, a test count, or a gate's pass/fail line into the PR body. Regenerate it after your final commit, before opening or editing the PR: `vinaya pr report --write` both runs the real gates (Group B) and recomputes the diff stat (Group A), so its own exit code doubles as the pre-open verification run — a red gate still writes the block (recording the failure honestly) but exits non-zero, so a scripted `--write && open-pr` never carries a failing suite onto the forge.
|
|
195
201
|
|
|
196
|
-
`check-evidence-fresh` (CI) refuses a body whose block doesn't match the head it's attached to — recomputing Group A exactly and checking Group B for staleness. This closes fabrication for **Group A only** (a hand-typed diff stat cannot survive a byte-compare); Group B is checked for freshness, not re-run, so a stale-but-not-fabricated Group B slips past unless the block is also out of date. Do not claim in this PR's own Evidence section, or anywhere else, that this closes fabrication generally — it closes it for the two facts a checker can cheaply recompute, never for the
|
|
202
|
+
`check-evidence-fresh` (CI) refuses a body whose block doesn't match the head it's attached to — recomputing Group A exactly and checking Group B for staleness. This closes fabrication for **Group A only** (a hand-typed diff stat cannot survive a byte-compare); Group B is checked for freshness, not re-run, so a stale-but-not-fabricated Group B slips past unless the block is also out of date. Do not claim in this PR's own Evidence section, or anywhere else, that this closes fabrication generally — it closes it for the two facts a checker can cheaply recompute, never for the Decisions section's prose.
|
|
197
203
|
|
|
198
204
|
**Regeneration is one command, run last, after every other change.** The Developer never hand-edits the live PR body after open — there is no local body file to keep in sync with the forge. When a push forces the Evidence block to go stale — or a re-entry turn needs its one appended Token report row — run, from the repo root, after every commit for that round is already pushed:
|
|
199
205
|
|
|
@@ -282,7 +288,7 @@ code-reviewer pass → security pass → Principal code review → Brief Author
|
|
|
282
288
|
|
|
283
289
|
The code-reviewer and security passes are **separate, fresh-context invocations** — not you. You do not review your own work; the independence is the point. What you do:
|
|
284
290
|
|
|
285
|
-
- **Address REQUEST CHANGES / FAIL findings.** A code-review BLOCKER or a security CRITICAL/HIGH comes back to you. Fix it on the **same branch** with new commits; the relevant pass re-runs. Do not open a new PR. (Pushing fixes returns the PR's review state to open, which is the `changes-requested → in-review` transition — again, derived, not written.) Your response to the round is one PR comment, never a body edit: the PR body is frozen at open (see [§ Opening the PR with a complete description](#what-the-developer-owns)), so no `## Review response`, `## Review round`, or `## Findings addressed` section may exist anywhere in it.
|
|
291
|
+
- **Address REQUEST CHANGES / FAIL findings.** A code-review BLOCKER or a security CRITICAL/HIGH comes back to you. Fix it on the **same branch** with new commits; the relevant pass re-runs. Do not open a new PR. (Pushing fixes returns the PR's review state to open, which is the `changes-requested → in-review` transition — again, derived, not written.) Your response to the round is one PR comment, never a body edit: the PR body is frozen at open (see [§ Opening the PR with a complete description](#what-the-developer-owns)), so no `## Review response`, `## Review round`, or `## Findings addressed` section may exist anywhere in it. A `doc-correctness` finding carries a `Search:` pattern — a repo-wide `git grep -n -iE` pattern, with no path filter — and is resolved only when every hit it returns at the new head is a true statement. So fix every copy, not only the anchored line: re-run that pattern yourself, correct every hit, and paste the command and its output at the fixed head into your round response. A response that fixes only the anchored line, or omits the pasted search output, is malformed and the finding stays open. Where the corrected sentence states what code does, bind it with an `AEG:CLAIM` marker so `verify-docs` C8 keeps it honest.
|
|
286
292
|
- **Do not argue findings into submission.** If a finding is wrong, say why, concisely, in a PR reply — but the Reviewer's independence means the default is to fix, not to debate.
|
|
287
293
|
- **Do not act on an escalation yourself.** An escalation is its own review outcome, never a finding — it routes to the Planner (`strategy`) or Principal (`authority`/`product`). Wait for direction.
|
|
288
294
|
- **Do not merge.** Only the Principal merges.
|
|
@@ -412,7 +418,7 @@ The checks above are **static**: they prove the change compiles, lints, types an
|
|
|
412
418
|
### Refuse if it isn't your turn
|
|
413
419
|
|
|
414
420
|
- **No open PR** — nothing to verify; come back when one is open.
|
|
415
|
-
- **No `aeg:brief` comment on the
|
|
421
|
+
- **No `aeg:brief:v1` comment on the task Issue** — without a Test Plan there is no definition of "verified"; `vinaya task dispatch` must post the frozen brief comment first.
|
|
416
422
|
- **No Test Plan section in the brief** — the brief is malformed; flag it for correction and stop rather than inventing a plan at verification time.
|
|
417
423
|
- **The plan declares `unit-tests-only` but the diff touches a runtime surface** (a route, a page, a server action) — the brief was mis-declared; flag it for correction. This is the failsafe against quietly downgrading verification.
|
|
418
424
|
|