@attalabs/vinaya 0.12.0 → 0.16.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 +26 -2
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +5 -5
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +2 -2
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +28 -5
- package/dist/checks/bin/check-brief-shape.js +28 -5
- package/dist/checks/bin/check-closes-n.js +28 -5
- package/dist/checks/bin/check-coherence.js +30 -6
- package/dist/checks/bin/check-dead-branch-push.js +28 -5
- package/dist/checks/bin/check-dispatch-readiness.js +30 -6
- package/dist/checks/bin/check-doc-coverage-push.js +30 -6
- package/dist/checks/bin/check-doc-coverage.js +30 -6
- package/dist/checks/bin/check-evidence-fresh.js +28 -5
- package/dist/checks/bin/check-first-push-dispatch.js +30 -6
- package/dist/checks/bin/check-issue-assignment.js +28 -5
- package/dist/checks/bin/check-no-disk-state.js +28 -5
- package/dist/checks/bin/check-reader-resolvable-prose.js +28 -5
- package/dist/checks/bin/check-registry-gates.js +28 -5
- package/dist/checks/bin/check-review-gate.js +33 -8
- package/dist/checks/bin/check-single-plan-pr.js +28 -5
- package/dist/checks/bin/check-test-plan.js +28 -5
- package/dist/index.js +777 -73
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1wsl6ro._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ieu4oi-pp_bz.js → 0ek6qlqhokys0.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1eo32v1tbjy7c.js → 0ywkq74falx-x.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t7yy_s34022p.js → 1s9ixp5nhnevp.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @attalabs/vinaya
|
|
2
2
|
|
|
3
|
-
The `vinaya` bin — Vinaya's npm-distributed CLI, published to the public npm registry as `@attalabs/vinaya`. The installed command is `vinaya`; only the package name carries the scope. This package ships the command router, the hierarchical config loader, the versioned `--json` output envelope, the check engine (`vinaya check` / `vinaya new check`), the install lifecycle (`init` / `doctor` / `upgrade` / `eject`), and validated forge writes (`pr` / `issue`).
|
|
3
|
+
The `vinaya` bin — Vinaya's npm-distributed CLI, published to the public npm registry as `@attalabs/vinaya`. The installed command is `vinaya`; only the package name carries the scope. This package ships the command router, the hierarchical config loader, the versioned `--json` output envelope, the check engine (`vinaya check` / `vinaya new check`), the install lifecycle (`init` / `doctor` / `upgrade` / `eject`), and validated forge writes (`pr` / `issue` / `review post`).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -16,9 +16,10 @@ 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
|
+
| `vinaya doctrine [--role <name>]` | Print the absolute path of the bundled doctrine's front door (`aeg-root/skills/aeg/SKILL.md`) on this machine. The committed root `VINAYA.md` pointer names the package, never a filesystem path — this command is the read-time resolution step it hands the reader. `--role <name>` resolves straight to a specific role's doctrine instead (`aeg-root/roles/<name>.md`, under the same resolved root), validated against the role names actually enumerated under `roles/*.md` at request time — an unknown name fails cleanly and lists the valid names, never a silent bad path. `--json` for the enveloped `{ root, entry }` form. |
|
|
20
20
|
| `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
|
|
21
21
|
| `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
|
|
22
|
+
| `vinaya review post --role code-reviewer \| security --pr <n> ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (verdict, findings, per-field text) instead of a hand-typed comment. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`Judged head:` line from validated inputs, never from caller-supplied text; refuses a contradictory verdict (a BLOCKER/CRITICAL-or-HIGH finding with a clean verdict) before posting anything; and after posting, re-fetches the comment and refuses to exit 0 unless it re-parses through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls. `--json` for the enveloped machine form. |
|
|
22
23
|
| `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. |
|
|
23
24
|
|
|
24
25
|
## Config
|
|
@@ -44,6 +45,25 @@ Today the schema carries one surface:
|
|
|
44
45
|
|
|
45
46
|
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.
|
|
46
47
|
|
|
48
|
+
### Blast-radius collision domains
|
|
49
|
+
|
|
50
|
+
`checkBlastRadiusScope` (the AEG task-Issue gate that refuses an under-declared blast radius) needs a list of shared collision domains — paths that couple work across project boundaries. It works out of the box, with **zero adopter file**:
|
|
51
|
+
|
|
52
|
+
- Every `packages/*` workspace member is derived live at check time — from `package.json`'s `workspaces` array, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo (pnpm does not read `package.json`'s `workspaces` key at all) — a shared package is a collision domain by construction, so this needs no declaration. A leading `!` negates an entry, same as npm/yarn/pnpm's own workspace-glob syntax.
|
|
53
|
+
- A built-in default set covers the common cross-cutting paths by presence-check: whichever lockfile exists (`bun.lock`/`package-lock.json`/`pnpm-lock.yaml`/`yarn.lock`), `turbo.json`, `biome.json`, `tsconfig.json`, `.github/workflows`, `.husky`.
|
|
54
|
+
|
|
55
|
+
To declare a domain beyond those two — a `migrations/` folder, a codegen output directory — add it to `vinaya.config.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"blastRadius": {
|
|
60
|
+
"extraDomains": ["migrations", "packages/generated"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**`.aeg/packages`** (the legacy static collision-domain file some repos still carry) is deprecated but not removed: if present, its entries still add to the derived + built-in set, so upgrading never silently drops coverage. `vinaya doctor` reports it when found, naming exactly which of its entries (if any) aren't already covered by derivation, the built-in defaults, or `blastRadius.extraDomains` — the migration checklist before deleting it.
|
|
66
|
+
|
|
47
67
|
## Where the git hooks live
|
|
48
68
|
|
|
49
69
|
`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.
|
|
@@ -106,6 +126,10 @@ The generated command embeds the member's directory and `bin` path. Both come fr
|
|
|
106
126
|
|
|
107
127
|
Branch protection is worth enabling, and it does not make the generated review gate unbypassable — a required status check is satisfied by a conclusion reported under its name, and under a `pull_request` trigger the workflow definition comes from the PR. That is true whether CI runs the published package or a vendored build; vendoring widens what the PR controls, it does not open the hole. The full statement, including where the shape degrades silently, is in [`specs/self-hosting.md`](./specs/self-hosting.md) in the source repository — it is not part of the published tarball.
|
|
108
128
|
|
|
129
|
+
## Doc-owners coverage
|
|
130
|
+
|
|
131
|
+
`.vinaya/doc-owners` binds code globs to the docs that must change with them; `vinaya check`'s C5 gate enforces that binding against each pull request's own diff — it fires only when a changed file matches a bound glob. That leaves a gap C5 cannot close on its own: a binding whose code was deleted or renamed wholesale matches nothing on any later diff, ever again, and reads as healthy forever. `vinaya doctor` closes it separately — it never fails `vinaya check` — by walking every binding against the repo's full tracked-file list and reporting any whose code glob matches zero tracked files anywhere in the repo, or whose in-repo doc pointer doesn't exist on disk. Report-only, like every other `vinaya doctor` diagnostic: it repoints or removes nothing itself.
|
|
132
|
+
|
|
109
133
|
## Known limits
|
|
110
134
|
|
|
111
135
|
The five core AEG checks (`coherence`, `dispatch-readiness`, and siblings) are bound to the Vinaya development repository — they read governance documents relative to it. Outside a Vinaya workspace, `vinaya check --all` reports those checks as `status: 'error'` rather than crashing.
|
|
@@ -88,7 +88,7 @@ A ready-to-fill skeleton of the full eight-field rationale lives at `aeg-root/te
|
|
|
88
88
|
A task Issue's body must carry all eight fields in one of these two forms. **Canonical implementation:** `packages/aeg-core/src/issue-validation.ts` (`checkIssueRationale`, `isTaskIssueLabelSet`) — the single grammar/parser, consumed at two enforcement points per `aeg-root/enforcement.md`'s ring model:
|
|
89
89
|
|
|
90
90
|
- **Ring 0 (creation gate):** `packages/aeg-core/bin/open-issue.ts` refuses to create or edit a task Issue (any Issue labeled `vinaya/tranche:<slug>`) whose body fails `checkIssueRationale`. **It also refuses on three content checks, which grade what the fields *say* rather than that they exist:**
|
|
91
|
-
- `checkBlastRadiusScope` — if **Boundary** or **Project(s) + blast radius** names a path under a collision domain
|
|
91
|
+
- `checkBlastRadiusScope` — if **Boundary** or **Project(s) + blast radius** names a path under a collision domain that none of the declared projects owns (ownership resolves against `.vinaya/projects.md`), the Issue must declare a second **registered** project or carry a `blast-radius-ack: <why one lens is enough>` line. `Project(s)` drives the review fan-out, so an under-declared blast radius under-governs the change. **Registered is the operative word:** a name with no row in `.vinaya/projects.md` buys no review lens, so it cannot buy the bypass either — the check filters declared names against the registry before counting them. The domain list itself is live-derived (every `packages/*` workspace member — `package.json`’s `workspaces`, or `pnpm-workspace.yaml`’s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks), with a legacy static `.aeg/packages` file (if present) and `vinaya.config.json`'s `blastRadius.extraDomains` both adding to it — see `blast-radius-domains.ts`. Dormant only when none of those sources yields any domain at all, which a real repo essentially never hits.
|
|
92
92
|
- `checkNoBriefContent` — the body must carry no `## References`, `Technical surface map`, `Premise`, `Step 0`, or `Test Plan` section. Those are Brief-Author artifacts; an Issue is not a brief's home (it would go stale before work starts).
|
|
93
93
|
- `checkRationaleNamesDocs` — **Docs to keep coherent** and/or **Traps to avoid** must name at least one concrete doc path (`aeg-root/…`, `.claude/skills/…`, `.claude/rules/…`, `apps/<x>/CLAUDE.md`, `apps/<x>/specs/…`, a repo-level `*.md`). A genuinely doc-less surface uses the explicit `no-doc-surface` sentinel — the same shape as `Test Plan: unit-tests-only`. This is the only read-obligation signal a forge write leaves: the skill-check hook fires on file edits, and cutting an Issue edits no file.
|
|
94
94
|
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -71,13 +71,13 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
71
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
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
|
-
| 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` |
|
|
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 (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any legacy `.aeg/packages`/`vinaya.config.json` `blastRadius.extraDomains` entries) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | `packages/aeg-core/bin/open-issue.ts` |
|
|
75
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
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
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
|
-
| 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.
|
|
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. Every core check in `registry.ts` carries an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@attalabs/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | `apps/cli/src/checks/runner.ts` |
|
|
81
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. **⚠️ BREAKING, live as of the execution flip:** this resolution is now AUTHORITATIVE — `check.ts`'s flat `[...coreCheckRegistry(), ...customSpecs]` concat is gone, so an override REPLACES the core check (it no longer runs alongside it), and `FAIL_CLOSED` refuses the ENTIRE run — exit 1, nothing executes, never core-only and never a partial ruleset. `--plan` and execution read the same `ResolveResult`, so plan-vs-execution agreement is structural rather than maintained. The two grace-period warnings `vinaya check` printed ahead of this flip are retired from check output and live on permanently as `vinaya doctor` diagnostics (the override class `warn`, the rejected-bare-key class `error`), so a refused config — which by definition runs nothing — is still diagnosable. | `apps/cli/src/checks/resolver.ts` |
|
|
82
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
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` |
|
|
@@ -111,7 +111,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
111
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` |
|
|
112
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
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` |
|
|
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` |
|
|
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). **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`. 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` |
|
|
115
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` |
|
|
116
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
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` |
|
|
@@ -160,13 +160,13 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
|
|
|
160
160
|
| Branch-ID must match a topology row | ✅ pre-push hook — delegates to aeg-core (`check-branch-topology.ts` → forge-derived Tranche via `deriveTrancheFromForge`, task 3a; originally `parseTranche`, task 32); formerly a hand-rolled grep fork of the same table-parse | — | — | Ring-0 only | **(a) — still a gap, not addressed by this task** |
|
|
161
161
|
| Code→doc ownership (C5) | ✅ pre-push hook + PR-open gate | ✅ `verify-docs --pr`, now a step of the AEG gate suite job (`verify-docs.yml` deleted — task 31) | — | Paired (pre-existing) | not a gap |
|
|
162
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 |
|
|
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
|
|
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 derive collision domains live from `package.json` + built-in defaults, plus any legacy `.aeg/packages`/`vinaya.config.json` entries, and read `projects.md`, which CI has, but no oracle check re-runs them over the live Issue stock) | — | Ring-0 only | **gap: no ring-1 half** (narrowed: the project-registry content check, added alongside A/B/D, does have one — it re-runs inside R1 below) |
|
|
164
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 |
|
|
165
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) |
|
|
166
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) |
|
|
167
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) |
|
|
168
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 |
|
|
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) |
|
|
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, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
|
|
170
170
|
|
|
171
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.
|
|
172
172
|
|
|
@@ -151,7 +151,7 @@ Decide by **verification-coupling** (not by project boundaries):
|
|
|
151
151
|
|
|
152
152
|
**Enumerate consumers by command, not by memory.** Before finalizing a task's **Project(s) + blast radius**, run the consumer enumeration for every shared package the task's Boundary or Project(s) names, and paste its output into your planning pass — in this repo: `git grep -l '@attalabs/<pkg>' -- '*/package.json'` (substitute your repo's package scope and manifest layout). The pasted list is the floor for the consumer set. A consumer list written from memory is a self-attestation, and the forgotten consumer is exactly the shipped regression this rule exists to stop. (One hit is the package's own manifest — its `name` field matches; every other hit is a consumer.)
|
|
153
153
|
|
|
154
|
-
**Mechanized.** This rule is no longer prose you have to remember. `checkBlastRadiusScope` reads
|
|
154
|
+
**Mechanized.** This rule is no longer prose you have to remember. `checkBlastRadiusScope` reads a collision-domain list derived live from the repo's `packages/*` workspace members (`package.json`'s `workspaces`, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks) — a legacy static `.aeg/packages` file and `vinaya.config.json`'s `blastRadius.extraDomains` both add to it, if present — and refuses a task Issue whose **Boundary** or **Project(s) + blast radius** names a path under a domain none of its declared projects owns (ownership resolves against `.vinaya/projects.md` — a task on `Project: aeg-core` editing `packages/aeg-core` owns its surface and passes). Satisfy it by listing the consumers, which is what this rule asks for anyway; or, when one review lens genuinely suffices, by an explicit `blast-radius-ack: <why>` line, which makes the judgment reviewable instead of silent.
|
|
155
155
|
|
|
156
156
|
**Only registered names count, and only the line-anchored project field is read.** The declared set comes from `projectsFromBody` — the same parser that derives a task's project for the board and dispatch — so the gate cannot disagree with the derivation about what a task declares, and a file path written in the surrounding prose can no longer be mistaken for a project. A listed consumer with no row in `.vinaya/projects.md` buys nothing: it resolves to no specs, no state and no reviewer, so it adds no review lens and does not satisfy the rule above (it is separately refused by `checkProjectsRegistered`). Listing a name you have not registered is not a way past this gate — register the project first.
|
|
157
157
|
|
|
@@ -177,7 +177,7 @@ This rationale is the **producer side of the `aeg-root/contracts/planner-brief.m
|
|
|
177
177
|
- **Durable** (goes in the rationale): why this is one task and not three; the dependency rationale; the sizing conclusion; which shared packages and consumers are in the blast radius; known traps to avoid; the suggested agent-class; stop-and-escalate conditions. These do not change before the task runs.
|
|
178
178
|
- **Perishable** (do NOT put in the rationale — it belongs in the just-in-time brief): exact function signatures, precise file lists, line-level specifics. These go stale as earlier tasks merge, so the Brief Author re-derives them at dispatch.
|
|
179
179
|
|
|
180
|
-
**Required fields in every Planner's rationale block** (these are the contract's producer fields — emit all eight). **Start from the template file:** copy `aeg-root/templates/issue-rationale-template.md` into the Issue body and fill each placeholder — it packages all eight fields in the rationale grammar, so you never reconstruct the shape from prose; the field definitions below remain the source of truth for content. **Write them in the rationale grammar** — either `**<Field>** — …` bold-inline or `### <Field>` heading, one of the two, so the field is machine-detectable. See `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for the full format spec; `verify-coherence`'s R1 check re-runs the same grammar continuously against the live Issue stock, and the ring-0 gate (`bin/open-issue.ts`) refuses a task Issue whose body fails it at creation time. **Beyond presence, the same gate grades three things about the content and refuses on each:** the declared surface may not reach a shared collision domain (`.aeg/packages`) that no declared project owns without a second **registered** project or a `blast-radius-ack:` line — this is the shared-package blast-radius rule below, mechanized; the body may carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content belongs in the brief; and **Docs to keep coherent** / **Traps to avoid** must name a concrete doc or skill path, or the explicit `no-doc-surface` sentinel. That last one exists because nothing else makes you read the surface you are planning: the skill-check hook fires on file edits, and cutting an Issue edits no file. A fourth check warns only — an undeclared collision-domain overlap with a sibling open Issue:
|
|
180
|
+
**Required fields in every Planner's rationale block** (these are the contract's producer fields — emit all eight). **Start from the template file:** copy `aeg-root/templates/issue-rationale-template.md` into the Issue body and fill each placeholder — it packages all eight fields in the rationale grammar, so you never reconstruct the shape from prose; the field definitions below remain the source of truth for content. **Write them in the rationale grammar** — either `**<Field>** — …` bold-inline or `### <Field>` heading, one of the two, so the field is machine-detectable. See `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for the full format spec; `verify-coherence`'s R1 check re-runs the same grammar continuously against the live Issue stock, and the ring-0 gate (`bin/open-issue.ts`) refuses a task Issue whose body fails it at creation time. **Beyond presence, the same gate grades three things about the content and refuses on each:** the declared surface may not reach a shared collision domain (live-derived `packages/*` workspace members + built-in cross-cutting defaults, plus any legacy `.aeg/packages`/`vinaya.config.json` `blastRadius.extraDomains` entries) that no declared project owns without a second **registered** project or a `blast-radius-ack:` line — this is the shared-package blast-radius rule below, mechanized; the body may carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content belongs in the brief; and **Docs to keep coherent** / **Traps to avoid** must name a concrete doc or skill path, or the explicit `no-doc-surface` sentinel. That last one exists because nothing else makes you read the surface you are planning: the skill-check hook fires on file edits, and cutting an Issue edits no file. A fourth check warns only — an undeclared collision-domain overlap with a sibling open Issue:
|
|
181
181
|
- **Boundary** — what this task is and, crucially, what it is *not* (what was deliberately split out).
|
|
182
182
|
- **Sizing** — that it passed the four "too big?" tests (or how a larger candidate was split).
|
|
183
183
|
- **Project(s) + blast radius** — every project touched, and for shared-package changes, which consumers are in the blast radius and whether each needs re-verification or edits.
|
|
@@ -75,7 +75,7 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
75
75
|
4. **Honest tests.** Do the tests prove real behavior, or do they mock the thing under test? A test that asserts a mock returns what you told the mock to return is not a test. Flag it.
|
|
76
76
|
5. **Spot-check code quality** on 2-3 of the most substantive files: clarity, obvious bugs, error handling, dead code, accidental debug/log leftovers, traces of skipped verification hooks.
|
|
77
77
|
6. **Doc coupling.** Tier 1+ work should carry spec/skill updates. If code changed contracts but no docs moved, flag it. (`verify-docs` also gates this in CI — your job is the judgment CI cannot make: are the docs *correct*, not just *present*.) For every doc named in the brief's documentation-update list: if it is absent from the diff, that is a **BLOCKER** (the list is a definition-of-done obligation, not guidance); if it is present but incorrect, that is also a BLOCKER. Check that compliance before reviewing logic. **Coverage of the `.vinaya/doc-owners` bindings is mechanical (`verify-docs` C5).** You no longer carry the "did the right doc move?" cognitive load — CI does. Your job shrinks to **judging correctness of the covered doc**: did the update actually reflect the code change, or is it a no-op edit / a misleading rewrite that silences C5 without reflecting reality? A passing C5 plus an incorrect doc update is a **BLOCKER**. A doc-coverage waiver is no longer a mechanism you weigh: the waiver body-grammar was removed, so a `doc-owners` obligation is deferred only when a principal applies the actor-verified `vinaya/waiver:docs` label — a Developer cannot self-serve it, and there is no body field for you to judge. What is still yours, because no CI gate can check it: whether a published doc reads complete to a stranger who lands on it cold. Hold every doc the brief surfaces to the reader-readability rule — a reader must resolve every symbol on the page from the page itself. A doc update that satisfies C5 mechanically but leaves a sentence leaning on a decision id or bare section number a stranger can't resolve is a MAJOR finding.
|
|
78
|
-
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention. **Before asserting blast-radius coverage is complete** — required whenever the brief lists more than one `Project:`, or the diff touches a path under a shared collision domain (
|
|
78
|
+
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention. **Before asserting blast-radius coverage is complete** — required whenever the brief lists more than one `Project:`, or the diff touches a path under a shared collision domain (live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any legacy `.aeg/packages`/`vinaya.config.json` `blastRadius.extraDomains` entries) even on a single-project brief; see `contracts/planner-brief.md` for the full domain-list derivation — run the consumer check for each touched shared package and quote its output in your review comment: `git grep -l '@attalabs/<pkg>' -- 'package.json' '*/package.json'` (this repo's form — substitute the touched package's published name; the two pathspecs are both needed, since `'*/package.json'` alone skips a repo-root manifest; use `git grep`, not `rg`, which silently skips gitignored doc trees). A consumer list you never generated is a consumer list you guessed. This applies to these two verdict fields only — the other checks keep their existing shape; evidence-on-everything is the "flag everything, get ignored" failure in another costume.
|
|
79
79
|
8. **Register and slop, in any reader-facing prose the diff adds or edits.** Two other defects in this same family — an unresolvable citation, and a coined term used without a definition — are checked mechanically now, not by you. This one still isn't, and it is a checkable property, not a stylistic preference: does a sentence narrate the work episode instead of stating the durable fact ("this fixes the bug from the last review," "I checked every case," a first-person aside) where a stranger reading the page later has no session to place it in? Does a padding adjective ("robust," "seamless," "comprehensive") carry no concrete referent a reader could verify? Flag the sentence and say what's wrong with it, the same way you'd flag a bug — this is not the taste-based-rewrite exclusion below; it fires only on a nameable defect (narration, unearned padding), never on a phrasing you'd merely have chosen differently.
|
|
80
80
|
|
|
81
81
|
## What you do NOT do
|
|
@@ -93,11 +93,22 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
93
93
|
|
|
94
94
|
## Output format
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
**Run `vinaya review post --role code-reviewer` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractCodeReviewVerdict` function the gate calls:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
vinaya review post --role code-reviewer --pr <n> --verdict APPROVE|REQUEST_CHANGES \
|
|
100
|
+
--brief-conformance <text> --spec-conformance <text> \
|
|
101
|
+
--findings-file <path> --scope <text> --tests <text> --docs <text> \
|
|
102
|
+
--task-id <task-id> --model <model> --tokens-in <n|-> --tokens-out <n|-> --cost <text|->
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
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`. Omit `--findings-file` for zero findings. 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):
|
|
97
106
|
|
|
98
107
|
```
|
|
99
108
|
VERDICT: APPROVE | REQUEST CHANGES
|
|
100
109
|
|
|
110
|
+
Judged head: <sha>
|
|
111
|
+
|
|
101
112
|
BRIEF CONFORMANCE: [does it do what the brief asked? 1-2 sentences]
|
|
102
113
|
SPEC CONFORMANCE: [does it agree with the Product spec? "n/a — no Product named" | "clean" | drift listed in findings]
|
|
103
114
|
|
|
@@ -110,6 +121,8 @@ TESTS: [honest | issues listed in findings]
|
|
|
110
121
|
DOCS: [tier-appropriate | missing items listed in findings]
|
|
111
122
|
```
|
|
112
123
|
|
|
124
|
+
`vinaya review post` also refuses before posting anything if you pass a BLOCKER finding together with `--verdict APPROVE` — that contradiction is caught mechanically, not left to review.
|
|
125
|
+
|
|
113
126
|
- **BLOCKER** — must fix before merge (wrong behavior, scope violation, dishonest test, missing required doc, **spec contradiction**).
|
|
114
127
|
- **MAJOR** — should fix before merge (likely bug, weak error handling, **spec drift that isn't an outright contradiction**).
|
|
115
128
|
- **MINOR** — note it; Developer's discretion.
|
|
@@ -126,8 +139,8 @@ If you discover something that needs a decision above review authority — the b
|
|
|
126
139
|
|
|
127
140
|
Phase 10 (Review) in `process.md`. The order is: **code-reviewer pass (you) → security pass (`roles/security.md`) → Principal code review → Brief Author spec review → merge.** Your verdict feeds the human reviews; it does not replace them.
|
|
128
141
|
|
|
129
|
-
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `APPROVE` verdict — `REQUEST CHANGES`, a missing verdict,
|
|
142
|
+
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `APPROVE` verdict that also covers the PR's current head commit (reviewed-commit binding) — `REQUEST CHANGES`, a missing verdict, an unclear one, or one bound to a superseded commit all fail the check and block merge, same as this repo's own security pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
130
143
|
|
|
131
144
|
## Turn-end: report your tokens in the verdict comment
|
|
132
145
|
|
|
133
|
-
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead,
|
|
146
|
+
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead, `vinaya review post`'s `--task-id`/`--model`/`--tokens-in`/`--tokens-out`/`--cost` flags render the closing one-line token report as part of the same posted comment: `Tokens: <task-id>: review — Reviewer — <model> — in/out/cost`. Review normally runs **operator-metered** — on a host that exposes no usage figure to the agent — so pass `-` (a literal hyphen, not this doc's `—`) for `--tokens-in`/`--tokens-out`/`--cost` when unknown; that host capability is the one sanctioned reason for a blank token cell (`tranche-model.md` §12), never inconvenience, and you never estimate. If your host does expose your own usage to you, pass the real figures instead. The per-task Archivist collects this report at close-out and appends the row to the ledger — see `roles/archivist.md`. A re-review (after the Developer pushes fixes) reports again — run `vinaya review post` again rather than editing the prior comment.
|
|
@@ -87,11 +87,22 @@ When the PR touches agent/skill/hook definitions, MCP configs, or anything under
|
|
|
87
87
|
|
|
88
88
|
## Output format
|
|
89
89
|
|
|
90
|
-
The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate.
|
|
90
|
+
**Run `vinaya review post --role security` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractSecurityReviewVerdict` function the gate calls:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
vinaya review post --role security --pr <n> --verdict PASS|FAIL \
|
|
94
|
+
--findings-file <path> --config-scan <text> \
|
|
95
|
+
--secrets <text> --secrets-evidence-file <path> \
|
|
96
|
+
--task-id <task-id> --model <model> --tokens-in <n|-> --tokens-out <n|-> --cost <text|->
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
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. 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):
|
|
91
100
|
|
|
92
101
|
```
|
|
93
102
|
VERDICT: PASS | FAIL
|
|
94
103
|
|
|
104
|
+
Judged head: <sha>
|
|
105
|
+
|
|
95
106
|
FINDINGS (ordered by severity):
|
|
96
107
|
1. [CRITICAL|HIGH|MEDIUM|LOW] <file:line> — <what and why>
|
|
97
108
|
2. ...
|
|
@@ -104,9 +115,9 @@ SECRETS: [none found | listed above, redacted]
|
|
|
104
115
|
- **HIGH** — likely exploitable misconfig or injection surface.
|
|
105
116
|
- **MEDIUM/LOW** — hardening notes.
|
|
106
117
|
|
|
107
|
-
Any CRITICAL or HIGH → VERDICT FAIL. Only MEDIUM/LOW → PASS with notes.
|
|
118
|
+
Any CRITICAL or HIGH → VERDICT FAIL. Only MEDIUM/LOW → PASS with notes. `vinaya review post` refuses before posting anything if you pass a CRITICAL/HIGH finding together with `--verdict PASS` — that contradiction is caught mechanically, not left to review.
|
|
108
119
|
|
|
109
|
-
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.
|
|
120
|
+
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.
|
|
110
121
|
|
|
111
122
|
## Escalation
|
|
112
123
|
|
|
@@ -116,8 +127,8 @@ A security finding that implies a product/architecture decision (e.g., "the whol
|
|
|
116
127
|
|
|
117
128
|
Phase 10 (Review) in `process.md`: code-reviewer pass → **security pass (you)** → Principal code review → Brief Author spec review → merge.
|
|
118
129
|
|
|
119
|
-
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `PASS` verdict — `FAIL`, a missing verdict,
|
|
130
|
+
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `PASS` verdict that also covers the PR's current head commit (reviewed-commit binding) — `FAIL`, a missing verdict, an unclear one, or one bound to a superseded commit all fail the check and block merge, same as the code-reviewer pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
120
131
|
|
|
121
132
|
## Turn-end: report your tokens in the verdict comment
|
|
122
133
|
|
|
123
|
-
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead,
|
|
134
|
+
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead, `vinaya review post`'s `--task-id`/`--model`/`--tokens-in`/`--tokens-out`/`--cost` flags render the closing one-line token report as part of the same posted comment: `Tokens: <task-id>: security — Security — <model> — in/out/cost`. A security pass normally runs **operator-metered** — on a host that exposes no usage figure to the agent — so pass `-` (a literal hyphen, not this doc's `—`) for `--tokens-in`/`--tokens-out`/`--cost` when unknown; that host capability is the one sanctioned reason for a blank token cell (`tranche-model.md` §12), never inconvenience, and you never estimate. If your host does expose your own usage to you, pass the real figures instead. The per-task Archivist collects this report at close-out and appends the row to the ledger — see `roles/archivist.md`. A re-pass after the Developer's fixes reports again — run `vinaya review post` again rather than editing the prior comment.
|
|
@@ -55,7 +55,7 @@ A tranche (`aeg-root/tranches/<name>.md`) is a **thin topology file**: task→Is
|
|
|
55
55
|
|
|
56
56
|
## 6. Conflicts and the two dispatch gates
|
|
57
57
|
|
|
58
|
-
Conflicts are **
|
|
58
|
+
Conflicts are **package-level, and mostly derived, not hand-declared** (collision domains live-derived from `package.json`'s `packages/*` workspace members, plus a built-in cross-cutting default set — a legacy static `.aeg/packages` file or `vinaya.config.json`'s `blastRadius.extraDomains` add anything beyond that) — there is **no dynamic path-overlap scanner**. When unsure two tasks collide, declare the conflict and serialize. Two gates, both forge-answerable with zero stored state:
|
|
59
59
|
- never start a task whose `depends-on` isn't **merged**;
|
|
60
60
|
- never start a task while a `conflicts-with` sibling's **PR is open**.
|
|
61
61
|
|
|
@@ -308,11 +308,11 @@ The lowest-commitment way to run AEG: read-only over a team's existing process.
|
|
|
308
308
|
- **Manifest integrity** — `verify-docs --full` and the coherence oracle validate the doc-owners manifest: pointer existence (M1), glob syntax (M2 advisory), and duplicate globs (M3). There is no decision-number sequencing: the log it validated is gone.
|
|
309
309
|
- **Closes #N gate** — task-branch PRs must declare `Closes #<its-issue>` in the PR body; absence fails CI. Non-task branches bypass this forward direction automatically. Real, installed at `.github/workflows/forge-lifecycle.yml::closes-n-gate`. Same enforcement substrate as above. **Forge-native-tranche fix (`fix/closes-n-forge-native`):** the gate's scoped `loadTrancheFiles(null, onlySlug)` call discovers which tranches exist by listing `aeg-root/tranches/*.md` filenames — a tranche with zero topology file never entered that enumeration, so the forge derivation was never invoked for it despite the tranche already being fully Milestone-capable, and the gate failed every such PR with a misleading "no topology file found." Fixed by falling back to a direct forge derivation (the Milestone-existence check plus the file-and-forge composition, today `indexTrancheMilestonesAsync` + `mergeFileTopology`) when the requested `onlySlug` isn't found via files, gated on an actual open Milestone existing so an unrecognized branch slug still fails honestly — the scoped-path counterpart to `verify-dispatch.ts`'s `otherActiveTrancheSlugs` fix. **Reverse direction (`fix/closes-n-reverse`):** a `Closes #N` that resolves to a real AEG task Issue (title matches the `[<tranche-slug>] <task-id>...` convention and carries a `vinaya/tranche:<slug>` label) now requires the branch to actually be named `task/<tranche-slug>/<task-id>`, regardless of what the branch itself is called — closing the blind spot that let a mis-named branch implement a real task Issue with zero forge-visible status. One batched forge query (`fetchTaskIssueRefs`) resolves each referenced Issue's task identity before `checkClosesN` runs. **Code-span hardening (`fix/closes-gate-hardening`):** both the pre-merge gate (`checkClosesN` in `brief-validation.ts`) and the coherence-side parse (`extractClosesReferences` in `coherence-checks.ts`) now `stripCode` the body before matching the closing keyword, as GitHub's own auto-close parser ignores `Closes #N` inside a code span or fenced block. Inline spans of **any backtick-run length** are covered (`` `x` ``, `` ``x`` ``, …) via CommonMark's `(`+)…\1` matched-run rule — the double-backtick form was a false-green in the first cut (peeled as two empty spans). Fenced blocks are matched by a line scanner that pairs a fence with its own closer by **character and run length**: `~~~` tilde fences, info strings (```` ```js ````), and runs longer than three (```` `````` ````) all strip correctly, and an unclosed fence runs to end of body as GitHub renders it — the earlier `` /```[\s\S]*?```/g `` regex missed all three. The keyword set matches GitHub's own: `close`/`closes`/`closed`, `fix`/`fixes`/`fixed`, `resolve`/`resolves`/`resolved`. Before this, a body whose only `Closes #N` was backticked passed the gate **green** yet merged **without** closing its Issue, then read red on every open PR via A3 `auto-close-misfire`; "verify-docs green" now implies "GitHub will auto-close" for fenced + inline-code forms. 4-space **indented** code blocks are stripped too, conservatively: a ≥4-column-indented run counts as code only when it follows a blank line *and* is not inside a list — since within a list item that indentation is the item's own content indent, which GitHub *does* auto-close (blanking it would be a false-red, the brief's over-strip stop condition). The strip runs on the **whole body, before any region is sliced out of it** — never on a slice. Every rule in it is block-structural (a fence pairs with its own closer; an indented run is code only after a blank line and outside list context), so a fragment strips differently from the same text in place: stripping the sliced `AEG:CLOSES` region blanked an anchor indented inside a list item — list content GitHub *does* auto-close — and the Archivist's `extractIssue` returned no Issue, stranding it on merge exactly as before. Markers are HTML comments and survive the strip, so selecting the region from stripped text loses nothing and subsumes the decoy protection: a decoy anchor inside code never survives to be sliced. The same grammar governs `maskCode`, the index-preserving variant `anchoredRegion` runs to find the `AEG:*` markers themselves — it is **upstream** of every `stripCode` call, so while it stayed on the naive fence/inline regexes a decoy `AEG:CLOSES` anchor inside a tilde fence, a ≥4-backtick fence, a double-backtick span, or an indented block won the region outright and the gate resolved a **wrong** Issue number (worse than the strandings above: the post-merge Archivist's `extractIssue` would explicitly close an unrelated Issue). Both now delegate to one pair of scanners, differing only in what they emit per code line — nothing for `stripCode`, same-length filler for `maskCode` — so a divergence is a compile-level impossibility rather than a convention. This hardens all five anchored fields (`CLOSES`, `PROJECT`, `TIER`, `PREMISE`, `TEST-PLAN`) at once, not just the closing reference. `stripCode` normalises `\r\n`/`\r` to `\n` before any of these scanners run: the fence scanners anchor per line, and JS's `.`/`$` never match `\r`, so a CRLF body opened no fence at all and let a fenced `Closes #N` walk free — the same false-green along a new axis, and it hit exactly the web-UI-authored bodies this CI backstop exists for (HTML normalises textarea newlines to CRLF on submit). The closing-keyword separator is **bounded** (`\s{0,8}:?\s{0,8}`, identical in both parsers): two adjacent unbounded `\s*` groups backtrack quadratically on `closes` + long whitespace + no `#` — ~2.0 s at GitHub's 65,536-char body cap, run twice on the fail path — where the bound is 0.1 ms. **Known residual:** a genuine indented code block *nested inside a list* is therefore left unstripped — the deliberately safe direction of that trade. A `Closes #N` surviving only inside code fails with an actionable message pointing at a bare reference in the `AEG:CLOSES` anchor. `stripCode` is the one shared stripper, exported from `anchored-region.ts` (no duplicated regex).
|
|
310
310
|
- *(The practitioner-facing map of every enforcement mechanism — prevention, detection, audit — lives in `aeg-root/enforcement.md`; this section remains the normative gate registry.)*
|
|
311
|
-
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain
|
|
311
|
+
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain — live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any legacy `.aeg/packages`/`vinaya.config.json` `blastRadius.extraDomains` entries — that no declared project owns, absent a second **registered** project or a `blast-radius-ack:` line), `checkNoBriefContent` (no brief-shaped section in the Issue), `checkRationaleNamesDocs` (the rationale names a concrete doc/skill path, or the `no-doc-surface` sentinel). `checkConflictCompleteness` warns on an undeclared collision-domain overlap and never blocks**). Prevention, not detection: a malformed PR/Issue body is refused at the agent's own tool call and never reaches the forge; the CI gates below re-run the identical aeg-core checks purely as a backstop for non-hook writers. Also enforced at the same layer: title grammar (`checkForgeTitle`, both wrappers), the single-plan-PR guard (`checkSinglePlanPr` — a diff touching a tranche's topology file is refused when another OPEN PR already touches that same tranche's topology file), `gh api` PATCH/curl write-method denies, and a `.husky/pre-push` gate refusing `task/<tranche>/<id>` pushes whose id has no topology row (mechanical). Operational rule: restart running agent sessions after merging hook/settings changes — hooks load at session start. Same mechanism as skill-check enforcement and the T9 merge gate.
|
|
312
312
|
- **Single-plan-PR CI backstop** — `checkSinglePlanPr` (`packages/aeg-core/src/single-plan-pr.ts`) was extracted from `open-pr.ts` (previously private to that wrapper) so the identical predicate could also run forge-side: `.github/workflows/forge-lifecycle.yml::single-plan-pr-gate` re-fetches this PR's touched files plus every other open PR's touched files and fails CI on the same violation the ring-0 hook refuses locally. Closes the gap where a PR opened directly via the GitHub web UI bypasses `open-pr.ts` entirely. No-ops (never fires) for an ordinary task-branch PR, since its diff never touches a tranche topology file. **Dormant in this repo entirely, as of the forge-native cutover:** a plan is now a Milestone plus labeled Issues, and `aeg-root/tranches/` holds no active topology file for a diff to collide over. The guard is kept, not deleted, because it is correct and live for any repo that does keep plans as files — but nothing here can trip it, and a gate that cannot fire should say so rather than read as active protection. One implementation, two enforcement points — no second copy of the predicate.
|
|
313
313
|
- **Coherence oracle (A1/A2/A3/T1/T2/T3/D1/M1/M3)** — `packages/aeg-core/bin/verify-coherence.ts` runs against every PR and genuinely blocks CI — the CLI's own exit code, non-zero on any `fail`-status check, is what the `coherence-gate` job exits with. Failures in A1 (closed-without-merge), A2 (archived-without-provenance), A3 (auto-close-misfire), T1 (phantom-issue-ref), T2 (orphan-task — **plan PRs only, see below**), T3 (tbd-in-active-tranche), D1 (dispatched-on-unmet-deps), M1 (manifest-dangling), and M3 (manifest-duplicate-glob) fail CI. L1–L5 and M2 are advisory (info-only) — every `checkL*` returns `status: info`, so a premature-archive, lifecycle-hygiene, Milestone-attachment-drift, or open-Milestone-all-closed finding is surfaced for a human to investigate but never fails CI. On A1 failures the relevant Issues receive the `vinaya/incoherent` label (Section 14). Real, installed at `.github/workflows/forge-lifecycle.yml::coherence-gate`. Same enforcement substrate as above. **The job's repo-state inputs (topology files, tranche list) are read from a freshly-fetched `origin/main` — not the checkout's `refs/pull/N/merge`, which GitHub materializes lazily and can lag behind main (5+ false-red cycles, 2026-07-03/04). A plan PR's own topology diff still reads from its head ref.**
|
|
314
314
|
- **Planner→Brief rationale completeness (R1)** — the same coherence oracle's **R1** check re-runs `checkIssueRationale` **and `checkProjectsRegistered`** (`packages/aeg-core`) against every open task Issue's body, batched per active tranche alongside T2. A non-grandfathered Issue missing any of the eight `contracts/planner-brief.md` rationale fields — or whose `Project:` field names a project with no row in the project registry — fails CI. Paired with the ring-0 creation gate (`bin/open-issue.ts`, same tool-layer-forge-gates row above) — R1 is the continuous half, the hook is the point-of-creation half; one grammar, two enforcement points (`aeg-root/enforcement.md`). Pre- Issues are grandfathered by explicit Issue number (`R1_GRANDFATHERED_ISSUES`), reported `info`, never blocking — see Section 15b. **Real.** Moves this seam's rationale-completeness half from Trusted (below) to Enforced; the "role doc matches contract prose" half of contract conformance remains Trusted.
|
|
315
|
-
- **Review gate — code-review + security-review verdicts** — `packages/aeg-core/bin/verify-review-gate.ts`, a step of the AEG gate suite job, blocks merge on a task-branch PR unless a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment both exist — `REQUEST CHANGES`, `FAIL`, a missing verdict,
|
|
315
|
+
- **Review gate — code-review + security-review verdicts, bound to the commit they judged** — `packages/aeg-core/bin/verify-review-gate.ts`, a step of the AEG gate suite job, blocks merge on a task-branch PR unless a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment both exist AND both cover the PR's current head commit — `REQUEST CHANGES`, `FAIL`, a missing verdict, an unclear one, or a clean verdict bound to a superseded commit (or carrying no binding at all) all fail CI (reviewed-commit binding). Reuses `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`verdict-extraction.ts`) — the exact detection the post-merge Archivist's provenance assembly already ran, previously advisory-only (a DANGLING note on the merged PR, never a block) — now also parsing a same-comment `Judged head: <sha>` line and comparing it against `gh pr view --json headRefOid`, resolved from GitHub, never an env var or local git. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, parameterized by label — see the waiver-label-actor row above); label presence alone is never sufficient. **Real, installed at `.github/workflows/forge-lifecycle.yml::aeg-gate-suite`.** Closes the gap where task PRs could previously merge with no review pass at all, and the second gap where a clean verdict could still merge stale code because nothing checked which commit it covered. **What remains Trusted, not Enforced: *dispatching* the code-reviewer/security-reviewer subagents in the first place** — see below; this gate only verifies a dispatched review's verdict is clean and current, it cannot make a review happen.
|
|
316
316
|
- **Registry load-bearing checks G3/G4/G5** — `packages/aeg-core/bin/verify-registry.ts`, a step of the AEG gate suite job, blocks merge on: G3 (a file making the exact class of GitHub-mutating call `check-forge-gates.sh` gates — PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes — with no guarding Ring-0 row); G4 (a `#NNN` cited anywhere in `aeg-root/enforcement.md`'s body that does not resolve to a real Issue or PR in the forge); G5 (a contract's `producer`/`consumer` that is not a real `role_id`, or a role with an empty `performs`/`refuses_when`). **G1 (implementation-exists) and G2 (no-orphan-hook/CLI) are report-only this tranche** — they run in the same step and print `info` findings but never affect the exit code, since flipping them to blocking immediately would retroactively fail in-flight work against a pre-existing orphan backlog; G1 flips to blocking in a later, separately-dispatched task. Pure evaluators in `packages/aeg-core/src/registry-checks.ts`, parsing `packages/aeg-core/src/registry-parse.ts` — same shape (pure predicate + thin I/O shim) as `coherence-checks.ts`/`verify-coherence.ts`, deliberately a separate mechanism (different registry: `enforcement.md`'s own ring tables, not the tranche/forge state coherence-checks.ts reads). `registry-parse.ts`'s `GateRow` additionally carries `summary`/`category` and `description`/`spec` — purely descriptive fields consumed by the Vinaya `/how-it-works` page, not read by any G-check; G1–G5's pass/fail behavior is unaffected. `description` is the row's plain-language sentence and is resolved by header name, not column index; `spec` is the enforcement column it must never be confused with (the "what must be true"/"Re-verifies"/"Catches" slot before `implementation`).
|
|
317
317
|
- **Canonical action set** — `packages/aeg-core/src/actions.ts` exports `ACTIONS`, the 10-entry list of every AEG action that either crosses into GitHub (`crosses: 'into-github'`) or hands work across a role-seam contract (`crosses: 'none'`) — 6 crossings plus 4 seam-only actions, with no duplicate id for the two seams already accomplished by a crossing. Pure data (zero I/O, same shape as `waiver-label.ts`); `commit-the-work` is `'none'` because `git commit` is local-only (only `git push`/`publish-the-branch` reaches the forge). G3's crossing-file detection uses the `crosses: 'into-github'` subset; the `DiagramModel` derivation (`deriveDiagramModel`, `packages/aeg-core/src/diagram-model.ts`) now exists and places each action on a ring-0 gate (via `guards` edges) or a role/contract edge (via `performs`/`produces`/`consumes`), consuming this same `ACTIONS` list plus its `CROSSING_KEYWORDS` map (promoted into `actions.ts` alongside `ACTIONS` this task) — so G3's completeness set and the diagram's edge count cannot drift apart. The derivation is pure and takes doctrine through a `DoctrineSource` seam (`packages/aeg-core/src/doctrine-source.ts`, I/O-free; file-backed adapter `createFileDoctrineSource` in `@attalabs/vinaya-sources`), never `aeg-root/` paths directly. `actions.test.ts` cross-checks every `into-github` action against a real Ring-0 gate row in `enforcement.md` and every `performedBy` against a real `role_id`. Each entry also carries a `summary` question and a **required** `description` — required, so a new action cannot be added without one; `deriveDiagramModel` threads both onto the action's node, along with its `crosses`, which a client renderer cannot read off `ACTIONS` itself (importing it drags `node:child_process` into the browser bundle).
|
|
318
318
|
- **Code-owned state machine + label vocabulary** — the same one-list-of-pure-data discipline, applied to status derivation. `packages/aeg-core/src/state-machine-model.ts` exports three parts: `FORGE_FACT_INPUTS` (every `ForgeFacts` field and the GitHub object it is read from), `DERIVED_STATUSES` (the 9-value set; `backlog` stays a member but derivation never emits it inside a tranche), and `DERIVATION_RULES` — the ordered rule list, first match wins, each entry carrying its predicate, its prose, and the reason its position matters. `deriveStatus` (`derive-tranche.ts`) executes that list, so the rules a reader sees rendered and the rules that actually run are the same objects. The list is total (its final rule matches unconditionally), and `state-machine-model.test.ts` pins the order rule-for-rule, proves every derivable status reachable, proves no rule is shadowed by an earlier one, and asserts equivalence with the pre-refactor `if`-chain across all 432 combinations of the facts derivation reads. The label vocabulary lives in `packages/aeg-forge-state/src/labels.ts` (`LABELS` + `AEG_BLOCKED_LABEL`) — in `aeg-forge-state`, not `aeg-core`, because the dependency direction is `aeg-core → aeg-forge-state → aeg-types` and the vocabulary's first consumer, `map-forge-facts.ts`, lives there and cannot import backward. Each label records the single orthogonal fact it carries; there is deliberately no `status:*` label, since status is derived and never written. `state-machine-model.ts` does **not** import that vocabulary: derivation works on `ForgeFacts`, never on label strings, and importing `@attalabs/aeg-forge-state` would drag its `node:child_process` into any browser bundle rendering the model — the same hazard the `ACTIONS` note above records.
|
|
@@ -126,7 +126,7 @@ The `Project(s)` column remains the authoritative blast-radius record per task;
|
|
|
126
126
|
Two tasks conflict if they touch the same **collision domain** and therefore must not run in parallel. The rules, after the panel's correction:
|
|
127
127
|
|
|
128
128
|
- **Conflicts are declared by the Planner** as `conflicts-with` edges in the thin file. Declared, not inferred.
|
|
129
|
-
- **Collision domains are packages**,
|
|
129
|
+
- **Collision domains are packages**, live-derived from the repo's own workspace declaration's `packages/*` members (`package.json`'s `workspaces` array, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) — no file to author or keep in sync. Known cross-cutting collision paths that are detectable by convention — **whichever lockfile exists, monorepo config (`tsconfig.json`/`biome.json`/`turbo.json`), `.github/workflows`, `.husky`** — ship as a built-in default set for the same reason. Paths that couple tasks across package boundaries but have no universal convention — **`migrations/`, codegen outputs (protobuf/GraphQL/OpenAPI)** — are declared as their own collision domains via `vinaya.config.json`'s `blastRadius.extraDomains`. A legacy static `.aeg/packages` file, if a repo already has one, still adds its entries on top of all of the above.
|
|
130
130
|
- **The conflict gate is forge-answerable with zero stored state:** "is a `conflicts-with` sibling's PR currently open?" If yes, don't start. That's it.
|
|
131
131
|
- **There is no dynamic path-overlap check.** (The panel's decisive correction.) Computing "which files is each in-flight task touching right now" would require a live task→changed-files map — exactly the mutable execution state the design eliminates. So it is forbidden (§10).
|
|
132
132
|
|
|
@@ -2368,15 +2368,25 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
|
2368
2368
|
return { ok: true, expectedIssue };
|
|
2369
2369
|
}
|
|
2370
2370
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
2371
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
2372
|
+
function extractHeadSha(comment) {
|
|
2373
|
+
const m = comment.match(HEAD_SHA_PATTERN);
|
|
2374
|
+
return m ? m[1].toLowerCase() : null;
|
|
2375
|
+
}
|
|
2371
2376
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
2372
2377
|
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
2373
2378
|
if (clearHits.length > 0) {
|
|
2374
2379
|
const latest = clearHits[clearHits.length - 1];
|
|
2375
2380
|
const m = latest.match(valuePattern);
|
|
2376
|
-
return {
|
|
2381
|
+
return {
|
|
2382
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
2383
|
+
headSha: extractHeadSha(latest),
|
|
2384
|
+
danglingNote: null
|
|
2385
|
+
};
|
|
2377
2386
|
}
|
|
2378
2387
|
return {
|
|
2379
2388
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
2389
|
+
headSha: null,
|
|
2380
2390
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
2381
2391
|
};
|
|
2382
2392
|
}
|
|
@@ -2390,6 +2400,11 @@ function extractSecurityReviewVerdict(comments) {
|
|
|
2390
2400
|
function isReviewGateExemptBranch(branch) {
|
|
2391
2401
|
return branch.startsWith("plan/");
|
|
2392
2402
|
}
|
|
2403
|
+
function isBoundToHead(extraction, headSha) {
|
|
2404
|
+
if (!extraction.headSha)
|
|
2405
|
+
return false;
|
|
2406
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
2407
|
+
}
|
|
2393
2408
|
function checkReviewGate(input) {
|
|
2394
2409
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2395
2410
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -2412,18 +2427,26 @@ function checkReviewGate(input) {
|
|
|
2412
2427
|
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2413
2428
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2414
2429
|
const securityClean = security.value === "PASS";
|
|
2415
|
-
|
|
2430
|
+
const codeReviewBound = isBoundToHead(codeReview, input.headSha);
|
|
2431
|
+
const securityBound = isBoundToHead(security, input.headSha);
|
|
2432
|
+
if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
|
|
2416
2433
|
return {
|
|
2417
2434
|
verdict: "pass",
|
|
2418
|
-
reason:
|
|
2435
|
+
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
|
|
2419
2436
|
waived: false
|
|
2420
2437
|
};
|
|
2421
2438
|
}
|
|
2422
2439
|
const problems = [];
|
|
2423
|
-
if (!codeReviewClean)
|
|
2440
|
+
if (!codeReviewClean) {
|
|
2424
2441
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
2425
|
-
if (!
|
|
2442
|
+
} else if (!codeReviewBound) {
|
|
2443
|
+
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2444
|
+
}
|
|
2445
|
+
if (!securityClean) {
|
|
2426
2446
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
2447
|
+
} else if (!securityBound) {
|
|
2448
|
+
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2449
|
+
}
|
|
2427
2450
|
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2428
2451
|
return {
|
|
2429
2452
|
verdict: "fail",
|