@attalabs/vinaya 0.19.3 → 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 +81 -0
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/tranche-archivist-planner.md +8 -8
- package/aeg-root/enforcement.md +18 -9
- package/aeg-root/milestone-model.md +3 -1
- package/aeg-root/process.md +5 -5
- package/aeg-root/roles/architect.md +2 -2
- package/aeg-root/roles/archivist.md +10 -11
- package/aeg-root/roles/brief-author.md +1 -1
- package/aeg-root/roles/developer.md +4 -3
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/principal.md +2 -1
- package/aeg-root/roles/tranche-archivist.md +5 -18
- package/aeg-root/skills/aeg/SKILL.md +30 -1
- package/aeg-root/skills/aeg-roles/SKILL.md +1 -1
- package/aeg-root/skills/brief-authoring/SKILL.md +2 -2
- package/aeg-root/state-machine.md +8 -6
- package/aeg-root/task-model.md +1 -1
- package/aeg-root/templates/issue-rationale-template.md +4 -4
- package/aeg-root/templates/pr-report-template.md +4 -0
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +473 -32
- package/dist/checks/bin/check-branch-topology.js +792 -11
- package/dist/checks/bin/check-brief-shape.js +353 -11
- package/dist/checks/bin/check-changeset-coverage.js +4156 -0
- package/dist/checks/bin/check-closes-n.js +792 -11
- package/dist/checks/bin/check-coherence.js +831 -15
- package/dist/checks/bin/check-dead-branch-push.js +353 -11
- package/dist/checks/bin/check-dispatch-readiness.js +996 -28
- package/dist/checks/bin/check-doc-coverage-push.js +449 -22
- package/dist/checks/bin/check-doc-coverage.js +449 -22
- package/dist/checks/bin/check-doctrine-portability.js +392 -15
- package/dist/checks/bin/check-evidence-fresh.js +462 -20
- package/dist/checks/bin/check-first-push-dispatch.js +898 -26
- package/dist/checks/bin/check-issue-assignment.js +792 -11
- package/dist/checks/bin/check-main-branch-refusal.js +3973 -0
- package/dist/checks/bin/check-no-disk-state.js +353 -11
- package/dist/checks/bin/check-quoted-command.js +4405 -0
- package/dist/checks/bin/check-reader-resolvable-prose.js +466 -20
- package/dist/checks/bin/check-registry-gates.js +467 -16
- package/dist/checks/bin/check-retired-vocabulary.js +465 -19
- package/dist/checks/bin/check-review-gate.js +392 -15
- package/dist/checks/bin/check-single-plan-pr.js +353 -11
- package/dist/checks/bin/check-test-plan.js +353 -11
- package/dist/checks/bin/check-token-collection-wired.js +3975 -0
- package/dist/checks/bin/check-workspace-escape.js +353 -11
- package/dist/index.js +1921 -420
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core/bin/verify-coherence.ts +1089 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +5 -0
- 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 +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/{[externals]__0pvgf28._.js → [externals]__1agj1ww._.js} +2 -2
- 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]__03jepya._.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/{3noqvds-2bvrd.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/{2iysedx-n06xc.js → 1ybrihl5_aocy.js} +1 -1
- 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/{3gehliafvmjo9.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/server.js +1 -1
- package/aeg-root/coordination.md +0 -292
- 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/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/apps/vinaya-studio/web/.next/static/chunks/42wxpzjhw7oy4.css +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -19,11 +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. |
|
|
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). |
|
|
23
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. |
|
|
24
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. |
|
|
25
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. |
|
|
26
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. |
|
|
27
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. |
|
|
28
32
|
|
|
29
33
|
## Config
|
|
@@ -72,6 +76,58 @@ To declare a domain beyond those two — a `migrations/` folder, a codegen outpu
|
|
|
72
76
|
|
|
73
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.
|
|
74
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
|
+
|
|
75
131
|
## Where the git hooks live
|
|
76
132
|
|
|
77
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.
|
|
@@ -82,6 +138,12 @@ Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory
|
|
|
82
138
|
|
|
83
139
|
On that legacy `.git/hooks` layout, note where the hook actually lives: hooks are never per-worktree, so its real home is the main checkout's shared hooks directory (`git rev-parse --git-common-dir`), which from a linked worktree is outside that worktree's own root. `vinaya eject` follows the hook there and bounds the removal to that directory's `hooks/` subtree, rather than to the worktree root — so ejecting from a linked worktree strips the hook instead of leaving it armed. A canonically spelled block path resolving anywhere else is refused, and the refusal is whole-run: `eject` removes nothing at all rather than making a partial destructive pass. (The `.git/` test is byte-exact, so a non-canonical spelling takes the working-tree branch instead — nothing vinaya generates produces one.)
|
|
84
140
|
|
|
141
|
+
## Claude Code Stop hook (transcript pointer)
|
|
142
|
+
|
|
143
|
+
When `--agents` includes `claude` (the default), `vinaya init`/`vinaya upgrade` also install a Claude Code `Stop` hook — `.claude/hooks/track-transcript.sh`, registered in `.claude/settings.json` — that records each session's transcript pointer (session id and transcript path, tab-separated) to `${TMPDIR:-/tmp}/claude-transcript-<sanitized-project-dir>.txt`. This is the pointer `packages/aeg-core/bin/report-tokens.ts`'s token-report adapter reads, so a fresh Claude Code adopter's token-report obligation resolves without an operator naming a transcript by hand, and without falling back to scanning `~/.claude/projects/` for the newest file (which grabs the wrong session's transcript when two worktrees run concurrently).
|
|
144
|
+
|
|
145
|
+
The script follows the same never-clobber discipline as the git hooks above: it is a marker-delimited managed block, appended onto an adopter's existing file at that path rather than overwritten. `.claude/settings.json`, by contrast, is refuse-if-foreign (created only when absent) — strict JSON has no comment syntax the marker convention could use, so an adopter's existing `settings.json` is left untouched; wire the hook in by hand (see the install diff's REFUSE guidance) if you already have one.
|
|
146
|
+
|
|
85
147
|
## Agent-native entry points (`--agents`)
|
|
86
148
|
|
|
87
149
|
`vinaya init` writes three agent-native entry points by default, one per vendor: `.agents/skills/vinaya-<role>/SKILL.md` (Agent skills, below), `.claude/commands/vinaya.md` (Claude Code command), and `.gemini/commands/vinaya.toml` (Gemini CLI command). Each one is a thin pointer that shells out to `vinaya doctrine --role <role>` at read time — there is exactly one source of role doctrine, never a copy baked into any of these files.
|
|
@@ -149,6 +211,7 @@ Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.att
|
|
|
149
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).
|
|
150
212
|
- Never self-enforce a timeout — the runner does that (`vinaya.config.json`'s `timeoutMs`, or the runner's default).
|
|
151
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.
|
|
152
215
|
|
|
153
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.
|
|
154
217
|
|
|
@@ -179,6 +242,24 @@ The generated review-authority workflows run only default-branch code: the requi
|
|
|
179
242
|
|
|
180
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.
|
|
181
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
|
+
|
|
182
263
|
## Brief-schema divergence
|
|
183
264
|
|
|
184
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.
|
|
@@ -57,7 +57,7 @@ Every output the per-task Archivist produces (left) has exactly one obligation f
|
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| **Provenance block comment** on each merged PR | Entry gate verification | The Tranche Archivist verifies every task PR has a provenance block comment before starting close-out. A missing provenance block means that task's per-task Archivist close-out was incomplete — stop and flag: *"Task N's PR has no provenance block — per-task Archivist did not run for this task. Flag for Principal before proceeding."* |
|
|
59
59
|
| **Lessons Issue comments** for any `BLOCKER`/`MAJOR` findings that merged | Retrospective assembly | The Tranche Archivist reads the pinned lessons Issue's comments since the tranche started and includes the patterns they identify in the retrospective's "What stalled or caused rework" and "Carry-forward lessons" sections. |
|
|
60
|
-
| **Follow-up Issues** opened for `STALE-SPEC` findings |
|
|
60
|
+
| **Follow-up Issues** opened for `STALE-SPEC` findings | Close-out report | The Tranche Archivist lists open follow-up Issues in the output report as DANGLING items — the follow-up Issue itself IS the durable record, open until resolved. A `STALE-SPEC` finding with no follow-up Issue is a DANGLING item — flag it for the Principal. (`now.md` and the pinned state Issue are both retired.) |
|
|
61
61
|
|
|
62
62
|
**Reading the table:** left is the producer obligation (per-task Archivist role doc and this contract enforce it), right is the consumer obligation (Tranche Archivist role doc and this contract enforce it). The two role docs must not contradict this table.
|
|
63
63
|
|
|
@@ -74,7 +74,7 @@ Every output the per-task Archivist produces (left) has exactly one obligation f
|
|
|
74
74
|
|
|
75
75
|
- Verify every task PR has a provenance block comment before starting. If any is missing, stop and flag — do not proceed with partial close-out. Partial close-out is worse than no close-out: it creates a plausible-looking but incomplete record.
|
|
76
76
|
- Read the pinned lessons Issue's comments since the tranche start date before assembling the retrospective. Carry-forward lessons that appear there but are not reflected in the retrospective are a gap.
|
|
77
|
-
-
|
|
77
|
+
- List open follow-up Issues in the close-out report as DANGLING items — the follow-up Issue itself is the durable record, open until resolved. If a `STALE-SPEC` finding has no follow-up Issue (the per-task Archivist missed it), flag it as DANGLING and open the Issue on behalf of the Principal. (`now.md` and the pinned state Issue are both retired.)
|
|
78
78
|
- Do not assemble the tranche retrospective from memory or inference — assemble it from merged PR summaries, the pinned lessons Issue's comments, and the tranche topology file. The retrospective is a structured projection of facts.
|
|
79
79
|
|
|
80
80
|
---
|
|
@@ -7,7 +7,7 @@ description: Carries a finished tranche’s real outcome to the planning of the
|
|
|
7
7
|
status: active
|
|
8
8
|
producer: tranche-archivist
|
|
9
9
|
consumer: planner
|
|
10
|
-
carrier:
|
|
10
|
+
carrier: closed-milestone, retrospective-comment
|
|
11
11
|
summary: Ever started planning the next phase on outdated info about the last one?
|
|
12
12
|
---
|
|
13
13
|
# Contract: Tranche Archivist → Planner
|
|
@@ -16,13 +16,13 @@ summary: Ever started planning the next phase on outdated info about the last on
|
|
|
16
16
|
|
|
17
17
|
This seam sits between the end of one tranche and the planning of the next. It exists because planning starts by reading the current state of a product, and a tranche that was never closed out leaves those records describing a product that no longer exists.
|
|
18
18
|
|
|
19
|
-
**What crosses** —
|
|
19
|
+
**What crosses** — two artefacts, both produced when a tranche closes. The closed Milestone with its attached closed Issues, which is both the physical signal that close-out happened and the forge-derived record of what the tranche shipped. And the retrospective, posted to the standing lessons thread — the durable record of what stalled and what carries forward. (The hand-maintained product state record that once also crossed here is retired — anything still outstanding is an ordinary open Issue, closed when resolved.)
|
|
20
20
|
|
|
21
|
-
**The hand-off is malformed when** —
|
|
21
|
+
**The hand-off is malformed when** — either is absent. Each has a matching check on the planning side, and the planning side stops rather than working around it: the archive is a fact to be confirmed, not an assumption to be made. "It was probably closed out" is not a passed check. The failure this was written against is real: tranches completed without close-out, and the next plans were then built on records describing the product as it had been before.
|
|
22
22
|
|
|
23
23
|
**What it does not carry** — a statement of what happens next. That is not archived, it is derived: the open work on the forge is the answer, read directly rather than maintained by hand in a file that would drift. Nor does it carry the power to move a task between tranches; that is a scoping decision, and it belongs to planning.
|
|
24
24
|
|
|
25
|
-
**How it physically runs** — the carriers are the
|
|
25
|
+
**How it physically runs** — the carriers are the closed Milestone and the retrospective comment. The ordering is normally close first, then plan. One exception: when a new plan absorbs an existing tranche's unstarted work, the move happens first and the close-out follows, because the close cannot proceed while that work is open and the plan is what empties it. Only unstarted work may move — anything with a branch or an open pull request is finished or dropped where it is, never relocated mid-flight — and every move leaves a note on the task, so a task that changed address can be told from one that vanished.
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
---
|
|
@@ -50,7 +50,7 @@ This contract formalizes the close-out outputs the Tranche Archivist must produc
|
|
|
50
50
|
Four artifacts, all produced by the Tranche Archivist at close-out:
|
|
51
51
|
|
|
52
52
|
1. The **archived tranche file** at `aeg-root/tranches/completed/<name>.md` — the physical signal.
|
|
53
|
-
2. The
|
|
53
|
+
2. The closed **Milestone** plus the tranche's `vinaya/tranche:*`-labeled closed Issues — the forge-derived record of what the tranche shipped. (The hand-edited pinned state Issue this item once named is retired: everything it held is forge-derived, in the retrospective, or an ordinary open Issue closed when resolved.)
|
|
54
54
|
3. The **retrospective** posted as a new comment on the pinned lessons Issue — the durable failure-mode record.
|
|
55
55
|
|
|
56
56
|
All three must exist before the Planner is authorized to plan the next tranche on the product. (`now.md` is retired. "What's next" is derived from the forge: open Issues without an assigned PR in the current tranche, plus `gh issue list --label "vinaya/tranche:<slug>" --state open`.)
|
|
@@ -64,7 +64,7 @@ Every artifact the Tranche Archivist produces (left) has exactly one obligation
|
|
|
64
64
|
| Tranche Archivist produces | Planner consumes at | What the consumption means |
|
|
65
65
|
|---|---|---|
|
|
66
66
|
| **Archived tranche file** at `aeg-root/tranches/completed/<name>.md` | Readiness gate item 8 | The Planner MUST confirm this file exists before planning any new tranche on the same product. Absence means the Tranche Archivist has not run — planning is blocked. |
|
|
67
|
-
| **
|
|
67
|
+
| **Closed Milestone + retrospective** on the lessons Issue | Readiness gate item 2 (specs reachable) | The Planner derives post-tranche state from the forge (closed Milestone, closed labeled Issues, merged PRs) and reads the retrospective for judgment-carrying observations. Planning against anything hand-maintained is planning against staleness — the forge cannot go stale. |
|
|
68
68
|
| **Retrospective** posted as a comment on the pinned lessons Issue | Readiness gate item 5 (prior decisions known) | The Planner reads lessons since the last tranche to avoid re-litigating resolved decisions or repeating known failure modes. A missing retrospective means the Planner plans blind to the tranche's carry-forward lessons. |
|
|
69
69
|
|
|
70
70
|
> **`now.md` is retired.** "What's next" is not a produced artifact — it is derived from the forge: `gh issue list --label "vinaya/tranche:<slug>" --state open` filtered to Issues with no open PR. The Planner runs this query directly rather than reading a file that would need hand-maintenance.
|
|
@@ -76,7 +76,7 @@ Every artifact the Tranche Archivist produces (left) has exactly one obligation
|
|
|
76
76
|
## Producer obligations (the Tranche Archivist)
|
|
77
77
|
|
|
78
78
|
- Move the tranche file to `completed/` — this is the **physical signal** the Planner's gate checks. A close-out that does everything else but fails to move the file is an incomplete close-out that correctly blocks planning.
|
|
79
|
-
-
|
|
79
|
+
- Leave no hand-maintained state behind: what the tranche shipped is derived from the forge (closed Milestone, closed labeled Issues, merged PRs); observations live in the retrospective; anything still owed (a pending manual op, a known production issue) is an ordinary open Issue, closed when resolved.
|
|
80
80
|
- Post the retrospective as a new comment on the pinned lessons Issue. These three outputs are the close-out contract. A close-out missing any of them is incomplete and the Planner's gate will correctly block.
|
|
81
81
|
- **Do not** update `now.md` — it no longer exists. "What's next" is derived from the forge by the Planner, not written by the Archivist.
|
|
82
82
|
|
|
@@ -85,7 +85,7 @@ Every artifact the Tranche Archivist produces (left) has exactly one obligation
|
|
|
85
85
|
- Run readiness gate item 8 before planning any tranche that includes a product: confirm `aeg-root/tranches/completed/<name>.md` exists for the previous tranche on each product in scope.
|
|
86
86
|
- If any prior tranche on an in-scope product exists in `aeg-root/tranches/` but NOT in `completed/`, STOP: *"The previous tranche `<name>` on `<product>` has not been archived — the Tranche Archivist has not run. Dispatch the Tranche Archivist for `<name>` before planning proceeds."*
|
|
87
87
|
- Do not improvise around a missing close-out. "The Tranche Archivist probably ran" is not a passed gate. The filesystem check is the gate. If the file isn't there, stop.
|
|
88
|
-
-
|
|
88
|
+
- Derive the current-state snapshot from the forge (closed Milestone, closed labeled Issues, merged PRs) and read the lessons Issue's retrospective — not a previous session's memory, not an earlier planning pass. These reflect what the tranche actually shipped; planning against anything else is planning against stale reality.
|
|
89
89
|
- Derive "what's next" from the forge: `gh issue list --label "vinaya/tranche:<slug>" --state open` filtered to Issues without an assigned open PR. Do not look for a `now.md` — it no longer exists.
|
|
90
90
|
|
|
91
91
|
---
|
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.
|
|
@@ -84,10 +84,10 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
84
84
|
| `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Managed `.git/hooks/pre-commit` block** (written by `vinaya init`/`upgrade` through the common-dir hook resolver, so every linked worktree shares it) | The ring-0 check suite passes: the hook runs `vinaya check --all --local` (in this vendoring repo, the built `node apps/cli/dist/index.js` invocation, guarded by a build-missing refusal). Build, lint and unit tests are NOT run by this hook here — they run in the repo-own `ci.yml` (ring 1). | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
85
85
|
| `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Managed `.git/hooks/pre-push` block** (same generator and common-dir resolver as pre-commit) | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2 detection backstop (see below, task 24 item 2). This check is deliberately **fail-open**: if the forge cannot be reached (network flake, auth issue, rate limit) it is treated the same as "no pull request exists yet" and the push is allowed — a transient reachability issue must never block every push. On a task branch's first push (before its PR exists), `verify-dispatch` also runs itself here — see the dedicated row below (task 25). Any changed code that has a designated owning document must have that document updated in the same branch, OR (task 29) carry an actor-verified `vinaya/waiver:docs` label on the open pull request, OR (task 4) carry a `Doc-neutral: <pointer> — <note>` PR-body declaration whose matched-file diff is verified comment/whitespace-only (`isMechanicallyNeutralDiff`) — a waiver is a forge-authenticated human act, never a parseable string, and a Doc-neutral declaration is never sufficient by itself; only diff-confirmed evidence satisfies it. Before a PR exists (first push) no waiver label can exist yet, so an owned-doc violation here is **warn-with-declared-intent, not a hard block**: the push always succeeds, and the printed message states plainly that ring 1 stays red until a principal applies the label, the doc is updated, or a Doc-neutral declaration with confirming evidence is added — this replaces the earlier first-push `Doc-waiver:` commit-trailer self-service, which is now dead code (that path is superseded). `Doc-ack:` (URL-pointer acknowledgment) is unaffected by the waiver change — still read from the branch's own last commit message before a PR exists, or — for a pre-authoring dry run via `verify-dispatch --simulate`, before any commit exists — from a local drafted-PR-body file. | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
86
86
|
| Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. | repo-own | `packages/aeg-core/bin/open-pr.ts` |
|
|
87
|
-
| Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no
|
|
87
|
+
| Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no registry row for anything to resolve against. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | repo-own | `packages/aeg-core/bin/open-issue.ts` |
|
|
88
88
|
| Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Validates forge writes before they reach GitHub — in this repo at the CLI layer, not at a tool-interception hook. | **Validated forge-write commands** (`vinaya issue create/edit`, `vinaya pr create/edit`) | No tool-interception layer is installed in this repo (`ring1_forgeWriteInterception: false` in `vinaya.config.json`), so a raw `gh` write is not refused at the tool call. The sanctioned path is the CLI's validated forge-write commands, which refuse a malformed task Issue (the eight-field Planner rationale) or PR body (the configured brief schema) before anything reaches the forge; ring-1 CI re-runs the same validators as the backstop for writes that bypass them. | repo-own | `apps/cli/src/lib/forge-write.ts` |
|
|
89
89
|
| Merging | Ever had a broken build get merged anyway? | event | Holds the merge on the forge side while anything the gates check is still failing. | **Required review-gate check + branch protection** | No agent-side merge hook is installed in this repo. The merge gate is ring 1: the required `review-gate` check (`vinaya check review-gate`; a clean code-reviewer APPROVE and a clean security-review PASS verdict from principal-allowlisted authors, or a principal-applied, actor-verified `vinaya/waiver:review` label) plus branch protection marking that check required. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
90
|
-
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
90
|
+
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. Optionally (task 10, `PREMISE_FILE=<brief.md> vinaya check dispatch-readiness`), also re-asserts a local brief's `Premise:` pins against current on-disk state and fails on any pin that no longer holds. | **`dispatch-readiness` check** (`vinaya check dispatch-readiness` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25; known parity gap: the shipped check's prior-tranche-archival predicate always reports empty — on this repo's own toolchain the unabridged derivation, including that predicate for real, is `packages/aeg-core/bin/verify-dispatch.ts`, named in the implementation column below) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | product | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
91
91
|
| Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | repo-own | `packages/aeg-core/src/brief-validation.ts` |
|
|
92
92
|
| Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | repo-own | `packages/aeg-core/bin/verify-task.ts` |
|
|
93
93
|
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. Every core check in `registry.ts` carries an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@attalabs/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | repo-own | `apps/cli/src/checks/runner.ts` |
|
|
@@ -96,6 +96,8 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
96
96
|
| Pushing to a branch whose pull request already resolved | Ever pushed more commits to an already-merged branch? | hook | Refuses a push to a task branch whose most recent pull request is already merged or closed. | **`check-push-target` standalone shim** (the registered `dead-branch-push` check is a thin adapter over the same `checkDeadBranchPush` evaluator; this row names the seeded standalone CLI form) | The branch's most recent pull request — one batched `gh pr list --head <branch>` call — is still `OPEN`, or none exists yet. Deliberately fail-open: any forge-reachability failure (auth, network, rate limit, malformed JSON) maps to `UNKNOWN`, treated as allow — a transient outage must never block every push. The ring-2 dead-branch-push audit below is this gate's detection backstop for writers the hook cannot reach. | repo-own | `packages/aeg-core/bin/check-push-target.ts` |
|
|
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
|
+
| 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` |
|
|
99
101
|
|
|
100
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.
|
|
101
103
|
|
|
@@ -111,7 +113,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
111
113
|
|
|
112
114
|
Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
113
115
|
|
|
114
|
-
> **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.
|
|
115
117
|
|
|
116
118
|
| CI check | Summary | Category | Description | Re-verifies | Audience | implementation |
|
|
117
119
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -124,9 +126,9 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
124
126
|
| Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | repo-own | `.github/workflows/ci.yml` |
|
|
125
127
|
| Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | States where formatting/naming conventions stand in this repo: currently unenforced. | **Nothing re-checks lint/format or commit-message grammar in this repo today** — the managed hooks run only the `vinaya check` suite, and the one CI workflow that builds the code (`ci.yml`, the implementation named here; its typecheck + unit-test substance belongs to the `Typecheck + unit tests` row) carries no lint or commitlint step. Those conventions are discipline, not a gate, until a step is added. | repo-own | `.github/workflows/ci.yml` |
|
|
126
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` |
|
|
127
|
-
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. | product | `packages/aeg-core/bin/verify-review-gate.ts` |
|
|
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` |
|
|
128
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` |
|
|
129
|
-
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts` is named by some row's `implementation` — the inverse direction of G1: no enforcement mechanism exists off-page. (In this repo the first two globs match nothing — neither directory exists — so the live coverage is the seeded `packages/aeg-core/bin/*.ts`
|
|
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` |
|
|
130
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` |
|
|
131
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` |
|
|
132
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` |
|
|
@@ -136,11 +138,17 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
136
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` |
|
|
137
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` |
|
|
138
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` |
|
|
139
143
|
|
|
140
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.
|
|
141
145
|
|
|
142
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.
|
|
143
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
|
+
|
|
144
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.
|
|
145
153
|
|
|
146
154
|
---
|
|
@@ -155,7 +163,8 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
155
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 | |
|
|
156
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` |
|
|
157
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` |
|
|
158
|
-
| 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` |
|
|
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` |
|
|
159
168
|
|
|
160
169
|
---
|
|
161
170
|
|
|
@@ -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
|
|
package/aeg-root/process.md
CHANGED
|
@@ -5,9 +5,9 @@ sidebar_title: Process
|
|
|
5
5
|
|
|
6
6
|
This document describes how work flows through the AEG operational model — from the moment the Principal has an idea to the moment that work merges to `main` with all specs and skills updated.
|
|
7
7
|
|
|
8
|
-
It is the canonical "how do we actually work?" document. Every other PM doc (`
|
|
8
|
+
It is the canonical "how do we actually work?" document. Every other PM doc (`state-machine.md`, `tranche-model.md`, role docs, `brief-authoring` skill) describes a slice of this process. This document stitches them together into a single readable walkthrough.
|
|
9
9
|
|
|
10
|
-
If you are starting a new session and need to understand the workflow, read this first. Then `
|
|
10
|
+
If you are starting a new session and need to understand the workflow, read this first. Then the `aeg` skill's session-start forge queries for orientation, then the role doc that applies to you, then any project-specific specs.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -140,7 +140,7 @@ The branch name `task/<tranche>/<n>` is the convention that links the task to it
|
|
|
140
140
|
|
|
141
141
|
**Who:** Developer (the coding agent — spawned or pasted).
|
|
142
142
|
|
|
143
|
-
Per `roles/developer.md`, the Developer reads
|
|
143
|
+
Per `roles/developer.md`, the Developer reads its role doc, derives live execution state from the forge (the `aeg` skill's session-start forge queries), reads the relevant skills (auto-loaded when matching code is touched) and project specs, confirms pre-flight (starting with the worktree), and works in small, frequent commits on the `task/<tranche>/<n>` branch. When dispatched by an automation layer, it streams progress events to that layer.
|
|
144
144
|
|
|
145
145
|
The Developer cannot author its own briefs, expand scope without escalation, modify files outside scope, skip verification hooks, skip the Task Done checklist, or **write status anywhere** (status is derived). If the brief is wrong or contradicts reality, it escalates (Phase 7) — it does not paper over confusion or improvise outside scope.
|
|
146
146
|
|
|
@@ -260,9 +260,9 @@ The test plan is split by who can structurally execute each item:
|
|
|
260
260
|
|
|
261
261
|
**Who:** Principal (or Brief Author if explicit per-PR delegation was set in the brief).
|
|
262
262
|
|
|
263
|
-
The Principal merges. Tier 3 work merges during a ratification window (`
|
|
263
|
+
The Principal merges. Tier 3 work merges during a ratification window (`roles/principal.md`); Tier 0/1 anytime. The merge **auto-closes the linked Issue** (via `Closes #N`) — and the merge *is* the `merged` status; nobody writes a label. An automation layer may surface a completion notification.
|
|
264
264
|
|
|
265
|
-
**Post-merge: the Archivist closes out** (`roles/archivist.md`) — confirms the Issue closed, docs coherent,
|
|
265
|
+
**Post-merge: the Archivist closes out** (`roles/archivist.md`) — confirms the Issue closed, docs coherent, `docs-index.md` regenerated. It **flags** (does not perform) orphaned branches and worktree removal. It writes no task status. (The hand-edited per-project state Issue is retired — non-derivable facts live as ordinary open Issues, closed when resolved; active state is derived from the forge.)
|
|
266
266
|
|
|
267
267
|
**Exit:** code is in main, Issue closed, close-out done.
|
|
268
268
|
|
|
@@ -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
|
---
|