@attalabs/vinaya 0.9.0 → 0.12.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 +7 -5
- package/aeg-root/enforcement.md +3 -3
- 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 +1 -1
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/roles/security.md +1 -1
- package/aeg-root/roles/tranche-archivist.md +1 -1
- package/aeg-root/state-machine.md +6 -6
- package/aeg-root/templates/pr-report-template.md +8 -4
- package/aeg-root/tranche-model.md +33 -11
- package/dist/checks/bin/check-branch-topology.js +204 -115
- package/dist/checks/bin/check-brief-shape.js +204 -115
- package/dist/checks/bin/check-closes-n.js +204 -115
- package/dist/checks/bin/check-coherence.js +204 -115
- package/dist/checks/bin/check-dead-branch-push.js +204 -115
- package/dist/checks/bin/check-dispatch-readiness.js +204 -115
- package/dist/checks/bin/check-doc-coverage-push.js +231 -124
- package/dist/checks/bin/check-doc-coverage.js +233 -126
- package/dist/checks/bin/check-evidence-fresh.js +3173 -0
- package/dist/checks/bin/check-first-push-dispatch.js +204 -115
- package/dist/checks/bin/check-issue-assignment.js +204 -115
- package/dist/checks/bin/check-no-disk-state.js +204 -115
- package/dist/checks/bin/check-reader-resolvable-prose.js +204 -115
- package/dist/checks/bin/check-registry-gates.js +204 -115
- package/dist/checks/bin/check-review-gate.js +204 -115
- package/dist/checks/bin/check-single-plan-pr.js +204 -115
- package/dist/checks/bin/check-test-plan.js +204 -115
- package/dist/index.js +487 -221
- 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/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/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]__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]__1wsl6ro._.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/_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/{3zyedaek9nvr_.js → 1eo32v1tbjy7c.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1t7yy_s34022p.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3antrj5z0uf_c.js → 2ieu4oi-pp_bz.js} +1 -1
- 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/2e68sbtdhi5dg.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
18
18
|
| `vinaya version` | Print the installed CLI version (`--json` for the enveloped machine form) |
|
|
19
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. |
|
|
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
22
|
| `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
23
|
|
|
24
24
|
## Config
|
|
@@ -52,12 +52,12 @@ One thing git cannot version is the config itself: **each fresh clone runs `git
|
|
|
52
52
|
|
|
53
53
|
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
54
|
|
|
55
|
-
Custom checks register under `checks`, one entry per check:
|
|
55
|
+
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
56
|
|
|
57
57
|
```json
|
|
58
58
|
{
|
|
59
59
|
"checks": {
|
|
60
|
-
"my-check": {
|
|
60
|
+
"myteam/my-check": {
|
|
61
61
|
"run": "./scripts/my-check.ts",
|
|
62
62
|
"scope": "diff",
|
|
63
63
|
"include": ["src/**/*.ts"],
|
|
@@ -67,6 +67,8 @@ Custom checks register under `checks`, one entry per check:
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
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.
|
|
71
|
+
|
|
70
72
|
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
73
|
|
|
72
74
|
## Check contract — quick reference
|
|
@@ -79,7 +81,7 @@ Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.att
|
|
|
79
81
|
- Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
|
|
80
82
|
- 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
83
|
|
|
82
|
-
`vinaya new check <
|
|
84
|
+
`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
85
|
|
|
84
86
|
## JSON output envelope
|
|
85
87
|
|
|
@@ -87,7 +89,7 @@ Every machine-readable (`--json`) output is wrapped in `{ schema: 1, data: ... }
|
|
|
87
89
|
|
|
88
90
|
## Generated CI in a repo that vendors this CLI
|
|
89
91
|
|
|
90
|
-
`init` and `upgrade` normally generate workflows that invoke the published package, `npx --yes @attalabs/vinaya
|
|
92
|
+
`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
93
|
|
|
92
94
|
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
95
|
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -77,8 +77,8 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
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. 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. All 16 core checks in `registry.ts` carry an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@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` |
|
|
@@ -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
|
|
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
|
|
|
@@ -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.
|
|
@@ -130,4 +130,4 @@ Phase 10 (Review) in `process.md`. The order is: **code-reviewer pass (you) →
|
|
|
130
130
|
|
|
131
131
|
## Turn-end: report your tokens in the verdict comment
|
|
132
132
|
|
|
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, close your verdict comment with a one-line token report: `Tokens: <task-id>: review — Reviewer — <model> — in/out/cost or — if unknown`.
|
|
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, close your verdict comment with a one-line token report: `Tokens: <task-id>: review — Reviewer — <model> — in/out/cost or — if unknown`. Review 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. 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, never edits the prior report.
|
|
@@ -120,4 +120,4 @@ Phase 10 (Review) in `process.md`: code-reviewer pass → **security pass (you)*
|
|
|
120
120
|
|
|
121
121
|
## Turn-end: report your tokens in the verdict comment
|
|
122
122
|
|
|
123
|
-
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead, close your verdict comment with a one-line token report: `Tokens: <task-id>: security — Security — <model> — in/out/cost or — if unknown`.
|
|
123
|
+
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead, close your verdict comment with a one-line token report: `Tokens: <task-id>: security — Security — <model> — in/out/cost or — if unknown`. A security pass 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. The per-task Archivist collects this report at close-out and appends the row to the ledger — see `roles/archivist.md`. A re-pass after the Developer's fixes reports again, never edits the prior report.
|
|
@@ -227,7 +227,7 @@ The token ledger lives on the forge, not a central file: post the `tranche-close
|
|
|
227
227
|
|
|
228
228
|
**Legacy exception:** if this tranche still has a pre-cutover `<name>.tokens.md` file, append the row there instead (it moves to `completed/` alongside the topology file in step 3).
|
|
229
229
|
|
|
230
|
-
When you
|
|
230
|
+
When you are **self-metering** — your host exposes your session's own usage to you — fill the numeric cells with the exact figures it reports. When you are **operator-metered** — the host exposes no usage to the agent — leave them `—` and the Principal fills them later (`tranche-model.md` §12).
|
|
231
231
|
|
|
232
232
|
---
|
|
233
233
|
|
|
@@ -134,20 +134,20 @@ Rows = artifact types. Columns = roles. "—" means no authority. The Reviewer i
|
|
|
134
134
|
| **Forge labels** (the Section 14 vocabulary) | Applies `vinaya/override:docs` (Principal-only) | Applies `vinaya/tier:*` (Planner, at cut) + `vinaya/needs:*-input` / `vinaya/blocked` (by hand or via automation) | Applies `vinaya/needs:*-input` / `vinaya/blocked` (by hand or via automation) | Applies `vinaya/needs:brief-correction`; asserts `vinaya/tier:*` label == PR-body `Tier:` (drift cron) |
|
|
135
135
|
| **Task status** | — | — | — | — *(nobody writes it — derived from the forge)* |
|
|
136
136
|
| **Provenance block** (on the merged PR) | Reads (audit) | Reads (audit) | — | Assembles + posts at close-out (append-only; from frozen facts) |
|
|
137
|
-
| **Token ledger** (`tranches/<name>.tokens.md`) | Fills a previously-`—`
|
|
137
|
+
| **Token ledger** (`tranches/<name>.tokens.md`) | Fills a previously-`—` cell left by an operator-metered role, from whatever usage view its host shows a human (forward-reference exception, §13); approves PR | Reports tokens at turn-end, does not append — Planner mode in the plan PR (or planning report if none), Brief Author mode in its report | Reports exact tokens in the PR body ("Token report" section) at PR open + one per re-push, collected through whatever mechanism its host exposes (`tranche-model.md` §12); does not append the row itself | **Sole writer** of the ledger — collects every role's token report for the task and appends all rows at close-out; flags missing-row drift |
|
|
138
138
|
| **Test Plan execution** (Verification phase, on the open PR) | Runs the `[principal]` half (auth-gated/key-dependent/visual items) in a browser and ticks those checkboxes on the PR | — | Runs the `[agent]` half (scriptable items) against the booted app and posts evidence comments on the PR; reports `[agent]` failures back to itself via new commits on the same branch | Flags an unticked checkbox / missing evidence comment as a merge-gate failure at close-out time (won't happen if discipline holds — merge is gated on ticked boxes) |
|
|
139
139
|
| **Worktrees** | Removes after merge | — | Works in (created at dispatch) | Flags merged worktrees as cleanup candidates |
|
|
140
140
|
| **Orchestration-tool runtime** (if used) | Edits config; reads (audit) | Reads | Appends events via the tool | — |
|
|
141
141
|
| **CI/forge Actions** | Approves workflow changes via PR | Proposes workflow changes via PR | — | Runs as forge-CI automation |
|
|
142
142
|
|
|
143
|
-
**Token ledger — a second read path:** the table row above still governs who *writes* `tranches/<name>.tokens.md` (the Archivist, sole writer) — that is unchanged. AEG Studio's tranche page, however, no longer *reads* that file to render totals: it re-derives the Developer/Reviewer/Security rows live from the task's own merged PR(s) (`packages/aeg-core/src/parse-token-report.ts`,
|
|
143
|
+
**Token ledger — a second read path:** the table row above still governs who *writes* `tranches/<name>.tokens.md` (the Archivist, sole writer) — that is unchanged. AEG Studio's tranche page, however, no longer *reads* that file to render totals: it re-derives the Developer/Reviewer/Security rows live from the task's own merged PR(s) (`aggregateTaskTokenRows`, `packages/aeg-core/src/parse-token-report.ts`, called from Studio's own forge-reading layer in the repo that hosts it) — narrower than the file (it cannot recover the Archivist's own row or the Planner's report; see `tranche-model.md` §12). The file is not deleted and remains the durable, complete record.
|
|
144
144
|
|
|
145
145
|
### Reviewer & Security review authority (extended)
|
|
146
146
|
|
|
147
147
|
The Reviewer role has two specializations — code review (`roles/reviewer.md`) and security review (`roles/security.md`) — and one narrow authority profile:
|
|
148
148
|
|
|
149
149
|
- **Read:** all Class 1 (repo) and Class 2 (forge) artifacts, plus the brief (in the PR body) and the PR diff. **Including the `Project:` spec(s) in `apps/*/specs/`** — the code Reviewer checks the diff for **spec-conformance**, not only brief-conformance: a diff can satisfy its brief and still contradict or drift from the project's specced behavior, and catching that gap is the Reviewer's job. A spec **contradiction** is a BLOCKER; **drift** is a MAJOR finding; if the diff is right but the spec is stale, that is a `severity:strategy` escalation, not a failure. This adds **no new persistent artifact** — it reads the project spec that already exists. Always read-only on canonical artifacts.
|
|
150
|
-
- **Write:** PR review verdicts and review comments only (a Class 2 object) — **nothing to disk**. The verdict is the structured block in the role doc (`APPROVE | REQUEST CHANGES` for code, with a `SPEC CONFORMANCE` line; `PASS | FAIL` for security). A REQUEST CHANGES sets the PR's review decision, which is the derived `changes-requested` status — the Reviewer writes no status field. **Plus a one-line token report** in the same verdict comment — numeric cells `—`
|
|
150
|
+
- **Write:** PR review verdicts and review comments only (a Class 2 object) — **nothing to disk**. The verdict is the structured block in the role doc (`APPROVE | REQUEST CHANGES` for code, with a `SPEC CONFORMANCE` line; `PASS | FAIL` for security). A REQUEST CHANGES sets the PR's review decision, which is the derived `changes-requested` status — the Reviewer writes no status field. **Plus a one-line token report** in the same verdict comment — numeric cells `—` when the role is **operator-metered** — the host exposes no usage figure to the agent — which is the reviewing role's usual case and the one sanctioned reason for a blank token cell (`tranche-model.md` §12). The per-task Archivist reads this report and appends the ledger row (`tranches/<name>.tokens.md`) at close-out (§13 append-only artifacts; `tranche-model.md` §12).
|
|
151
151
|
- **Cannot:** edit code, specs, skills, PM docs; mutate labels; or merge. The Reviewer reports; the Developer remediates; the Principal merges.
|
|
152
152
|
- **Independence:** fresh context (a separate invocation), never reviewing work it authored. This is the whole point.
|
|
153
153
|
- **Escalation:** a finding that exceeds review authority is marked `[ESCALATE] severity:strategy|product` and routed to the Planner or Principal.
|
|
@@ -306,7 +306,7 @@ The lowest-commitment way to run AEG: read-only over a team's existing process.
|
|
|
306
306
|
- **Brief validation** — the Archivist's `brief-validation` job runs `packages/aeg-core/bin/verify-brief.ts` against the PR body and checks presence of every required brief section (Tier, `For:` model attribution, `Project:` — both read from the PR body's header block, before the first `##` heading, the same region the post-merge Archivist's provenance assembly reads — tagged Test Plan, surface map, doc-update list, worktree Step 0, stop conditions, autonomy clause, `Closes #N`); flags malformed briefs (`vinaya/needs:brief-correction`) and fails CI. **The trigger is the body, not the branch (`fix/brief-gate-nontask`):** the gate runs when the branch is `task/<tranche>/<n>` **or** the body is brief-shaped (`isBriefShaped` — ≥2 of surface map / doc-update list / stop conditions / autonomy clause, matched on `stripCode`'d text so a brief *quoted inside a fence* stays exempt). Bodies that are neither still bypass — that exemption is for an ordinary non-AEG PR (a one-line dependency bump) which carries no brief and must not be forced to grow one. The old branch-only bypass was the wrong proxy for it: a standalone `fix/*` brief **is** a brief, and it skipped every section check — confirmed live on `fix/studio-tranche-href`, which shipped with no §7 documentation-update list because `checkDocUpdateList`, the checker that exists for exactly that, never ran. `Closes #N` remains **task-branch-only** (`BriefSectionsOptions.requireClosesN`): a standalone fix brief has no task Issue to close, and a `plan/*` PR is *forbidden* to carry one by the plan-PR guard below — requiring it there would make the two gates jointly unsatisfiable. The same validator also runs at **authoring time** via `bun packages/aeg-core/bin/verify-brief.ts --body-file <brief.md>`, before a PR or even a branch exists, so a Brief Author gates a brief before dispatch rather than after the Developer has done the work (`skills/brief-authoring/SKILL.md` protocol step 4); with no `BRANCH`, the branch is read from the brief's own Step 0 `git worktree add … -b` line. **Includes the plan-PR Closes guard**: a `plan/*` branch whose PR body carries `Closes #N` fails CI before the non-task bypass even runs — a plan PR creates Issues, it does not resolve one (`roles/planner.md`); this closed a confirmed pattern of plan PRs prematurely closing task Issues on merge. **Real**, now the `Brief Validation` step (9/9) of the AEG gate suite job in `.github/workflows/forge-lifecycle.yml`. Presence-only — it cannot judge whether a Test Plan item is truly scriptable or whether a declared `unit-tests-only` is justified by the surface map; those remain Reviewer + Verification judgment. Same enforcement substrate as above.
|
|
307
307
|
- **Per-task Archivist close-out** — `.github/workflows/archivist.yml::post-merge` (`packages/aeg-core/bin/archive-task.ts`) runs on every push to `main`, resolves the merge commit's PR via `gh api repos/{owner}/{repo}/commits/{sha}/pulls`, and — for task-branch PRs only — assembles and posts the Archivist provenance block (`roles/archivist.md` item 8) and explicitly closes the task's Issue, confirming the closed state before exiting. Idempotent **per PR**, not per Issue (an Issue can legitimately accrue multiple merged PRs over its life; idempotency never spans PRs): skips silently if the PR already carries a provenance comment. Skips silently on non-task branches (`plan/*`, `fix/*`, …). Fails loud (non-zero exit) on any `gh`/permission error — never a silent no-op. Real, installed with `contents: read`, `issues: write`, `pull-requests: write` permissions. Does not automate the Tranche Archivist or items 2–7 of per-task close-out (docs coherence, per-project state, `docs-index.md`, token ledger) — those remain dispatched-Archivist judgment work.
|
|
308
308
|
- **Manifest integrity** — `verify-docs --full` and the coherence oracle validate the doc-owners manifest: pointer existence (M1), glob syntax (M2 advisory), and duplicate globs (M3). There is no decision-number sequencing: the log it validated is gone.
|
|
309
|
-
- **Closes #N gate** — task-branch PRs must declare `Closes #<its-issue>` in the PR body; absence fails CI. Non-task branches bypass this forward direction automatically. Real, installed at `.github/workflows/forge-lifecycle.yml::closes-n-gate`. Same enforcement substrate as above. **Forge-native-tranche fix (`fix/closes-n-forge-native`):** the gate's scoped `loadTrancheFiles(null, onlySlug)` call discovers which tranches exist by listing `aeg-root/tranches/*.md` filenames — a tranche with zero topology file never entered that enumeration, so
|
|
309
|
+
- **Closes #N gate** — task-branch PRs must declare `Closes #<its-issue>` in the PR body; absence fails CI. Non-task branches bypass this forward direction automatically. Real, installed at `.github/workflows/forge-lifecycle.yml::closes-n-gate`. Same enforcement substrate as above. **Forge-native-tranche fix (`fix/closes-n-forge-native`):** the gate's scoped `loadTrancheFiles(null, onlySlug)` call discovers which tranches exist by listing `aeg-root/tranches/*.md` filenames — a tranche with zero topology file never entered that enumeration, so the forge derivation was never invoked for it despite the tranche already being fully Milestone-capable, and the gate failed every such PR with a misleading "no topology file found." Fixed by falling back to a direct forge derivation (the Milestone-existence check plus the file-and-forge composition, today `indexTrancheMilestonesAsync` + `mergeFileTopology`) when the requested `onlySlug` isn't found via files, gated on an actual open Milestone existing so an unrecognized branch slug still fails honestly — the scoped-path counterpart to `verify-dispatch.ts`'s `otherActiveTrancheSlugs` fix. **Reverse direction (`fix/closes-n-reverse`):** a `Closes #N` that resolves to a real AEG task Issue (title matches the `[<tranche-slug>] <task-id>...` convention and carries a `vinaya/tranche:<slug>` label) now requires the branch to actually be named `task/<tranche-slug>/<task-id>`, regardless of what the branch itself is called — closing the blind spot that let a mis-named branch implement a real task Issue with zero forge-visible status. One batched forge query (`fetchTaskIssueRefs`) resolves each referenced Issue's task identity before `checkClosesN` runs. **Code-span hardening (`fix/closes-gate-hardening`):** both the pre-merge gate (`checkClosesN` in `brief-validation.ts`) and the coherence-side parse (`extractClosesReferences` in `coherence-checks.ts`) now `stripCode` the body before matching the closing keyword, as GitHub's own auto-close parser ignores `Closes #N` inside a code span or fenced block. Inline spans of **any backtick-run length** are covered (`` `x` ``, `` ``x`` ``, …) via CommonMark's `(`+)…\1` matched-run rule — the double-backtick form was a false-green in the first cut (peeled as two empty spans). Fenced blocks are matched by a line scanner that pairs a fence with its own closer by **character and run length**: `~~~` tilde fences, info strings (```` ```js ````), and runs longer than three (```` `````` ````) all strip correctly, and an unclosed fence runs to end of body as GitHub renders it — the earlier `` /```[\s\S]*?```/g `` regex missed all three. The keyword set matches GitHub's own: `close`/`closes`/`closed`, `fix`/`fixes`/`fixed`, `resolve`/`resolves`/`resolved`. Before this, a body whose only `Closes #N` was backticked passed the gate **green** yet merged **without** closing its Issue, then read red on every open PR via A3 `auto-close-misfire`; "verify-docs green" now implies "GitHub will auto-close" for fenced + inline-code forms. 4-space **indented** code blocks are stripped too, conservatively: a ≥4-column-indented run counts as code only when it follows a blank line *and* is not inside a list — since within a list item that indentation is the item's own content indent, which GitHub *does* auto-close (blanking it would be a false-red, the brief's over-strip stop condition). The strip runs on the **whole body, before any region is sliced out of it** — never on a slice. Every rule in it is block-structural (a fence pairs with its own closer; an indented run is code only after a blank line and outside list context), so a fragment strips differently from the same text in place: stripping the sliced `AEG:CLOSES` region blanked an anchor indented inside a list item — list content GitHub *does* auto-close — and the Archivist's `extractIssue` returned no Issue, stranding it on merge exactly as before. Markers are HTML comments and survive the strip, so selecting the region from stripped text loses nothing and subsumes the decoy protection: a decoy anchor inside code never survives to be sliced. The same grammar governs `maskCode`, the index-preserving variant `anchoredRegion` runs to find the `AEG:*` markers themselves — it is **upstream** of every `stripCode` call, so while it stayed on the naive fence/inline regexes a decoy `AEG:CLOSES` anchor inside a tilde fence, a ≥4-backtick fence, a double-backtick span, or an indented block won the region outright and the gate resolved a **wrong** Issue number (worse than the strandings above: the post-merge Archivist's `extractIssue` would explicitly close an unrelated Issue). Both now delegate to one pair of scanners, differing only in what they emit per code line — nothing for `stripCode`, same-length filler for `maskCode` — so a divergence is a compile-level impossibility rather than a convention. This hardens all five anchored fields (`CLOSES`, `PROJECT`, `TIER`, `PREMISE`, `TEST-PLAN`) at once, not just the closing reference. `stripCode` normalises `\r\n`/`\r` to `\n` before any of these scanners run: the fence scanners anchor per line, and JS's `.`/`$` never match `\r`, so a CRLF body opened no fence at all and let a fenced `Closes #N` walk free — the same false-green along a new axis, and it hit exactly the web-UI-authored bodies this CI backstop exists for (HTML normalises textarea newlines to CRLF on submit). The closing-keyword separator is **bounded** (`\s{0,8}:?\s{0,8}`, identical in both parsers): two adjacent unbounded `\s*` groups backtrack quadratically on `closes` + long whitespace + no `#` — ~2.0 s at GitHub's 65,536-char body cap, run twice on the fail path — where the bound is 0.1 ms. **Known residual:** a genuine indented code block *nested inside a list* is therefore left unstripped — the deliberately safe direction of that trade. A `Closes #N` surviving only inside code fails with an actionable message pointing at a bare reference in the `AEG:CLOSES` anchor. `stripCode` is the one shared stripper, exported from `anchored-region.ts` (no duplicated regex).
|
|
310
310
|
- *(The practitioner-facing map of every enforcement mechanism — prevention, detection, audit — lives in `aeg-root/enforcement.md`; this section remains the normative gate registry.)*
|
|
311
311
|
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain from `.aeg/packages` that no declared project owns, absent a second **registered** project or a `blast-radius-ack:` line), `checkNoBriefContent` (no brief-shaped section in the Issue), `checkRationaleNamesDocs` (the rationale names a concrete doc/skill path, or the `no-doc-surface` sentinel). `checkConflictCompleteness` warns on an undeclared collision-domain overlap and never blocks**). Prevention, not detection: a malformed PR/Issue body is refused at the agent's own tool call and never reaches the forge; the CI gates below re-run the identical aeg-core checks purely as a backstop for non-hook writers. Also enforced at the same layer: title grammar (`checkForgeTitle`, both wrappers), the single-plan-PR guard (`checkSinglePlanPr` — a diff touching a tranche's topology file is refused when another OPEN PR already touches that same tranche's topology file), `gh api` PATCH/curl write-method denies, and a `.husky/pre-push` gate refusing `task/<tranche>/<id>` pushes whose id has no topology row (mechanical). Operational rule: restart running agent sessions after merging hook/settings changes — hooks load at session start. Same mechanism as skill-check enforcement and the T9 merge gate.
|
|
312
312
|
- **Single-plan-PR CI backstop** — `checkSinglePlanPr` (`packages/aeg-core/src/single-plan-pr.ts`) was extracted from `open-pr.ts` (previously private to that wrapper) so the identical predicate could also run forge-side: `.github/workflows/forge-lifecycle.yml::single-plan-pr-gate` re-fetches this PR's touched files plus every other open PR's touched files and fails CI on the same violation the ring-0 hook refuses locally. Closes the gap where a PR opened directly via the GitHub web UI bypasses `open-pr.ts` entirely. No-ops (never fires) for an ordinary task-branch PR, since its diff never touches a tranche topology file. **Dormant in this repo entirely, as of the forge-native cutover:** a plan is now a Milestone plus labeled Issues, and `aeg-root/tranches/` holds no active topology file for a diff to collide over. The guard is kept, not deleted, because it is correct and live for any repo that does keep plans as files — but nothing here can trip it, and a gate that cannot fire should say so rather than read as active protection. One implementation, two enforcement points — no second copy of the predicate.
|
|
@@ -513,7 +513,7 @@ Sibling to `verify-docs.ts`, both in `packages/aeg-core/bin/`. Runnable as CLI (
|
|
|
513
513
|
|
|
514
514
|
**Three inputs:**
|
|
515
515
|
1. **Forge facts** — GitHub Issue state + PR merge events via `fetch-forge-facts.ts` (`@octokit/graphql` + `timelineItems(CLOSED_EVENT)`). Same adapter the Studio uses; not forked.
|
|
516
|
-
2. **Tranche topology** — `loadTrancheFiles` (`verify-coherence.ts`). For every tranche file NOT touched by the current PR's own diff (the repo-state side of every comparison — a plan PR's own topology diff still reads from its head ref, per the T2 point-of-power relocation note below), `id`/`issue` per task are derived from the forge (`@attalabs/aeg-forge-state`'s `
|
|
516
|
+
2. **Tranche topology** — `loadTrancheFiles` (`verify-coherence.ts`). For every tranche file NOT touched by the current PR's own diff (the repo-state side of every comparison — a plan PR's own topology diff still reads from its head ref, per the T2 point-of-power relocation note below), `id`/`issue` per task are derived from the forge (a Milestone + `vinaya/tranche:<slug>`-labeled Issues — `@attalabs/aeg-forge-state`'s `trancheFromIssues`, the pure half of `deriveTrancheFromForge`), falling back to `aeg-root/tranches/*.md` via `parseTranche` (`@attalabs/aeg-core`) if forge derivation fails. **The sweep enumerates before it fetches:** every tranche is listed first (topology files, then Milestones), then the Milestone index is read once for the whole run and each tranche's labeled Issues exactly once, `FORGE_FETCH_CONCURRENCY` (4) in flight — bounded rather than a `Promise.all` over every tranche, because an unbounded fan-out of `gh` calls is the burst shape GitHub's secondary rate limits penalise. L4 and L5 then read that same Milestone index and those same Issue lists rather than re-querying for the `milestone` field a `Tranche` drops. Deriving per tranche instead re-pulled the whole Milestone list once per slug and serialised every Issue query behind the last, which is what let this sweep's cost grow with the repo until it outran the timeout its own regression test allows; the derivation and therefore the verdict are identical either way. `dependsOn`/`conflictsWith` and `#TBD` rows (no Issue cut yet — structurally invisible to forge derivation) still come from the topology table **when that table exists** and are merged onto the forge-derived task list. **No active tranche carries a topology file at all anymore** — every remaining topology file was backfilled onto the forge and deleted — so `dependsOn`/`conflictsWith` is now genuinely forge-native for every active tranche; the file-merge code path is kept but permanently dormant. **The general (unscoped) sweep additionally enumerates every open AND closed Milestone** (`indexTrancheMilestonesAsync`, one `state=all` read serving the active list, the archived list, and every per-slug Milestone fact) not already found via directory listing — with the directory now permanently empty of active files, this closes the gap that would otherwise make the sweep blind to every tranche, silently. That read is **paginated**: Milestones are append-only, so a single 100-item page bounds a repo's accumulated history rather than its query, and silent truncation of the enumeration authority drops whole tranches out of every check with nothing to notice. **Forge unavailability is reported, never thrown and never passed over.** In `--json` mode this sweep's stdout is consumed as a single parseable document, so an unreachable forge has to arrive as a finding rather than as a stack trace over an empty one — and it may never arrive as a pass. Three shapes, all `FORGE` at `severity:infra`: a lost Milestone index with nothing local left to enumerate refuses the whole run and states that no check evaluated anything; a lost index with topology files still enumerable **withholds L4 and L5** — that index is their only active-tranche authority, and an empty list reads to both as "no drift" — and reports the withheld pair; a per-tranche Issue-list read that fails with no topology file to stand in names the omitted slugs. What the three share is this tree's own rule: a check that did not run may not be reported as a check that passed. A task Issue whose `vinaya/tranche:<slug>` label was never removed after being dropped/consolidated out of the topology table reappears via forge derivation even though its row is gone — the fix is removing the stale label at the source, not filtering in code. (`readFromHead`'s guard inside `loadTrancheFiles` was rewritten from `prContext !== null && prContext.touchedFiles.has(relPath)` to `prContext?.touchedFiles.has(relPath) ?? false` for `noOptionalChain` lint hygiene — same pattern as the internal-organization note above: behavior is unchanged, both forms are `boolean`-typed and identical on every input, so this binding is satisfied by this note rather than a behavioral rewrite.)
|
|
517
517
|
3. **Decision logs** — consulted via N/M checks (delegated to T2).
|
|
518
518
|
|
|
519
519
|
**Check catalog:**
|
|
@@ -527,7 +527,7 @@ Sibling to `verify-docs.ts`, both in `packages/aeg-core/bin/`. Runnable as CLI (
|
|
|
527
527
|
| T2 | `orphan-task` | Every open Issue labeled `vinaya/tranche:X` appears in X's topology. **Blocks CI only for a plan PR** (its diff touches that tranche's topology file) as of see "T2 branch scoping" below |
|
|
528
528
|
| T3 | `tbd-in-active-tranche` | No `#TBD` rows in an active tranche |
|
|
529
529
|
| D1 | `dispatched-on-unmet-deps` | An open-PR task has all `depends-on` Issues closed |
|
|
530
|
-
| R1 | `missing-rationale-field` | Every open task Issue's body carries all eight planner-brief rationale fields (`checkIssueRationale`), and every project its line-anchored `Project:` field names has a registry row (`checkProjectsRegistered`, resolving names through the same `projectsFromBody` parser the task derivation uses; dormant when the repo has no project registry) |
|
|
530
|
+
| R1 | `missing-rationale-field` | Every open task Issue's body carries all eight planner-brief rationale fields (`checkIssueRationale`), and every project its line-anchored `Project:` field names has a registry row (`checkProjectsRegistered`, resolving names through the same `projectsFromBody` parser the task derivation uses; dormant when the repo has no project registry). A `Project:` field that is present but resolves to **no** name (prose, a parenthetical, an empty value) also fails: an unparseable declaration is not the same as no declaration, and the check may not pass on a name it never received. A `Project:` line inside code — a balanced fence OR a ≥4-column-indented block — is an example, not a declaration, and is not read; a body whose FENCES do **not** balance fails closed, because an unterminated fence swallows the foot field and the read cannot be trusted (an indented block has no equivalent unterminated state — it always ends by definition, so it stays a pass like any other example). Still NOT closed, and out of this check's scope: a `Project:` line inside an HTML comment outranks the real one, since a comment is not code |
|
|
531
531
|
| L1 | `stale-active-tranche` | Active tranche with zero open Issues → should archive |
|
|
532
532
|
| L2 | `premature-archive` | Archived tranche with any open Issue → investigate |
|
|
533
533
|
| L3 | informational | Active tranche count (does not affect exit code) |
|
|
@@ -5,7 +5,7 @@ sidebar_title: "Template: PR report"
|
|
|
5
5
|
|
|
6
6
|
**Copy the block below the divider into the PR body file (opened via `open-pr.ts --body-file`) and replace every `[…]` placeholder with real content.** This packages the canonical PR-body form defined by `aeg-root/roles/developer.md` § "PR body — canonical form" — that section remains the contract for what each field requires (exact `Tier:` syntax, tagging rules, the optional `Conforms-to:`/`Doc-ack:`/`Doc-waiver:` fields); this file is the container.
|
|
7
7
|
|
|
8
|
-
**The anchor comments are load-bearing.** Each gate-read field — `Closes #N`, `Project:`, `Tier:`, the Test Plan section, the Premise block — sits inside an AEG anchor pair (an HTML comment pair, invisible on the rendered PR). When an anchor pair for a field is present, every gate reads that field **exclusively from inside the pair**, ignoring identical-looking text anywhere else in the body — a pasted reference brief, a quoted example, a duplicate section can no longer be mistaken for the real field. Bodies without anchors remain fully recognized (prose recognition is the compatibility fallback)
|
|
8
|
+
**The anchor comments are load-bearing.** Each gate-read field — `Closes #N`, `Project:`, `Tier:`, the Test Plan section, the Premise block, the Evidence block — sits inside an AEG anchor pair (an HTML comment pair, invisible on the rendered PR). When an anchor pair for a field is present, every gate reads that field **exclusively from inside the pair**, ignoring identical-looking text anywhere else in the body — a pasted reference brief, a quoted example, a duplicate section can no longer be mistaken for the real field. Bodies without anchors remain fully recognized (prose recognition is the compatibility fallback) for every field **except Evidence**, which has no prose fallback — it is never hand-typed. Use at most one anchor pair per field. Keep the anchors when you fill this in.
|
|
9
9
|
|
|
10
10
|
**The `AEG:PREMISE` anchor is not optional when the brief carried a `Premise:` block.** Without it, `premise-recheck` scans the *whole* body for anything premise-shaped — including the brief's own original pins, pasted verbatim into the `<details>` reference copy at the bottom — and re-asserts those against the code you just changed. A premise pinning the *pre-fix* state will correctly fail once your fix lands, because the pin describes what you just changed away from. Put a fresh, post-fix, currently-true assertion inside `<!-- AEG:PREMISE:START -->` / `<!-- AEG:PREMISE:END -->` so the re-check asserts something true of the shipped diff, not the brief's stale snapshot.
|
|
11
11
|
|
|
@@ -22,7 +22,7 @@ Closes #[N]
|
|
|
22
22
|
|
|
23
23
|
## Summary
|
|
24
24
|
|
|
25
|
-
[SUMMARY — one paragraph: what shipped
|
|
25
|
+
[SUMMARY — one paragraph: what shipped and the durable why. Then the decisions you made that weren't explicit in the brief — name the alternatives and why you picked yours, so the Principal can reverse a wrong call. No verification claims here (no "typecheck passes", no diff stats, no test counts) — those go in the emitted Evidence block below, never typed by hand.]
|
|
26
26
|
|
|
27
27
|
## Test plan
|
|
28
28
|
|
|
@@ -40,9 +40,13 @@ Closes #[N]
|
|
|
40
40
|
|
|
41
41
|
[Omit this whole section — anchors and all — only when §4 of the brief had no real code surface (a Tier 0 doc-only or planning-only change). Any brief with a `Premise:` block gets a fresh one here; do not leave the brief's original block as the only copy in the body, since it lives inside the `<details>` reference copy below and unanchored will be picked up by the re-check instead of this one.]
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Evidence
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Run `vinaya pr report --write <this-body-file>` and commit its output — this block is generated, never hand-typed. `check-evidence-fresh` refuses a body whose block doesn't match the head it's attached to.
|
|
46
|
+
|
|
47
|
+
<!-- AEG:EVIDENCE:START -->
|
|
48
|
+
[run `vinaya pr report --write` to populate — do not type this block by hand]
|
|
49
|
+
<!-- AEG:EVIDENCE:END -->
|
|
46
50
|
|
|
47
51
|
## Scope
|
|
48
52
|
|