@attalabs/vinaya 0.20.1 → 0.21.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 +74 -0
- package/aeg-root/enforcement.md +13 -6
- package/aeg-root/milestone-model.md +3 -1
- package/aeg-root/roles/architect.md +2 -2
- package/aeg-root/roles/archivist.md +1 -1
- package/aeg-root/roles/developer.md +3 -2
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +1 -0
- package/aeg-root/roles/tranche-archivist.md +2 -2
- package/aeg-root/templates/issue-rationale-template.md +4 -4
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +281 -5
- package/dist/checks/bin/check-branch-topology.js +274 -0
- package/dist/checks/bin/check-brief-shape.js +249 -0
- package/dist/checks/bin/check-changeset-coverage.js +4156 -0
- package/dist/checks/bin/check-closes-n.js +274 -0
- package/dist/checks/bin/check-coherence.js +305 -3
- package/dist/checks/bin/check-dead-branch-push.js +249 -0
- package/dist/checks/bin/check-dispatch-readiness.js +381 -20
- package/dist/checks/bin/check-doc-coverage-push.js +280 -3
- package/dist/checks/bin/check-doc-coverage.js +280 -3
- package/dist/checks/bin/check-doctrine-portability.js +280 -3
- package/dist/checks/bin/check-evidence-fresh.js +249 -0
- package/dist/checks/bin/check-first-push-dispatch.js +372 -14
- package/dist/checks/bin/check-issue-assignment.js +274 -0
- package/dist/checks/bin/check-main-branch-refusal.js +249 -0
- package/dist/checks/bin/check-no-disk-state.js +249 -0
- package/dist/checks/bin/check-quoted-command.js +4405 -0
- package/dist/checks/bin/check-reader-resolvable-prose.js +282 -5
- package/dist/checks/bin/check-registry-gates.js +282 -0
- package/dist/checks/bin/check-retired-vocabulary.js +282 -5
- package/dist/checks/bin/check-review-gate.js +280 -3
- package/dist/checks/bin/check-single-plan-pr.js +249 -0
- package/dist/checks/bin/check-test-plan.js +249 -0
- package/dist/checks/bin/check-token-collection-wired.js +3975 -0
- package/dist/checks/bin/check-workspace-escape.js +249 -0
- package/dist/index.js +1523 -630
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.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 +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 +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 +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 +3 -3
- 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 +3 -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 +3 -3
- 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 +3 -3
- 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 +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]__02a-3g8._.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]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__040smoa._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__16cuydh._.js → [root-of-the-server]__0ozbaq0._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0ykun-j._.js → [root-of-the-server]__0z6dav-._.js} +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.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]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{apps_vinaya-studio_web_src_app_studio_18kqbvs._.js → apps_vinaya-studio_web_src_app_studio_06it6s-._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.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/0r4i_ks9z02bw.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 1hfhxjy577l5d.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1k916ih3p4nsg.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 1t2kft7bxzuxn.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 1yqs2zgvwa4p1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 25kbt5k-xb2a5.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 2_k6j1o9q5nn4.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2hmannd4bjhzy.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3mqu_uhluy19i.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0la0hhmjprdxm.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1lk7h-0buf4ab.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
- /package/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -19,12 +19,15 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
19
19
|
| `vinaya doctrine [--role <name>]` | Print the absolute path of the bundled doctrine's front door (`aeg-root/skills/aeg/SKILL.md`) on this machine. The committed root `VINAYA.md` pointer names the package, never a filesystem path — this command is the read-time resolution step it hands the reader. `--role <name>` resolves straight to a specific role's doctrine instead (`aeg-root/roles/<name>.md`, under the same resolved root), validated against the role names actually enumerated under `roles/*.md` at request time — an unknown name fails cleanly and lists the valid names, never a silent bad path. `--json` for the enveloped `{ root, entry }` form. |
|
|
20
20
|
| `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
|
|
21
21
|
| `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
|
|
22
|
+
| `vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] [--model <id>] [--transcript <path>]` | Emit two generated, never-hand-typed blocks a PR body carries: `AEG:EVIDENCE` (the head sha, the width-invariant `git diff --numstat` against `origin/main`'s merge-base, and the result of `vinaya check --all --diff-only`) and `AEG:TOKENS` (real usage figures collected the same way `vinaya tokens` collects them, rendered into the `## Token report` heading's table). Without `--write`, prints the Evidence block to stdout. With `--write <body-file>`, replaces the Evidence block in place but APPENDS a new row to the Tokens block on every run — a re-entry after `CHANGES_REQUESTED` reports again, and a second report is a second row, never a sum or an overwrite. `--phase`/`--role` default to `<n>: develop`/`Developer` (`<n>` parsed off a `task/<tranche>/<n>` branch); `--transcript` names a session transcript directly, skipping Stop-hook pointer-file resolution. An incapable host (no transcript resolves, or it resolves to zero usage records) gets an all-`—` row carrying the probe's reason inline in the Agent/Model cell, never a fabricated `0/0/—`. Exits non-zero when any gate in Group B fails. |
|
|
22
23
|
| `vinaya review post --role code-reviewer \| security --pr <n> ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (verdict, findings, per-field text) instead of a hand-typed comment. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`Judged head:` line from validated inputs, never from caller-supplied text; refuses a contradictory verdict (a BLOCKER/CRITICAL-or-HIGH finding with a clean verdict) before posting anything; and after posting, re-fetches the comment and refuses to exit 0 unless it re-parses through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls. `--json` for the enveloped machine form. |
|
|
23
24
|
| `vinaya tokens --phase <phase> --role <role> [--model <id>] [--transcript <path> \| --in <n> --out <n>]` | Print a role's `Tokens: …` report line — the portable front door over the token-report collection adapter, resolving `@attalabs/aeg-core`'s `resolveMeteringCapability`/`summarizeTranscript`/`formatTokensLine` from the installed package rather than a repo-relative path, so it works in an adopter checkout with no local `packages/`. `--transcript <path>` reads a session transcript directly; omitted, it resolves via the Stop-hook pointer file where one is installed. `--in <n> --out <n>` bypasses transcript resolution entirely for a host whose figures arrive by some other means. Refuses rather than emitting `0/0/—` when no transcript resolves, a resolved one can't be read, or it summarizes to zero usage records — the same capability probe backs a `vinaya doctor` finding when it reports incapable. |
|
|
25
|
+
| `vinaya archive [--merge-sha <sha>]` | Post-merge Archivist: resolves the merged PR from a merge SHA (`HEAD` by default), assembles and posts the `### AEG provenance` comment via `@attalabs/aeg-core`'s `buildProvenanceBlock`, then closes the PR's `Closes #N` Issue. Idempotent — a PR that already carries the block (`hasProvenance`) is skipped, posting nothing new. Appends its own one-line `Tokens: …` report to that same comment, collected via the same metering adapter `vinaya tokens` uses: the sanctioned all-`—` line on an incapable host, or — when the probe reports capable but summarized to zero tokens — the line is omitted and flagged `DANGLING (tokens): …` instead, since a capable host's blank is never licensed to read as `—`. Provenance and Issue-closure always proceed regardless of that outcome; a missing token row never withholds either. `vinaya archive tranche <slug> [--yes]` closes a tranche's legacy Milestone once every `vinaya/tranche:<slug>`-labeled Issue is closed. `rings.ring2_asyncAudits: true` skips this command's work entirely (see Config below). |
|
|
24
26
|
| `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. `--port <n>` binds an exact port — see below. |
|
|
25
27
|
| `vinaya milestone create --title <title> --body-file <path>` | Create a GitHub Milestone from a validated body. The title is free text, never parsed for a version — the description's optional `Release:` field is the sole authority for one. Refuses before any `gh` write when the goal is absent, `Release:` is present but malformed, or an `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
|
|
26
28
|
| `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]` | Move one or more existing tranches into a target Milestone: reattaches every Issue carrying each `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact is gathered and refused-or-passed as ONE batch before any write — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone refuses the whole invocation, not just its own slug. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
|
|
27
29
|
| `vinaya milestone edit <n> --body-file <path>` | Edit an existing Milestone's description after the same `checkMilestoneShape` gate `create` runs — the gated replacement for a raw `gh api PATCH` against a Milestone. Only the description changes; the title is untouched. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
|
|
30
|
+
| `vinaya milestone close --slug <slug>` | Close a tranche's Milestone — the gated replacement for the raw `gh api .../milestones/<n> -X PATCH -f state=closed` recipe the Tranche Archivist used to run on faith. Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge. `--validate-only` verifies attachment without closing; `--json` for the enveloped machine form. |
|
|
28
31
|
| `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
|
|
29
32
|
|
|
30
33
|
## Config
|
|
@@ -73,6 +76,58 @@ To declare a domain beyond those two — a `migrations/` folder, a codegen outpu
|
|
|
73
76
|
|
|
74
77
|
`checkBlastRadiusScope` is one of three content checks `vinaya issue create`/`vinaya issue edit` run automatically on a task Issue (any `vinaya/tranche:*` label) — unconditional, not something `vinaya.config.json`'s `briefSchema` opts into or out of. The other two: `checkNoBriefContent` refuses an Issue body carrying a brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan` — those belong in the brief, not the Issue); `checkRationaleNamesDocs` refuses a rationale whose "Docs to keep coherent"/"Traps" fields name no concrete doc/skill path, unless it carries the explicit `no-doc-surface` sentinel. All three grade what the eight-field Planner rationale (`checkIssueRationale`) *says*, once that gate has confirmed the fields exist.
|
|
75
78
|
|
|
79
|
+
### Config-native project metadata (`projects`)
|
|
80
|
+
|
|
81
|
+
`vinaya init product <name>` has always appended a row to `.vinaya/projects.md` (the project registry). It now *also* appends an entry to `vinaya.config.json`'s `projects` array — a second, config-native home for the same declared fact, alongside the registry file rather than instead of it:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"projects": [
|
|
86
|
+
{ "name": "mobile", "path": "apps/mobile", "description": "The mobile client" }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`name` is required (the dedup key, matching the registry row's own `Project` column); `description` and `path` are optional. This key is display metadata only — no gate or resolver reads it, and single-project repos rightly have none. `vinaya doctor` reports, at `info` severity, when a registry row and a `projects` entry name the same project but only one of the two exists — never an error, since keeping only the registry file is a fully supported shape.
|
|
92
|
+
|
|
93
|
+
### Token-usage collection for non-Claude-Code hosts (`tokens.collect`)
|
|
94
|
+
|
|
95
|
+
`vinaya tokens` ships one collection adapter, for Claude Code — it reads that host's own session transcript. A repo whose coding-agent host is something else (Codex, Grok build, or any other harness) has no route to a real `Tokens:` line without declaring one:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"tokens": {
|
|
100
|
+
"collect": "node scripts/collect-usage.js"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**`tokens.collect` must be exactly `"<interpreter> <repo-relative-script-path>"`** — two whitespace-separated tokens, nothing else: no flags, no extra arguments, no shell syntax (`&&`/`|`/`;`), no quoting. `vinaya tokens` spawns the interpreter directly with the script as its one argument — never through a shell — whenever it is declared and `--in`/`--out` are not given, and parses its stdout as a JSON object shaped:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"inputTokens": 0,
|
|
110
|
+
"outputTokens": 0,
|
|
111
|
+
"cacheCreationInputTokens": 0,
|
|
112
|
+
"cacheReadInputTokens": 0,
|
|
113
|
+
"model": "your-model-id-or-null"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Absent, `vinaya tokens` falls back to the shipped Claude Code transcript adapter unchanged — this key only adds a second route, never removes the first, and a Claude Code adopter continues to get working enforcement on `vinaya upgrade` having declared nothing. When declared, a command that exits non-zero or prints output that doesn't parse into that shape fails loudly rather than silently falling back to the transcript route or emitting a plausible `0/0/—`.
|
|
118
|
+
|
|
119
|
+
This is an opt-in collection route, not a capability declaration: whether a host is treated as capable of metering itself is always *probed*, never read from this key — there is no `tokens.metering` field.
|
|
120
|
+
|
|
121
|
+
Read from the repo-root config only, same trust class as `checks`/`principals`/`releaseActor`: a value that decides what command runs on this turn must come from the reviewed, committed per-repo file, never a machine-wide personal config — a global `~/.vinaya/config.json`'s `tokens` key is stripped at load time with a loud stderr warning, never resolved.
|
|
122
|
+
|
|
123
|
+
Unlike `ci.setup` — which only ever executes inside a generated, reviewed CI workflow step, under the runner's own isolation — a declared `tokens.collect` script executes IN-PROCESS, unsandboxed, on whatever machine runs the ordinary `vinaya tokens` command. Three layers close that gap:
|
|
124
|
+
|
|
125
|
+
- **Rigid grammar.** The `"<interpreter> <script>"` restriction above is not a style preference — it is what makes the content pin below rigorous rather than heuristic. Because a declaration can only ever mean one file, `vinaya tokens` never has to guess which token of an open-ended shell string "looks like a path".
|
|
126
|
+
- **Trust gate, content-pinned.** `vinaya tokens` refuses to run `tokens.collect` at all until a human has explicitly approved this exact interpreter/script declaration, AT the script's exact current content, for this repo, on this machine: run `vinaya tokens --trust-collect` once. Approval is keyed to this repo's git common directory, not to any one worktree, so it survives a fresh `git worktree add` of the same repo; a different interpreter, a different script path, or so much as one byte of script content changing — committed or not — needs its own fresh approval. Approvals live in `~/.vinaya/tokens-collect-trust.json`, machine-local and never read from any committed file, so a pull request can no more grant itself trust than it can add itself to `principals`.
|
|
127
|
+
- **Printed audit trail.** Once trusted, `vinaya tokens` still prints the exact interpreter/script to stderr immediately before every run, so nothing executes invisibly even after approval.
|
|
128
|
+
|
|
129
|
+
This is a trust-then-verify design, not a blocking interactive prompt — the unattended-agent path this key exists for keeps working once a human has approved the script's content a single time. Editing only the script, never `vinaya.config.json`, requires that same fresh approval again — this is the specific gap two earlier, less rigid designs left open (security review, PR #303), and the reason the grammar above is fixed rather than an arbitrary shell string.
|
|
130
|
+
|
|
76
131
|
## Where the git hooks live
|
|
77
132
|
|
|
78
133
|
`vinaya init` installs the ring-0 hooks (`pre-commit`, `pre-push`, `commit-msg`) into a **tracked** `.vinaya/hooks/` directory and points git at it with `git config core.hooksPath .vinaya/hooks` — commit that directory. Raw `.git/hooks` is never versioned by git, so hooks installed there exist only on the installing machine; tracked hooks travel with the repo into every clone and every linked worktree checkout.
|
|
@@ -156,6 +211,7 @@ Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.att
|
|
|
156
211
|
- `agent_recovery_prompt` is a corrective **instruction**, not a restated diagnosis — it tells the model what to do, not what is wrong (that's `message`'s job).
|
|
157
212
|
- Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
|
|
158
213
|
- 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).
|
|
214
|
+
- `token-collection-wired` (ring 0, part of `vinaya check --all --local`'s managed hooks) is a worked example of this contract's narrowest shape: it consults only local `fs`/`process.env` — no PR body, no network — and refuses a commit only when the host's token-metering probe (`resolveMeteringCapability`, `@attalabs/aeg-core`) found a transcript pointer that is this session's on one of two grounds — its recorded id matches ours and the transcript it named could not be reached, or its id could not be read at all yet the file sits at this project's own pointer path owned by this user, which is broken wiring whoever wrote it. A host never wired to meter passes, and so does one holding only a pointer that provably belongs to another session — a plain human terminal carrying an earlier session's leftover, or a second agent session whose Stop hook has not fired yet. Both are the sanctioned operator-metered case, not a wiring defect.
|
|
159
215
|
|
|
160
216
|
`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.
|
|
161
217
|
|
|
@@ -186,6 +242,24 @@ The generated review-authority workflows run only default-branch code: the requi
|
|
|
186
242
|
|
|
187
243
|
`.vinaya/doc-owners` binds code globs to the docs that must change with them; `vinaya check`'s C5 gate enforces that binding against each pull request's own diff — it fires only when a changed file matches a bound glob. That leaves a gap C5 cannot close on its own: a binding whose code was deleted or renamed wholesale matches nothing on any later diff, ever again, and reads as healthy forever. `vinaya doctor` closes it separately — it never fails `vinaya check` — by walking every binding against the repo's full tracked-file list and reporting any whose code glob matches zero tracked files anywhere in the repo, or whose in-repo doc pointer doesn't exist on disk. Report-only, like every other `vinaya doctor` diagnostic: it repoints or removes nothing itself.
|
|
188
244
|
|
|
245
|
+
## Changeset coverage
|
|
246
|
+
|
|
247
|
+
The `changeset-coverage` core check couples a diff touching a published package's shipped files to a `.changeset/*.md` entry in the same diff — for each member of `.changeset/config.json`'s `fixed` group, a changed path counts as shipped iff it falls under that member's own `package.json` `files` allowlist, read live from every workspace member's own manifest, never a hardcoded path list. The Changesets-release branch itself is exempt by construction. Report-only (`scope: diff`, ring 0): findings print at `warning` severity and the check's own exit code always stays `0`, so installing it cannot newly redden an existing repo's CI. Dormant when the repo carries no `.changeset/config.json`, or none of its `fixed`-group members resolve.
|
|
248
|
+
|
|
249
|
+
## Quoted-command staleness
|
|
250
|
+
|
|
251
|
+
A doc that quotes a command or config line verbatim, in backticks, as a statement of present fact ("what runs today: `X`") goes stale silently once the thing it quotes changes — every other gate can pass while the quotation is simply wrong. The `quoted-command` core check catches this, but **only for spans an author explicitly marks** — it never infers from a command-looking span, because an adopter-facing instruction (a README's `npx @attalabs/vinaya init`) is not a claim and flagging it is exactly the false-positive shape that gets a check disabled.
|
|
252
|
+
|
|
253
|
+
Opt a span in by wrapping it in an `AEG:QUOTES-FILE` marker pair naming the file it quotes:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
one job — <!-- AEG:QUOTES-FILE:START:.github/workflows/ci.yml -->`npm test`<!-- AEG:QUOTES-FILE:END --> in CI
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Both markers are HTML comments — invisible on render — and may sit inline within a sentence or on their own lines around a fenced block. The check reads the text between them (an inline `` `span` ``, a fenced block, or bare text — one layer of wrapping is stripped either way) and asserts it still appears verbatim in the named file's current content; a marker inside a fenced/inline code example (e.g. one demonstrating this very syntax) is ignored, never mistaken for a real annotation. A finding names both sides: what the doc claims, and which file no longer contains it.
|
|
260
|
+
|
|
261
|
+
Swept corpus: the same `ships`/`reader-facing` governed-doc classes `reader-resolvable-prose` sweeps (`<doctrineRoot>/**` by default, plus any configured reader-facing pages) — never `apps/*/specs/**` or a `CLAUDE.md`. The cited file itself can be anywhere in the repo. Report-only (`scope: diff`, ring 0): findings print at `warning` severity and the check's own exit code always stays `0`, so installing it cannot newly redden an existing repo's CI. Dormant everywhere no doc carries the marker.
|
|
262
|
+
|
|
189
263
|
## Brief-schema divergence
|
|
190
264
|
|
|
191
265
|
`briefSchema` in `vinaya.config.json` is yours: `vinaya upgrade` preserves it wholesale and never rewrites it. On its own that ownership has a silent cost — nothing else reads it either, so a builtin deleted to work around a defect stays deleted, with no later upgrade to repair it and nothing to surface it.
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -16,11 +16,11 @@ provenance: >
|
|
|
16
16
|
they are not in the surface map of a task that only meant to add a gate:
|
|
17
17
|
|
|
18
18
|
- packages/aeg-core/src/registry-parse.test.ts asserts the TOTAL row count
|
|
19
|
-
across all three rings (currently
|
|
19
|
+
across all three rings (currently 47).
|
|
20
20
|
- packages/aeg-core/src/markdown-table.test.ts asserts per-ring counts
|
|
21
|
-
(currently ring 0 =
|
|
21
|
+
(currently ring 0 = 17, ring 1 = 22, ring 2 = 8).
|
|
22
22
|
- packages/aeg-core/src/docs/node-route.test.ts asserts the ring-0 count
|
|
23
|
-
(currently
|
|
23
|
+
(currently 17) alongside the actions/roles/contracts counts.
|
|
24
24
|
|
|
25
25
|
Bumping those numbers is a mechanical consequence of a correct edit, not a
|
|
26
26
|
design change — fix them in the same commit and say so in the PR body.
|
|
@@ -97,6 +97,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
97
97
|
| 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. | product | `packages/aeg-core/bin/check-first-push-dispatch.ts` |
|
|
98
98
|
| A task branch's first push (Issue self-assignment) | Ever had no idea who was actually working a task? | hook | Assigns the task's Issue to the authenticated pusher on the branch's genuinely first push — visibility automation, deliberately not a gate. | **`assign-task-issue` standalone shim** (the registered `issue-assignment` check mirrors it; the Issue-self-assignment note below this table is the same mechanism's prose record) | Nothing — this row names a mechanism, not a refusal. Fail-open by contract: it runs after every blocking gate has passed, degrades every failed forge call to a skip, warns and exits 0 on any API failure, and nothing gates on its exit code — a failed assignment can never block a legitimate push. Idempotent: an already-assigned Issue is a no-op, and a push to an already-existing branch never re-triggers it. | repo-own | `packages/aeg-core/bin/assign-task-issue.ts` |
|
|
99
99
|
| Committing or pushing while checked out on the default branch | Ever committed straight onto main because you forgot to cut a worktree first? | hook | Refuses a commit or push whose current branch IS the repo's default branch — mechanizing the worktree-plus-PR rule at ring 0 for every adopter, not just this repository's own hand-written pre-push script (the `git push` row above, `repo-own`, predates this check and covers a different class: a push whose destination *ref* is the default branch, not a local checkout parked on it). Registered in `coreCheckRegistry()` (task 9), so `vinaya init` ships it to every adopter through the generated `check --all --local` hooks — closing the gap where this rule previously reached only this repo's own maintainers. | **`main-branch-refusal` check** (`vinaya check main-branch-refusal`, part of `vinaya check --all --local`) | The discriminator is the SYMBOLIC current branch, not any derived name: `git symbolic-ref --short HEAD` equaling the repo's default branch refuses; a detached HEAD (every CI checkout, or a deliberate `git checkout --detach`) has no symbolic branch at all and always passes; any other named branch passes. A plain `vinaya check --all` run while parked on the default branch locally WILL refuse — intended, since work belongs in a worktree, never a bug. The default branch is derived from the already-fetched local `refs/remotes/origin/HEAD`, never hardcoded as `main`; when that ref cannot be resolved (never fetched, or a remote other than `origin`) the check fails OPEN with a `warning` finding naming why, rather than risk a false refusal. Otherwise a real failure (`error`, exit 1) — this is an action refusal, not a doctrine-parity report, so warn-only would be the check refusing to do its one job; it cannot redden existing CI, since CI always runs on a detached HEAD. Local-only: both facts come from `git` alone, no forge call. The ring-2 `direct-push` audit (`checkDirectMainPush`, `direct-main-push.ts`) stays the untouched, unrelated detection backstop for a commit that lands on the default branch some other way this hook cannot reach at all (a repo-admin action, or an adopter who never ran `init`). | product | `apps/cli/src/checks/bin/check-main-branch-refusal.ts` |
|
|
100
|
+
| Committing when the token-metering adapter is wired but unreachable | Ever had a host that could genuinely meter itself silently report `—` because one specific path broke? | hook | Refuses a commit when the token-metering probe finds a wiring point resolved — a transcript pointer naming a path — but cannot reach what it names. A host never wired to meter at all still passes unchanged. | **`token-collection-wired` check** (`vinaya check token-collection-wired`, part of `vinaya check --all --local`) | `resolveMeteringCapability`'s verdict is `capable: true`, or `capable: false` with reason `no-transcript-resolved` — no pointer at all, or one that provably belongs to another session. That is the sanctioned operator-metered case (**Operator-metered** — a role whose host exposes no usage figure to the agent, leaving a human the only source), and it covers a plain human terminal and a second agent session whose own Stop hook has not fired yet. Refusal needs the pointer to be THIS session's on one of two grounds, and only one of them involves a session id: either its recorded id matches ours (`transcript-unreadable`, `transcript-empty` — we followed our own pointer and could not reach figures), or the id could not be read at all and the file nonetheless sits at this project's own pointer path owned by this user (`pointer-unusable` — the pointer itself is unreadable or malformed, which is broken wiring whoever wrote it, cleared by removing the file). Local and offline only: no PR body is read (none exists yet at pre-commit) and no network call is made. | product | `apps/cli/src/checks/bin/check-token-collection-wired.ts` |
|
|
100
101
|
|
|
101
102
|
**Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
|
|
102
103
|
|
|
@@ -112,7 +113,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
112
113
|
|
|
113
114
|
Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
114
115
|
|
|
115
|
-
> **Packaging (this repo):** every deterministic row below runs inside **one job —
|
|
116
|
+
> **Packaging (this repo):** every deterministic row below runs inside **one job — <!-- AEG:QUOTES-FILE:START:.github/workflows/vinaya-checks.yml -->`vinaya check --all --diff-only`<!-- AEG:QUOTES-FILE:END -->** in `.github/workflows/vinaya-checks.yml` (the generated, vinaya-managed workflow), not as its own job: one checkout/build, with each check reporting its own pass/fail line in that job's log. The two exceptions are `Typecheck + unit tests` (the hand-written `.github/workflows/ci.yml`) and the Review gate (its own required check in `.github/workflows/vinaya-review.yml`, re-triggered by `vinaya-review-verdict.yml` when a verdict comment lands). Same checks, same strictness, at ring 0 and ring 1 — the runner is one codebase invoked from both.
|
|
116
117
|
|
|
117
118
|
| CI check | Summary | Category | Description | Re-verifies | Audience | implementation |
|
|
118
119
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -127,7 +128,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
127
128
|
| AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Requires independent review verdicts to exist on every pull request before merge. | No automated reviewer workflow is installed in this repo — review passes are separate, fresh-context dispatched agent sessions that post verdict comments on the PR. `vinaya-review-verdict.yml` evaluates a landed verdict comment (only principal-allowlisted authors count — the same verdict-author verification rule the Review gate applies) and re-runs the required review-gate check so it can go green natively. Until clean verdicts exist, the required check stays red by design. | repo-own | `.github/workflows/vinaya-review-verdict.yml` |
|
|
128
129
|
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. | product | `packages/aeg-core/bin/verify-review-gate.ts` |
|
|
129
130
|
| G1 — implementation exists | Ever read about a safeguard that turned out not to actually exist? | ci | Re-checks that every gate the doctrine describes has real code behind it. | Every ring's non-empty `implementation` cell on this very page resolves to a real path on disk — makes this page's registry columns load-bearing instead of decorative. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
130
|
-
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing — neither directory exists — so the live coverage is the seeded `packages/aeg-core/bin/*.ts`
|
|
131
|
+
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, `apps/cli/src/checks/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing — neither directory exists — so the live coverage is the two seeded bin sets, `packages/aeg-core/bin/*.ts` and `apps/cli/src/checks/bin/*.ts` — the latter is where most core check bins actually live.) **Scaffold-writer** (task 22, later widened to cover that second location): `verify-registry.ts --scaffold` auto-inserts a stub row for an orphan candidate whose ring is mechanically derivable — a `packages/aeg-core/bin/*.ts` file resolving through `GATE_AUDIENCE` to a registered check (its ring comes from that check's own `apps/cli/src/checks/registry.ts` entry), an `apps/cli/src/checks/bin/*.ts` file whose `check-`-stripped name resolves through the analogous `CLI_CHECK_RING` mirror table, or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered bin under either directory) gets no stub and stays a plain finding — the writer never guesses. A second, independent half of G2 scans every row's `summary`/`description`/`spec` cells for the literal placeholder marker `[undocumented — fill in why]` the scaffold writes into every non-mechanical cell: a stub row that filled only `implementation` would otherwise read as "documented" to the orphan half above and silence G2 outright, which is the exact trap this second half exists to close. The writer only ever appends a new row after a ring table's last existing row — it never edits, reorders, or removes one, so a hand-authored row (including `workspace-escape`'s and `main-branch-refusal`'s own) is untouched. Repo-side only: the writer lives on `verify-registry.ts` (aeg-core), never on the shipped `check-registry-gates.ts` (apps/cli) — an adopter's checks stay read-only over doctrine. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
131
132
|
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-0 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
132
133
|
| G4 — cited forge numbers resolve | Ever read a doc that cited a ticket number that didn't exist? | ci | Re-checks that every Issue and PR number cited in the docs resolves to a real one. | Every `#NNN` cited anywhere in this page's body is a real Issue or PR in the forge — a fabricated citation fails the build. **Currently scans nothing by design** (task 14): this page's body carries zero forge citations, since task 3 banned them from `aeg-root/**` as doctrine and the `reader-resolvable-prose` check (task 15) now enforces that ban directly. G4 is a standing guard against reintroduction, not a live proof of ongoing citation correctness — it fires the moment a `#NNN` reappears here and doesn't resolve, but has nothing to scan while the ban holds. Verified live: `packages/aeg-core/src/registry-checks.test.ts` appends a fabricated citation to this page's real content and asserts `checkG4` catches it. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
133
134
|
| G5 — role/contract integrity | Ever had a process doc reference a role that was never actually defined? | ci | Re-checks that every role and contract the doctrine references is really defined. | Every `aeg-root/contracts/*.md`'s `producer`/`consumer` names a real `role_id` from `aeg-root/roles/*.md`; every role's `performs`/`refuses_when` frontmatter is present and non-empty. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
@@ -137,11 +138,17 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
137
138
|
| doctrine-portability | Ever shipped doctrine that named a file only you could see? | ci | Re-checks that shipped doctrine doesn't cite a path that exists only in the authoring repository. | A cited path's top segment judged against an allow-list: the caller-supplied ships prefix itself (`<doctrineRoot>/`, `aeg-root/` by default — doctrine citing its own tree, wherever that tree actually lives for the caller, checked dynamically so a non-default `doctrineRoot` never falsely flags an adopter's own self-citations), plus a fixed set of static portable prefixes (`roles/`, `contracts/`, `skills/`, and adopter-owned `.github/`, `.vinaya/`, `.claude/`, `.git/`, `.husky/`) — an unclassified prefix is a finding, never a silent pass, so a never-seen author-repo directory (e.g. a build artifact under `apps/*/dist/`) cannot slip through the way it would under a deny-list. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrinePortability`; the bin supplies the only I/O, reading `<doctrineRoot>/**` twice — once from the working tree, once from `BASE_SHA` (`origin/main` by default) via `git show`/`git ls-tree` — and reports only citations present in the former and absent from the latter, so day-one install of an unfixed backlog never turns every adopter's CI red. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`. | product | `apps/cli/src/checks/bin/check-doctrine-portability.ts` |
|
|
138
139
|
| No new on-disk state | Ever had a file and the forge disagree about the same fact? | ci | Blocks a diff that creates a new on-disk state file duplicating what the forge already derives. | No live per-tranche topology file appears under `aeg-root/tranches/` (a top-level file there fails on add or edit; under `completed/**` only genuinely NEW files are refused, so the legacy archives stay editable), and no new `*.tokens.md` appears anywhere — token figures live in the PR body, and the ledger row is appended post-merge. Status-aware (`git diff --name-status`), never fired by a deletion; a rename counts as an addition at its new path. The registered `no-disk-state` check runs the same `isNewDiskStateFile` predicate at ring 0 (the managed hooks) and here; this row names the standalone CLI shim of that gate. | product | `packages/aeg-core/bin/check-no-disk-state.ts` |
|
|
139
140
|
| workspace-escape | Ever deleted a package only to break a sibling's tests through a path nobody's dependency graph saw? | ci | Re-checks that no source file's constructed filesystem reference reaches outside its own workspace package, or points at a path that does not exist. | Every `readFileSync`/`readFile` call with a literal relative-path argument, and every `new URL('…', import.meta.url)`, across every `.ts`/`.tsx` file under `apps/*`/`packages/*` — resolved against the citing file's own directory and judged against its own two-segment workspace-package prefix (`apps/<name>`, `packages/<name>`), never against `import`/`require` module specifiers (the dependency graph already owns those). Zero I/O in `@attalabs/aeg-core`'s `findWorkspaceEscapes`; the bin supplies the only I/O, walking the working tree once for source content and once for the existence-check universe (files AND directories, so a reference to a directory itself resolves as existing). Registered in `coreCheckRegistry()` (task 17), report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability` — this repo's own corpus carries one pre-existing genuine escape (`packages/sources/src/commands-router-coverage.test.ts` reaching into `apps/cli/src/index.ts`, structurally the same incident this check exists to catch), and a blocking check on day one would fail this repo's own CI on that finding before a separate task has triaged fixing it. | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
|
|
141
|
+
| changeset-coverage | Ever shipped a change to a published package and the release train never picked it up? | ci | Re-checks that a diff touching a published package's own shipped files also carries a changeset in the same diff. | For each member of `.changeset/config.json`'s `fixed` group, a changed path counts as SHIPPED iff it falls under that member's own `package.json` `files` allowlist, read live from every workspace member's own manifest — never a hardcoded path list, never a directory heuristic. A diff that hits at least one shipped path with no `.changeset/*.md` entry (excluding `README.md`) in the same diff is a finding, naming the shipped paths hit. The Changesets-release branch itself is exempt by construction (its diff IS the changesets being consumed) — the exemption reuses `@attalabs/aeg-core`'s own release-branch constant, the same one `body-bare-digits`'s Changesets-release exemption is keyed on, rather than inventing a second branch-name special-case; unlike that check it never live-fetches a PR author, since a report-only, always-exit-`0` check has no gate for a spoofed branch name to bypass. Written after four separate published-package pull requests, over the course of one day, each merged with no changeset and caught only by a human after the fact — the obligation existed in no enforceable place before this check: not in `roles/developer.md`'s deliverable conventions (now amended alongside this row), not in any registered check. Zero I/O in the pure predicate (`changeset-coverage-logic.ts`); the bin supplies the only I/O — reading the config, every fixed-group member's own manifest, and the diff. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape` above — graduating to a blocking check, and any waiver-label escape that would need, is a later, separately-dispatched decision once the false-positive rate is observed; v1 carries no such escape. | product | `apps/cli/src/checks/bin/check-changeset-coverage.ts` |
|
|
142
|
+
| quoted-command | Ever read a doc that quoted a command as "what runs today", and the command changed underneath it? | ci | Re-checks that a doc's explicitly marked quote of a command or config line still matches, verbatim, the file it names as its source. | Marker-based only, by explicit Principal decision, not inferred from command-looking spans: a doc opts a span in with `<!-- AEG:QUOTES-FILE:START:<path> -->…<!-- AEG:QUOTES-FILE:END -->` (the same invisible-on-render, code-blind HTML-comment idiom `anchored-region.ts` uses for its own six PR/Issue-body fields, reusing that file's masking primitive rather than a second parser — but a distinct marker kind, since the cited path is free text, not one of that file's six closed field names). An adopter-facing command in a README with no marker is instruction, not a claim, and is never flagged — inference over command-looking spans was explicitly rejected as the false-positive shape that gets a gate disabled. `findCitedQuotes` sweeps the identical `ships`/`reader-facing` corpus `reader-resolvable-prose` sweeps (never a second notion of "governed doc"); `evaluateCitedQuotes` then checks each marked span against its cited file's live content, wherever that file lives in the repo, naming both sides in a finding — what the doc claims, and which file no longer contains it verbatim. Zero I/O in `@attalabs/aeg-core`'s pure predicate (`quoted-command.ts`), corpus-tested against the real `aeg-root/**` tree; the bin supplies the only I/O. Ships with one real, live annotation (this very paragraph's own sibling callout above, quoting the CI invocation this page names) rather than only synthetic fixtures. Written after a measured incident: a CI invocation was pinned to a version in one pull request; this page quoted the pre-pin form verbatim as current fact; every registered check passed, `doctor` reported healthy, security passed, and a code-review agent found the drift only after independent re-derivation. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape`/`changeset-coverage` above — graduating to a blocking check is a later, separately-dispatched decision once the false-positive rate is observed against real adopter corpora; v1 carries no waiver escape. | product | `apps/cli/src/checks/bin/check-quoted-command.ts` |
|
|
140
143
|
|
|
141
144
|
**G-checks rollout (task 3):** G1/G2 shipped report-only at first — they could only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surfaced as visible debt without retroactively failing in-flight work. G3/G4/G5 have been blocking since that same tranche. **G1 flipped to blocking in task 8**, once its report-only window had let the orphan backlog get cleaned up — a permanent `info` finding on every run is indistinguishable from silence, which is exactly how the gap task 8 closes (G6, below) stayed invisible for as long as it did. G2's inverse sweep (no orphan hook/CLI) stays report-only; its rollout is unchanged. **G6 — doctrine-registry parity**, also added in task 8, ships blocking from the start: it checks that every row this page marks `product` (the `Audience` column, above) actually resolves to a real `coreCheckRegistry()` entry, closing the gap where a row could claim shipped enforcement that no adopter's `vinaya check` ever actually ran.
|
|
142
145
|
|
|
143
146
|
**reader-resolvable-prose / retired-vocabulary rollout (task 15; task 7 registered both in `coreCheckRegistry()`):** both ship report-only, same precedent as G1/G2 above — each can only ever print a `warning` finding, never fail CI (the check's own exit code always stays `0`), so any real backlog surfaces as visible debt rather than a wave of newly-red pull requests (161 findings, all class 2, across `aeg-root` on the day `reader-resolvable-prose` first shipped). A later, separately-dispatched task flips either to blocking once its own backlog is cleared.
|
|
144
147
|
|
|
148
|
+
**changeset-coverage rollout:** ships report-only, same precedent as the report-only rows above — the check's own exit code always stays `0`, so registering it cannot newly redden any existing install. Graduating to blocking, and the waiver-label escape that graduation would need, are both deliberately deferred to a later, separately-dispatched task once the check's own false-positive rate is observed in practice; v1 carries no self-serve or principal-applied escape at all.
|
|
149
|
+
|
|
150
|
+
**quoted-command rollout:** ships report-only, same precedent as every row above — findings print at `warning` severity and the check's own exit code always stays `0`, so registering it cannot newly redden any existing install, and coverage starts at exactly one annotated quote (this page's own CI-invocation callout, above) rather than a retroactive sweep. Graduating to blocking, and any waiver-label escape that graduation would need, are both deliberately deferred to a later, separately-dispatched task once the marker's real-world false-positive rate is observed; v1 carries no such escape.
|
|
151
|
+
|
|
145
152
|
Red CI is now unmergeable by any actor — the repository ruleset's `required_status_checks` rule carries `bypass_actors: []`, so the Principal has no ambient override either. The Principal's override paths are explicit and auditable instead: edit the ruleset itself, or apply an actor-verified `vinaya/waiver:review` label to skip the review gate specifically.
|
|
146
153
|
|
|
147
154
|
---
|
|
@@ -156,7 +163,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
156
163
|
| **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 | repo-own | |
|
|
157
164
|
| 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). | repo-own | `packages/aeg-core/bin/check-direct-main-push.ts` |
|
|
158
165
|
| 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.) | repo-own | `packages/aeg-core/bin/dead-branch-audit.ts` |
|
|
159
|
-
| 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. | repo-own | `
|
|
166
|
+
| 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. | repo-own | `apps/cli/src/commands/tokens.ts` |
|
|
160
167
|
| Published lifecycle audit | Ever wondered if the artifact people actually install still works? | event | Runs the full shipped-command lifecycle against the real published `@attalabs/vinaya` npm artifact — never this workspace's own source. | Weekly (`schedule`), or manual `workflow_dispatch` | Whether the published artifact still passes the shipped-command lifecycle. The window between merging a Version Packages PR and running `changeset publish` by hand names a version the registry does not have yet — a guard step checks the registry first and skips with a loud `::notice::` (exit `0`) in that window, so a red here always means the published artifact is broken, never "not published yet." No secrets — installs a public package. | repo-own | `.github/workflows/published-lifecycle.yml` |
|
|
161
168
|
|
|
162
169
|
---
|
|
@@ -42,7 +42,9 @@ A Milestone stores no status. Its lifecycle (§4) is computed from its tranches,
|
|
|
42
42
|
|
|
43
43
|
The **Architect** (`roles/architect.md`) is the only role that creates a Milestone, via `vinaya milestone create --title <title> --body-file <path>`. The Planner does not create Milestones — a tranche it plans needs no Milestone at all, and most don't (`tranche-model.md` §6). `checkMilestoneShape` refuses a malformed body — missing goal, a `Release:` field present but not a version, an unparseable `Tranche intents` section — before any write reaches the forge, the same discipline `checkIssueRationale` applies to a task Issue.
|
|
44
44
|
|
|
45
|
-
Moving an **existing** tranche into a Milestone is a separate act: `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]`, never done by hand. It reattaches every Issue carrying each named tranche label to the target Milestone's native `milestone` field (GitHub-view hygiene — no reader in this model consults that field, only the label — the same reason `
|
|
45
|
+
Moving an **existing** tranche into a Milestone is a separate act: `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]`, never done by hand. It reattaches every Issue carrying each named tranche label to the target Milestone's native `milestone` field (GitHub-view hygiene — no reader in this model consults that field, only the label — the same reason `vinaya issue create` auto-attaches a brand-new task Issue to its tranche's open Milestone at creation time, resolving `resolveMilestoneAttachTarget`'s legacy-title-or-intent-declared match to the Milestone's own title before handing it to `gh`), then closes (never deletes) each slug's old legacy tranche-Milestone. `checkAdoptable` gathers every fact for every named slug and refuses the whole invocation before any write — an unknown slug, a slug whose label carries no Issues, a target that doesn't exist or is closed, a slug already adopted elsewhere — so one bad slug blocks the whole call, never a partial move.
|
|
46
|
+
|
|
47
|
+
Correcting an already-written Milestone's goal or `Release:` field — `vinaya milestone edit <n> --body-file <path>`, gated by the same `checkMilestoneShape` check `create` uses — belongs to neither role above. Revising a stated goal is the same product call declaring it was (§5, stage 1: "the goal and its scope are a product call, not a derivable fact"), so it is the Principal's (`roles/principal.md` "What the Principal owns"), never the Architect's (its one write is create-once, `roles/architect.md` "What you own") and never the Planner's (its altitude is tranche shape, not Milestone-body grammar).
|
|
46
48
|
|
|
47
49
|
---
|
|
48
50
|
|
|
@@ -62,7 +62,7 @@ If any of these isn't true, refuse rather than write a Milestone `checkMilestone
|
|
|
62
62
|
|
|
63
63
|
## What you own
|
|
64
64
|
|
|
65
|
-
**The Milestone, once.** `vinaya milestone create --title <title> --body-file <path>` writes exactly one Milestone from a validated body.
|
|
65
|
+
**The Milestone, once.** `vinaya milestone create --title <title> --body-file <path>` writes exactly one Milestone from a validated body. `vinaya milestone edit <n> --body-file <path>` exists and is wired — it runs the identical `checkMilestoneShape` gate `create` does — but you do not invoke it. Correcting a Milestone's goal or `Release:` field after it is written is the same class of call as declaring it in the first place, not a bug fix on your own output: `milestone-model.md` §5 already calls the goal and its scope "a product call, not a derivable fact," and a correction to that goal is that same call made again. That call is the Principal's (`roles/principal.md` "What the Principal owns"), not yours. You do not maintain a Milestone across its life; you declare it once.
|
|
66
66
|
|
|
67
67
|
**The `Release:` grammar.** Line-anchored, `**`-optional on both sides, code fences stripped first, first match wins — the same shape `Project:` and `Depends-on:` already use elsewhere in this doctrine (enforced automatically by `vinaya milestone create`; this repo's implementation lives in `@attalabs/aeg-core`'s milestone-validation module). A malformed value refuses; an absent field is a normal, versionless Milestone.
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ If any of these isn't true, refuse rather than write a Milestone `checkMilestone
|
|
|
75
75
|
- **Cut a task Issue.** That's the Planner's canonical plan act (`roles/planner.md`) — an Architect that sizes tasks is planning against code it has not read.
|
|
76
76
|
- **Size a task, or decide a tranche's dependency/conflict edges.** Not your altitude.
|
|
77
77
|
- **Parse a version out of the title.** The title is free text for humans. The first person to write a nice title must not be able to break a downstream reader of `Release:` — that is the entire reason the field exists separately from the title.
|
|
78
|
-
- **Move, close, or edit an existing Milestone.** Adoption and movement are a later tranche's job (`milestone-model.md` §3, `vinaya milestone adopt`) — you create, once, and stop.
|
|
78
|
+
- **Move, close, or edit an existing Milestone.** `vinaya milestone edit` exists and is shipped, but editing a Milestone's already-declared goal or `Release:` field is a product call, not a maintenance task on your own artifact — that's the Principal's (`roles/principal.md`). Adoption and movement are a later tranche's job (`milestone-model.md` §3, `vinaya milestone adopt`) — you create, once, and stop.
|
|
79
79
|
- **Write status anywhere.** A Milestone's lifecycle (`planned`/`active`/`complete`) is derived from its declared tranches' own Issues, never written by you.
|
|
80
80
|
|
|
81
81
|
---
|
|
@@ -163,4 +163,4 @@ You are the last step of the flow (`process.md`) — the close-out that sits und
|
|
|
163
163
|
|
|
164
164
|
## Turn-end: record the ledger rows for every role that turned on this task
|
|
165
165
|
|
|
166
|
-
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 (
|
|
166
|
+
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 (that is `vinaya tokens --phase "<task-id>: archive" --role Archivist`, run against your own session; another host is satisfied by its own means — and if this specific command is unreachable on a self-metering host, read the transcript/log yourself rather than writing `—`, since `—` is sanctioned only when the host itself has no usage figure to expose, not when one adapter for reading it is missing). 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.
|
|
@@ -111,7 +111,7 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
|
|
|
111
111
|
|
|
112
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 one specific case — **the host itself exposes no usage figure to the agent at all** (the Cost cell is always `—`, separately). **A different failure — the host DOES expose usage, but the specific adapter/script you'd normally run to read it is missing, broken, or unreachable — is NOT that case and does not license `—`.** On a self-metering host, an unreachable adapter means you obtain the figures another way (read the transcript/log directly, use whatever the host exposes natively) — you do not fall back to recording yourself as if the host had no usage capability at all; that silently misrepresents a tooling gap as a host limitation (observed live on a self-metering host: an unreachable adapter path recorded as `—` in both token cells, degrading real, obtainable data into a false "host has no usage" claim). 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 or writing `—` for a reason that isn't actually "the host has no usage API."
|
|
113
113
|
|
|
114
|
-
> **On
|
|
114
|
+
> **On a Claude Code host, one command does it:** `vinaya tokens --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. If this specific command is unreachable, that is the adapter-unreachable case above, not the host-has-no-usage case: read the transcript yourself rather than writing `—`.
|
|
115
115
|
|
|
116
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.
|
|
117
117
|
|
|
@@ -205,7 +205,7 @@ All of the following must pass before the PR is opened:
|
|
|
205
205
|
- [ ] Code passes lint/format (this repo: `bun run format-and-lint`)
|
|
206
206
|
- [ ] Tests pass if applicable (this repo: `bun test`)
|
|
207
207
|
- [ ] PR description follows the template, carries the brief, and declares `Tier: 0`
|
|
208
|
-
- [ ] "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
|
|
208
|
+
- [ ] "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 a Claude Code host, `vinaya tokens`) — and again on each re-push after `CHANGES_REQUESTED`; the Archivist appends the ledger row post-merge, you do not
|
|
209
209
|
|
|
210
210
|
### Tier 1 checklist
|
|
211
211
|
|
|
@@ -330,6 +330,7 @@ After every commit: `git log --oneline -3` to confirm the new commit is a direct
|
|
|
330
330
|
- Reference the task's Issue in the PR body (`Closes #N`), not necessarily in every commit message
|
|
331
331
|
- Do not include agent self-attribution / "generated by" trailers in commit messages
|
|
332
332
|
- Never skip verification hooks on commits unless the brief explicitly authorizes it
|
|
333
|
+
- A change to a published package's shipped files carries its `.changeset/*.md` entry in the same PR — the `changeset-coverage` check (`aeg-root/enforcement.md`) reports, but does not yet block, a diff that misses this
|
|
333
334
|
|
|
334
335
|
---
|
|
335
336
|
|
|
@@ -88,7 +88,7 @@ Before planning, you MUST verify every one of these and explicitly confirm them
|
|
|
88
88
|
5. **The relevant prior decisions are known.** You've read the specs and skills that bear on this work, so you don't plan a task that re-litigates a settled call. A past choice's reasoning lives in the spec it governs and in the pull request that made it. If you cannot reach them, STOP.
|
|
89
89
|
6. **The registry resolves every project you'll assign** (`.vinaya/projects.md`) — see the entry gate. "Resolves" is a claim about a file, so back it with the file: for each `Project:` name you will declare, quote that name's exact registry row text in your readiness check. A bare "all projects resolve" with no quoted rows is a self-attestation, not a check — the unregistered name it hides surfaces later, at Issue creation (`checkProjectsRegistered`), after the whole plan is already built on it.
|
|
90
90
|
7. **Open ambiguities are surfaced, not assumed.** If, after the above, real decisions remain unmade (which DB owns this? is structured output required on all vendors?), collect them and put them to the Principal BEFORE planning — do not pick an answer and plan on top of a guess. A plan built on an unstated assumption is a plan that ships the wrong thing.
|
|
91
|
-
8. **
|
|
91
|
+
8. **A finished-but-unarchived prior tranche on each in-scope product is caught.** For every product in scope, check each other active tranche that touches it: if it has **zero open task Issues** (`vinaya/tranche:<slug>`-labeled, `--state open`) — i.e. it is finished — but the Tranche Archivist has not run for it, STOP: *"The previous tranche `<name>` on `<product>` is finished but not archived. Dispatch the Tranche Archivist for it before planning proceeds."* A prior tranche with genuinely open task work is **not** a blocker here — it is an active sibling, not an unarchived one; the correct instrument for it is a declared `conflicts-with` edge wherever the two tranches' surfaces actually overlap (file granularity — strictly better information than product granularity). **Legacy topology file:** for a tranche still carrying a pre-cutover file, the same finished-but-unarchived signal is: the file still sits at `aeg-root/tranches/<name>.md` (not yet moved to `aeg-root/tranches/completed/<name>.md`) *and* it has zero open task Issues. This is the legacy case only — the forge-native default (see the top of this doc) carries no such file to check at all. The contract governing this gate is `aeg-root/contracts/tranche-archivist-planner.md`. **Supersession carve-out:** this gate does NOT apply to a prior tranche that *this* plan is superseding — i.e. absorbing `todo`/backlog tasks from. For that one source tranche you refactor it in-place during this plan (see "Tranche refactor & cross-tranche task-movement" below) and the Tranche Archivist archives it *after* the plan lands; the order is refactor-and-plan → then archive, never archive-then-plan. The gate still fully applies to every *unrelated* prior tranche.
|
|
92
92
|
|
|
93
93
|
**State the readiness check explicitly at the top of your planning pass** — a short "Readiness: I have X, Y, Z; I verified I can read A, B; the following are unresolved and I need answers before I proceed: …". This makes it visible that the gate was run, not skipped. A plan emitted without a passed readiness check is malformed. (This is also conversational-protocol step 6 — announce the gate's result before moving on.)
|
|
94
94
|
|
|
@@ -53,6 +53,7 @@ The Principal holds final authority over:
|
|
|
53
53
|
- **The merge button.** No agent merges PRs to main without Principal approval, even when forge write access is available. Merge authority is Principal-only unless the brief explicitly delegates it for a specific PR and the brief was authored by the Brief Author.
|
|
54
54
|
- **Right to reject.** The Principal can reject a direction at any phase — idea, brief, PR, or post-merge — and the system accepts that without pushback. Agents can surface concerns but not override.
|
|
55
55
|
- **Ratification.** Type 1 decisions are not ratified until the Principal explicitly says so. PENDING decisions wait; they do not auto-promote.
|
|
56
|
+
- **Editing an already-declared Milestone.** `vinaya milestone edit <n> --body-file <path>` corrects a Milestone's goal or `Release:` field after the Architect's one-time write — the same product call declaring it was (`milestone-model.md` §5), never the Architect's or Planner's (`roles/architect.md` "What you do NOT do").
|
|
56
57
|
|
|
57
58
|
---
|
|
58
59
|
|
|
@@ -106,8 +106,8 @@ If you don't have the information to fill a field, write "unknown — Principal
|
|
|
106
106
|
|
|
107
107
|
### 3. Close the Milestone
|
|
108
108
|
|
|
109
|
-
- `
|
|
110
|
-
- The Issues themselves are already closed (verified in step 1) and
|
|
109
|
+
- `vinaya milestone close --slug <slug>` — never the raw `gh api` recipe this step used to carry. It resolves the tranche's Milestone (legacy exact-title match, or an intent-declared match), verifies every Issue carrying `vinaya/tranche:<slug>` is actually attached to it via GitHub's native `milestone` field, and refuses — naming each unattached or foreign Issue and its repair path — before closing anything. Closing the Milestone IS the tranche's lifecycle transition to `complete`. This is a forge action, not a repo commit.
|
|
110
|
+
- The Issues themselves are already closed (verified in step 1) and, once `milestone close` has run, are verified attached to the closed Milestone — not merely assumed to be — which is what makes that attachment the durable historical record.
|
|
111
111
|
- **Legacy exception:** if this tranche still has a pre-cutover topology file at `aeg-root/tranches/<name>.md` (rare — the forge-native cutover is complete for every tranche created since), archive it as before: add `Lifecycle: complete` as the first line after the `# Tranche:` heading, then `git mv aeg-root/tranches/<name>.md aeg-root/tranches/completed/<name>.md`. Do NOT delete it — the rationale is durable history. Confirm the move landed and the source path no longer exists.
|
|
112
112
|
|
|
113
113
|
> **Step 4 (a pinned per-project state Issue, hand-edited in place) is retired.** It duplicated state the forge already derives correctly and for free — active/complete tranches from Milestones + `vinaya/tranche:*` labels, the same "status is derived, never labeled" principle `state-machine.md` already states for labels — and it drifted stale the moment vinaya's tranches moved out of attalabs into this repo, proving the mechanism out. "Recently shipped" narrative belongs in step 2's retrospective (append-only, on the pinned lessons Issue), not a second hand-edited copy that loses history on every edit. Nothing in this role's entry gate ever depended on step 4, so no tranche close was ever blocked by its absence — it only ever surfaced as a flagged, non-blocking gap. **`now.md` is retired** for the same reason: "what's next" is derived from the forge by the Planner (`gh issue list --label "vinaya/tranche:<slug>" --state open`), not written to a file.
|
|
@@ -9,6 +9,10 @@ sidebar_title: "Template: Issue rationale"
|
|
|
9
9
|
|
|
10
10
|
[tranche-slug] [n] — [task title, repeated from the Issue title]
|
|
11
11
|
|
|
12
|
+
**Tier:** [0 | 1 | 3]
|
|
13
|
+
**Project:** [project(s), comma-separated, matching the blast radius stated in the "Project(s) + blast radius" field below]
|
|
14
|
+
**Type:** [build | chore | docs | feat | fix | perf | refactor | revert | style | test — the commit-type word this task belongs to; free-text metadata, not a forge label]
|
|
15
|
+
|
|
12
16
|
## Planner's rationale
|
|
13
17
|
|
|
14
18
|
**Boundary** — [BOUNDARY — what this task is and, crucially, what it is NOT: what was deliberately split out, where the edges sit. Make the in/out sets concrete enough that the Brief Author can bound a file surface from them.]
|
|
@@ -30,7 +34,3 @@ sidebar_title: "Template: Issue rationale"
|
|
|
30
34
|
## Origin
|
|
31
35
|
|
|
32
36
|
[ORIGIN — where this task came from: Principal-directed, backlog item, incident follow-up — with dates and the Issue/PR references that motivated it.]
|
|
33
|
-
|
|
34
|
-
**Tier:** [0 | 1 | 3]
|
|
35
|
-
**Project:** [project(s), comma-separated, matching the blast radius above]
|
|
36
|
-
**Type:** [build | chore | docs | feat | fix | perf | refactor | revert | style | test — one, matching a `vinaya/type:*` label]
|
|
@@ -312,7 +312,7 @@ Self-metering capture is real, not estimated: the adapter sums the session's own
|
|
|
312
312
|
|
|
313
313
|
### The collection adapter AEG ships (one layer-2 instance, not the requirement)
|
|
314
314
|
|
|
315
|
-
AEG ships exactly one collection adapter, for the Claude Code host
|
|
315
|
+
AEG ships exactly one collection adapter, for the Claude Code host, exposed as `vinaya tokens`. 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.
|
|
316
316
|
|
|
317
317
|
**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` (`@attalabs/aeg-core`'s Claude Code transcript module), 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.
|
|
318
318
|
|