@attalabs/vinaya 0.5.0 → 0.7.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 CHANGED
@@ -16,6 +16,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
16
16
  |---------|-------------|
17
17
  | `vinaya help` | Usage text |
18
18
  | `vinaya version` | Print the installed CLI version (`--json` for the enveloped machine form) |
19
+ | `vinaya doctrine` | Print the absolute path of the bundled doctrine's front door (`aeg-root/skills/aeg/SKILL.md`) on this machine. The committed root `VINAYA.md` pointer names the package, never a filesystem path — this command is the read-time resolution step it hands the reader. `--json` for the enveloped `{ root, entry }` form. |
19
20
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
20
21
  | `vinaya new check <name>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<name>.ts`, ready to register in `vinaya.config.json` |
21
22
 
@@ -42,6 +43,14 @@ Today the schema carries one surface:
42
43
 
43
44
  Both `rings` fields are plain booleans — no conditional logic. Ring 0 (git hooks) and the CI/branch-protection guarantee are never represented in this schema, by design — they are not configurable.
44
45
 
46
+ ## Where the git hooks live
47
+
48
+ `vinaya init` installs the ring-0 hooks (`pre-commit`, `pre-push`) into a **tracked** `.vinaya/hooks/` directory and points git at it with `git config core.hooksPath .vinaya/hooks` — commit that directory. Raw `.git/hooks` is never versioned by git, so hooks installed there exist only on the installing machine; tracked hooks travel with the repo into every clone and every linked worktree checkout.
49
+
50
+ One thing git cannot version is the config itself: **each fresh clone runs `git config core.hooksPath .vinaya/hooks` once** to arm the hooks (linked worktrees inherit it — the config is shared, the path is relative). `vinaya doctor` reports an unarmed clone as an error naming that exact command; `vinaya init` and `vinaya upgrade` also set it.
51
+
52
+ Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory (husky's `prepare` script owns per-clone wiring), and a repo with its own active raw hooks in `.git/hooks` stays on the legacy append-a-managed-block layout there — re-routing `core.hooksPath` would silently disable the adopter's own hooks. On that legacy layout `vinaya doctor` warns that clones have no hooks, and `vinaya upgrade` migrates to the tracked layout as soon as nothing foreign would be disabled.
53
+
45
54
  Custom checks register under `checks`, one entry per check:
46
55
 
47
56
  ```json
@@ -200,10 +200,9 @@ Locked. Spec filenames are `{product}-spec.md` or `{component}-spec.md` — no `
200
200
  **Universal rule, every role.** Any change to a repo-tracked file — code, specs, skills, role docs, the tranche topology — reaches `main` through a worktree branch + PR + green merge. **No role commits or pushes directly to `main`.** This applies to the Planner editing the tranche file just as much as the Developer editing code: "I only touched a doc" is not an exemption. The drift that produced this rule was a plan commit landing on `main` with no worktree and nothing stopping it.
201
201
 
202
202
  This is **mechanically enforced**, not merely asked:
203
- - `.husky/pre-commit` refuses a commit while the current branch is `main`; `.husky/pre-push` refuses any push whose target is `refs/heads/main`. (Husky activates per-worktree via the post-checkout hook, so the guards fire in every worktree.)
204
- - The merge-gate hook `.claude/hooks/check-pr-green.sh` (a `PreToolUse` hook wired in `.claude/settings.json`, sibling to `check-skill.sh`) intercepts every agent merge path — `gh pr merge`, `gh api …/merge`, `curl …/merge`, and the GitHub MCP `merge_pull_request` tool — and **denies the merge unless `gh pr checks <pr>` is all-green**. A red or pending PR cannot be merged by an agent. The gate fails closed: if greenness can't be proven, the merge is denied.
203
+ - **This repo has no local main-branch guard and no agent-side merge interception.** The managed `.git/hooks/pre-commit`/`pre-push` blocks run the `vinaya check` suite (shared across every linked worktree via git's common hooks directory), but nothing locally refuses a commit on `main` or an agent merge of a red PR. What holds the invariants here: the required `vinaya review gate` check plus branch protection make a red merge unmergeable on the forge side, and a stray direct push to `main` is detected after the fact by the ring-2 `direct-main-push` audit (the `vinaya/direct-main-push` label). (The attalabs reference implementation additionally refuses these locally — per-worktree main-guards in its hooks and a session-level merge-gate hook that denies any merge path unless the PR is all-green.)
205
204
 
206
- The green-merge + no-direct-commit invariants now live in both these hooks and a GitHub setting: repository ruleset `17656829` ("Main protection") requires the `AEG gate suite`, `Typecheck + unit tests`, and `Review gate` checks on `~DEFAULT_BRANCH`, with `bypass_actors: []`. The exception path is the same as everywhere: a `Lock`/override is a Principal action, not an agent one.
205
+ The green-merge invariant's forge half is a GitHub setting: attalabs pins it with a repository ruleset requiring its gate checks with `bypass_actors: []`; this repo's equivalent is branch protection marking the `vinaya-checks` and review-gate checks required — `vinaya init` prints the enabling command and `vinaya doctor` reports its absence. The exception path is the same as everywhere: a `Lock`/override is a Principal action, not an agent one.
207
206
 
208
207
  The only thing a non-Developer role does *not* route through a worktree is a pure forge action (cutting an Issue, posting a comment, merging via the gate) — those touch the forge, not repo files. Every repo *file* edit goes through the worktree + PR.
209
208
 
@@ -16,11 +16,11 @@ provenance: >
16
16
  they are not in the surface map of a task that only meant to add a gate:
17
17
 
18
18
  - packages/aeg-core/src/registry-parse.test.ts asserts the TOTAL row count
19
- across all three rings (currently 34).
19
+ across all three rings (currently 40).
20
20
  - packages/aeg-core/src/markdown-table.test.ts asserts per-ring counts
21
- (currently ring 0 = 12, ring 1 = 16, ring 2 = 6).
21
+ (currently ring 0 = 16, ring 1 = 17, ring 2 = 7).
22
22
  - packages/aeg-core/src/docs/node-route.test.ts asserts the ring-0 count
23
- (currently 12) alongside the actions/roles/contracts counts.
23
+ (currently 16) alongside the actions/roles/contracts counts.
24
24
 
25
25
  Bumping those numbers is a mechanical consequence of a correct edit, not a
26
26
  design change — fix them in the same commit and say so in the PR body.
@@ -67,18 +67,22 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
67
67
 
68
68
  | Action | Summary | Category | Description | Gate | What must be true before the action is allowed | implementation |
69
69
  | --- | --- | --- | --- | --- | --- | --- |
70
- | Editing a governed file | Ever had a teammate change code they clearly never read the docs for? | hook | Refuses an edit to a governed file until the document that governs it has been read in this session. | **Edit gate** | The architecture document that owns the file has been loaded in this sessionno editing governed code from pattern-matching alone | `.claude/hooks/check-skill.sh` |
71
- | `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Git pre-commit hooks** | The code compiles, passes lint and formatting, the unit tests pass, and the commit message follows the repo's message convention | `.husky/pre-commit` |
72
- | `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Git pre-push hook** | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2 detection backstop (see below, task 24 item 2). This check is deliberately **fail-open**: if the forge cannot be reached (network flake, auth issue, rate limit) it is treated the same as "no pull request exists yet" and the push is allowed — a transient reachability issue must never block every push. On a task branch's first push (before its PR exists), `verify-dispatch` also runs itself here — see the dedicated row below (task 25). Any changed code that has a designated owning document must have that document updated in the same branch, OR (task 29) carry an actor-verified `vinaya/waiver:docs` label on the open pull request, OR (task 4) carry a `Doc-neutral: <pointer> — <note>` PR-body declaration whose matched-file diff is verified comment/whitespace-only (`isMechanicallyNeutralDiff`) — a waiver is a forge-authenticated human act, never a parseable string, and a Doc-neutral declaration is never sufficient by itself; only diff-confirmed evidence satisfies it. Before a PR exists (first push) no waiver label can exist yet, so an owned-doc violation here is **warn-with-declared-intent, not a hard block**: the push always succeeds, and the printed message states plainly that ring 1 stays red until a principal applies the label, the doc is updated, or a Doc-neutral declaration with confirming evidence is added — this replaces the earlier first-push `Doc-waiver:` commit-trailer self-service, which is now dead code (that path is superseded). `Doc-ack:` (URL-pointer acknowledgment) is unaffected by the waiver change — still read from the branch's own last commit message before a PR exists, or — for a pre-authoring dry run via `verify-dispatch --simulate`, before any commit exists — from a local drafted-PR-body file. | `.husky/pre-push` |
70
+ | Editing a governed file | Ever had a teammate change code they clearly never read the docs for? | hook | Couples an edit to a governed code surface to its owning document, so the two cannot drift apart in one change. | **C5 doc-coverage (update-or-waive)** | No session read-gate hook is installed in this repo. The nearest enforcement is C5 doc-coverage: a changed code file matching a `.vinaya/doc-owners` binding must have its owning doc in the same diff (or a `Doc-ack:`/principal-applied `vinaya/waiver:docs` escape) warn-only at the ring-0 hooks, blocking at ring 1. Dormant until `.vinaya/doc-owners` carries a binding for the surface. | `apps/cli/src/checks/bin/check-doc-coverage-push.ts` |
71
+ | `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Managed `.git/hooks/pre-commit` block** (written by `vinaya init`/`upgrade` through the common-dir hook resolver, so every linked worktree shares it) | The ring-0 check suite passes: the hook runs `vinaya check --all --local` (in this vendoring repo, the built `node apps/cli/dist/index.js` invocation, guarded by a build-missing refusal). Build, lint and unit tests are NOT run by this hook here — they run in the repo-own `ci.yml` (ring 1). | `apps/cli/src/checks/runner.ts` |
72
+ | `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Managed `.git/hooks/pre-push` block** (same generator and common-dir resolver as pre-commit) | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2 detection backstop (see below, task 24 item 2). This check is deliberately **fail-open**: if the forge cannot be reached (network flake, auth issue, rate limit) it is treated the same as "no pull request exists yet" and the push is allowed — a transient reachability issue must never block every push. On a task branch's first push (before its PR exists), `verify-dispatch` also runs itself here — see the dedicated row below (task 25). Any changed code that has a designated owning document must have that document updated in the same branch, OR (task 29) carry an actor-verified `vinaya/waiver:docs` label on the open pull request, OR (task 4) carry a `Doc-neutral: <pointer> — <note>` PR-body declaration whose matched-file diff is verified comment/whitespace-only (`isMechanicallyNeutralDiff`) — a waiver is a forge-authenticated human act, never a parseable string, and a Doc-neutral declaration is never sufficient by itself; only diff-confirmed evidence satisfies it. Before a PR exists (first push) no waiver label can exist yet, so an owned-doc violation here is **warn-with-declared-intent, not a hard block**: the push always succeeds, and the printed message states plainly that ring 1 stays red until a principal applies the label, the doc is updated, or a Doc-neutral declaration with confirming evidence is added — this replaces the earlier first-push `Doc-waiver:` commit-trailer self-service, which is now dead code (that path is superseded). `Doc-ack:` (URL-pointer acknowledgment) is unaffected by the waiver change — still read from the branch's own last commit message before a PR exists, or — for a pre-authoring dry run via `verify-dispatch --simulate`, before any commit exists — from a local drafted-PR-body file. | `apps/cli/src/checks/runner.ts` |
73
73
  | Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. | `packages/aeg-core/bin/open-pr.ts` |
74
74
  | Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no per-project state to update. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (`.aeg/packages`) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | `packages/aeg-core/bin/open-issue.ts` |
75
- | Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Refuses raw API writes to pull requests and Issues, so nothing can edit them behind the gates. | **Forge command gate** | Refused outright there is no unvalidated path | `.claude/hooks/check-forge-gates.sh` |
76
- | Merging | Ever had a broken build get merged anyway? | event | Refuses a merge while anything the gates check is still failing. | **Merge gate** | Every CI check is green red or pending cannot be merged by an agent | `.claude/hooks/check-pr-green.sh` |
77
- | Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **`.husky/pre-push`** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
75
+ | Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Validates forge writes before they reach GitHub in this repo at the CLI layer, not at a tool-interception hook. | **Validated forge-write commands** (`vinaya issue create/edit`, `vinaya pr create/edit`) | No tool-interception layer is installed in this repo (`ring1_forgeWriteInterception: false` in `vinaya.config.json`), so a raw `gh` write is not refused at the tool call. The sanctioned path is the CLI's validated forge-write commands, which refuse a malformed task Issue (the eight-field Planner rationale) or PR body (the configured brief schema) before anything reaches the forge; ring-1 CI re-runs the same validators as the backstop for writes that bypass them. | `apps/cli/src/lib/forge-write.ts` |
76
+ | Merging | Ever had a broken build get merged anyway? | event | Holds the merge on the forge side while anything the gates check is still failing. | **Required review-gate check + branch protection** | No agent-side merge hook is installed in this repo. The merge gate is ring 1: the required `vinaya review gate` check (a clean code-reviewer APPROVE and a clean security-review PASS verdict from principal-allowlisted authors, or a principal-applied, actor-verified `vinaya/waiver:review` label) plus branch protection marking that check required. | `apps/cli/src/checks/bin/check-review-gate.ts` |
77
+ | Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
78
78
  | Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | `packages/aeg-core/src/brief-validation.ts` |
79
79
  | Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | `packages/aeg-core/bin/verify-task.ts` |
80
80
  | Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. All 15 core checks in `registry.ts` carry an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@atta/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | `apps/cli/src/checks/runner.ts` |
81
- | `vinaya check` / `vinaya check --plan` (a CLI mechanism, not a git hook — this row is `.husky`-free) | Ever had a config entry silently double-run alongside the core check it was meant to replace, with no way to see that from the outside? | event | Resolves core-registered and config-registered checks into one deterministic table before anything runs, instead of letting a config entry run alongside the core check it collides with, unannounced. | **Checks-side resolver** (`resolveChecks`/`isValidNamespacedKey`, `apps/cli/src/checks/resolver.ts`) | An exact-key match against a core check ID is an **override** — a complete replacement of the spec, never a merge. A key containing exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*` and the `vinaya` prefix rejected as an exact segment match, is **additive**. Anything else — a bare key with no `/` matching no core ID — is a loud `FAIL_CLOSED` refusal: never silently dropped, never defaulted, never last-wins. `--plan` never resolves or prints an env *value*, only how each one resolves. **This release feeds `vinaya check --plan` / `--plan --json` only** — `vinaya check`'s real execution (`check.ts`'s flat `[...coreCheckRegistry(), ...customSpecs]` concat) is unaffected until a later task wires it in. | `apps/cli/src/checks/resolver.ts` |
81
+ | `vinaya check` / `vinaya check --plan` (a CLI mechanism, not a git hook — this row names no hook path) | Ever had a config entry silently double-run alongside the core check it was meant to replace, with no way to see that from the outside? | event | Resolves core-registered and config-registered checks into one deterministic table before anything runs, instead of letting a config entry run alongside the core check it collides with, unannounced. | **Checks-side resolver** (`resolveChecks`/`isValidNamespacedKey`, `apps/cli/src/checks/resolver.ts`) | An exact-key match against a core check ID is an **override** — a complete replacement of the spec, never a merge. A key containing exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*` and the `vinaya` prefix rejected as an exact segment match, is **additive**. Anything else — a bare key with no `/` matching no core ID — is a loud `FAIL_CLOSED` refusal: never silently dropped, never defaulted, never last-wins. `--plan` never resolves or prints an env *value*, only how each one resolves. **This release feeds `vinaya check --plan` / `--plan --json` only** — `vinaya check`'s real execution (`check.ts`'s flat `[...coreCheckRegistry(), ...customSpecs]` concat) is unaffected until a later task wires it in. | `apps/cli/src/checks/resolver.ts` |
82
+ | Pushing a task branch that matches no planned task | Ever pushed a task branch that belonged to no plan? | hook | Refuses to push a task branch whose name matches no task row derived from the forge. | **`check-branch-topology` standalone shim** (in this repo the managed pre-push hook runs the same gate as the registered `branch-topology` check — a thin adapter in `apps/cli/src/checks/bin/` over the same pure evaluator; this row names the seeded standalone CLI form) | The branch's `task/<tranche>/<n>` fields resolve against the forge-derived tranche (a Milestone plus labeled task Issues): the tranche derives, and some task row's id literal-equals `<n>`. Fail-closed — forge/network unreachability refuses the push, matching `verify-dispatch`'s precedent for hard gates, unlike the fail-open dead-branch guard below. Known prose staleness, accepted: its refusal text still names a per-tranche topology file under `aeg-root/tranches/` that post-cutover repos no longer carry — the gate's logic is forge-native and correct, but the message text is a byte-identical-compatibility contract shared with the registered adapter and asserted verbatim by tests, so rewording it is a real change to that contract, not a message tweak. | `packages/aeg-core/bin/check-branch-topology.ts` |
83
+ | Pushing to a branch whose pull request already resolved | Ever pushed more commits to an already-merged branch? | hook | Refuses a push to a task branch whose most recent pull request is already merged or closed. | **`check-push-target` standalone shim** (the registered `dead-branch-push` check is a thin adapter over the same `checkDeadBranchPush` evaluator; this row names the seeded standalone CLI form) | The branch's most recent pull request — one batched `gh pr list --head <branch>` call — is still `OPEN`, or none exists yet. Deliberately fail-open: any forge-reachability failure (auth, network, rate limit, malformed JSON) maps to `UNKNOWN`, treated as allow — a transient outage must never block every push. The ring-2 dead-branch-push audit below is this gate's detection backstop for writers the hook cannot reach. | `packages/aeg-core/bin/check-push-target.ts` |
84
+ | A task branch's first push (dispatch re-check) | Ever found out mid-push that your task was never dispatchable? | hook | Re-runs the dispatch-readiness gate once, on a task branch's first push, before its pull request exists. | **`check-first-push-dispatch` standalone shim** (wraps the unchanged `verify-dispatch` gate mode — see the dedicated `verify-dispatch` row above; the registered `first-push-dispatch` check classifies the same readiness through the shared evaluator) | On a `task/*/*` branch with no pull request yet, `verify-dispatch`'s `dispatch-readiness:` line reads `READY`. Only that line is read — never the combined exit code, which also folds in leftover-detection and would false-block every push after the first on a task legitimately mid-flight. Fail-open on infrastructure, loudly: a failed `gh auth status` probe (or `verify-dispatch`'s own infra marker) classifies as `UNKNOWN` and the push is allowed; unparseable output refuses — fail loud, never silently allow. Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/check-first-push-dispatch.ts` |
85
+ | A task branch's first push (Issue self-assignment) | Ever had no idea who was actually working a task? | hook | Assigns the task's Issue to the authenticated pusher on the branch's genuinely first push — visibility automation, deliberately not a gate. | **`assign-task-issue` standalone shim** (the registered `issue-assignment` check mirrors it; the Issue-self-assignment note below this table is the same mechanism's prose record) | Nothing — this row names a mechanism, not a refusal. Fail-open by contract: it runs after every blocking gate has passed, degrades every failed forge call to a skip, warns and exits 0 on any API failure, and nothing gates on its exit code — a failed assignment can never block a legitimate push. Idempotent: an already-assigned Issue is a no-op, and a push to an already-existing branch never re-triggers it. | `packages/aeg-core/bin/assign-task-issue.ts` |
82
86
 
83
87
  **Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
84
88
 
@@ -94,7 +98,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
94
98
 
95
99
  Every pull request, on open and on every push, re-runs the same checks in CI:
96
100
 
97
- > **Packaging (task 31, item 3):** every row below except `Typecheck + unit tests` (and the LLM-based claude-review workflow) runs as a sequential **step of one consolidated "AEG gate suite" job** in `.github/workflows/forge-lifecycle.yml`, not as its own job one checkout/setup/install instead of nine (GitHub bills per-job, rounded up to the minute; the old packaging burned ~9 billed minutes of pure setup overhead per push). Same checks, same strictness, same pass/fail criteria. Independent failure visibility is preserved: every gate step is guarded by `!cancelled()` (an earlier failure never skips a later gate), and each step reports its own red/green in the job's step list.
101
+ > **Packaging (this repo):** every deterministic row below runs inside **one job `vinaya check --all --diff-only`** in `.github/workflows/vinaya-checks.yml` (the generated, vinaya-managed workflow), not as its own job: one checkout/build, with each check reporting its own pass/fail line in that job's log. The two exceptions are `Typecheck + unit tests` (the hand-written `.github/workflows/ci.yml`) and the Review gate (its own required check in `.github/workflows/vinaya-review.yml`, re-triggered by `vinaya-review-verdict.yml` when a verdict comment lands). Same checks, same strictness, at ring 0 and ring 1 the runner is one codebase invoked from both.
98
102
 
99
103
  | CI check | Summary | Category | Description | Re-verifies | implementation |
100
104
  | --- | --- | --- | --- | --- | --- |
@@ -105,15 +109,16 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
105
109
  | Documentation gate | Ever shipped a change and forgot to update the doc explaining it? | ci | Re-checks that a change carrying real code also updates the docs explaining it. | Tier declaration and tier-appropriate documentation, including code→document ownership | `packages/aeg-core/bin/verify-docs.ts` |
106
110
  | Test-plan state | Ever merged a PR with an unchecked "did you test this" box? | ci | Re-checks that the pull request’s test plan is genuinely ticked. | Unticked test-plan boxes block merge readiness | `packages/aeg-core/bin/verify-test-plan.ts` |
107
111
  | 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) | `.github/workflows/ci.yml` |
108
- | Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | Re-checks formatting and naming against the repo’s own conventions. | Lint/format, commit-message grammar, no hardcoded UI colors | `.github/workflows/forge-lifecycle.yml` |
109
- | AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Puts an automated reviewer on every pull request, whatever the hour. | Posts a judgment review of the change. **Advisory only, including its verdict markers** (ruled 2026-08-09, verdict-author verification): the Review gate below counts a verdict comment only when its author is on the principal allowlist, and this job posts under a bot identity deliberately excluded, because the job reads the PR's own content, so a crafted PR can steer what it writes; letting its `APPROVE` gate merges would re-open the forged-verdict channel author verification closes. In practice this changes nothing: the gate has only ever flipped on principal-identity verdicts. The bot's review remains visible input to the human/dispatched reviews | `.github/workflows/claude-code-review.yml` |
112
+ | 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. | `.github/workflows/ci.yml` |
113
+ | 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. | `.github/workflows/vinaya-review-verdict.yml` |
110
114
  | 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). **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`. 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. | `packages/aeg-core/bin/verify-review-gate.ts` |
111
115
  | 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. | `packages/aeg-core/bin/verify-registry.ts` |
112
- | 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. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. | `packages/aeg-core/bin/verify-registry.ts` |
113
- | 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 the exact class of GitHub-mutating call `.claude/hooks/check-forge-gates.sh` itself gates (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. | `packages/aeg-core/bin/verify-registry.ts` |
116
+ | 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. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing — neither directory exists — so the live coverage is the seeded `packages/aeg-core/bin/*.ts` set.) | `packages/aeg-core/bin/verify-registry.ts` |
117
+ | G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-0 row above — but the scan's definition of "mutating call" is unchanged.) | `packages/aeg-core/bin/verify-registry.ts` |
114
118
  | G4 — cited forge numbers resolve | Ever read a doc that cited a ticket number that didn't exist? | ci | Re-checks that every Issue and PR number cited in the docs resolves to a real one. | Every `#NNN` cited anywhere in this page's body is a real Issue or PR in the forge — a fabricated citation fails the build. **Currently scans nothing by design** (task 14): this page's body carries zero forge citations, since task 3 banned them from `aeg-root/**` as doctrine and the `reader-resolvable-prose` check (task 15) now enforces that ban directly. G4 is a standing guard against reintroduction, not a live proof of ongoing citation correctness — it fires the moment a `#NNN` reappears here and doesn't resolve, but has nothing to scan while the ban holds. Verified live: `packages/aeg-core/src/registry-checks.test.ts` appends a fabricated citation to this page's real content and asserts `checkG4` catches it. | `packages/aeg-core/bin/verify-registry.ts` |
115
119
  | G5 — role/contract integrity | Ever had a process doc reference a role that was never actually defined? | ci | Re-checks that every role and contract the doctrine references is really defined. | Every `aeg-root/contracts/*.md`'s `producer`/`consumer` names a real `role_id` from `aeg-root/roles/*.md`; every role's `performs`/`refuses_when` frontmatter is present and non-empty. | `packages/aeg-core/bin/verify-registry.ts` |
116
- | reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `aeg-root/**` doc and every public-site `(site)/**/page.tsx`, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes `retired-vocabulary.test.ts` proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `aeg-root/glossary.md`'s own entry headings, never hard-coded. Registered as the fifth check in `coreCheckRegistry()`, so it ships at both ring 0 (`pre-push --diff-only`) and ring 1 (CI `vinaya check --all`) of any repo `vinaya init` installs into; this repo's own `forge-lifecycle.yml` does not yet route through `vinaya check` for any of its five core checks, so today it runs here only on direct invocation (`vinaya check reader-resolvable-prose` / `--all`). | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
120
+ | reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `aeg-root/**` doc and every public-site `(site)/**/page.tsx`, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes `retired-vocabulary.test.ts` proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `aeg-root/glossary.md`'s own entry headings, never hard-coded. NOT registered in `coreCheckRegistry()` its scan hardcodes the attalabs monorepo's own doctrine layout and site paths, a scope-registration decision recorded in `apps/cli/src/checks/registry.ts` so in this repo it runs only on direct invocation (`vinaya check reader-resolvable-prose`); it is part of neither the managed hooks nor `vinaya-checks.yml`. | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
121
+ | No new on-disk state | Ever had a file and the forge disagree about the same fact? | ci | Blocks a diff that creates a new on-disk state file duplicating what the forge already derives. | No live per-tranche topology file appears under `aeg-root/tranches/` (a top-level file there fails on add or edit; under `completed/**` only genuinely NEW files are refused, so the legacy archives stay editable), and no new `*.tokens.md` appears anywhere — token figures live in the PR body, and the ledger row is appended post-merge. Status-aware (`git diff --name-status`), never fired by a deletion; a rename counts as an addition at its new path. The registered `no-disk-state` check runs the same `isNewDiskStateFile` predicate at ring 0 (the managed hooks) and here; this row names the standalone CLI shim of that gate. | `packages/aeg-core/bin/check-no-disk-state.ts` |
117
122
 
118
123
  **G-checks rollout (task 3):** G1/G2 ship report-only this tranche — they can only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surface as visible debt without retroactively failing in-flight work. G3/G4/G5 are blocking from this tranche onward. G1 flips to blocking in a later, separately-dispatched task, once the report-only period has let the orphan backlog get cleaned up.
119
124
 
@@ -133,6 +138,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
133
138
  | **Staleness audits** | Ever had documentation flatly contradict a decision everyone agreed to? | event | Flags documentation that has fallen behind the decisions it is meant to follow. | Dispatched periodically | Documentation whose claims contradict recorded decisions; each contradiction becomes a tracked fix Issue | |
134
139
  | Direct-main-push detection | Ever had someone push straight to main and nobody noticed for a while? | event | Catches pushes that reached main anyway, including from writers the hooks cannot reach. | Every push to `main`, or manual `workflow_dispatch` (task 31) | Whether the pushed commit is introduced by a merged pull request, via the commits→pulls association API. No associated merged PR → opens an incident Issue (`aeg:direct-main-push` label, idempotent per SHA) and **fails the run loudly** — the one ring-2 check in this table that IS allowed to go red, because nothing it does blocks a pull request or a merge; it only makes an already-irreversible direct push visible. **Detection only, never mutation** — no revert, no force-push; permissions are read-only plus `issues: write`, including the `pull-requests: read` the association API requires — as shipped by task 24 that scope was missing, so the job 403'd (`Resource not accessible by integration`) on every real `main` push instead of detecting anything; fixed by task 31 (item 2). | `packages/aeg-core/bin/check-direct-main-push.ts` |
135
140
  | Dead-branch-push audit | Ever kept committing to a branch after its PR had already merged? | event | Catches commits still landing on a branch whose pull request already resolved. | Scheduled (`0 2 * * *`) or manual `workflow_dispatch` | Every remote `task/*` branch whose most recent PR already resolved (`MERGED`/`CLOSED`): flags it when the branch's current tip commit is dated strictly after that resolution — commits kept landing on a branch after the forge considered its work done (the exact "six topology rows landed on a merged plan PR's branch" incident class, 2026-07-03, now caught within a day instead of never). `aeg:dead-branch-push` label plus one idempotent tracking comment on the branch's own PR. **Notification only, never a gate** — `continue-on-error: true`. (2026-07-13: this job previously also ran a stuck row-adjacent-blocker check, `stale-blocker.ts`, retired along with the dispatch-gate predicate it watched for.) | `packages/aeg-core/bin/dead-branch-audit.ts` |
141
+ | Token self-report | Ever seen a cost figure nobody could trace to a source? | event | Reads a role's exact token usage from its own session transcript, feeding the PR-body token report the post-merge ledger row is built from. | By each terminal role at turn-end, before its pull request opens or re-pushes — mandated by the role docs, not hook-fired | Reporting, not enforcement — it refuses no repo action; its refusals guard only its own output. A stale transcript pointer (written by a previous session in a reused worktree) or a transcript yielding zero usage data throws loudly instead of emitting a plausible-looking wrong or zero figure — the misattribution and fabrication classes that would otherwise flow into the PR body and, from there, the post-merge token ledger. | `packages/aeg-core/bin/report-tokens.ts` |
136
142
 
137
143
  ---
138
144
 
@@ -142,7 +148,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
142
148
 
143
149
  **Two gap families:**
144
150
  - **(a) Local-only gates lacking a forge backstop** — a ring-0 mechanism with no ring-1 (CI) or ring-2 (audit) counterpart, so a writer that skips the local hook skips the rule entirely. This task closes three.
145
- - **(b) Manual CLIs lacking a forcing mechanism** — a tool that mechanizes a check (`verify-dispatch`, `verify-task`) but nothing yet *requires* an agent to run it; an agent can simply skip invoking it. **Closed by task 25**: `verify-dispatch` now runs itself on a task branch's first push (`.husky/pre-push`); `verify-task` now runs itself inside `open-pr.ts` for task branches. Both land at ring 0 only (no ring-1/ring-2 backstop yet) — the same accepted, tracked shape as the branch-ID check below, not a new gap this task opened.
151
+ - **(b) Manual CLIs lacking a forcing mechanism** — a tool that mechanizes a check (`verify-dispatch`, `verify-task`) but nothing yet *requires* an agent to run it; an agent can simply skip invoking it. **Closed by task 25**: `verify-dispatch` now runs itself on a task branch's first push (the managed `.git/hooks/pre-push` block); `verify-task` now runs itself inside `open-pr.ts` for task branches. Both land at ring 0 only (no ring-1/ring-2 backstop yet) — the same accepted, tracked shape as the branch-ID check below, not a new gap this task opened.
146
152
 
147
153
  | Enforcement | Ring 0 | Ring 1 | Ring 2 | Paired status | Gap family |
148
154
  |---|---|---|---|---|---|
@@ -156,10 +162,10 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
156
162
  | Brief-section grammar / title convention | ✅ `open-pr.ts`/`open-issue.ts` | ✅ Brief Validation, now a step of the AEG gate suite job (moved out of `archivist.yml` — task 31) | — | Paired (pre-existing) | not a gap |
157
163
  | Planner-rationale **content** — blast radius / brief-content / docs-read | ✅ `open-issue.ts` (A/B/D refuse; C warns) | — (no continuous half yet: the checks read `.aeg/packages` + `projects.md`, which CI has, but no oracle check re-runs them over the live Issue stock) | — | Ring-0 only | **gap: no ring-1 half** (narrowed: the project-registry content check, added alongside A/B/D, does have one — it re-runs inside R1 below) |
158
164
  | Planner-rationale completeness + project-registry resolution (R1) | ✅ `open-issue.ts` — on `edit`, gate applicability now reads the target Issue's **actual forge labels** (`gh issue view --json labels`, unioned with argv; fetch failure = hard refusal), not just this command's own `--label` flags, which are absent on normal body edits (task 36 — every task-Issue body edit previously bypassed the gate silently) | ✅ coherence oracle's R1 — which re-runs the project-registry check too, so an Issue whose `Project:` names an unregistered project is caught even when it was written by a writer ring 0 cannot reach (the web UI, a raw API call) or predates the gate | ✅ `--json`/audit mode | Paired (pre-existing); **edit-path bypass fixed** (task 36) | not a gap |
159
- | `verify-dispatch` (pre-work readiness) | ✅ **new** — `.husky/pre-push` first-push gate (task branch's first push only; fail-open on forge-unreachable) | — | — | **Forced, ring-0 only (this task, item 1)** | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
165
+ | `verify-dispatch` (pre-work readiness) | ✅ **new** — managed `.git/hooks/pre-push` first-push gate (task branch's first push only; fail-open on forge-unreachable) | — | — | **Forced, ring-0 only (this task, item 1)** | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
160
166
  | `verify-task` (pre-PR exit composite) | ✅ **new** — `open-pr.ts` runs it wholesale for task branches (`gatePlanForBranch`); on `edit <n>` the gate set now resolves from the target PR's **actual forge head branch** (`gh pr view --json headRefName`, fetch failure = hard refusal), not the local checkout/`BRANCH` env, which silently skipped `closes-n`/`verify-task` on task-PR body edits from a `main` checkout (task 36 bundled finding) | — | — | **Forced, ring-0 only (this task, item 2)**; **edit-path gate-selection bypass fixed** (task 36) | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
161
167
  | Runtime Test Plan checkbox state (`verify-test-plan`) | n/a (CI-only check) | ✅ now a step of the AEG gate suite job (`verify-test-plan.yml` deleted — task 31; still runs on PR-body `edited` events) — **reads `BRANCH`; parses both the inline `**Test Plan:**` marker and the `## N. Test Plan` heading form** (task 25, item 3) | — | Paired (pre-existing); **parser bug fixed** | n/a — not a coverage gap, a correctness fix (a live-fire gap: heading-form sections were never matched, so a task PR with an unticked `[principal]` box advisory-PASSED) |
162
- | Waiver-label-actor verification (task 29; **extended to `vinaya/waiver:review`, review-gate task 1**) | ✅ **new** — pre-push is warn-only (no `vinaya/waiver:docs`/`vinaya/waiver:review` label can exist before a PR does) **+ tool-layer**: `.claude/hooks/check-forge-gates.sh` denies any agent-session command mutating either label (closes the shared-credential hole — a local agent otherwise acts with the Principal's own `gh` PAT) | ✅ **new** — a GraphQL `LABELED_EVENT`/REST timeline step resolves the label's actor, verified by `isWaiverLabelActorVerified` (now parameterized by label name) and fed into `verify-docs --pr`'s C5 waiver check (`vinaya/waiver:docs`) and `verify-review-gate.ts`'s review check (`vinaya/waiver:review`) | — | **New (this task)**; **extended (review-gate task 1)** | n/a — new mechanism |
168
+ | Waiver-label-actor verification (task 29; **extended to `vinaya/waiver:review`, review-gate task 1**) | ✅ **new** — pre-push is warn-only (no `vinaya/waiver:docs`/`vinaya/waiver:review` label can exist before a PR does) **+ tool-layer (attalabs only)**: the attalabs reference implementation adds a session hook denying any agent-session command that mutates either label; this repo has no tool-interception layer (see the raw-API ring-0 row), so the shared-credential hole is OPEN here — a local agent using the Principal's own `gh` credential can apply a waiver label whose timeline actor then reads as the Principal, and the actor-verification step cannot distinguish it. Until an interception layer exists, waiver labels in this repo rest on credential discipline, not a gate | ✅ **new** — a GraphQL `LABELED_EVENT`/REST timeline step resolves the label's actor, verified by `isWaiverLabelActorVerified` (now parameterized by label name) and fed into `verify-docs --pr`'s C5 waiver check (`vinaya/waiver:docs`) and `verify-review-gate.ts`'s review check (`vinaya/waiver:review`) | — | **New (this task)**; **extended (review-gate task 1)** | n/a — new mechanism |
163
169
  | Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only) |
164
170
 
165
171
  This table describes **installed mechanisms only** (the page's own change-discipline rule, below). The `verify-dispatch`/`verify-task` rows and the `verify-test-plan` fix landed in task 25's own change set, per that rule.
@@ -250,7 +250,7 @@ The test plan is split by who can structurally execute each item:
250
250
 
251
251
  **The merge gate.** A PR is not mergeable while any Test Plan checkbox is unticked — `[agent]` items waiting for evidence, or `[principal]` items waiting for Principal confirmation. An `[agent]` item that fails returns the PR to the Developer on the same branch (same loop as a `CHANGES_REQUESTED` review); a `[principal]` failure does the same. Re-running an item appends a fresh evidence comment; it does not edit the previous one.
252
252
 
253
- **Enforcement note:** the `verify-test-plan` CI check (a checkbox-state parse over the PR body) is **live and mandatory** — `packages/aeg-core/bin/verify-test-plan.ts` runs unconditionally as one of the nine always-produced verdicts of the `aeg-gate-suite` job (`.github/workflows/forge-lifecycle.yml`). It is not opt-in and not decided per-tranche. What remains **trusted discipline** is the judgment the parse cannot make — whether a ticked box was genuinely run. The doctrine (Phase 11 exists; the brief carries a tagged Test Plan; an unticked box means not-yet-mergeable) holds whether the CI enforcer is on or off. Brief Validation rejects a brief that touches a runtime surface and has no tagged Test Plan.
253
+ **Enforcement note:** the `verify-test-plan` CI check (a checkbox-state parse over the PR body) is **live and mandatory** — `packages/aeg-core/bin/verify-test-plan.ts` runs unconditionally in CI on every pull request — in this repo as the `test-plan` check inside the `vinaya check --all --diff-only` job (`.github/workflows/vinaya-checks.yml`); in the attalabs reference implementation, as a step of that repo's consolidated gate job. It is not opt-in and not decided per-tranche. What remains **trusted discipline** is the judgment the parse cannot make — whether a ticked box was genuinely run. The doctrine (Phase 11 exists; the brief carries a tagged Test Plan; an unticked box means not-yet-mergeable) holds whether the CI enforcer is on or off. Brief Validation rejects a brief that touches a runtime surface and has no tagged Test Plan.
254
254
 
255
255
  **Exit:** every `[agent]` Test Plan item has an evidence comment on the PR and is ticked; every `[principal]` item is ticked by the Principal. Or: the brief declared `Test Plan: unit-tests-only` and the §4 surface is pure-logic.
256
256
 
@@ -85,7 +85,7 @@ Before writing any code, validate the following — and refuse if any fails:
85
85
  6. **Branch-ID verification (hard STOP before step 0).** Before executing step 0, confirm via the forge (`vinaya/tranche:<slug>`-labeled Issue titled `[<slug>] <n> — …`, and its Milestone) — not `aeg-root/tranches/<name>.md` — that the branch-name suffix in the Step 0 command you were just handed literal-matches this task's forge-derived id `<n>` — character for character: no added prefix, no case change, no truncation. If it doesn't: *"The Step 0 branch name `task/<tranche>/<X>` doesn't match this task's topology ID `<Y>` — STOP, do not create the worktree/branch; report the mismatch to the Brief Author/Principal rather than silently using either name."* Do not begin work.
86
86
  7. **Row-existence precondition (hard STOP before step 0).** Before executing step 0, confirm via the forge (`vinaya/tranche:<slug>`-labeled Issue titled `[<slug>] <n> — …`, and its Milestone) — not `aeg-root/tranches/<name>.md` — that this task's row exists **at all**. This is distinct from and prior to item 3's `#TBD`/blank check: a missing row means the plan/Issue for this task has not merged/opened yet, and there is nothing to inspect — no Issue, no dependencies, no `Project(s)` value. If the row is absent: STOP: *"Task <id> is not present in tranche `<name>`'s forge-derived task list (no `vinaya/tranche:<name>`-labeled Issue with this task id yet) — the plan/Issue for this task hasn't merged/opened. Not dispatchable until it does."* Do not begin work.
87
87
 
88
- **Mechanized version of items 3, 5, and 7.** Items 3, 5, and 7 above (Issue-existence, prior-tranche-archival, row-existence) are all re-derivable in one command: `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>`, run against a freshly-fetched `origin/main` and the live forge. Run it before step 0. A `NOT READY` result names the exact failing predicate and is the same STOP each item above describes — read the printed blocker rather than re-deriving the fact by hand. The prose above remains the *why* (what each precondition means, and the manual `gh`/`jq` fallback if the tool is ever unavailable); item 6 (branch-ID verification) is a static check against the brief's own Step 0 text, not a mechanized command, and stays manual. Item 4 is superseded and no longer part of this composed check — `checkDispatchReadiness` no longer evaluates it. **This gate now also runs mechanically** (task 25) — `.husky/pre-push` invokes it on a task branch's first push, before its PR exists — but running it yourself before step 0 remains the cheaper, earlier catch: the hook fires only at push time, after you've already done the work.
88
+ **Mechanized version of items 3, 5, and 7.** Items 3, 5, and 7 above (Issue-existence, prior-tranche-archival, row-existence) are all re-derivable in one command: `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>`, run against a freshly-fetched `origin/main` and the live forge. Run it before step 0. A `NOT READY` result names the exact failing predicate and is the same STOP each item above describes — read the printed blocker rather than re-deriving the fact by hand. The prose above remains the *why* (what each precondition means, and the manual `gh`/`jq` fallback if the tool is ever unavailable); item 6 (branch-ID verification) is a static check against the brief's own Step 0 text, not a mechanized command, and stays manual. Item 4 is superseded and no longer part of this composed check — `checkDispatchReadiness` no longer evaluates it. **This gate now also runs mechanically** (task 25) — the managed `.git/hooks/pre-push` block invokes it on a task branch's first push, before its PR exists — but running it yourself before step 0 remains the cheaper, earlier catch: the hook fires only at push time, after you've already done the work.
89
89
 
90
90
  If the brief carries a `Premise:` block, also re-assert it before step 0: `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --premise <body-file>` (the body-file being the dispatched brief text). A failed premise means the surface moved since the brief was authored — STOP and re-dig; see `aeg-root/contracts/brief-developer.md`.
91
91
 
@@ -260,7 +260,7 @@ Once an Issue is assigned (`todo`), a Developer picks it up: reads the rationale
260
260
 
261
261
  ## Step 0 — creating the tranche itself needs no worktree, no PR, no commit
262
262
 
263
- **Creating a Milestone and cutting labeled Issues are forge actions, not repo-file changes — there is nothing to commit.** The old requirement to open a `plan/<tranche>` worktree + PR existed because the topology file was a repo file, and every repo-file change reaches `main` through a worktree branch + PR + green merge, same as a Developer's. That still applies **only if this planning act also writes an actual repo file** — most commonly a spec change. If your plan produces no repo-file change at all (the common case — Milestone + Issues only), skip this section entirely: no worktree, no plan PR, nothing for `.husky` or `check-pr-green.sh` to gate.
263
+ **Creating a Milestone and cutting labeled Issues are forge actions, not repo-file changes — there is nothing to commit.** The old requirement to open a `plan/<tranche>` worktree + PR existed because the topology file was a repo file, and every repo-file change reaches `main` through a worktree branch + PR + green merge, same as a Developer's. That still applies **only if this planning act also writes an actual repo file** — most commonly a spec change. If your plan produces no repo-file change at all (the common case — Milestone + Issues only), skip this section entirely: no worktree, no plan PR, nothing for the git hooks or a merge gate to gate.
264
264
 
265
265
  When a plan **does** write a repo file — a spec change, most often — open it the same way any doc change does: a worktree off the main branch, commit, pull request:
266
266
 
@@ -268,7 +268,7 @@ When a plan **does** write a repo file — a spec change, most often — open it
268
268
  git worktree add .worktrees/plan/<tranche> -b plan/<tranche> origin/main && cd .worktrees/plan/<tranche> && bun install --frozen-lockfile --silent
269
269
  ```
270
270
 
271
- The `.husky/pre-commit` / `pre-push` guards refuse a direct commit or push to `main`, and the merge-gate hook (`.claude/hooks/check-pr-green.sh`) refuses a red merge so this is enforced, not merely asked, for the cases where a file is actually being written.
271
+ In the attalabs reference implementation, pre-commit/pre-push guards refuse a direct commit or push to `main` and a session merge-gate hook refuses a red merge. This repo has neither local guard: a direct push to `main` is detected after the fact by the ring-2 `direct-main-push` audit, and a red merge is held by the required review-gate check plus branch protection — detected-and-gated on the forge side rather than refused locally.
272
272
 
273
273
  **Only one open plan PR per tranche, at a time — mechanically enforced (task 19), for the case where a plan PR exists at all.** Two concurrent plan PRs for the same tranche, each cut from `origin/main` before the other merged, is the race that produced two competing plan PRs for the same tranche. `packages/aeg-core/bin/open-pr.ts`'s single-plan-PR guard (`checkSinglePlanPr`) still refuses outright to open or edit a plan PR whose diff touches a tranche's topology file while another OPEN PR's diff already touches it — relevant now mainly to the historical `completed/*.md` files, since new tranches no longer have a live topology file to race on.
274
274
 
@@ -72,7 +72,7 @@ Principal → Planner → Brief Author → Developer → Reviewer (code + securi
72
72
  - **Tiers**: Tier 0 trivial · Tier 1 implementation · Tier 3 project/roadmap. No Tier 2. When in doubt, Tier 3.
73
73
  - **Decisions** are recorded in the change that makes them — the pull-request body is the durable record. Historical entries sit in a frozen archive outside the harness; nothing reads it and no new entries are added.
74
74
  - **Ratification windows**: 1–2 daily; the Principal resolves Type 1s, Tier 3 merges, `severity:product` escalations.
75
- - **verify-docs** (`packages/aeg-core/bin/verify-docs.ts`, run as a step of the `aeg-gate-suite` job in `.github/workflows/forge-lifecycle.yml` the standalone `verify-docs.yml` workflow was consolidated away) is a real blocking CI gate: changed specs need a `Status:` block, Tier 1+ code needs a doc change, Tier 3 states its reasoning in the pull request.
75
+ - **verify-docs** (`packages/aeg-core/bin/verify-docs.ts`; in this repo it runs at authoring time — `open-pr.ts` / `bun run verify-docs --pr` with its CI counterparts the `brief-shape`/`doc-coverage` checks in `.github/workflows/vinaya-checks.yml`; in the attalabs reference implementation it is a step of that repo's consolidated gate job) is a real blocking gate: changed specs need a `Status:` block, Tier 1+ code needs a doc change, Tier 3 states its reasoning in the pull request.
76
76
 
77
77
  ## 10. The anti-regression rules — never violate
78
78
 
@@ -30,7 +30,7 @@ The Brief Author's stages — name them, and say which you're in:
30
30
 
31
31
  2. **Dig** — the deep pass for the *perishable* detail (current signatures, exact file list, final model pick). Narrate the load-bearing reads: *"Reading `llm.ts` now to get the current vendor-branch shape for the surface map."* If the dig **contradicts** the rationale (boundary moved, sizing broke), STOP and say so — that's a `severity:strategy` escalation back to the Planner, announced, not a silent fix.
32
32
 
33
- **Mechanized pre-authoring gate.** Before beginning the rest of the Dig, run `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>`. It mechanically re-derives the precondition checks below — row-existence, Issue-existence, prior-tranche archival — directly from a freshly-fetched `origin/main` and the live forge, and prints the exact failing predicate by name (plus a leftover-branch verdict and an informational finding-count baseline). A `NOT READY` result is the same STOP this section describes below — read the printed blocker and act on it; do not re-derive the fact by hand. The manual `gh`/`jq` procedures that follow remain as the **why** (what each precondition means, and how to verify it by hand if `verify-dispatch` is ever unavailable) — they are no longer the primary workflow. This exists because four Developer agents independently re-derived, and stopped on, the exact same archival fact from scratch during the 2026-07-02/03 dispatch wave, at real token cost, hours after it first became true — a fact any of these checks answers deterministically in seconds. **This gate now also runs mechanically** — `.husky/pre-push` invokes it on a task branch's first push — but running it yourself here, before the Dig, remains the cheaper, earlier catch: it stops you before any work is spent, not after. (2026-07-13: prior-task archival — the row-adjacency check formerly listed here — was removed from this composed gate; see check (c) below, now superseded.)
33
+ **Mechanized pre-authoring gate.** Before beginning the rest of the Dig, run `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>`. It mechanically re-derives the precondition checks below — row-existence, Issue-existence, prior-tranche archival — directly from a freshly-fetched `origin/main` and the live forge, and prints the exact failing predicate by name (plus a leftover-branch verdict and an informational finding-count baseline). A `NOT READY` result is the same STOP this section describes below — read the printed blocker and act on it; do not re-derive the fact by hand. The manual `gh`/`jq` procedures that follow remain as the **why** (what each precondition means, and how to verify it by hand if `verify-dispatch` is ever unavailable) — they are no longer the primary workflow. This exists because four Developer agents independently re-derived, and stopped on, the exact same archival fact from scratch during the 2026-07-02/03 dispatch wave, at real token cost, hours after it first became true — a fact any of these checks answers deterministically in seconds. **This gate now also runs mechanically** — the managed `.git/hooks/pre-push` block invokes it on a task branch's first push — but running it yourself here, before the Dig, remains the cheaper, earlier catch: it stops you before any work is spent, not after. (2026-07-13: prior-task archival — the row-adjacency check formerly listed here — was removed from this composed gate; see check (c) below, now superseded.)
34
34
 
35
35
  **Read obligation — complete during Dig, before Draft:** As part of the Dig, identify any specs/skills/docs relevant to this task's code surface and read them. This obligation is conditional — if no docs exist for this surface, it is trivially satisfied. The Planner's "Docs to keep coherent" rationale field is the starting point; your own reading may surface additional docs the Planner missed. Then:
36
36
  - (a) Surface in **Context (§2)** what the Developer must know from those docs — explicitly, not by reference ("read X for context" is not surfacing knowledge).
@@ -2948,7 +2948,8 @@ var CheckEntrySchema = z.object({
2948
2948
  args: z.array(z.string()).optional(),
2949
2949
  timeoutMs: z.number().optional(),
2950
2950
  env: z.record(z.string(), EnvEntrySchema).optional(),
2951
- requiresOpenPr: z.boolean().optional()
2951
+ requiresOpenPr: z.boolean().optional(),
2952
+ ownWorkflow: z.boolean().optional()
2952
2953
  }).superRefine((entry2, ctx) => {
2953
2954
  if (!entry2.env)
2954
2955
  return;
@@ -3027,7 +3028,8 @@ var VinayaConfigSchema = z.object({
3027
3028
  checks: z.record(z.string(), CheckEntrySchema).optional(),
3028
3029
  briefSchema: BriefSchemaSchema.optional(),
3029
3030
  managed: ManagedManifestSchema.optional(),
3030
- principals: z.array(z.string()).min(1).optional()
3031
+ principals: z.array(z.string()).min(1).optional(),
3032
+ ci: z.object({ setup: z.string().min(1) }).optional()
3031
3033
  });
3032
3034
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
3033
3035
  var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
@@ -2949,7 +2949,8 @@ var CheckEntrySchema = z.object({
2949
2949
  args: z.array(z.string()).optional(),
2950
2950
  timeoutMs: z.number().optional(),
2951
2951
  env: z.record(z.string(), EnvEntrySchema).optional(),
2952
- requiresOpenPr: z.boolean().optional()
2952
+ requiresOpenPr: z.boolean().optional(),
2953
+ ownWorkflow: z.boolean().optional()
2953
2954
  }).superRefine((entry2, ctx) => {
2954
2955
  if (!entry2.env)
2955
2956
  return;
@@ -3028,7 +3029,8 @@ var VinayaConfigSchema = z.object({
3028
3029
  checks: z.record(z.string(), CheckEntrySchema).optional(),
3029
3030
  briefSchema: BriefSchemaSchema.optional(),
3030
3031
  managed: ManagedManifestSchema.optional(),
3031
- principals: z.array(z.string()).min(1).optional()
3032
+ principals: z.array(z.string()).min(1).optional(),
3033
+ ci: z.object({ setup: z.string().min(1) }).optional()
3032
3034
  });
3033
3035
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
3034
3036
  var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
@@ -2947,7 +2947,8 @@ var CheckEntrySchema = z.object({
2947
2947
  args: z.array(z.string()).optional(),
2948
2948
  timeoutMs: z.number().optional(),
2949
2949
  env: z.record(z.string(), EnvEntrySchema).optional(),
2950
- requiresOpenPr: z.boolean().optional()
2950
+ requiresOpenPr: z.boolean().optional(),
2951
+ ownWorkflow: z.boolean().optional()
2951
2952
  }).superRefine((entry2, ctx) => {
2952
2953
  if (!entry2.env)
2953
2954
  return;
@@ -3026,7 +3027,8 @@ var VinayaConfigSchema = z.object({
3026
3027
  checks: z.record(z.string(), CheckEntrySchema).optional(),
3027
3028
  briefSchema: BriefSchemaSchema.optional(),
3028
3029
  managed: ManagedManifestSchema.optional(),
3029
- principals: z.array(z.string()).min(1).optional()
3030
+ principals: z.array(z.string()).min(1).optional(),
3031
+ ci: z.object({ setup: z.string().min(1) }).optional()
3030
3032
  });
3031
3033
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
3032
3034
  var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");