@attalabs/vinaya 0.27.0 → 0.28.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 +4 -3
- package/aeg-root/enforcement.md +4 -4
- package/aeg-root/roles/developer.md +23 -17
- package/aeg-root/roles/planner.md +2 -0
- package/aeg-root/roles/principal.md +4 -0
- package/aeg-root/roles/reviewer.md +3 -3
- package/aeg-root/roles/security.md +3 -3
- package/dist/checks/bin/check-body-bare-digits.js +915 -240
- package/dist/checks/bin/check-branch-topology.js +820 -180
- package/dist/checks/bin/check-brief-shape.js +915 -240
- package/dist/checks/bin/check-changeset-coverage.js +1534 -262
- package/dist/checks/bin/check-closes-n.js +824 -184
- package/dist/checks/bin/check-coherence.js +984 -265
- package/dist/checks/bin/check-dead-branch-push.js +805 -176
- package/dist/checks/bin/check-dispatch-readiness.js +1046 -276
- package/dist/checks/bin/check-doc-coverage-push.js +1530 -258
- package/dist/checks/bin/check-doc-coverage.js +1532 -260
- package/dist/checks/bin/check-doctrine-no-procedures.js +915 -240
- package/dist/checks/bin/check-doctrine-portability.js +1529 -257
- package/dist/checks/bin/check-evidence-fresh.js +1875 -258
- package/dist/checks/bin/check-exec-bits.js +1527 -255
- package/dist/checks/bin/check-first-push-dispatch.js +932 -246
- package/dist/checks/bin/check-issue-assignment.js +822 -182
- package/dist/checks/bin/check-issue-milestone-attach.js +5734 -0
- package/dist/checks/bin/check-issue-objectives-numbering.js +5736 -0
- package/dist/checks/bin/check-issue-parts-coverage.js +5736 -0
- package/dist/checks/bin/check-issue-surface-globs.js +6910 -0
- package/dist/checks/bin/check-issue-title-grammar.js +5736 -0
- package/dist/checks/bin/check-issue-tranche-label.js +5736 -0
- package/dist/checks/bin/check-main-branch-refusal.js +805 -176
- package/dist/checks/bin/check-no-disk-state.js +805 -176
- package/dist/checks/bin/check-pr-premise-reassert.js +915 -240
- package/dist/checks/bin/check-pr-report-density.js +805 -176
- package/dist/checks/bin/check-quoted-command.js +1508 -255
- package/dist/checks/bin/check-reader-resolvable-prose.js +1512 -259
- package/dist/checks/bin/check-registry-gates.js +892 -176
- package/dist/checks/bin/check-retired-vocabulary.js +1506 -253
- package/dist/checks/bin/check-review-gate.js +1000 -245
- package/dist/checks/bin/check-single-plan-pr.js +805 -176
- package/dist/checks/bin/check-surface-scope.js +830 -182
- package/dist/checks/bin/check-test-plan.js +834 -189
- package/dist/checks/bin/check-token-collection-wired.js +805 -176
- package/dist/checks/bin/check-token-report.js +805 -176
- package/dist/checks/bin/check-workspace-escape.js +1525 -253
- package/dist/index.js +9835 -5597
- package/dist/lib/pre-push-changed-files.js +57 -0
- package/dist/lib/pre-push-select-tests.js +1473 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -26,9 +26,10 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
26
26
|
| `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
|
|
27
27
|
| `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
|
|
28
28
|
| `vinaya brief render <tranche> <n> --surfaces <glob,...> [--out <path>]` | Emit the twelve-section brief skeleton from the task Issue and the tree, with every mechanically-derivable section filled: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses, naming the missing section, when a section cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder. Never writes under `aeg-root/` or to the Issue — stdout, or `--out <path>`, only; a brief is pasted to the Developer, never committed. |
|
|
29
|
-
| `vinaya task brief <tranche> <n>` | Preparation only: render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. Starts no agent under any circumstances — there is no `--agent` flag. Principal-only: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. The successor to `task dispatch` for the preparation step; `task run` is the full unattended run. |
|
|
29
|
+
| `vinaya task brief <tranche> <n>` \| `vinaya task brief --issue <n>` | Preparation only: render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. `--issue <n>` names a backlog Issue directly — one carrying no `vinaya/tranche:*` label — and renders its brief from that Issue's own `## Objectives`/`## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` sections, no tranche or Milestone involved. Starts no agent under any circumstances — there is no `--agent` flag. Principal-only: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. The successor to `task dispatch` for the preparation step; `task run` is the full unattended run. |
|
|
30
30
|
| `vinaya task dispatch <tranche> <n> [--agent claude \| codex \| gemini] [--model <name>]` | **Deprecated** — prefer `task brief` (preparation only) or `task run` (the full unattended loop). Renders the same brief `brief render` renders, then posts it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. With `--agent`, starts the Developer through `dispatchRole` (`apps/cli/src/lib/dispatch.ts`) when that function is available; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block. `--model <name>` names the model to run, passed through to `dispatchRole` and always taking precedence over any class-derived resolution; without it, the task's own Issue rationale is read for its "Suggested agent-class" (`high`/`mid`/`fast`) and resolved through the chosen vendor's own class-to-model table, falling through to that vendor's default when neither resolves. Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. |
|
|
31
|
-
| `vinaya task run <tranche> <n> --agent <claude \| codex \| gemini>` | One command from a planned Issue to a reviewed pull request, exactly one developer started. Composes `task brief`'s own preparation (starts no agent) with `dev-review-loop` (whose own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task) — nothing else. A brief already frozen on the Issue is reused, not re-posted; a task whose Issue refuses preparation is refused before any agent starts, with nothing posted; a frozen brief whose developer branch already has an open pull request refuses a second start. Exit `0` with the PR URL on a published, reviewed pull request; exit `1` with the exact `vinaya dev-review-loop --resume <pr>` command on a pause; exit `2` on a usage/argv error (an unrecognized flag, a missing `--agent`); exit `3` on any other failure (a refused preparation, an open-PR refusal) — never sharing `1` with a pause, so an unattended host tells the two apart from the exit code alone. The loop's own existing `--resume` flag is how a paused run continues, never a flag on this command. |
|
|
31
|
+
| `vinaya task run <tranche> <n> --agent <claude \| codex \| gemini>` \| `vinaya task run --issue <n> --agent …` | One command from a planned Issue to a reviewed pull request, exactly one developer started. `--issue <n>` runs a backlog Issue — no `vinaya/tranche:*` label, no Milestone — through the identical unattended path on branch `task/issue-<n>`, same brief, same loop, same gate. Composes `task brief`'s own preparation (starts no agent) with `dev-review-loop` (whose own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task) — nothing else. A brief already frozen on the Issue is reused, not re-posted; a task whose Issue refuses preparation is refused before any agent starts, with nothing posted; a frozen brief whose developer branch already has an open pull request refuses a second start. Exit `0` with the PR URL on a published, reviewed pull request; exit `1` with the exact `vinaya dev-review-loop --resume <pr>` command on a pause; exit `2` on a usage/argv error (an unrecognized flag, a missing `--agent`); exit `3` on any other failure (a refused preparation, an open-PR refusal) — never sharing `1` with a pause, so an unattended host tells the two apart from the exit code alone. The loop's own existing `--resume` flag is how a paused run continues, never a flag on this command. **Known gap:** a backlog Issue's run state (`running`/`paused`/`published`) does not yet appear on Vinaya Studio's backlog page — that reader lives in the separate `apps/vinaya-studio` checkout (attalabs monorepo, out of this repository's own Surface) and has not been updated for the `task/issue-<n>` shape; `vinaya task status --issue <n>` is the reader in the meantime. |
|
|
32
|
+
| `vinaya task status [<tranche> <n>] [--json]` | Every open task Issue carrying a frozen brief, its pull request, and whether its dev-review-loop is `running` (naming the driver pid, `review-validity-v1` task 7's pid record), `paused` (naming the reason from the pause record), `published` (the newest round's reviewer and security verdict effect markers both read `posted`), or has `no driver` — read from the outbox (`<outboxRoot>/dev-review-loop/<task>/`) and the forge, never from a `ps` scan and never by re-parsing posted verdict comments. Read-only: no write, no change to any loop. `vinaya task status <tranche> <n>` narrows to one task and adds the last round's held or published verdict lines plus the exact `vinaya dev-review-loop --resume <pr>` command when paused. `--json` for the enveloped machine form (schema: 1). `vinaya task status <tranche> <n> --follow` (and `--issue <n> --follow` for a backlog Issue) tails that task's per-driver log (`~/.vinaya/loops/<owner>-<repo>/<issue>.log`) live, `tail -f` style — every driver (`task run`, `dev-review-loop`) tees its own role-prefixed stream there regardless of where it was launched, so the state of any run is one command away with no path to remember. |
|
|
32
33
|
| `vinaya pr create --body-file <path> --title <title> [--label ...] [--validate-only] [--json]` | Open a pull request after full brief-schema validation of the body against `vinaya.config.json`'s configured sections — never a body containing the retired `<!-- aeg:brief:start -->`/`<!-- aeg:brief:end -->` markers, refused outright: the brief lives on the task Issue's `aeg:brief:v1` comment now (`vinaya task dispatch`), never split out of or pasted into the PR body. `--validate-only` runs every gate and reports PASS without opening anything; `--json` for the enveloped machine form. |
|
|
33
34
|
| `vinaya issue objectives edit <n> --add "<sentence>" \| --drop O<k> \| --replace O<k> "<sentence>" --reason "<text>"` | Rewrite a task Issue's `## Objectives` section by command — the Principal's way to change a task's scope mid-flight, findable and versioned rather than a silent hand-edit. Exactly one of `--add`/`--drop`/`--replace` plus a non-empty `--reason` is required; the edit runs through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself. `--drop` never renumbers the survivors — a drop that leaves the list non-contiguous from `O1` is refused with `objectivesOf`'s own parser message, since task 1's contiguous-from-O1 grammar and this command's never-renumber rule can genuinely contradict on a real drop, and that contradiction is reported rather than papered over. Splices the rendered section back in place, leaving every other byte of the body untouched, then posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new version hash — `k` counted on the forge at post time, never from a local file. |
|
|
34
35
|
| `vinaya log flush --issue <n> \| --pr <n>` | Post a target Issue or PR's outbox (`~/.vinaya/outbox/<owner>-<repo>/<issue-or-none>.ndjson`, written by `log()`) as one or more comments, each opening with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` followed by a fenced `ndjson` block. Splits first at `run_id` boundaries — a maximal run of consecutive lines sharing one `run_id`, so an interleaved outbox never produces a range spanning a gap — then at `FORGE_COMMENT_MAX_CHARS` (65536); a single line too large to fit alone is refused by its seq, never split. `--pr <n>` resolves the Issue from that PR's body `Closes #N` line and posts on the PR instead of the Issue; a body with none is refused. Logs its own `forge_write` line (`validated` before posting, `written` with every comment id after the last post succeeds, or `refused` with gh's error) through `log()` into the same outbox before truncating, so that line rides to the next flush; the outbox is truncated only to the lines confirmed posted, and a gh failure leaves the rest untouched. `--json` for the enveloped machine form. |
|
|
@@ -46,7 +47,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
46
47
|
| `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
|
|
47
48
|
| `vinaya release [--dry-run] [--allow-any-commit]` | Run this repo's own publish sequence in one command (`apps/cli/specs/self-hosting.md`, "How the published version is produced"). Refuses unless HEAD is the default branch, the tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix. Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and a real `git push origin --tags`, so the repo's own generated pre-push hook sees the push exactly as any other push would. Afterward prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version. `--dry-run` stops after the preconditions and prints the plan; publishes nothing. |
|
|
48
49
|
| `vinaya dispatch <role> --agent claude \| codex \| gemini --prompt-file <path> [--task <n> \| --pr <n>] [--round <n>] [--resume <id>] [--model <name>]` | Start a role's headless agent session, attributed: `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` are set on the CHILD's environment only, never on this process's own — a role started this way is what makes its later `vinaya` calls (the Stop hook, a nested dispatch) attributable instead of `unattributed`. The prompt is read from `--prompt-file` and sent on the child's stdin, never argv. `--model <name>` is passed to the chosen vendor through that vendor's own model flag, and the dispatch log's `model` field never records the vendor name; omitted, no model flag reaches the vendor and its own default decides, exactly as before this flag existed. A model shaped as another vendor's own (e.g. a Claude alias passed to `codex`) is refused by name before any spawn, naming the vendor and what it accepts. Before the vendor produces any output, and for any dispatch that never completes, the log records the requested value as a marked `requested:<name>` label (or `default` when none was named) — never presented as a confirmed observation. Once the vendor's own headless output actually reports which model ran (Claude's `modelUsage` object key, Gemini's `stats.models` object key(s) — Codex emits no such receipt in any event), that bare, unprefixed model name is recorded instead, even when it differs from what was requested or when no model was requested at all. Refuses by name, before any spawn, when the named vendor binary is absent from `PATH` or present but not executable. Every line this command and the loop it can drive print to the terminal is prefixed with its role (e.g. `[developer]`) and colour-coded per role, with the coordinator/loop's own lines in a distinct colour from every role — colour is applied only on a live TTY and is disabled entirely, prefix kept, when `NO_COLOR` (https://no-color.org) is set to any value. While the child runs, its raw stdout/stderr bytes are teed (never taken from the same stream the outcome parser reads) to a machine-local file under `~/.vinaya/dispatch-output/<effect-id>.log` — never inside the repo tree — whose path is printed once to this process's own stderr so an operator can `tail -f` it; a heartbeat line reporting elapsed time prints there every minute so a working agent is distinguishable from a hung one without inspecting processes by hand. A wall-time ceiling (`dispatch.timeoutMs` in config, default four hours) sends `SIGTERM` then, after a grace window, `SIGKILL`; a warning prints before the ceiling fires (at most 5 minutes ahead, sooner for a shorter configured ceiling) so a timeout is an expected, visible event rather than a silent disappearance. Records `dispatched`/`outcome_received`/`dispatch_failed` through the Vinaya Log's `dispatch` family (`apps/cli/specs/log.md`); when `--task`/`--pr` is given, flushes that outbox via `vinaya log flush` immediately after. A successful dispatch's own vendor session/thread identifier is returned as `resumeId` (printed alongside the other fields); passing that value back as `--resume <id>` on a later call swaps in that vendor's own resume invocation instead of starting a fresh session. That id is also durably recorded — never only printed — at `~/.vinaya/dispatch-resume/<owner>-<repo>|unresolved/<role>-<agent>-<issue<n>\|pr<n>\|unscoped>.json` (the repo segment keeps two different repos' own same-numbered task from overwriting each other's record on this one machine-wide home), overwritten by each dispatch's own outcome; a run that stops with a question is answered by reading the id from that file (or from the run's own printed output, if that terminal is still open) and running `vinaya dispatch <role> --agent <vendor> --resume <id> --prompt-file <answer-file>` — the same session continues from where it stopped, rather than being killed and re-dispatched fresh. There is no live channel: an unattended dispatch never blocks on a read waiting for an answer, so a stop condition always degrades to record-and-exit, and the resume path above is how it is answered afterward. |
|
|
49
|
-
| `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` / `vinaya dev-review-loop --resume <pr> --agent … [--json]` | Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`) before `publish`. At `publish`, the two held verdicts and a `renderSummary` comment are posted, in order, each re-read back through the same extractors the merge gate uses, each idempotent across a rerun. At `pause`, one comment marked `<!-- aeg:loop:paused:<reason> -->` is posted (carrying the exact `--resume <pr>` command) and the process exits non-zero; `--resume <pr>` reads that held state and a since-posted Principal ruling off the same PR and continues. Full account: `apps/cli/specs/loop.md`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
|
|
50
|
+
| `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` / `vinaya dev-review-loop --resume <pr> --agent … [--json]` | `--issue <n>` is `--task <n>`'s exact synonym — the loop's own `task` field is already the Issue number, tranche or not, so a backlog Issue (no `vinaya/tranche:*` label) needs no separate flag, only the naming that matches `task run --issue <n>`. Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`) before `publish`. At `publish`, the two held verdicts and a `renderSummary` comment are posted, in order, each re-read back through the same extractors the merge gate uses, each idempotent across a rerun. At `pause`, one comment marked `<!-- aeg:loop:paused:<reason> -->` is posted (carrying the exact `--resume <pr>` command) and the process exits non-zero; `--resume <pr>` reads that held state and a since-posted Principal ruling off the same PR and continues. Full account: `apps/cli/specs/loop.md`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
|
|
50
51
|
|
|
51
52
|
Which one lib function backs each command, and today's dated exemptions where it calls more than that one: `apps/cli/specs/surface.md`, enforced by `apps/cli/tests/surface-index.test.ts`.
|
|
52
53
|
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -76,8 +76,8 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
76
76
|
|
|
77
77
|
**The two rules a gate author meets before adding a check:**
|
|
78
78
|
|
|
79
|
-
1. **Decide from parsed structure, never from prose.** A gate that must decide whether a task touches a domain, resolves a path, or crosses a boundary decides from a field a writer fills in a fixed grammar — a glob list, a numbered citation, a table row — never from scanning prose for a phrase that would mean the right thing if read charitably. Prose can name a thing to include it or to exclude it, and a scanning gate cannot tell the two apart; a heuristic built to guess which one a sentence meant is wrong in both directions, and a blocking gate that is wrong in either direction is worse than one that runs less often. Where the parsed structure does not yet exist for an artifact (an Issue below the cutover that first mandates a `## Surface`), the gate degrades to the old prose scan for that artifact only — it neither invents structure nobody asked for, nor extends the prose scan's blast radius to cover stock that predates it. `
|
|
80
|
-
2. **One fact, one implementation, resolved once and passed down.** A fact two gates must agree on — whether a glob resolves to a real tracked file, whether a domain falls under a path — is computed by one function and passed to every caller that needs the answer; it is never re-derived by a second hand-written implementation that merely happens to agree with the first today. Two implementations of the same predicate are a race the moment either one changes: the day they diverge is invisible until an artifact one side accepts and the other refuses reaches the seam between them, and by then neither implementation is provably the bug. Inject the shared function into whichever side cannot compute it itself — a pure,
|
|
79
|
+
1. **Decide from parsed structure, never from prose.** A gate that must decide whether a task touches a domain, resolves a path, or crosses a boundary decides from a field a writer fills in a fixed grammar — a glob list, a numbered citation, a table row — never from scanning prose for a phrase that would mean the right thing if read charitably. Prose can name a thing to include it or to exclude it, and a scanning gate cannot tell the two apart; a heuristic built to guess which one a sentence meant is wrong in both directions, and a blocking gate that is wrong in either direction is worse than one that runs less often. Where the parsed structure does not yet exist for an artifact (an Issue below the cutover that first mandates a `## Surface`), the gate degrades to the old prose scan for that artifact only — it neither invents structure nobody asked for, nor extends the prose scan's blast radius to cover stock that predates it. `checkBlastRadiusScope` is the worked example: cutover-gated, it decides from `## Surface`'s `in:` glob list once one is mandatory, never again from `Boundary`/`Project(s) + blast radius` prose, whatever that prose says — naming a shared package to explicitly *exclude* it no longer trips a gate that used to read prose span-blind to intent.
|
|
80
|
+
2. **One fact, one implementation, resolved once and passed down.** A fact two gates must agree on — whether a glob resolves to a real tracked file, whether a domain falls under a path — is computed by one function and passed to every caller that needs the answer; it is never re-derived by a second hand-written implementation that merely happens to agree with the first today. Two implementations of the same predicate are a race the moment either one changes: the day they diverge is invisible until an artifact one side accepts and the other refuses reaches the seam between them, and by then neither implementation is provably the bug. Inject the shared function into whichever side cannot compute it itself — a pure, filesystem-free module takes the answer as an injected parameter (a `(glob: string) => boolean`, a resolved list); the impure caller that already computes it (the forge-write path, injecting the brief renderer's own glob expansion) is the one and only place that does, so the authoring gate and the brief renderer read the identical resolution and can never disagree about whether a Surface resolves.
|
|
81
81
|
|
|
82
82
|
**The own-PR fixture rule.** A pull request that adds a check which reads a PR body ships a fixture test running that check over **that PR's own body**. A body-reading check is the one class whose real input exists at the moment the PR opens and is never exercised by a synthetic fixture the author also wrote: the author's fixture agrees with the author's mental model by construction, and the first real body it meets is the one it was supposed to grade. Running it over the body in hand costs one test and converts "it should work on a real body" from a belief into a passing assertion.
|
|
83
83
|
|
|
@@ -137,7 +137,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
137
137
|
| Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | repo-own | `.github/workflows/ci.yml` |
|
|
138
138
|
| Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | States where formatting/naming conventions stand in this repo: currently unenforced. | **Nothing re-checks lint/format or commit-message grammar in this repo today** — the managed hooks run only the `vinaya check` suite, and the one CI workflow that builds the code (`ci.yml`, the implementation named here; its typecheck + unit-test substance belongs to the `Typecheck + unit tests` row) carries no lint or commitlint step. Those conventions are discipline, not a gate, until a step is added. | repo-own | `.github/workflows/ci.yml` |
|
|
139
139
|
| AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Requires independent review verdicts to exist on every pull request before merge. | No automated reviewer workflow is installed in this repo — review passes are separate, fresh-context dispatched agent sessions that post verdict comments on the PR. `vinaya-review-verdict.yml` evaluates a landed verdict comment (only principal-allowlisted authors count — the same verdict-author verification rule the Review gate applies) and re-runs the required review-gate check so it can go green natively. Until clean verdicts exist, the required check stays red by design. | repo-own | `.github/workflows/vinaya-review-verdict.yml` |
|
|
140
|
-
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **A verdict is also bound to the newest PRINCIPAL RULING on the PR** (`review-validity-v1` task 3): the winning comment's `Ruling ordinal: <k>` line (`0` when the PR carried no ruling at cast time — this line renders UNCONDITIONALLY, unlike the objectives-version line) must equal `input.rulingOrdinal`, the newest ruling ordinal `check-review-gate.ts` counts off the PR's own principal-authored `<!-- aeg:principal:ruling:<pr>-<k> -->` comments — a ruling posted after a verdict was cast no longer counts as clean, even when its head and objectives bindings still hold, and the gate names the newer ruling in its failure message. Unlike `objectivesVersion`, there is no `null`-means-skip input value here — a PR's ruling count is never ambiguous — but a verdict predating this feature (no `Ruling ordinal:` line at all) still binds when the PR's newest ruling ordinal is `0`, the same "nothing to bind against" case expressed as an equality rather than a skip. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
140
|
+
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **A verdict is also bound to the newest PRINCIPAL RULING on the PR** (`review-validity-v1` task 3): the winning comment's `Ruling ordinal: <k>` line (`0` when the PR carried no ruling at cast time — this line renders UNCONDITIONALLY, unlike the objectives-version line) must equal `input.rulingOrdinal`, the newest ruling ordinal `check-review-gate.ts` counts off the PR's own principal-authored `<!-- aeg:principal:ruling:<pr>-<k> -->` comments — a ruling posted after a verdict was cast no longer counts as clean, even when its head and objectives bindings still hold, and the gate names the newer ruling in its failure message. Unlike `objectivesVersion`, there is no `null`-means-skip input value here — a PR's ruling count is never ambiguous — but a verdict predating this feature (no `Ruling ordinal:` line at all) still binds when the PR's newest ruling ordinal is `0`, the same "nothing to bind against" case expressed as an equality rather than a skip. **Every binding above is now ONE typed manifest, compared by ONE function** (`review-validity-v1` task 4): one `ReviewInputManifest` — head, the frozen brief's own hash, objectives version, newest ruling ordinal, and the effective review policy's digest — is built by the driver before reviewers are dispatched and rendered into every verdict/escalation comment; `compareManifest` is the SAME comparison `checkReviewGate` and the dev-review-loop's own publication self-check both call, so no field binds on one side and is ignored on the other. Two more bindings ride this manifest: **a verdict is also bound to the FROZEN BRIEF it was judged against** — the winning comment's `Brief hash:` line (a `sha256` of the brief's posted content) must equal `input.briefHash` (optional, defaulting to `null` — skip — for every caller predating this field); a Planner superseding the frozen brief after a verdict was cast voids it the same way an objectives edit does. **A verdict is also bound to the REVIEW POLICY in effect when it was cast** — the winning comment's `Policy digest:` line (rendered unconditionally, right after `Ruling ordinal:`) must equal the current policy's digest; unlike the other bindings above, a `null` echo (a comment predating this line, or one stripped of it) is never grandfathered — a policy is always resolvable, so there is no genuine "nothing to bind against" case on the current side the way an unset objectives list or a ruling-free PR has, and a blanket forever-exempt reading of a missing line would let it silently outlive a real threshold change (found live during `review-validity-v1` task 4's own review). A digest that no longer matches a since-changed policy, missing or present, invalidates the verdict the same way a newer ruling does. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
141
141
|
| G1 — implementation exists | Ever read about a safeguard that turned out not to actually exist? | ci | Re-checks that every gate the doctrine describes has real code behind it. | Every ring's non-empty `implementation` cell on this very page resolves to a real path on disk — makes this page's registry columns load-bearing instead of decorative. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
142
142
|
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, `apps/cli/src/checks/bin/*.ts` is named by some row's `implementation`, OR — **twin-form recognition (O14)** — resolves, through the same `claimedCheckNames` derivation G6 uses, to a `coreCheckRegistry()` name some OTHER row's `implementation` already claims: the same enforcement mechanism routinely ships as both a `packages/aeg-core/bin/*.ts` standalone form and an `apps/cli/src/checks/bin/check-*.ts` CLI-registered form, and a row's single `implementation` cell can only name one — the other is not a second undocumented fact, it is the first fact read through its other physical shape. A `NON_GATE_BINS`-listed aeg-core bin (a forge writer, a one-shot reporter, an eval harness — `report-tokens.ts`, `eval-agent-compliance.ts`, …) and a listed non-gate hook script (`.claude/hooks/track-transcript.sh`, real Stop-hook plumbing feeding two already-documented mechanisms' own inputs, with no pass/fail decision of its own) implement no enforcement mechanism to be named for — the honest fix for those is that this gate stops asking, never an invented row (`.husky/*` matches nothing in this repo today — that directory does not exist here). **Blocking as of this task** (re-graded from report-only: the twin-form gap above was G2's entire non-scaffold-placeholder backlog, and a 0-finding G2 run means something again now that it can fail). **Scaffold-writer** (task 22, later widened to cover that second location): `verify-registry.ts --scaffold` auto-inserts a stub row for an orphan candidate whose ring is mechanically derivable — a `packages/aeg-core/bin/*.ts` file resolving through `GATE_AUDIENCE` to a registered check (its ring comes from that check's own `apps/cli/src/checks/registry.ts` entry), an `apps/cli/src/checks/bin/*.ts` file whose `check-`-stripped name resolves through the analogous `CLI_CHECK_RING` mirror table, or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered bin under either directory) gets no stub and stays a plain finding — the writer never guesses. A second, independent half of G2 scans every row's `summary`/`description`/`spec` cells for the literal placeholder marker `[undocumented — fill in why]` the scaffold writes into every non-mechanical cell: a stub row that filled only `implementation` would otherwise read as "documented" to the orphan half above and silence G2 outright, which is the exact trap this second half exists to close. The writer only ever appends a new row after a ring table's last existing row — it never edits, reorders, or removes one, so a hand-authored row (including `workspace-escape`'s and `main-branch-refusal`'s own) is untouched. Repo-side only: the writer lives on `verify-registry.ts` (aeg-core), never on the shipped `check-registry-gates.ts` (apps/cli) — an adopter's checks stay read-only over doctrine. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
143
143
|
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-1 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
@@ -154,7 +154,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
154
154
|
| quoted-command | Ever read a doc that quoted a command as "what runs today", and the command changed underneath it? | ci | Re-checks that a doc's explicitly marked quote of a command or config line still matches, verbatim, the file it names as its source. | Marker-based only, by explicit Principal decision, not inferred from command-looking spans: a doc opts a span in with `<!-- AEG:QUOTES-FILE:START:<path> -->…<!-- AEG:QUOTES-FILE:END -->` (the same invisible-on-render, code-blind HTML-comment idiom `anchored-region.ts` uses for its own seven PR/Issue-body fields, reusing that file's masking primitive rather than a second parser — but a distinct marker kind, since the cited path is free text, not one of that file's seven closed field names). An adopter-facing command in a README with no marker is instruction, not a claim, and is never flagged — inference over command-looking spans was explicitly rejected as the false-positive shape that gets a gate disabled. `findCitedQuotes` sweeps the identical `ships`/`reader-facing` corpus `reader-resolvable-prose` sweeps (never a second notion of "governed doc"); `evaluateCitedQuotes` then checks each marked span against its cited file's live content, wherever that file lives in the repo, naming both sides in a finding — what the doc claims, and which file no longer contains it verbatim. Zero I/O in `@attalabs/aeg-core`'s pure predicate (`quoted-command.ts`), corpus-tested against the real `aeg-root/**` tree; the bin supplies the only I/O. Ships with one real, live annotation (this very paragraph's own sibling callout above, quoting the CI invocation this page names) rather than only synthetic fixtures. Written after a measured incident: a CI invocation was pinned to a version in one pull request; this page quoted the pre-pin form verbatim as current fact; every registered check passed, `doctor` reported healthy, security passed, and a code-review agent found the drift only after independent re-derivation. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape`/`changeset-coverage` above — graduating to a blocking check is a later, separately-dispatched decision once the false-positive rate is observed against real adopter corpora; v1 carries no waiver escape. | product | `apps/cli/src/checks/bin/check-quoted-command.ts` |
|
|
155
155
|
| token-report | Ever seen a cost figure nobody could trace to a source? | ci | Re-checks that a PR's "Token report" section actually carries real numeric Tokens in/out figures, on any host this repo can prove is metering-capable. | The presence and shape of the `## Token report` block `roles/developer.md` requires every self-metering role to paste at turn-end — never whether the reported figures are TRUE, which CI structurally cannot recompute (the same bounded honesty this page's `evidence-fresh` row states for its own Group B). Runs `resolveMeteringCapability` (task 1's probe, `@attalabs/aeg-core`) fresh in THIS process; an incapable verdict (any reason) passes silently — the sanctioned operator-metered case, same discipline `token-collection-wired` above already applies. **Ring derived mechanically as `1`, not chosen** (a 2026-08-29 amendment correcting the original brief rationale's now-retracted ring-0/1 instruction): `requiresOpenPr: true` makes this check CI-only by `CoreCheckRing`'s own derivation rule, since the PR body it reads does not exist before a PR does. A probe that itself fails to run (an unexpected `exists`/`readFile` throw the probe does not catch) is never read as a clean incapable verdict: the bin lets it propagate uncaught, which the runner surfaces as `status: 'error'`, distinct from the `status: 'pass'` a real incapable verdict produces. **A ledger row must exist, not merely be well-formed when present** (O13): before this task, `!capability.capable` returned a silent pass BEFORE checking whether the body carried a "Token report" row at all, so a merged task on an incapable host could leave the ledger with no row at all for its own spend — a silent hole, never a stated `—`. The row-existence check now runs unconditionally (any capability), and only the numeric-cell shape check stays capability-gated — `tranche-model.md` §12's operator-metered case writes `—` in a row; it never omits the section. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
|
|
156
156
|
| Bare code-fact digits in a PR body | Ever had a PR body cite a line number that drifted the moment the file changed? | ci | Re-checks that a pull request's narrative prose carries no bare `<path>.<ext>:<digits>` code-fact pointer outside a fenced code span or a `Premise:` pin. | A bare digit anywhere in the body outside a fenced/indented code block or a documented anchor (`Closes`/`Project`/`Tier`/`Evidence`) — an Issue/PR ref, a date, a version, a path, a section number, a pass count, a duration, all need their own backticks or a fenced block, never bare prose. Reports every violation the body carries in one pass, not only the first (O12) — a mechanically-fixable body is corrected in one round, not one round-trip per digit. Its own workflow (`ownWorkflow: true`), `pull_request_target`-triggered only, never `pull_request` — a live-fetched author/branch check a `pull_request`-triggered run of this same file could otherwise spoof via the PR's own workflow YAML. | product | `apps/cli/src/checks/bin/check-body-bare-digits.ts` |
|
|
157
|
-
| Evidence-block freshness | Ever seen a PR's own "tests pass" claim go stale the moment someone pushed again? | ci | Re-checks that a PR's
|
|
157
|
+
| Evidence-block freshness | Ever seen a PR's own "tests pass" claim go stale the moment someone pushed again? | ci | Re-checks that a PR's Evidence block still matches a fresh recompute at the PR's current head. | Group A (the diff stat) is recomputed exactly and byte-compared — a hand-typed or stale diff stat cannot survive this. Group B (the real gate run) and Group C (the `[agent]` fenced command list `vinaya pr report` runs) are checked for staleness against the current head, never re-run here — this closes fabrication for the two facts a checker can cheaply recompute, never for the Decisions section's prose. Resolves the real PR head via `gh pr view --json headRefOid`, never `HEAD` (the merge commit in CI). | product | `apps/cli/src/checks/bin/check-evidence-fresh.ts` |
|
|
158
158
|
| Documentation gate (PR open/edit) | Ever opened a PR, watched the doc-coverage gate pass, then pushed a second commit that broke it? | ci | Re-checks C5 doc-coverage — the SAME code→doc binding the push-time row above enforces — again at PR create/edit time, not only on push. | Every code change matching a `.vinaya/doc-owners` binding carries its owning doc in the same diff, a `Doc-ack:`/`Doc-neutral:` declaration, or an actor-verified `vinaya/waiver:docs` label, resolved live via `gh` from `PR_NUMBER` (fixing a prior silent-unreachable label lookup in an adopter's CI, where the label was never actually forwarded to this check). `roles/developer.md` names this as one of two chokepoints "at every push... and again at pull-request creation and editing" — this row is the second, previously undocumented despite already shipping and already running from `vinaya pr create`'s own pre-flight. | product | `apps/cli/src/checks/bin/check-doc-coverage.ts` |
|
|
159
159
|
| Surface-scope (out-of-boundary file) | Ever had a "small" task quietly touch a file its own plan said it wouldn't? | ci | Re-checks that a task branch's changed files stay inside its own Issue's declared `## Surface` — never inside a declared `out:` glob. | Every file in the branch's diff against `origin/main` is checked against the task's own Issue (resolved from the branch name → forge topology → Issue number → `## Surface` `out:` list) with the same `globCoversPath` predicate the Issue-authoring gate and the blast-radius check (O4) both already use — an undeclared boundary crossing is caught mechanically, naming the file and the glob it crosses, rather than depending on a reviewer noticing. Same check runs at the `git push` hook (ring 0) and again here — same runner, one codebase, two enforcement points. Dormant (never blocks) when the branch is not a task branch, the forge/repo cannot be resolved or reached, the task's row carries no Issue yet, the Issue's `## Surface` doesn't parse (below the brief-sections cutover, or malformed — caught at authoring time instead), or the Issue declares no `out:` globs at all. | product | `apps/cli/src/checks/bin/check-surface-scope.ts` |
|
|
160
160
|
| PR-body premise reassertion | Ever had a brief's pinned fact quietly go stale, and the merge never noticed? | ci | Re-checks, in CI, that a pull request body's `Premise:` pins still hold against the PR's own current tree — not only at Step 0, authoring time. | Every `contains`/`absent`/`sha256` pin `parsePremiseBlock` extracts from the PR body, re-asserted via the same frozen `checkPremises`/`reassertPremiseFile` (`apps/cli/src/checks/premise-reassert-logic.ts`) `verify-dispatch --premise` already uses at Step 0 — a second caller, not a second implementation, so a pin the PR's own diff falsifies fails CI instead of merging as decoration. Trigger is the block's presence alone: a body with no `Premise:` header parses to zero pins and this check is silent, on every branch — there is no branch-name condition anywhere in it. | product | `apps/cli/src/checks/bin/check-pr-premise-reassert.ts` |
|
|
@@ -109,6 +109,8 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
|
|
|
109
109
|
|
|
110
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.
|
|
111
111
|
|
|
112
|
+
**Push and PR-open are foreground, never backgrounded.** A dispatched turn cannot run a shell command in the background at all: `vinaya dispatch` wires a permission deny rule into the session's settings for exactly this (Bash's own `run_in_background: true`), so the tool call itself is refused, with the refusal text naming the foreground alternative — this is enforcement, not a convention you are trusted to follow unprompted. The push and the `pr create` (or `pr edit`) call that opens or updates the pull request are foreground steps, not something you fire and end your turn on. Run each to completion and confirm it: your turn does not end until `gh pr view` on that branch shows the pull request. A turn that ends with the push or the PR-open still running in the background — or with commits made but never pushed — is a contract violation, not a completed turn; the driver treats it as incomplete and resumes you, once, with the exact commands to finish (found live, twice: a backgrounded `git push` that reported "running in the background" died with the session, leaving only local commits, while the loop polled for a pull request that no process was ever going to open). If the branch is behind the base in a way that conflicts, merge or rebase and resolve before pushing — never push, or end a turn, with an unresolved conflict against the base; a resume that finds you here names the conflicting files.
|
|
113
|
+
|
|
112
114
|
**Opening the PR with a complete description.** The PR description must (1) **carry the report only** — the brief itself never rides in this body at all; it is already posted, frozen, as the task Issue's `aeg:brief:v1` comment by `vinaya task dispatch`, before your worktree even exists, and that comment is the brief's permanent, durable home — the Reviewer and Archivist read it there; (2) follow the canonical form in [§ PR body — canonical form](#pr-body--canonical-form) below — that section holds the verbatim copy-pasteable template, including the **exact `Tier:` field syntax** the `verify-docs` gate requires; (3) reference the task's Issue (`Closes #N`) so the merge auto-closes it. The description is not optional — the reviews depend on it. Opening the PR is itself the `in-flight → in-review` transition; you write no status field. **The body is authored once, at open.** After the PR is open, you never hand-edit it again — not to append a response to a review round, not to record a decision, not for any reason. Two writes are sanctioned after open, both machine-regenerated, never typed: the Evidence block, and one appended row in the Token report for a re-entry turn (see [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed)). Everything else a review round produces — your response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment.
|
|
113
115
|
|
|
114
116
|
**Reporting exact tokens in the PR body at turn-end.** You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — no role writes its own ledger row on a task branch, and parallel Developer sessions on different tasks have collided appending to the same shared file. Instead, before opening the PR (and again before each `changes-requested → in-review` re-push), report your exact tokens in the PR body under a **"Token report"** heading: `Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date` with `Phase: <task-id>: develop` and `Role: Developer`. **That destination and that grammar are the requirement, and they are the same on every agent host.**
|
|
@@ -201,15 +203,11 @@ The `AEG:EVIDENCE` block is populated by running `vinaya pr report --write <body
|
|
|
201
203
|
|
|
202
204
|
`check-evidence-fresh` (CI) refuses a body whose block doesn't match the head it's attached to — recomputing Group A exactly and checking Group B for staleness. This closes fabrication for **Group A only** (a hand-typed diff stat cannot survive a byte-compare); Group B is checked for freshness, not re-run, so a stale-but-not-fabricated Group B slips past unless the block is also out of date. Do not claim in this PR's own Evidence section, or anywhere else, that this closes fabrication generally — it closes it for the two facts a checker can cheaply recompute, never for the Decisions section's prose.
|
|
203
205
|
|
|
204
|
-
**
|
|
205
|
-
|
|
206
|
-
`vinaya pr report --push <n>`
|
|
206
|
+
**After open, regenerating `AEG:EVIDENCE` is the driver's job, not yours.** Before this fix, a push-forced-stale Evidence block was the Developer's own turn to re-close: run `vinaya pr report --push <n>` and wait for it, a re-run of the real gate suite that could take past ten minutes and stall the whole loop on the Developer's single tool call. That command still exists — the underlying engine module both `vinaya pr report --push` and the driver's own in-process call now share, a command never calling a command — but you no longer run it. Once your turn ends at the push (below), the driver runs the SAME engine function itself, in-process, the moment the head's CI turns green, and posts the round marker comment in your place too. You never see a stale Evidence block to fix, because you never reach for the command that used to fix it.
|
|
207
207
|
|
|
208
|
-
**
|
|
208
|
+
**`--push <n> --body-file <path>` still exists as a narrower, separate mode** — for the one case a routine splice cannot cover: a section outside the two generated blocks (a Decisions bullet, most often) that only ever existed in a local draft, never yet posted. It does not relax "the body is authored once, at open" above — reaching for it to restate the routine splice's own job is scope creep, not a shortcut. This mode is still yours to run by hand if you ever need it; the driver's own automatic call never uses `--body-file`.
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
After open the Developer changes nothing outside the `AEG:EVIDENCE` anchor and one appended `AEG:TOKENS` row. The Principal's `[principal]` ticks are the Principal's writes and must survive every Developer edit. Everything else a review round produces — the response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment, never a body edit.
|
|
210
|
+
After open, the Developer changes nothing in the PR body at all — the driver's own automatic call regenerates `AEG:EVIDENCE` every round; the Principal's `[principal]` ticks are the Principal's writes and must survive every edit. **`AEG:TOKENS` is deliberately untouched by the driver's automatic call:** the driver runs in its own session, not yours, so a token row it collected would misattribute the driver's own usage to your `<task-id>: develop` phase. Token reporting for a re-entry round therefore has no automatic mechanism right now — a known, accepted gap this fix does not close, not a silent one: if you need a re-entry round's tokens recorded, run `vinaya pr report --write <body-file>` yourself and hand-splice the result, or ask the Principal to record it, rather than treating the missing row as this fix's oversight. Everything else a review round produces — the response to findings, any disclosure the brief didn't anticipate — is a PR comment, never a body edit.
|
|
213
211
|
|
|
214
212
|
---
|
|
215
213
|
|
|
@@ -265,16 +263,18 @@ A spike is exploratory, not a permanent excuse to skip documentation. The pull r
|
|
|
265
263
|
|
|
266
264
|
---
|
|
267
265
|
|
|
268
|
-
## After you open the PR —
|
|
266
|
+
## After you open the PR — your turn ends here
|
|
267
|
+
|
|
268
|
+
Opening the PR is the end of your turn — for round 1, and for every later round too: after you push a fix in response to review findings, your turn ends at that push, on the same branch, no new PR. You do not run `vinaya pr report --push`, you do not post a round comment, and you do not tick anything.
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
This is a change from before. It used to take four more steps — merge main if behind, regenerate the Evidence block, post a `Head: <sha>` comment carrying the round marker, tick nothing — and the second of those, a full re-run of the real gate suite, could take past ten minutes on a real Test Plan and stall the whole loop waiting on your one tool call to finish. None of those four steps are yours any more:
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
- **The `AEG:EVIDENCE` block** is regenerated by the driver itself, in-process, the moment your head's required CI turns green — the same engine module `vinaya pr report --push` always called, now also called directly by the loop's own driver rather than shelled out to as a subprocess.
|
|
273
|
+
- **The round marker comment** — `Head: <sha>`, `<!-- aeg:developer:round-<n> -->`, and (starting from round 2) the ids of the findings you addressed — is composed and posted by the driver too, from a small side-channel file you write before your turn ends: see the next paragraph.
|
|
274
|
+
- **A branch behind its base** is caught by the driver's own mergeability check before it ever dispatches a reviewer or runs the report — you never need to check this yourself; a conflicting head is sent back to you with the conflicting files named, same as before.
|
|
275
|
+
- **Ticking `[agent]`/`[principal]` boxes** was never yours to begin with (task 12; Principal ruling: an agent never ticks a box or edits a PR body) — nothing changes there.
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
**Citing which findings you addressed, from round 2 on.** When your resumed turn is sent back to fix review findings, write a `FINDING_IDS:` line — the same comma-separated grammar the reviewer's own `report.txt` already uses — to `.vinaya-round-response` at the root of your worktree, naming the ids (e.g. `F1`, `F2`) you addressed this round, before your turn ends. The driver reads and clears this file itself once your push lands and the head goes green, and folds it into the round comment it composes. This file is best-effort by design: if you forget it, or your turn crashes before writing it, the driver still posts the round comment — with no citation line — and the round is never judged stalled for want of one. You are never resumed a second time just to write it.
|
|
278
278
|
|
|
279
279
|
Then stop. Review is a separate invocation.
|
|
280
280
|
|
|
@@ -309,6 +309,8 @@ Escalate with the appropriate severity — a manual escalation note, or, if you
|
|
|
309
309
|
|
|
310
310
|
The brief's stop conditions tell you when to STOP and ask. Honor them. If the stop conditions say "STOP if you discover X" and you discover X, you stop. You do not improvise a workaround.
|
|
311
311
|
|
|
312
|
+
**Refusing or escalating before you have ever pushed.** The entry-gate refusals in this doc, and a stop condition hit before your first commit, happen before a branch or pull request exists — there is no PR yet to comment on, and an unattended loop has nothing else to read but the task Issue. In that case only, post your refusal or escalation as a comment on the task Issue itself, with `<!-- aeg:developer:stop -->` as the comment's own first line, followed by your reason. This is what lets an automation layer end the run at once rather than wait out a full poll budget for a pull request you were never going to open. Once you have pushed at least once, escalate normally — a PR exists, and every later escalation goes there per your automation layer's own request-input mechanism, never this marker.
|
|
313
|
+
|
|
312
314
|
---
|
|
313
315
|
|
|
314
316
|
## Stop conditions
|
|
@@ -411,7 +413,7 @@ Items 1–4 are also composed into one command, `bun packages/aeg-core/bin/verif
|
|
|
411
413
|
|
|
412
414
|
The checks above are **static**: they prove the change compiles, lints, types and matches its declared surface. They do not prove the feature works. Verification is the separate, mandatory phase that runs the brief's Test Plan against a booted app, after the review passes and before the Principal merges.
|
|
413
415
|
|
|
414
|
-
**It is a phase, not an actor.** There is no Verifier to dispatch. The plan splits by who can structurally execute it: `vinaya pr report` runs the `[agent]` half's fenced command list from the PR head and writes it into `AEG:EVIDENCE`; the Principal runs the `[principal]` half in a real signed-in browser and ticks its boxes. Both halves must be satisfied before a merge is allowed — the `[agent]` half by the Evidence block existing and matching a fresh recompute (`evidence-fresh`), the `[principal]` half by every unticked `[principal]` box in the PR body, which `
|
|
416
|
+
**It is a phase, not an actor.** There is no Verifier to dispatch. The plan splits by who can structurally execute it: `vinaya pr report` runs the `[agent]` half's fenced command list from the PR head and writes it into `AEG:EVIDENCE`; the Principal runs the `[principal]` half in a real signed-in browser and ticks its boxes. Both halves must be satisfied before a merge is allowed — the `[agent]` half by the Evidence block existing and matching a fresh recompute (`evidence-fresh`), the `[principal]` half by every unticked `[principal]` box in the PR body, which `review-gate` refuses a merge while any remain unticked — `test-plan` grades the `[agent]` half and the plan's structure only; it is `principalOwed`, so its own `pending` failure never blocks the loop's mechanical gate, and enforcement of the unticked box lives at `review-gate` instead.
|
|
415
417
|
|
|
416
418
|
**Why it exists:** four consecutive features once merged with green CI and were broken at runtime — a missing migration, a missing environment variable, a missing provider, an unexecuted test plan. The static gates ran and passed; the reviews read the diff; nobody booted the app. Verification is the phase that closes that gap.
|
|
417
419
|
|
|
@@ -424,13 +426,17 @@ The checks above are **static**: they prove the change compiles, lints, types an
|
|
|
424
426
|
|
|
425
427
|
If the brief declares `unit-tests-only` and the diff really is pure logic, the phase is satisfied by the unit-test gate; record that as the outcome.
|
|
426
428
|
|
|
427
|
-
### The `[agent]` half — yours
|
|
429
|
+
### The `[agent]` half — under the loop, the driver's; standalone, yours
|
|
430
|
+
|
|
431
|
+
**Under the automated dev-review loop, this already ran.** The driver's own per-round evidence report (see [§ After you open the PR — your turn ends here](#after-you-open-the-pr--your-turn-ends-here)) executes the SAME §9 fenced command list, from the SAME PR head, into the SAME `AEG:EVIDENCE` block, the moment your head's CI turns green — every round, automatically. You do not separately run this phase; by the time review finishes, it has already run.
|
|
432
|
+
|
|
433
|
+
**If you are working outside the loop** — dispatched by hand, with no driver watching this PR — the phase is still yours to run explicitly:
|
|
428
434
|
|
|
429
435
|
1. **Boot the app(s)** named in the brief from the worktree, and wait until each is reachable, if your §9 fenced commands need one running. If it does not boot, that is the failure — the plan never gets a chance to run.
|
|
430
436
|
2. **Run `vinaya pr report --push <n>`.** It executes every line in your §9 fenced command list from the PR head and writes each command plus its actual output into `AEG:EVIDENCE` — never a hand-pasted comment, never a checkbox tick. Round-tripping through prose is how falsely-passing claims slip through; a command this tool did not run is not evidence. **Accepted risk, Principal default:** `pr report --push` executes the PR's own §9 commands on the machine running it, with no check of who is running it — only the PR's author runs it; nothing enforces that today.
|
|
431
437
|
3. **Stop there.** Do not execute `[principal]` items; you structurally cannot. Mark them as awaiting the Principal.
|
|
432
438
|
|
|
433
|
-
A failed `[agent]` command makes the PR unmergeable (`vinaya pr report`'s own exit code reflects it, and `evidence-fresh` binds the recorded output to the PR head). Fix on the same branch
|
|
439
|
+
A failed `[agent]` command makes the PR unmergeable (`vinaya pr report`'s own exit code reflects it, and `evidence-fresh` binds the recorded output to the PR head). Fix on the same branch — under the loop, the next round's own automatic report overwrites the block with fresh output; standalone, re-run `vinaya pr report --push <n>` yourself — either way it overwrites, never appends a second copy.
|
|
434
440
|
|
|
435
441
|
### The `[principal]` half — not yours
|
|
436
442
|
|
|
@@ -43,6 +43,8 @@ You turn an intent and a slice of work into a whole tranche — not one task, an
|
|
|
43
43
|
|
|
44
44
|
**Forge-native by default — no topology file, no plan PR, no commit, and — usually — no Milestone.** Cut task Issues labeled `vinaya/tranche:<slug>` with the full Planner's rationale (see "The Planner's rationale" below) in each body. `@attalabs/aeg-forge-state` derives topology, dependencies, and lifecycle purely from those forge objects — nothing to write to `main`, nothing for `verify-coherence` to fall back to a file for. This cutover is now complete for every active tranche. Do not create a new topology file for a new tranche; if you find yourself about to write one, stop — the forge-native path below is the whole job. Creating a Milestone is not your job either — that's the Architect's (`roles/architect.md`), and most tranches never get one. If a Milestone already exists naming this slug in its `### Tranche intents` section, this tranche's goal is picked up from that intent line automatically; you neither create nor edit the Milestone to make that happen.
|
|
45
45
|
|
|
46
|
+
**A backlog Issue (no `vinaya/tranche:*` label at all) is out of the Planner's scope entirely.** A quick fix with no relationships to size against another task doesn't need a tranche, a topology, or this role — `vinaya issue create` (no `--label`) opens it directly, with the same body grammar (Objectives, Surface, Parts, Test plan, Stop conditions) every task Issue carries, validated by the same gates minus the tranche/Milestone attach, and `vinaya task run --issue <n>` dispatches it straight off that Issue, no planning stage in between. This is not a narrower tranche of one — it is the doctrine's other dispatchable shape, for the one thing the tranche model was never meant to size: a task with no dependency, no conflict, and no sibling to relate it to. If a Type-1 decision, an unrelated shared-package consumer, or a second task depending on it turns up while you're looking at one, that is your signal it needed a tranche after all — cut it into one rather than leaving it a backlog Issue.
|
|
47
|
+
|
|
46
48
|
Read this with `tranche-model.md` (the model) and the `aeg` skill's session-start forge queries (orientation). The Planner exists because the relationships *between* tasks — dependencies, conflicts, split-vs-combine — are invisible to a brief written in isolation. Seeing them is the whole job.
|
|
47
49
|
|
|
48
50
|
---
|
|
@@ -82,6 +82,10 @@ In a typical working period:
|
|
|
82
82
|
6. Reviews PRs on the forge. Code review for correctness and scope compliance.
|
|
83
83
|
7. Merges PRs after Planner spec review and CI passes. (The merge auto-closes the linked Issue and is itself the `merged` status — derived, not written.)
|
|
84
84
|
|
|
85
|
+
**A quick fix skips step 3 entirely.** For a backlog Issue — no `vinaya/tranche:*` label, no relationship to another task worth a Planner's dig — `vinaya issue create` (no `--label`) opens it directly, and `vinaya task run --issue <n> --agent <vendor>` dispatches it straight off that Issue: same brief grammar, same unattended loop, same review gate, on branch `task/issue-<n>`. This is the Principal's own dispatch act for the one shape that never needed a tranche — step 4 above, run without steps 1–3 ever happening. If the work turns out to need a dependency, a conflict, or a Type-1 decision after all, that is the signal to route it to the Planner instead, cut into a real tranche.
|
|
86
|
+
|
|
87
|
+
`vinaya task status` answers "what's running right now" across every dispatched task in one glance — each open task Issue with a frozen brief, its pull request, and whether its loop is `running` (with the driver pid), `paused` (with the reason), `published`, or has `no driver` — read from the outbox and the forge, never from a terminal-by-terminal `ps` scan. `vinaya task status <tranche> <n>` narrows to one task with its last round's verdict lines and the exact resume command.
|
|
88
|
+
|
|
85
89
|
The Principal does not need to be present during task execution. Dispatch and escalation routing are handled by the Planner (and an automation layer, if used); the Developer executes. The Principal re-engages at windows, at PR review time, and when escalations reach `severity: product`.
|
|
86
90
|
|
|
87
91
|
---
|
|
@@ -111,7 +111,7 @@ vinaya review post --role code-reviewer --pr <n> --verdict APPROVE|REQUEST_CHANG
|
|
|
111
111
|
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function checkDocCorrectnessSearch(description: string, lineNumber: number): void { -->
|
|
112
112
|
The findings file is one finding per line, `SEVERITY|file:line|description` (`|`-delimited: `file:line` already contains a colon), severity one of `BLOCKER|MAJOR|MINOR`. The `description` field begins with the finding's id and class, `F<n> <class>: <what is wrong>` — class is one of `correctness`, `type-safety`, `performance`, `resource-leak`, `maintainability`, `scope`, `test-honesty`, `doc-correctness`, or `other:<slug>` when none fits. 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. It goes at the end of the description; run it before writing it down and confirm it matches the anchored line. The pattern carries no `|` character — this file is `|`-delimited — so reach for a character class, or the stem the copies share, rather than alternation. `vinaya review post` refuses the finding outright when the pattern is missing or path-filtered: a false sentence is a text-duplication defect, not a location defect. What no check can judge, and what stays yours, is whether the pattern really matches the claim and whether each hit it returns is true. This is free text that carries no `|` character inside the existing field, not a grammar change. Omit `--findings-file` for zero findings.
|
|
113
113
|
|
|
114
|
-
The objectives file is one line per objective, `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` — the same `|`-delimited shape, evidence being the rest of the line. **Judge MET/NOT MET from `git diff`, never from the Developer's own report** — the objective is a fact about the code, and the Developer's Decisions section is not evidence for it, the same discipline check 3's `SCOPE:` line already holds you to. `--objectives-file` is required whenever the closed Issue (or the PR body's own `## Objectives` section, when the PR closes none) has a list to judge; its ids must cover that list exactly — a missing or extra `O<n>` is refused before posting. An Issue that predates the objectives cutover renders no `Objectives version:` line and no block at all, matching the gate's own skip for that stock. The command renders this exact shape (kept here so a human or a debugging agent can still read what it produces — this is documentation, not something to write by hand):
|
|
114
|
+
The objectives file is one line per objective, `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` — the same `|`-delimited shape, evidence being the rest of the line. **Judge MET/NOT MET from `git diff`, never from the Developer's own report** — the objective is a fact about the code, and the Developer's Decisions section is not evidence for it, the same discipline check 3's `SCOPE:` line already holds you to. **`NOT MET` requires a code or test location as its evidence** — a real `file:line`, the same shape a finding's own location takes, naming where the objective is unmet in the diff. Evidence that names only a PR body section, a comment, or a role file is not a location the objective's own unmetness lives at — the dev-review-loop's own report parser reclassifies such a line `MET (prose note)` before it ever reaches a round's outcome, the same `isProseLocation` predicate (`@attalabs/aeg-core`) the body-located `MINOR` cap already applies to a finding's location, so writing one costs the round nothing but a wasted line: it never blocks, it is never re-litigated next round, and it is not what you intended. If the objective is genuinely unmet, point at the code that fails to meet it. **`NOT MET` means you verified the objective is not met — never a decline.** An objective outside your own lens (a security-shaped objective reaching a code-reviewer verdict, or the reverse) is `MET`, citing the other reviewer's evidence or verifying it yourself directly — never `NOT MET` with an out-of-scope note; a reviewer that declines an objective this way forces a review round over nothing. `--objectives-file` is required whenever the closed Issue (or the PR body's own `## Objectives` section, when the PR closes none) has a list to judge; its ids must cover that list exactly — a missing or extra `O<n>` is refused before posting. An Issue that predates the objectives cutover renders no `Objectives version:` line and no block at all, matching the gate's own skip for that stock. The command renders this exact shape (kept here so a human or a debugging agent can still read what it produces — this is documentation, not something to write by hand):
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
VERDICT: APPROVE | REQUEST CHANGES
|
|
@@ -144,12 +144,12 @@ DOCS: [tier-appropriate | missing items listed in findings]
|
|
|
144
144
|
`vinaya review post` also refuses before posting anything if you pass a BLOCKER finding together with `--verdict APPROVE`, or any `NOT MET` objective together with `--verdict APPROVE` — both contradictions are caught mechanically, not left to review. Before its own post reaches the forge, it refuses to post anything the gate would misread: it runs the exact same `VERDICT:`/`Judged head:`/`Objectives version:`/`Ruling ordinal:` extraction the merge gate uses over the rendered comment, and requires exactly the intended verdict to come back. Free text in a finding, a conformance field, an objective's evidence, or `--scope-evidence-file` can say `VERDICT` or span multiple lines without risk — the extraction reads only a comment's first five lines (the ruling ordinal's own first-seven-line window is wider still), and in a code-reviewer or security comment a caller field never opens one of those lines (it only trails a fixed, renderer-owned label); an escalation's `--summary` occupies line 7 at minimum (`Ruling ordinal:` renders unconditionally ahead of it), which is exactly why this pre-post re-parse exists rather than construction alone.
|
|
145
145
|
|
|
146
146
|
- **BLOCKER** — blocks merge. Wrong behavior; a dishonest test; a document the brief's documentation-update list names that is absent from the diff or states the changed behavior backwards; a scope violation; a **spec contradiction**.
|
|
147
|
-
- **MAJOR** —
|
|
147
|
+
- **MAJOR** — blocks when the repository's policy says so. A likely bug, weak error handling, spec drift short of contradiction, a wrong sentence in a document the brief did not name. Listed in the verdict, shown to the Principal at the go (the Principal's decision, before merge, on whether any surfaced finding blocks this change), published in the record — the Principal decides whether a surfaced finding blocks this change.
|
|
148
148
|
- **MINOR** — noted; Developer's discretion. Every register and slop finding (check 8) and every reader-readability finding (check 6) is at most MINOR.
|
|
149
149
|
|
|
150
150
|
The `SCOPE:` line, and any blast-radius assertion under check 7, are evidence-backed claims: each may be written only after its named check has run — `git diff origin/main...HEAD --stat` for scope, the consumer grep for multi-project reach — with the output quoted in the same review comment the verdict lands in: a fenced block directly below the verdict block, so evidence sits in one predictable place. The other verdict lines carry no such requirement.
|
|
151
151
|
|
|
152
|
-
VERDICT is `REQUEST CHANGES` if and only if at least one BLOCKER finding exists. Otherwise VERDICT is `APPROVE`, with every MAJOR and MINOR finding still listed under FINDINGS — an APPROVE is not silence about them, it is a statement that none of them blocks. (A REQUEST CHANGES sets the PR's review decision to `CHANGES_REQUESTED`, which is the derived `changes-requested` status — no one writes it down.) You do not type that decision by hand: `vinaya review post` derives it from the findings file you pass it — REQUEST CHANGES iff a BLOCKER is present, APPROVE otherwise — and renders the bare `VERDICT:` line and the `Judged head:` binding itself. `--verdict` is optional; if you pass one anyway, the command refuses before posting anything when it disagrees with the derivation, naming the derived value. It refuses to exit 0 unless its own post re-parses clean through the gate's extractors. The severity you assign to each finding is caller-asserted and not checked — the derivation trusts your severities, not your arithmetic.
|
|
152
|
+
VERDICT is `REQUEST CHANGES` if and only if at least one BLOCKER finding exists. Otherwise VERDICT is `APPROVE`, with every MAJOR and MINOR finding still listed under FINDINGS — an APPROVE is not silence about them, it is a statement that none of them blocks. (A REQUEST CHANGES sets the PR's review decision to `CHANGES_REQUESTED`, which is the derived `changes-requested` status — no one writes it down.) You do not type that decision by hand: `vinaya review post` derives it from the findings file you pass it — REQUEST CHANGES iff a BLOCKER is present, APPROVE otherwise — and renders the bare `VERDICT:` line and the `Judged head:` binding itself. `--verdict` is optional; if you pass one anyway, the command refuses before posting anything when it disagrees with the derivation, naming the derived value. It refuses to exit 0 unless its own post re-parses clean through the gate's extractors. The severity you assign to each finding is caller-asserted and not checked — the derivation trusts your severities, not your arithmetic. A finding whose own `file:line` names the PR body, a comment, or a role file is capped to MINOR by the policy evaluator before it counts toward the threshold, regardless of the severity you assign it — write its real severity anyway; the cap is applied for you, never something to pre-empt by under-reporting.
|
|
153
153
|
|
|
154
154
|
A re-review (a fresh-context reviewer invoked again after the Developer pushes fixes) does two things, in order. First, it reports the state of every prior id — `F1`, `F2`, … — before listing any new finding, one of exactly `open`, `fix-claimed`, `reproduced`, or `resolved` per id, confirmed by re-checking the artifact, never by assuming a push means a fix. For a prior doc-correctness finding, "re-checking the artifact" means re-running its `Search:` pattern at the new head, with no path filter, and reading every hit; the finding is resolved only when every remaining hit is a true statement, and a still-false sibling copy keeps the original id open rather than becoming a new finding. Write that state directly in the finding's own description, `F<n> <class> <state>: <text>` — that is what `vinaya review post` reads back on the next round to confirm every prior id is still accounted for; a findings file that drops a prior id with no state token is refused before posting. An id is assigned once, when a finding is first reported, and never renumbered; rewording a finding's description does not create a new id. The prior ids and the previously judged head are read from the prior verdict comment on the PR: its FINDINGS list and its `Judged head:` line. Every prior objective reappears too — the same rule, one level up: a re-review's `--objectives-file` that drops a prior `O<n>` is refused before posting, the id read from the prior comment's own `OBJECTIVES:` block. Second, round two is delta-only for every non-blocking severity: it judges only the lines changed since the previously judged head, and `vinaya review post` refuses a MAJOR or MINOR finding whose `file:line` falls outside that diff. A BLOCKER outside the delta still drives the verdict on any round and is always accepted. A prior BLOCKER you mark `resolved` keeps its BLOCKER severity in the record but no longer drives the verdict — `vinaya review post` derives the verdict only from findings not marked `resolved`; mark `fix-claimed` or `reproduced` instead if it is not actually fixed. After round two the Principal decides; there is no round three unless the Principal orders it.
|
|
155
155
|
|
|
@@ -104,7 +104,7 @@ vinaya review post --role security --pr <n> --verdict PASS|FAIL \
|
|
|
104
104
|
|
|
105
105
|
The findings file is one finding per line, `SEVERITY|file:line|description` (`|`-delimited: `file:line` already contains a colon), severity one of `CRITICAL|HIGH|MEDIUM|LOW`. Omit `--findings-file` for zero findings.
|
|
106
106
|
|
|
107
|
-
The objectives file is one line per objective, `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` — the same `|`-delimited shape, evidence being the rest of the line. **Judge MET/NOT MET from the diff, never from the Developer's own report.** `--objectives-file` is required whenever the closed Issue (or the PR body's own `## Objectives` section) has a list to judge; its ids must cover that list exactly. An Issue that predates the objectives cutover renders no `Objectives version:` line and no block at all. The command renders this exact shape (kept here so a human or a debugging agent can still read what it produces — this is documentation, not something to write by hand):
|
|
107
|
+
The objectives file is one line per objective, `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` — the same `|`-delimited shape, evidence being the rest of the line. **Judge MET/NOT MET from the diff, never from the Developer's own report.** **`NOT MET` requires a code or test location as its evidence** — a real `file:line`, the same shape a finding's own location takes, naming where the objective is unmet in the diff. Evidence that names only a PR body section, a comment, or a role file is not a location the objective's own unmetness lives at — the dev-review-loop's own report parser reclassifies such a line `MET (prose note)` before it ever reaches a round's outcome, the same `isProseLocation` predicate (`@attalabs/aeg-core`) the body-located `MINOR` cap already applies to a finding's location, so writing one costs the round nothing but a wasted line: it never blocks, it is never re-litigated next round, and it is not what you intended. If the objective is genuinely unmet, point at the code that fails to meet it. **`NOT MET` means you verified the objective is not met — never a decline.** An objective outside your own lens (a code-reviewer-shaped objective reaching a security verdict, or the reverse) is `MET`, citing the other reviewer's evidence or verifying it yourself directly — never `NOT MET` with an out-of-scope note; a reviewer that declines an objective this way forces a review round over nothing. `--objectives-file` is required whenever the closed Issue (or the PR body's own `## Objectives` section) has a list to judge; its ids must cover that list exactly. An Issue that predates the objectives cutover renders no `Objectives version:` line and no block at all. The command renders this exact shape (kept here so a human or a debugging agent can still read what it produces — this is documentation, not something to write by hand):
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
VERDICT: PASS | FAIL
|
|
@@ -136,9 +136,9 @@ Before its own post reaches the forge, `vinaya review post` refuses to post anyt
|
|
|
136
136
|
- **HIGH** — likely exploitable misconfig or injection surface.
|
|
137
137
|
- **MEDIUM/LOW** — hardening notes.
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
CRITICAL always drives VERDICT FAIL. HIGH, MEDIUM, and LOW block only when the repository's policy says so — this repository's own `reviewPolicy.securityThreshold` decides how far down the `CRITICAL > HIGH > MEDIUM > LOW` scale a finding still fails the round. You do not type that decision by hand: `vinaya review post` derives it from the findings file you pass it, against that configured threshold — FAIL iff a finding at or above the threshold is present, PASS otherwise — and refuses before posting anything if `--verdict` disagrees with the derivation, naming the derived value. A finding whose own `file:line` names the PR body, a comment, or a role file is capped to MINOR by the policy evaluator before it counts — MINOR is not on this scale at all, so such a finding never fails the round regardless of the severity you assign it. Write its real severity anyway; the cap is applied for you.
|
|
140
140
|
|
|
141
|
-
A re-pass after the Developer's fixes follows the same re-review rule as the code role: report the state of every prior id (`open`, `fix-claimed`, `reproduced`, `resolved`) in the finding's own description, `F<n> <class> <state>: <text>`, before listing anything new — `vinaya review post` refuses a findings file that drops a prior id with no state token. Every prior objective reappears too — a re-pass's `--objectives-file` that drops a prior `O<n>` is refused before posting, the id read from the prior comment's own `OBJECTIVES:` block. Round two is delta-only for
|
|
141
|
+
A re-pass after the Developer's fixes follows the same re-review rule as the code role: report the state of every prior id (`open`, `fix-claimed`, `reproduced`, `resolved`) in the finding's own description, `F<n> <class> <state>: <text>`, before listing anything new — `vinaya review post` refuses a findings file that drops a prior id with no state token. Every prior objective reappears too — a re-pass's `--objectives-file` that drops a prior `O<n>` is refused before posting, the id read from the prior comment's own `OBJECTIVES:` block. Round two is delta-only for every non-blocking severity under this repository's policy: a finding below the configured `securityThreshold` whose `file:line` falls outside the diff since the previously judged head is refused. A finding at or above the threshold outside the delta still drives the verdict on any round and is always accepted. A prior CRITICAL/HIGH you mark `resolved` keeps its severity in the record but no longer drives the verdict — `vinaya review post` derives the verdict only from findings not marked `resolved`; mark `fix-claimed` or `reproduced` instead if it is not actually fixed.
|
|
142
142
|
|
|
143
143
|
The `SECRETS:` line is evidence-backed, not asserted: the secret scanner's pasted output (check 1) must appear in the verdict comment above it — necessary evidence that the scan ran, never sufficient on its own, since the judgment half of check 1 still stands behind the claim. `SECRETS: none found` with no scan output pasted is an unbacked self-attestation — the exact claim this check exists to catch in others' work, not to commit in your own. `vinaya review post` mechanizes this: passing `--secrets "none found"` without `--secrets-evidence-file <path>` (the actual pasted scanner output) is refused outright.
|
|
144
144
|
|