@attalabs/vinaya 0.9.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 +33 -7
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +7 -7
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/developer.md +29 -5
- package/aeg-root/roles/planner.md +3 -3
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/roles/tranche-archivist.md +1 -1
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +8 -8
- package/aeg-root/templates/pr-report-template.md +8 -4
- package/aeg-root/tranche-model.md +34 -12
- package/dist/checks/bin/check-branch-topology.js +232 -120
- package/dist/checks/bin/check-brief-shape.js +232 -120
- package/dist/checks/bin/check-closes-n.js +232 -120
- package/dist/checks/bin/check-coherence.js +234 -121
- package/dist/checks/bin/check-dead-branch-push.js +232 -120
- package/dist/checks/bin/check-dispatch-readiness.js +234 -121
- package/dist/checks/bin/check-doc-coverage-push.js +261 -130
- package/dist/checks/bin/check-doc-coverage.js +263 -132
- package/dist/checks/bin/check-evidence-fresh.js +3196 -0
- package/dist/checks/bin/check-first-push-dispatch.js +234 -121
- package/dist/checks/bin/check-issue-assignment.js +232 -120
- package/dist/checks/bin/check-no-disk-state.js +232 -120
- package/dist/checks/bin/check-reader-resolvable-prose.js +232 -120
- package/dist/checks/bin/check-registry-gates.js +232 -120
- package/dist/checks/bin/check-review-gate.js +237 -123
- package/dist/checks/bin/check-single-plan-pr.js +232 -120
- package/dist/checks/bin/check-test-plan.js +232 -120
- package/dist/index.js +1232 -262
- 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/required-server-files.json +3 -1
- 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 +3 -3
- 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 +2 -3
- 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]__05t6482._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
- 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]__0rr_lq7._.js +51 -0
- 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]__19jyp3m._.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]__1p19382._.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 +5 -4
- 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/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.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/{3antrj5z0uf_c.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/{3zyedaek9nvr_.js → 0ywkq74falx-x.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/templates/custom-check.template.ts +5 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → 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
|
-
| `vinaya new check <
|
|
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.
|
|
@@ -52,12 +72,12 @@ One thing git cannot version is the config itself: **each fresh clone runs `git
|
|
|
52
72
|
|
|
53
73
|
Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory (husky's `prepare` script owns per-clone wiring), and a repo with its own active raw hooks in `.git/hooks` stays on the legacy append-a-managed-block layout there — re-routing `core.hooksPath` would silently disable the adopter's own hooks. On that legacy layout `vinaya doctor` warns that clones have no hooks, and `vinaya upgrade` migrates to the tracked layout as soon as nothing foreign would be disabled.
|
|
54
74
|
|
|
55
|
-
Custom checks register under `checks`, one entry per check:
|
|
75
|
+
Custom checks register under `checks`, one entry per check. **Every key must be namespaced `<yourname>/<id>`** — exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*`, with `vinaya` reserved as a prefix:
|
|
56
76
|
|
|
57
77
|
```json
|
|
58
78
|
{
|
|
59
79
|
"checks": {
|
|
60
|
-
"my-check": {
|
|
80
|
+
"myteam/my-check": {
|
|
61
81
|
"run": "./scripts/my-check.ts",
|
|
62
82
|
"scope": "diff",
|
|
63
83
|
"include": ["src/**/*.ts"],
|
|
@@ -67,6 +87,8 @@ Custom checks register under `checks`, one entry per check:
|
|
|
67
87
|
}
|
|
68
88
|
```
|
|
69
89
|
|
|
90
|
+
The one exception is a key that exactly matches a **core** check id: that is an override, and it **replaces** the core check — the core one stops running. Anything else — a bare, un-namespaced key matching no core id — is rejected, and `vinaya check` then refuses the **entire** run (exit 1, nothing executes) rather than running a partial ruleset. Note that a prefix alone is not always enough: if the bare name already breaks the segment grammar (`my_check`, `QALint`), it still breaks it after prefixing and needs a real rename. Run `vinaya check --plan` to see exactly how your config resolves before it runs, and `vinaya doctor` to diagnose a config that is being refused.
|
|
91
|
+
|
|
70
92
|
Glob scoping (`include`) is permitted; conditional logic (`if`/`unless`/`except`) is **never** part of this grammar — see the check-contract quick reference below for the full grammar and the error contract every registered `run` executable must honor.
|
|
71
93
|
|
|
72
94
|
## Check contract — quick reference
|
|
@@ -79,7 +101,7 @@ Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.att
|
|
|
79
101
|
- Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
|
|
80
102
|
- Never reach the network unless explicitly declared as an exception (today: none of the custom-check surface; the core `coherence`/`dispatch-readiness` checks are the only declared exceptions).
|
|
81
103
|
|
|
82
|
-
`vinaya new check <
|
|
104
|
+
`vinaya new check <yourname>/<id>` scaffolds a worked, self-contained example that honors this contract out of the box, and prints the exact — namespaced — registration to paste.
|
|
83
105
|
|
|
84
106
|
## JSON output envelope
|
|
85
107
|
|
|
@@ -87,7 +109,7 @@ Every machine-readable (`--json`) output is wrapped in `{ schema: 1, data: ... }
|
|
|
87
109
|
|
|
88
110
|
## Generated CI in a repo that vendors this CLI
|
|
89
111
|
|
|
90
|
-
`init` and `upgrade` normally generate workflows that invoke the published package, `npx --yes @attalabs/vinaya
|
|
112
|
+
`init` and `upgrade` normally generate workflows that invoke the published package at the exact version that generated them, `npx --yes @attalabs/vinaya@<version>` — the same pin the generated git hooks carry, so CI's version is a committed fact rather than whatever the runner happens to resolve. (An unpinned `npx` would not mean "latest". If you declare `ci.setup`, the workflow installs your dependencies first, so a repo carrying this CLI as a devDependency silently runs *that* copy; if you declare no `ci.setup`, no install step is generated and a bare spec resolves whatever the registry serves that day. `vinaya upgrade` re-pins; `vinaya doctor` reports a stale pin as drift.) In a repo whose own `workspaces` include a member named `@attalabs/vinaya`, that invocation cannot work: npm matches the name against the workspace member *before* reading any version spec, resolves that member's `bin`, and execs a file nothing has built — `sh: vinaya: command not found`. Pinning a version does not help, because the name is matched first.
|
|
91
113
|
|
|
92
114
|
So `init` detects that case and generates a different shape for it: install, build the vendored member, and invoke its built `bin` by path. The detection is exact — a workspace member whose `package.json` `name` is `@attalabs/vinaya`, which is precisely the condition npm itself branches on. Every other repo keeps the published invocation unchanged.
|
|
93
115
|
|
|
@@ -104,6 +126,10 @@ The generated command embeds the member's directory and `bin` path. Both come fr
|
|
|
104
126
|
|
|
105
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.
|
|
106
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
|
+
|
|
107
133
|
## Known limits
|
|
108
134
|
|
|
109
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,14 +71,14 @@ 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.
|
|
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.
|
|
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
|
+
| `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` |
|
|
84
84
|
| A task branch's first push (dispatch re-check) | Ever found out mid-push that your task was never dispatchable? | hook | Re-runs the dispatch-readiness gate once, on a task branch's first push, before its pull request exists. | **`check-first-push-dispatch` standalone shim** (wraps the unchanged `verify-dispatch` gate mode — see the dedicated `verify-dispatch` row above; the registered `first-push-dispatch` check classifies the same readiness through the shared evaluator) | On a `task/*/*` branch with no pull request yet, `verify-dispatch`'s `dispatch-readiness:` line reads `READY`. Only that line is read — never the combined exit code, which also folds in leftover-detection and would false-block every push after the first on a task legitimately mid-flight. Fail-open on infrastructure, loudly: a failed `gh auth status` probe (or `verify-dispatch`'s own infra marker) classifies as `UNKNOWN` and the push is allowed; unparseable output refuses — fail loud, never silently allow. Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/check-first-push-dispatch.ts` |
|
|
@@ -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` |
|
|
@@ -138,7 +138,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
138
138
|
| **Staleness audits** | Ever had documentation flatly contradict a decision everyone agreed to? | event | Flags documentation that has fallen behind the decisions it is meant to follow. | Dispatched periodically | Documentation whose claims contradict recorded decisions; each contradiction becomes a tracked fix Issue | |
|
|
139
139
|
| Direct-main-push detection | Ever had someone push straight to main and nobody noticed for a while? | event | Catches pushes that reached main anyway, including from writers the hooks cannot reach. | Every push to `main`, or manual `workflow_dispatch` (task 31) | Whether the pushed commit is introduced by a merged pull request, via the commits→pulls association API. No associated merged PR → opens an incident Issue (`aeg:direct-main-push` label, idempotent per SHA) and **fails the run loudly** — the one ring-2 check in this table that IS allowed to go red, because nothing it does blocks a pull request or a merge; it only makes an already-irreversible direct push visible. **Detection only, never mutation** — no revert, no force-push; permissions are read-only plus `issues: write`, including the `pull-requests: read` the association API requires — as shipped by task 24 that scope was missing, so the job 403'd (`Resource not accessible by integration`) on every real `main` push instead of detecting anything; fixed by task 31 (item 2). | `packages/aeg-core/bin/check-direct-main-push.ts` |
|
|
140
140
|
| Dead-branch-push audit | Ever kept committing to a branch after its PR had already merged? | event | Catches commits still landing on a branch whose pull request already resolved. | Scheduled (`0 2 * * *`) or manual `workflow_dispatch` | Every remote `task/*` branch whose most recent PR already resolved (`MERGED`/`CLOSED`): flags it when the branch's current tip commit is dated strictly after that resolution — commits kept landing on a branch after the forge considered its work done (the exact "six topology rows landed on a merged plan PR's branch" incident class, 2026-07-03, now caught within a day instead of never). `aeg:dead-branch-push` label plus one idempotent tracking comment on the branch's own PR. **Notification only, never a gate** — `continue-on-error: true`. (2026-07-13: this job previously also ran a stuck row-adjacent-blocker check, `stale-blocker.ts`, retired along with the dispatch-gate predicate it watched for.) | `packages/aeg-core/bin/dead-branch-audit.ts` |
|
|
141
|
-
| Token self-report | Ever seen a cost figure nobody could trace to a source? | event |
|
|
141
|
+
| Token self-report (Claude Code collection adapter) | Ever seen a cost figure nobody could trace to a source? | event | Collects a role's exact token usage on **one** host, by reading that host's own session transcript, and emits the line the PR-body token report is built from. The obligation to report tokens is host-agnostic doctrine; this row is only the adapter that satisfies its collection step on Claude Code, never the requirement itself — an adopter on another harness collects by their own means and ships no equivalent of this row. | By each role that is **self-metering** — its host exposes the session's own usage to the agent — at turn-end, before its pull request opens or re-pushes; mandated by the role docs, not hook-fired | Reporting, not enforcement — it refuses no repo action; its refusals guard only its own output. A stale transcript pointer (written by a previous session in a reused worktree) or a transcript yielding zero usage data throws loudly instead of emitting a plausible-looking wrong or zero figure — the misattribution and fabrication classes that would otherwise flow into the PR body and, from there, the post-merge token ledger. | `packages/aeg-core/bin/report-tokens.ts` |
|
|
142
142
|
|
|
143
143
|
---
|
|
144
144
|
|
|
@@ -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
|
|
package/aeg-root/glossary.md
CHANGED
|
@@ -17,6 +17,8 @@ Vinaya uses a handful of words in a specific, non-obvious sense — on the pages
|
|
|
17
17
|
|
|
18
18
|
**Impact tier** — A number (0, 1, or 3) assigned to a task that decides how much documentation, testing and record-keeping it owes before it can merge. A higher number means a bigger blast radius and a stricter checklist.
|
|
19
19
|
|
|
20
|
+
**Operator-metered** — Said of an agent whose tool gives it no way to see how much work it just did, so only a person can supply that figure. Such an agent still files its report at the end of its turn — which task, which role, which model, what date — but leaves the count itself blank rather than guessing. A person may fill the blank in later from whatever usage screen the tool shows them. The opposite is self-metering, defined below.
|
|
21
|
+
|
|
20
22
|
**Provenance** — A short, durable record, left as a comment on a merged pull request, proving the work behind it was actually reviewed and closed out — not just merged. A pull request carrying no such record means that close-out step never happened.
|
|
21
23
|
|
|
22
24
|
**Ratification** — Sign-off, from the person ultimately accountable for the product, on a decision or a piece of work that would be hard to undo. Higher-impact changes wait for a scheduled sign-off window rather than merging the moment they're technically ready.
|
|
@@ -25,6 +27,8 @@ Vinaya uses a handful of words in a specific, non-obvious sense — on the pages
|
|
|
25
27
|
|
|
26
28
|
**Resolution state** — One of three states — `default`, `overridden`, or `additive` — that every check or role ID resolves to. `default` means shipped-and-unmodified; `overridden` means a config entry currently claims that ID and satisfies its contract; `additive` means a wholly new, namespaced entry the shipped product has no opinion on.
|
|
27
29
|
|
|
30
|
+
**Self-metering** — Said of an agent whose tool lets it read, by itself, how much work it just did — so it can report a real figure with nobody watching. How it reads that figure differs from tool to tool, and is the one part of this system that has to be written fresh for each one. An agent that can do this is not allowed to report a blank token count instead. The opposite is operator-metered, defined above.
|
|
31
|
+
|
|
28
32
|
**Seam** — The handoff point between two people or roles, where one produces something — a document, a decision, a record — and the other consumes it. Each seam is written down once, in its own place, so both sides can agree on exactly what crosses it.
|
|
29
33
|
|
|
30
34
|
**Step 0** — The literal first command someone runs before touching any code on a task: the one that creates that task's own isolated copy of the repository and its own branch. Nothing else happens until this command has run.
|
|
@@ -94,7 +94,7 @@ Work through this checklist for the merged task. Confirm each against reality
|
|
|
94
94
|
2. **Docs updated.** The tier-required docs the brief listed actually moved. (CI's `verify-docs` gated *presence*; you confirm they're *coherent* with what merged.)
|
|
95
95
|
3. **Per-project status updated — for every project the task listed.** Per-project operational state is no longer a `state.md` file — it lives on a **pinned GitHub Issue**, one per project, created at the migration (one per project with its own folder; the root ecosystem-wide bucket — every project without one plus cross-project facts — is its own pinned Issue). Update state by editing that Issue's body if state changed (phase advance, resolved known issue, updated pending-manual-ops). A multi-project task updates *every* listed project's pinned Issue. This is the one place you write to per-project state — and note: this is project *status documentation*, not task status (task status stays derived from the forge). (`now.md` no longer exists.)
|
|
96
96
|
4. **`docs-index.md`** updated if files were added, removed, or renamed.
|
|
97
|
-
5. **Token ledger rows recorded.** No role appends its own row on a task branch. **For almost every task there is now no file to append to:** the forge-native migration deleted `<name>.tokens.md` for every active tranche, and `packages/aeg-core/bin/check-no-disk-state.ts` CI-blocks adding a new one anywhere in the repo (tokens live in the PR body, not a committed ledger). Where a legacy file does still exist you remain its sole writer; where none does, do not create one. Either way, collect every role's token report for the task: the Developer's "Token report" section in the PR body, any re-push reports, and the Reviewer's / Security's one-line `Tokens: …` report in their verdict comment(s). For each report found, append one row (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`) — one row per role-turn, including re-entry rows (a second Developer turn, a re-review), and including your own turn (`Phase: <task-id>: archive`, `Role: Archivist`). Use the exact figures a
|
|
97
|
+
5. **Token ledger rows recorded.** No role appends its own row on a task branch. **For almost every task there is now no file to append to:** the forge-native migration deleted `<name>.tokens.md` for every active tranche, and `packages/aeg-core/bin/check-no-disk-state.ts` CI-blocks adding a new one anywhere in the repo (tokens live in the PR body, not a committed ledger). Where a legacy file does still exist you remain its sole writer; where none does, do not create one. Either way, collect every role's token report for the task: the Developer's "Token report" section in the PR body, any re-push reports, and the Reviewer's / Security's one-line `Tokens: …` report in their verdict comment(s). For each report found, append one row (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`) — one row per role-turn, including re-entry rows (a second Developer turn, a re-review), and including your own turn (`Phase: <task-id>: archive`, `Role: Archivist`). Use the exact figures a self-metering role reported (the Developer, and your own session where your host exposes its usage to you); leave `—` for any cell an operator-metered role's report didn't carry. If a role's report is missing entirely (e.g. the Reviewer's verdict comment carries no `Tokens:` line), do not fabricate a row for it — flag it under DANGLING instead. **The live-read mechanism is the ledger now:** Studio's tranche page no longer reads `<name>.tokens.md` to render token totals — it fetches every merged PR on the task's own branch and re-derives the same rows live (`aggregateTaskTokenRows`, `packages/aeg-core/src/parse-token-report.ts`, called from Studio's own forge-reading layer in the repo that hosts it). Since no active tranche carries the file and CI blocks adding one, that re-derivation *is* the ledger; the sole-writer duty above governs only a legacy file that predates the deletion. One real, load-bearing gap in the live mechanism, discovered building it: it can only recover rows from a PR's own body/comments, so it cannot see your own `Phase: <task-id>: archive` row (you have no PR to report it through) or the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task without false-positive cross-task matches — see that file's own docstring). Those two sources therefore have **no durable home today** — that is a known, open gap, not a licence to create a `.tokens.md` for them.
|
|
98
98
|
6. **Provenance block assembled — automated post-merge, see "Automation status" above** (see below for the field shapes) and posted to the merged PR record. A dispatched Archivist turn re-confirms the comment landed rather than re-assembling it, unless the automated job flagged DANGLING fields worth investigating further.
|
|
99
99
|
|
|
100
100
|
## The provenance block
|
|
@@ -164,4 +164,4 @@ You are the last step of the flow (`process.md`) — the close-out that sits und
|
|
|
164
164
|
|
|
165
165
|
## Turn-end: record the ledger rows for every role that turned on this task
|
|
166
166
|
|
|
167
|
-
You are the **sole writer** of `aeg-root/tranches/<name>.tokens.md` for a task branch — no other role appends its own row. **In practice there is usually no such file:** it is deleted for every active tranche and CI blocks creating a new one (item 6 above), so for those tasks the rows live in the forge — the Developer's PR-body "Token report" entries and the
|
|
167
|
+
You are the **sole writer** of `aeg-root/tranches/<name>.tokens.md` for a task branch — no other role appends its own row. **In practice there is usually no such file:** it is deleted for every active tranche and CI blocks creating a new one (item 6 above), so for those tasks the rows live in the forge — the Developer's PR-body "Token report" entries and the reviewing roles' `Tokens: …` verdict lines — and are re-derived on read. Do not create the file to have somewhere to write. Only when a legacy file survives do you append to it; then append one row per role-turn you collected in item 7 above (`Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date`), then append your own turn's row last (`Phase: <task-id>: archive`, `Role: Archivist`). When you run as automation on a host that exposes your session's own usage to you, you are **self-metering** — a role that can read its own figures directly — for your own row: collect them by that host's mechanism and fill your numeric cells with the exact values (on this repo's toolchain that is `bun packages/aeg-core/bin/report-tokens.ts --phase "<task-id>: archive" --role Archivist`, run against your own session; another host is satisfied by its own means). When your host exposes nothing, you are **operator-metered** — the host offers no usage figure to the agent — and you leave those cells `—`, the same rule every other role follows (`tranche-model.md` §12). Every row you record for another role carries whatever that role reported — `—` where the report itself had no numeric figure; you never estimate, and you never fill in another role's cell yourself. Drift cron: as part of the close-out checks, flag any merged task in this tranche that has **no Developer row** for `<task-id>: develop` (the role obligation was missed), and any inline `## Token ledger` section that violates the append-only rule (an existing row was edited rather than a new one appended). The ledger is append-only.
|
|
@@ -107,7 +107,13 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
|
|
|
107
107
|
|
|
108
108
|
**Opening the PR with a complete description.** The PR description must (1) **carry the full brief** — paste it into the PR body; it is the brief's permanent, durable home, and the Reviewer and Archivist read it there; (2) follow the canonical form in [§ PR body — canonical form](#pr-body--canonical-form) below — that section holds the verbatim copy-pasteable template, including the **exact `Tier:` field syntax** the `verify-docs` gate requires; (3) reference the task's Issue (`Closes #N`) so the merge auto-closes it. The description is not optional — the reviews depend on it. Opening the PR is itself the `in-flight → in-review` transition; you write no status field.
|
|
109
109
|
|
|
110
|
-
**Reporting exact tokens in the PR body at turn-end.** You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — no role writes its own ledger row on a task branch, and parallel Developer sessions on different tasks have collided appending to the same shared file. Instead, before opening the PR (and again before each `changes-requested → in-review` re-push), report your exact tokens in the PR body under a **"Token report"** heading: `Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date` with `Phase: <task-id>: develop` and `Role: Developer`.
|
|
110
|
+
**Reporting exact tokens in the PR body at turn-end.** You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — no role writes its own ledger row on a task branch, and parallel Developer sessions on different tasks have collided appending to the same shared file. Instead, before opening the PR (and again before each `changes-requested → in-review` re-push), report your exact tokens in the PR body under a **"Token report"** heading: `Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date` with `Phase: <task-id>: develop` and `Role: Developer`. **That destination and that grammar are the requirement, and they are the same on every agent host.**
|
|
111
|
+
|
|
112
|
+
*How* you obtain the figures is host-specific and is the one part of this obligation that differs by toolchain (`tranche-model.md` §12 calls this layer 2). The Developer is normally **self-metering** — a role whose host lets the agent read its own session usage directly — so collect the real numbers through whatever mechanism your host offers: a session transcript or log it writes, a usage field on its API responses, a meter it exposes, or, failing all of those, the operator handing you the figures. Report **real figures, not `—`**: a blank token cell is sanctioned only for a host that exposes no usage to the agent at all (the Cost cell is always `—`, separately), which is not your situation if any of the above worked. Never estimate. If your host genuinely cannot produce the numbers and no operator can supply them, say so explicitly in the report rather than inventing a plausible one.
|
|
113
|
+
|
|
114
|
+
> **On this repo's toolchain (Claude Code), one command does it:** `bun packages/aeg-core/bin/report-tokens.ts --phase "<task-id>: develop" --role Developer`, which reads the session transcript and emits the line to paste. Pass `--transcript <path>` when you already know which transcript is yours. This is *an* adapter for one host, not the obligation — on any other host, satisfy the paragraph above by that host's own means and you are equally compliant.
|
|
115
|
+
|
|
116
|
+
The per-task Archivist reads this report at close-out and appends the ledger row post-merge — see `roles/archivist.md`. Re-entry (a second turn after `CHANGES_REQUESTED`) adds a **new** "Token report" entry to the PR body — never edits the first.
|
|
111
117
|
|
|
112
118
|
---
|
|
113
119
|
|
|
@@ -122,8 +128,10 @@ This form is **forge-agnostic.** It depends on no GitHub feature, no `.github/PU
|
|
|
122
128
|
```markdown
|
|
123
129
|
## Summary
|
|
124
130
|
|
|
125
|
-
<one paragraph: what shipped
|
|
126
|
-
|
|
131
|
+
<one paragraph: what shipped and the durable why. Links to `Closes #<N>` go
|
|
132
|
+
here. No verification claims — no "typecheck passes", no diff stats, no
|
|
133
|
+
test counts. Those are the Evidence block below, and it is the ONLY sanctioned
|
|
134
|
+
home for them: emitted by `vinaya pr report --write`, never hand-typed.>
|
|
127
135
|
|
|
128
136
|
## Test plan
|
|
129
137
|
|
|
@@ -138,6 +146,15 @@ empty list.>
|
|
|
138
146
|
check — e.g. signing in with Clerk and running a real BYOK audit. The
|
|
139
147
|
Principal runs this in a browser and ticks the box.>
|
|
140
148
|
|
|
149
|
+
## Evidence
|
|
150
|
+
|
|
151
|
+
Run `vinaya pr report --write <body-file>` and commit its output. Do not type
|
|
152
|
+
this block by hand — see [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed).
|
|
153
|
+
|
|
154
|
+
<!-- AEG:EVIDENCE:START -->
|
|
155
|
+
[populated by `vinaya pr report --write` — never edited by hand]
|
|
156
|
+
<!-- AEG:EVIDENCE:END -->
|
|
157
|
+
|
|
141
158
|
## Scope
|
|
142
159
|
|
|
143
160
|
<one-paragraph summary of the blast radius — projects touched, packages
|
|
@@ -151,10 +168,11 @@ field on its own line:>
|
|
|
151
168
|
|
|
152
169
|
| Field | Requirement |
|
|
153
170
|
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
154
|
-
| Summary | One paragraph. Closes the Issue with `Closes #<N>` somewhere in the body.
|
|
171
|
+
| Summary | One paragraph. Closes the Issue with `Closes #<N>` somewhere in the body. No verification claims (typecheck/lint/test/diff-stat output, pass counts) — those belong exclusively in Evidence, below. |
|
|
155
172
|
| Test plan | Every runtime check tagged `[agent]` or `[principal]`. The brief-authoring skill makes this a **required** field — empty plans use `Test Plan: unit-tests-only` as the sentinel. |
|
|
156
173
|
| `[agent]` items | Items the Developer-agent can run end-to-end before opening the PR. Paste the **actual command output**, not a paraphrase. (This is the `[agent]` half of the Verification phase, see `state-machine.md` § Verification.) |
|
|
157
174
|
| `[principal]` items | Items only the Principal can run (auth-gated, vendor-key-dependent, visual). The agent **does not tick these** — the Principal does, after running in a real browser. |
|
|
175
|
+
| Evidence | The `AEG:EVIDENCE` block — emitted by `vinaya pr report --write`, never hand-typed. See [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed). `check-evidence-fresh` refuses a body whose block doesn't match the head it's attached to. |
|
|
158
176
|
| Scope | One paragraph + the Tier field. Ends with `**Tier:** 0 \| 1 \| 3` on its own line. |
|
|
159
177
|
| **Tier syntax** | Exactly `Tier: 0`, `Tier: 1`, `Tier: 3` (plain) — or `**Tier:** 0`, `**Tier:** 1`, `**Tier:** 3` (bold). `Tier 1` (no colon), `Tier-1`, `Tier:1` (no space) are **rejected** by CI. |
|
|
160
178
|
| `Doc-ack:` | Optional. `Doc-ack: <pointer> — <note>` — acknowledges an external (URL) binding in `.vinaya/doc-owners` that fired on this PR. `<pointer>` must exactly match the binding URL. Separator is flexible — em-dash `—`, en-dash `–`, or a plain ASCII hyphen `-` (with surrounding whitespace) are all accepted, so `Doc-ack: <pointer> - <note>` parses identically. **Body field, not a label.** (state-machine.md Section 15) |
|
|
@@ -162,6 +180,12 @@ field on its own line:>
|
|
|
162
180
|
|
|
163
181
|
**What this section is NOT:** not a style guide, not exhaustive PR etiquette. It is the **contract** for the shapes `verify-docs` (C0–C5), Brief Validation, the Verification phase, and the Pre-merge gate all read. Add anything you want beneath the four sections; don't omit or reshape any of them.
|
|
164
182
|
|
|
183
|
+
### Evidence is emitted, never typed
|
|
184
|
+
|
|
185
|
+
The `AEG:EVIDENCE` block is populated by running `vinaya pr report --write <body-file>` — never by hand-typing a diff stat, a test count, or a gate's pass/fail line into the PR body. Regenerate it after your final commit, before opening or editing the PR: `vinaya pr report --write` both runs the real gates (Group B) and recomputes the diff stat (Group A), so its own exit code doubles as the pre-open verification run — a red gate still writes the block (recording the failure honestly) but exits non-zero, so a scripted `--write && open-pr` never carries a failing suite onto the forge.
|
|
186
|
+
|
|
187
|
+
`check-evidence-fresh` (CI) refuses a body whose block doesn't match the head it's attached to — recomputing Group A exactly and checking Group B for staleness. This closes fabrication for **Group A only** (a hand-typed diff stat cannot survive a byte-compare); Group B is checked for freshness, not re-run, so a stale-but-not-fabricated Group B slips past unless the block is also out of date. Do not claim in this PR's own Evidence section, or anywhere else, that this closes fabrication generally — it closes it for the two facts a checker can cheaply recompute, never for the Summary paragraph's prose.
|
|
188
|
+
|
|
165
189
|
---
|
|
166
190
|
|
|
167
191
|
## Documentation is part of every task
|
|
@@ -180,7 +204,7 @@ All of the following must pass before the PR is opened:
|
|
|
180
204
|
- [ ] Code passes lint/format (this repo: `bun run format-and-lint`)
|
|
181
205
|
- [ ] Tests pass if applicable (this repo: `bun test`)
|
|
182
206
|
- [ ] PR description follows the template, carries the brief, and declares `Tier: 0`
|
|
183
|
-
- [ ] "Token report" section in the PR body
|
|
207
|
+
- [ ] "Token report" section in the PR body carrying your turn's real token figures, collected by whatever means your host offers (see the token-reporting section above; on this repo's toolchain, `bin/report-tokens.ts`) — and again on each re-push after `CHANGES_REQUESTED`; the Archivist appends the ledger row post-merge, you do not
|
|
184
208
|
|
|
185
209
|
### Tier 1 checklist
|
|
186
210
|
|
|
@@ -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.
|
|
@@ -295,4 +295,4 @@ AEG defines **task close-out** (the per-task Archivist) and **tranche close-out*
|
|
|
295
295
|
|
|
296
296
|
## Turn-end: report your tokens, don't append them
|
|
297
297
|
|
|
298
|
-
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — self-append was retired for every role. When the planning session closes, report your tokens instead: `Tokens: planning — Planner — <model> — in/out/cost or — if unknown`, in the plan PR body if one exists, or in your planning report to the Principal otherwise.
|
|
298
|
+
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — self-append was retired for every role. When the planning session closes, report your tokens instead: `Tokens: planning — Planner — <model> — in/out/cost or — if unknown`, in the plan PR body if one exists, or in your planning report to the Principal otherwise. Planning normally runs **operator-metered** — on a host that exposes no usage figure to the agent — so report `—` for the numeric cells; 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, report the real figures instead. Re-planning a wave reports again, never edits the prior report. The ledger is append-only.
|
|
@@ -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.
|