@attalabs/vinaya 0.9.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -7
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +7 -7
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/developer.md +29 -5
- package/aeg-root/roles/planner.md +3 -3
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/roles/tranche-archivist.md +1 -1
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +8 -8
- package/aeg-root/templates/pr-report-template.md +8 -4
- package/aeg-root/tranche-model.md +34 -12
- package/dist/checks/bin/check-branch-topology.js +232 -120
- package/dist/checks/bin/check-brief-shape.js +232 -120
- package/dist/checks/bin/check-closes-n.js +232 -120
- package/dist/checks/bin/check-coherence.js +234 -121
- package/dist/checks/bin/check-dead-branch-push.js +232 -120
- package/dist/checks/bin/check-dispatch-readiness.js +234 -121
- package/dist/checks/bin/check-doc-coverage-push.js +261 -130
- package/dist/checks/bin/check-doc-coverage.js +263 -132
- package/dist/checks/bin/check-evidence-fresh.js +3196 -0
- package/dist/checks/bin/check-first-push-dispatch.js +234 -121
- package/dist/checks/bin/check-issue-assignment.js +232 -120
- package/dist/checks/bin/check-no-disk-state.js +232 -120
- package/dist/checks/bin/check-reader-resolvable-prose.js +232 -120
- package/dist/checks/bin/check-registry-gates.js +232 -120
- package/dist/checks/bin/check-review-gate.js +237 -123
- package/dist/checks/bin/check-single-plan-pr.js +232 -120
- package/dist/checks/bin/check-test-plan.js +232 -120
- package/dist/index.js +1232 -262
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +3 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +2 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__05t6482._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +51 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1p19382._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3antrj5z0uf_c.js → 0ek6qlqhokys0.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3zyedaek9nvr_.js → 0ywkq74falx-x.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/templates/custom-check.template.ts +5 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
|
@@ -87,11 +87,22 @@ When the PR touches agent/skill/hook definitions, MCP configs, or anything under
|
|
|
87
87
|
|
|
88
88
|
## Output format
|
|
89
89
|
|
|
90
|
-
The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate.
|
|
90
|
+
**Run `vinaya review post --role security` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractSecurityReviewVerdict` function the gate calls:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
vinaya review post --role security --pr <n> --verdict PASS|FAIL \
|
|
94
|
+
--findings-file <path> --config-scan <text> \
|
|
95
|
+
--secrets <text> --secrets-evidence-file <path> \
|
|
96
|
+
--task-id <task-id> --model <model> --tokens-in <n|-> --tokens-out <n|-> --cost <text|->
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The findings file is one finding per line, `SEVERITY|file:line|description` (`|`-delimited: `file:line` already contains a colon), severity one of `CRITICAL|HIGH|MEDIUM|LOW`. Omit `--findings-file` for zero findings. The command renders this exact shape (kept here so a human or a debugging agent can still read what it produces — this is documentation, not something to write by hand):
|
|
91
100
|
|
|
92
101
|
```
|
|
93
102
|
VERDICT: PASS | FAIL
|
|
94
103
|
|
|
104
|
+
Judged head: <sha>
|
|
105
|
+
|
|
95
106
|
FINDINGS (ordered by severity):
|
|
96
107
|
1. [CRITICAL|HIGH|MEDIUM|LOW] <file:line> — <what and why>
|
|
97
108
|
2. ...
|
|
@@ -104,9 +115,9 @@ SECRETS: [none found | listed above, redacted]
|
|
|
104
115
|
- **HIGH** — likely exploitable misconfig or injection surface.
|
|
105
116
|
- **MEDIUM/LOW** — hardening notes.
|
|
106
117
|
|
|
107
|
-
Any CRITICAL or HIGH → VERDICT FAIL. Only MEDIUM/LOW → PASS with notes.
|
|
118
|
+
Any CRITICAL or HIGH → VERDICT FAIL. Only MEDIUM/LOW → PASS with notes. `vinaya review post` refuses before posting anything if you pass a CRITICAL/HIGH finding together with `--verdict PASS` — that contradiction is caught mechanically, not left to review.
|
|
108
119
|
|
|
109
|
-
The `SECRETS:` line is evidence-backed, not asserted: the secret scanner's pasted output (check 1) must appear in the verdict comment above it — necessary evidence that the scan ran, never sufficient on its own, since the judgment half of check 1 still stands behind the claim. `SECRETS: none found` with no scan output pasted is an unbacked self-attestation — the exact claim this check exists to catch in others' work, not to commit in your own.
|
|
120
|
+
The `SECRETS:` line is evidence-backed, not asserted: the secret scanner's pasted output (check 1) must appear in the verdict comment above it — necessary evidence that the scan ran, never sufficient on its own, since the judgment half of check 1 still stands behind the claim. `SECRETS: none found` with no scan output pasted is an unbacked self-attestation — the exact claim this check exists to catch in others' work, not to commit in your own. `vinaya review post` mechanizes this: passing `--secrets "none found"` without `--secrets-evidence-file <path>` (the actual pasted scanner output) is refused outright.
|
|
110
121
|
|
|
111
122
|
## Escalation
|
|
112
123
|
|
|
@@ -116,8 +127,8 @@ A security finding that implies a product/architecture decision (e.g., "the whol
|
|
|
116
127
|
|
|
117
128
|
Phase 10 (Review) in `process.md`: code-reviewer pass → **security pass (you)** → Principal code review → Brief Author spec review → merge.
|
|
118
129
|
|
|
119
|
-
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `PASS` verdict — `FAIL`, a missing verdict,
|
|
130
|
+
**Your verdict is also a mechanical merge gate (the review-gate tranche, task 1).** A required, blocking CI check (`packages/aeg-core/bin/verify-review-gate.ts`) reads every PR comment from a **principal-allowlisted author** (verdict-author verification, 2026-08-09 — bot and unknown-author comments are ignored) for a clean `PASS` verdict that also covers the PR's current head commit (reviewed-commit binding) — `FAIL`, a missing verdict, an unclear one, or one bound to a superseded commit all fail the check and block merge, same as the code-reviewer pass. This is not advisory: it is the same enforcement class as typecheck or lint. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (`aeg-root/enforcement.md`) — label presence alone is never sufficient.
|
|
120
131
|
|
|
121
132
|
## Turn-end: report your tokens in the verdict comment
|
|
122
133
|
|
|
123
|
-
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead,
|
|
134
|
+
You do not append your own row to `aeg-root/tranches/<name>.tokens.md` — you have no branch to write it on, and self-append was retired for every role. Instead, `vinaya review post`'s `--task-id`/`--model`/`--tokens-in`/`--tokens-out`/`--cost` flags render the closing one-line token report as part of the same posted comment: `Tokens: <task-id>: security — Security — <model> — in/out/cost`. A security pass normally runs **operator-metered** — on a host that exposes no usage figure to the agent — so pass `-` (a literal hyphen, not this doc's `—`) for `--tokens-in`/`--tokens-out`/`--cost` when unknown; that host capability is the one sanctioned reason for a blank token cell (`tranche-model.md` §12), never inconvenience, and you never estimate. If your host does expose your own usage to you, pass the real figures instead. The per-task Archivist collects this report at close-out and appends the row to the ledger — see `roles/archivist.md`. A re-pass after the Developer's fixes reports again — run `vinaya review post` again rather than editing the prior comment.
|
|
@@ -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
|
|
|
@@ -55,7 +55,7 @@ A tranche (`aeg-root/tranches/<name>.md`) is a **thin topology file**: task→Is
|
|
|
55
55
|
|
|
56
56
|
## 6. Conflicts and the two dispatch gates
|
|
57
57
|
|
|
58
|
-
Conflicts are **
|
|
58
|
+
Conflicts are **package-level, and mostly derived, not hand-declared** (collision domains live-derived from `package.json`'s `packages/*` workspace members, plus a built-in cross-cutting default set — a legacy static `.aeg/packages` file or `vinaya.config.json`'s `blastRadius.extraDomains` add anything beyond that) — there is **no dynamic path-overlap scanner**. When unsure two tasks collide, declare the conflict and serialize. Two gates, both forge-answerable with zero stored state:
|
|
59
59
|
- never start a task whose `depends-on` isn't **merged**;
|
|
60
60
|
- never start a task while a `conflicts-with` sibling's **PR is open**.
|
|
61
61
|
|
|
@@ -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,13 +306,13 @@ 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
|
-
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain
|
|
311
|
+
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain — live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any legacy `.aeg/packages`/`vinaya.config.json` `blastRadius.extraDomains` entries — that no declared project owns, absent a second **registered** project or a `blast-radius-ack:` line), `checkNoBriefContent` (no brief-shaped section in the Issue), `checkRationaleNamesDocs` (the rationale names a concrete doc/skill path, or the `no-doc-surface` sentinel). `checkConflictCompleteness` warns on an undeclared collision-domain overlap and never blocks**). Prevention, not detection: a malformed PR/Issue body is refused at the agent's own tool call and never reaches the forge; the CI gates below re-run the identical aeg-core checks purely as a backstop for non-hook writers. Also enforced at the same layer: title grammar (`checkForgeTitle`, both wrappers), the single-plan-PR guard (`checkSinglePlanPr` — a diff touching a tranche's topology file is refused when another OPEN PR already touches that same tranche's topology file), `gh api` PATCH/curl write-method denies, and a `.husky/pre-push` gate refusing `task/<tranche>/<id>` pushes whose id has no topology row (mechanical). Operational rule: restart running agent sessions after merging hook/settings changes — hooks load at session start. Same mechanism as skill-check enforcement and the T9 merge gate.
|
|
312
312
|
- **Single-plan-PR CI backstop** — `checkSinglePlanPr` (`packages/aeg-core/src/single-plan-pr.ts`) was extracted from `open-pr.ts` (previously private to that wrapper) so the identical predicate could also run forge-side: `.github/workflows/forge-lifecycle.yml::single-plan-pr-gate` re-fetches this PR's touched files plus every other open PR's touched files and fails CI on the same violation the ring-0 hook refuses locally. Closes the gap where a PR opened directly via the GitHub web UI bypasses `open-pr.ts` entirely. No-ops (never fires) for an ordinary task-branch PR, since its diff never touches a tranche topology file. **Dormant in this repo entirely, as of the forge-native cutover:** a plan is now a Milestone plus labeled Issues, and `aeg-root/tranches/` holds no active topology file for a diff to collide over. The guard is kept, not deleted, because it is correct and live for any repo that does keep plans as files — but nothing here can trip it, and a gate that cannot fire should say so rather than read as active protection. One implementation, two enforcement points — no second copy of the predicate.
|
|
313
313
|
- **Coherence oracle (A1/A2/A3/T1/T2/T3/D1/M1/M3)** — `packages/aeg-core/bin/verify-coherence.ts` runs against every PR and genuinely blocks CI — the CLI's own exit code, non-zero on any `fail`-status check, is what the `coherence-gate` job exits with. Failures in A1 (closed-without-merge), A2 (archived-without-provenance), A3 (auto-close-misfire), T1 (phantom-issue-ref), T2 (orphan-task — **plan PRs only, see below**), T3 (tbd-in-active-tranche), D1 (dispatched-on-unmet-deps), M1 (manifest-dangling), and M3 (manifest-duplicate-glob) fail CI. L1–L5 and M2 are advisory (info-only) — every `checkL*` returns `status: info`, so a premature-archive, lifecycle-hygiene, Milestone-attachment-drift, or open-Milestone-all-closed finding is surfaced for a human to investigate but never fails CI. On A1 failures the relevant Issues receive the `vinaya/incoherent` label (Section 14). Real, installed at `.github/workflows/forge-lifecycle.yml::coherence-gate`. Same enforcement substrate as above. **The job's repo-state inputs (topology files, tranche list) are read from a freshly-fetched `origin/main` — not the checkout's `refs/pull/N/merge`, which GitHub materializes lazily and can lag behind main (5+ false-red cycles, 2026-07-03/04). A plan PR's own topology diff still reads from its head ref.**
|
|
314
314
|
- **Planner→Brief rationale completeness (R1)** — the same coherence oracle's **R1** check re-runs `checkIssueRationale` **and `checkProjectsRegistered`** (`packages/aeg-core`) against every open task Issue's body, batched per active tranche alongside T2. A non-grandfathered Issue missing any of the eight `contracts/planner-brief.md` rationale fields — or whose `Project:` field names a project with no row in the project registry — fails CI. Paired with the ring-0 creation gate (`bin/open-issue.ts`, same tool-layer-forge-gates row above) — R1 is the continuous half, the hook is the point-of-creation half; one grammar, two enforcement points (`aeg-root/enforcement.md`). Pre- Issues are grandfathered by explicit Issue number (`R1_GRANDFATHERED_ISSUES`), reported `info`, never blocking — see Section 15b. **Real.** Moves this seam's rationale-completeness half from Trusted (below) to Enforced; the "role doc matches contract prose" half of contract conformance remains Trusted.
|
|
315
|
-
- **Review gate — code-review + security-review verdicts** — `packages/aeg-core/bin/verify-review-gate.ts`, a step of the AEG gate suite job, blocks merge on a task-branch PR unless a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment both exist — `REQUEST CHANGES`, `FAIL`, a missing verdict,
|
|
315
|
+
- **Review gate — code-review + security-review verdicts, bound to the commit they judged** — `packages/aeg-core/bin/verify-review-gate.ts`, a step of the AEG gate suite job, blocks merge on a task-branch PR unless a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment both exist AND both cover the PR's current head commit — `REQUEST CHANGES`, `FAIL`, a missing verdict, an unclear one, or a clean verdict bound to a superseded commit (or carrying no binding at all) all fail CI (reviewed-commit binding). Reuses `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`verdict-extraction.ts`) — the exact detection the post-merge Archivist's provenance assembly already ran, previously advisory-only (a DANGLING note on the merged PR, never a block) — now also parsing a same-comment `Judged head: <sha>` line and comparing it against `gh pr view --json headRefOid`, resolved from GitHub, never an env var or local git. A principal can waive it for one PR with an actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, parameterized by label — see the waiver-label-actor row above); label presence alone is never sufficient. **Real, installed at `.github/workflows/forge-lifecycle.yml::aeg-gate-suite`.** Closes the gap where task PRs could previously merge with no review pass at all, and the second gap where a clean verdict could still merge stale code because nothing checked which commit it covered. **What remains Trusted, not Enforced: *dispatching* the code-reviewer/security-reviewer subagents in the first place** — see below; this gate only verifies a dispatched review's verdict is clean and current, it cannot make a review happen.
|
|
316
316
|
- **Registry load-bearing checks G3/G4/G5** — `packages/aeg-core/bin/verify-registry.ts`, a step of the AEG gate suite job, blocks merge on: G3 (a file making the exact class of GitHub-mutating call `check-forge-gates.sh` gates — PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes — with no guarding Ring-0 row); G4 (a `#NNN` cited anywhere in `aeg-root/enforcement.md`'s body that does not resolve to a real Issue or PR in the forge); G5 (a contract's `producer`/`consumer` that is not a real `role_id`, or a role with an empty `performs`/`refuses_when`). **G1 (implementation-exists) and G2 (no-orphan-hook/CLI) are report-only this tranche** — they run in the same step and print `info` findings but never affect the exit code, since flipping them to blocking immediately would retroactively fail in-flight work against a pre-existing orphan backlog; G1 flips to blocking in a later, separately-dispatched task. Pure evaluators in `packages/aeg-core/src/registry-checks.ts`, parsing `packages/aeg-core/src/registry-parse.ts` — same shape (pure predicate + thin I/O shim) as `coherence-checks.ts`/`verify-coherence.ts`, deliberately a separate mechanism (different registry: `enforcement.md`'s own ring tables, not the tranche/forge state coherence-checks.ts reads). `registry-parse.ts`'s `GateRow` additionally carries `summary`/`category` and `description`/`spec` — purely descriptive fields consumed by the Vinaya `/how-it-works` page, not read by any G-check; G1–G5's pass/fail behavior is unaffected. `description` is the row's plain-language sentence and is resolved by header name, not column index; `spec` is the enforcement column it must never be confused with (the "what must be true"/"Re-verifies"/"Catches" slot before `implementation`).
|
|
317
317
|
- **Canonical action set** — `packages/aeg-core/src/actions.ts` exports `ACTIONS`, the 10-entry list of every AEG action that either crosses into GitHub (`crosses: 'into-github'`) or hands work across a role-seam contract (`crosses: 'none'`) — 6 crossings plus 4 seam-only actions, with no duplicate id for the two seams already accomplished by a crossing. Pure data (zero I/O, same shape as `waiver-label.ts`); `commit-the-work` is `'none'` because `git commit` is local-only (only `git push`/`publish-the-branch` reaches the forge). G3's crossing-file detection uses the `crosses: 'into-github'` subset; the `DiagramModel` derivation (`deriveDiagramModel`, `packages/aeg-core/src/diagram-model.ts`) now exists and places each action on a ring-0 gate (via `guards` edges) or a role/contract edge (via `performs`/`produces`/`consumes`), consuming this same `ACTIONS` list plus its `CROSSING_KEYWORDS` map (promoted into `actions.ts` alongside `ACTIONS` this task) — so G3's completeness set and the diagram's edge count cannot drift apart. The derivation is pure and takes doctrine through a `DoctrineSource` seam (`packages/aeg-core/src/doctrine-source.ts`, I/O-free; file-backed adapter `createFileDoctrineSource` in `@attalabs/vinaya-sources`), never `aeg-root/` paths directly. `actions.test.ts` cross-checks every `into-github` action against a real Ring-0 gate row in `enforcement.md` and every `performedBy` against a real `role_id`. Each entry also carries a `summary` question and a **required** `description` — required, so a new action cannot be added without one; `deriveDiagramModel` threads both onto the action's node, along with its `crosses`, which a client renderer cannot read off `ACTIONS` itself (importing it drags `node:child_process` into the browser bundle).
|
|
318
318
|
- **Code-owned state machine + label vocabulary** — the same one-list-of-pure-data discipline, applied to status derivation. `packages/aeg-core/src/state-machine-model.ts` exports three parts: `FORGE_FACT_INPUTS` (every `ForgeFacts` field and the GitHub object it is read from), `DERIVED_STATUSES` (the 9-value set; `backlog` stays a member but derivation never emits it inside a tranche), and `DERIVATION_RULES` — the ordered rule list, first match wins, each entry carrying its predicate, its prose, and the reason its position matters. `deriveStatus` (`derive-tranche.ts`) executes that list, so the rules a reader sees rendered and the rules that actually run are the same objects. The list is total (its final rule matches unconditionally), and `state-machine-model.test.ts` pins the order rule-for-rule, proves every derivable status reachable, proves no rule is shadowed by an earlier one, and asserts equivalence with the pre-refactor `if`-chain across all 432 combinations of the facts derivation reads. The label vocabulary lives in `packages/aeg-forge-state/src/labels.ts` (`LABELS` + `AEG_BLOCKED_LABEL`) — in `aeg-forge-state`, not `aeg-core`, because the dependency direction is `aeg-core → aeg-forge-state → aeg-types` and the vocabulary's first consumer, `map-forge-facts.ts`, lives there and cannot import backward. Each label records the single orthogonal fact it carries; there is deliberately no `status:*` label, since status is derived and never written. `state-machine-model.ts` does **not** import that vocabulary: derivation works on `ForgeFacts`, never on label strings, and importing `@attalabs/aeg-forge-state` would drag its `node:child_process` into any browser bundle rendering the model — the same hazard the `ACTIONS` note above records.
|
|
@@ -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
|
|
|
@@ -126,7 +126,7 @@ The `Project(s)` column remains the authoritative blast-radius record per task;
|
|
|
126
126
|
Two tasks conflict if they touch the same **collision domain** and therefore must not run in parallel. The rules, after the panel's correction:
|
|
127
127
|
|
|
128
128
|
- **Conflicts are declared by the Planner** as `conflicts-with` edges in the thin file. Declared, not inferred.
|
|
129
|
-
- **Collision domains are packages**,
|
|
129
|
+
- **Collision domains are packages**, live-derived from the repo's own workspace declaration's `packages/*` members (`package.json`'s `workspaces` array, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) — no file to author or keep in sync. Known cross-cutting collision paths that are detectable by convention — **whichever lockfile exists, monorepo config (`tsconfig.json`/`biome.json`/`turbo.json`), `.github/workflows`, `.husky`** — ship as a built-in default set for the same reason. Paths that couple tasks across package boundaries but have no universal convention — **`migrations/`, codegen outputs (protobuf/GraphQL/OpenAPI)** — are declared as their own collision domains via `vinaya.config.json`'s `blastRadius.extraDomains`. A legacy static `.aeg/packages` file, if a repo already has one, still adds its entries on top of all of the above.
|
|
130
130
|
- **The conflict gate is forge-answerable with zero stored state:** "is a `conflicts-with` sibling's PR currently open?" If yes, don't start. That's it.
|
|
131
131
|
- **There is no dynamic path-overlap check.** (The panel's decisive correction.) Computing "which files is each in-flight task touching right now" would require a live task→changed-files map — exactly the mutable execution state the design eliminates. So it is forbidden (§10).
|
|
132
132
|
|
|
@@ -260,32 +260,54 @@ Append-only. Each row records one role's turn at a phase. Re-entry appends a **n
|
|
|
260
260
|
|
|
261
261
|
- **Phase** — free-text. Convention: `<task-id>: <phase>` for per-task work (e.g. `9: develop`, `9: review`), or a bare phase for tranche-wide work (e.g. `planning`). Phase is opaque to the parser; the convention exists so a future view can pivot by task.
|
|
262
262
|
- **Role** — the AEG role doing the work (`Planner`, `Brief Author`, `Developer`, `Reviewer`, `Security`, `Archivist`).
|
|
263
|
-
- **Agent/Model** — the role's agent + model, with the surface in parentheses
|
|
263
|
+
- **Agent/Model** — free text: the role's agent + model, with a short tag for the host surface in parentheses (`claude-opus-4-7 (CC)` for a coding-agent session, `claude-opus-4-7 (chat)` for a conversational one — the tags the archived ledgers already use). Nothing parses the tag; it is there because the surface is what tells a reader which collection capability applied (below), and therefore whether a `—` cell was sanctioned.
|
|
264
264
|
- **Tokens in / Tokens out** — integers from the meter, or `—` for "not yet known."
|
|
265
|
-
- **Cost** — USD as `$X.XXXX
|
|
265
|
+
- **Cost** — USD as `$X.XXXX`, or `—`. *(V1 honesty: no maintained $/token pricing table for current models ships in this package, so `formatTokensLine` renders this cell as `—` on every row it emits. Historical rows may carry a `$X.XXXX` value supplied by hand or by an earlier table. Tokens are still exact; pricing is a known backlog dependency, not a ledger bug.)*
|
|
266
266
|
- **Date** — `YYYY-MM-DD`.
|
|
267
267
|
|
|
268
268
|
### The append rule (read this exactly the way you read derived status)
|
|
269
269
|
|
|
270
|
-
- **No role appends its own row on a task branch.** Two live-fire incidents forced this:
|
|
270
|
+
- **No role appends its own row on a task branch.** Two live-fire incidents forced this: read-only roles (Reviewer, Security, Planner, Brief Author) structurally cannot append — they hold no task branch, and some never touch the repo's filesystem at all; and parallel Developer sessions on different tasks collided appending to the same shared `tokens.md` file. Instead, every role **reports** its token spend in the artifact its turn already produces — the PR body ("Token report" section) for a role holding a branch, the verdict comment for a reviewing role, the plan PR or planning report for the Planner — and the per-task **Archivist appends every row at task close-out**, one row per role-turn, including its own.
|
|
271
271
|
- **Never edit** an existing row. If you discover a mistake, append a new row that supersedes it in prose (or fix the source file in a separate, declared edit — same exception that `state-machine.md` §13 carves for forward-reference fields).
|
|
272
272
|
- **Re-entry appends.** A Developer dispatched for `9: develop`, asked for changes, and re-running for `9: develop` again produces a **second** `9: develop` report, which the Archivist appends as a **second** row — never a sum, never an overwrite. The two rows both count.
|
|
273
273
|
- The **tranche total is `sum(rows)`**, derived at read time, never stored. This is the same philosophy as forge-derived status (don't store the aggregate; sum the immutable entries). A stored total reintroduces the merge-collision + stale-aggregate problem.
|
|
274
274
|
|
|
275
|
-
###
|
|
275
|
+
### Three layers — two portable, one host-specific
|
|
276
276
|
|
|
277
|
-
Token reporting is
|
|
277
|
+
Token reporting is three separable things, and only one of them depends on the agent host a role happens to run in. Keeping them apart is what lets this model be adopted on a harness AEG has never seen.
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
1. **The obligation — portable.** Every role reports its own turn's usage at turn-end. This does not weaken, is not best-effort, and is not conditional on the host making it convenient. It exists because the figure is otherwise unrecoverable after the fact (see the retraction below).
|
|
280
|
+
2. **Collection — host-specific.** *How* a role obtains true figures for its own turn. This is the only layer an adapter belongs in, and the only layer that may name a host, a transcript format, or a script path.
|
|
281
|
+
3. **The destination and the grammar — portable.** The `Tokens: …` line, in the artifact the role's turn already produced: the PR body's "Token report" section for a role that holds a branch, the verdict comment for a reviewing role, the plan PR or planning report for the Planner. The ledger stays append-only, and the Archivist transcribes what each role reported rather than re-deriving it.
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
- **claude.ai roles run in chat (Planner; Brief Author; Reviewer; Security).** A claude.ai conversation **cannot read its own token count** via tool or API, and has no transcript file the way a Claude Code session does — the reporter has nothing to run against. The role still reports at turn-end — phase, role, model, date, in its verdict comment or planning report — but leaves the numeric cells as `—`. The Archivist copies the report as-is; the **Principal** may later supply the real figures from the claude.ai UI usage figure, filling a previously-`—` cell (the one narrow forward-reference exception `state-machine.md` §13 allows).
|
|
283
|
+
The destination was never the coupled part — no role writes a shared ledger file on a task branch, so layer 3 is satisfied through the forge for every task-branch role. Two sources remain the known open gap recorded in `roles/archivist.md`: the Archivist's own row, and the Planner's tranche-wide report. **An adopter implements layer 2 for their own harness and inherits layers 1 and 3 unchanged.** A doctrine sentence that names a tool belongs in layer 2 or nowhere.
|
|
283
284
|
|
|
284
|
-
|
|
285
|
+
### Two collection capabilities (the design constraint, not a bug)
|
|
286
|
+
|
|
287
|
+
Token reporting is asymmetric — and any honest design has to encode that, because the asymmetry is a property of the **host's capability**, not of AEG and not of the role. The asymmetry lives in *what a role can collect*, not in *who writes the ledger file*: the Archivist writes every row, but the precision of the numbers it copies depends on whether the reporting role's host exposes usage to the agent at all.
|
|
288
|
+
|
|
289
|
+
The split is **operator vs. agent, not terminal vs. chat** — a figure a human reads off an interactive prompt or a usage dashboard is not reachable from an unattended agent session (dispatched, automated, no human at the keyboard), whatever kind of surface it runs on. (2026-08-08: retracted — an earlier revision of this section claimed a terminal role reports exact numbers "from `/cost`", an operator-typed slash command in one particular host; no agent session ever could, in that host or any other.)
|
|
290
|
+
|
|
291
|
+
- **Self-metering** — a role whose host exposes the running session's own usage to the agent itself, so the role can collect real figures with no operator step. Typically the Developer, and the Archivist when it runs as automation. The role collects through its host's layer-2 mechanism and reports the exact numbers in its PR body; the Archivist copies them verbatim into the row it appends at close-out. **A self-metering role may not report `—` in the `Tokens in` or `Tokens out` cells** — real figures, or the collection step is broken and *that* is what to report. The `Cost` cell is outside this rule: it reads `—` on every row the shipped renderer emits, for the pricing-table reason given above.
|
|
292
|
+
- **Operator-metered** — a role whose host exposes no usage to the agent, leaving a human the only source of the figure. Typically the Planner, Brief Author, Reviewer and Security, which run in conversational surfaces with no session record the agent can read and no meter it can query. The role **still reports at turn-end** — phase, role, model, date, in its verdict comment or planning report — and leaves the numeric cells as `—` when no figure reached it. The blank is licensed by the **absence of a figure**, not by the role's label: where an operator hands the role real numbers at report time (a route layer 2 explicitly allows), those numbers go in the cells and `—` is not correct. The Archivist copies the report as-is; the **Principal** may later supply the real figures from whatever usage view the host does offer a human, filling a previously-`—` cell (the one narrow forward-reference exception `state-machine.md` §13 allows).
|
|
293
|
+
|
|
294
|
+
**Per-cell optionality is conditioned on host capability, never on convenience.** `—` in a **token** cell is sanctioned in exactly one situation: the host cannot expose the figure to the agent. (The **Cost** cell is separate and is always `—` while this package carries no maintained $/token table — see the Cost bullet above.) It is never a shortcut for a role that could have collected the number, and no role ever estimates a figure or fills in another role's cell — an invented number in an append-only ledger is worse than an honest unknown, because nothing downstream can tell the two apart.
|
|
295
|
+
|
|
296
|
+
Which capability applies is a fact about the **host**, not a fixed property of the role: the same role is self-metering on a harness that exposes usage and operator-metered on one that does not. Read the role names above as today's common case, not as an allocation.
|
|
297
|
+
|
|
298
|
+
Self-metering capture is real, not estimated: the adapter sums the session's own usage records, so a Developer or automated Archivist turn's numbers are exact rather than approximated. Whether it also runs with *no* operator step depends on the host's wiring — where a Stop hook writes a transcript pointer — configured in the repo or in the operator's own host settings — the adapter finds it unaided; with no pointer written, the caller names the transcript with `--transcript <path>`. Either way the figures are read, never guessed. The remaining manual seam is operator-metered roles only — closing it depends on the host giving a session a way to read its own usage, which conversational surfaces generally do not today. **Known gap (flagged, not solved):** tranche-wide operator-metered turns with no task PR to report into — a Planner session outside a plan PR, a Brief Author session — have no established recording path; see `roles/planner.md` "Plan-PR close-out."
|
|
299
|
+
|
|
300
|
+
### The collection adapter AEG ships (one layer-2 instance, not the requirement)
|
|
301
|
+
|
|
302
|
+
AEG ships exactly one collection adapter, for the Claude Code host: `packages/aeg-core/bin/report-tokens.ts`. It reads that host's session transcript — a JSONL file carrying a `usage` object per assistant message — sums it, and emits a layer-3 `Tokens: …` line. Every doctrine citation of it is an **example of one way to satisfy layer 2**, never the requirement; a role on another host satisfies the same obligation by other means and is equally compliant.
|
|
303
|
+
|
|
304
|
+
**The seam is the summary shape, not the file tree.** An adapter's whole job is to produce a `TranscriptSummary` — four integers (fresh input, cache creation, cache read, output) plus a model id. Everything downstream of that shape is portable and already shipped: `formatTokensLine` renders the grammar, `parse-token-report.ts` reads it back. Two things sit adapter-side because both know host-specific facts: `summarizeTranscript` (`packages/aeg-core/src/claude-code-transcript.ts`), which knows Claude Code's transcript JSONL and its `usage` field names, and the bin itself, which knows how that host points a session at its own transcript. The `bin/` vs `src/` split does **not** mark this seam and never did.
|
|
305
|
+
|
|
306
|
+
**To satisfy layer 2 on another harness:** obtain your turn's usage figures by whatever means your host offers — its own transcript or log, an API usage response, a meter the harness exposes, or an operator reading them off a dashboard and handing them over — and write the layer-3 line yourself. Reusing this adapter is not required, and neither is writing any code: the requirement is the reported figures, in the grammar, in the right artifact. If your host exposes nothing to the agent, you are operator-metered and the `—` rule above applies — that is the sanctioned outcome, not a failure to comply.
|
|
285
307
|
|
|
286
308
|
### Live reads (Studio) — a second, narrower read path
|
|
287
309
|
|
|
288
|
-
Vinaya Studio's tranche page no longer reads `<name>.tokens.md` off disk to render token totals — it re-derives the same row shape live off the forge: every MERGED PR on a task's own branch (`task/<tranche>/<id>`), parsing the Developer's "Token report" entries from the PR body (every one, including re-push entries) and the Reviewer's/Security's `Tokens: …` lines from that PR's comments (`packages/aeg-core/src/parse-token-report.ts`'s `aggregateTaskTokenRows`, fetched by
|
|
310
|
+
Vinaya Studio's tranche page no longer reads `<name>.tokens.md` off disk to render token totals — it re-derives the same row shape live off the forge: every MERGED PR on a task's own branch (`task/<tranche>/<id>`), parsing the Developer's "Token report" entries from the PR body (every one, including re-push entries) and the Reviewer's/Security's `Tokens: …` lines from that PR's comments (`packages/aeg-core/src/parse-token-report.ts`'s `aggregateTaskTokenRows`, fetched by Studio's own forge-reading layer in the repo that hosts it). Same row shape, same `sumLedger` totals math (`parse-ledger.ts`) — different source.
|
|
289
311
|
|
|
290
312
|
This is deliberately **narrower** than what the Archivist collects into the file: it cannot recover the Archivist's own `<task-id>: archive` row (no PR carries it — the file itself is that row's only record) or the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task from the forge alone without false-positive cross-task matches, confirmed live during 4b's build — a task Issue's cross-reference timeline picks up ANY PR that merely mentions its number in passing prose, not just its own plan PR). Both remain recoverable only from `.tokens.md`, which is why the file is not deleted here (task 7's job, once the live mechanism is proven in wider use). A task's report that's missing or malformed (e.g. a "Token report" heading with no table and no parseable text after it) yields no row for that report, never a fabricated one — same discipline as the Archivist's own DANGLING convention.
|
|
291
313
|
|
|
@@ -294,7 +316,7 @@ This is deliberately **narrower** than what the Archivist collects into the file
|
|
|
294
316
|
The ledger is a Section-13 append-only artifact. The familiar forbidden moves apply:
|
|
295
317
|
|
|
296
318
|
- **No stored total.** Do not add a "current total" row, header field, or anything that has to be edited when a row appends. Derive it.
|
|
297
|
-
- **No edits to past rows** (except the forward-reference exception in §13: filling a previously `—` numeric cell from the
|
|
319
|
+
- **No edits to past rows** (except the forward-reference exception in §13: the Principal filling a previously `—` numeric cell from whatever usage view the host offers a human is permitted — it does not change history, only completes it).
|
|
298
320
|
- **No "current spend"** field anywhere — including the tranche file's header. The forge holds execution state; the ledger file holds the cost history. The thin tranche file holds topology. Three artifacts, three concerns.
|
|
299
321
|
|
|
300
322
|
---
|