@attalabs/vinaya 0.16.0 → 0.17.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 +8 -2
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +2 -2
- package/aeg-root/roles/developer.md +1 -0
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +1 -1
- package/aeg-root/templates/pr-report-template.md +2 -0
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +3545 -0
- package/dist/checks/bin/check-branch-topology.js +72 -6
- package/dist/checks/bin/check-brief-shape.js +72 -6
- package/dist/checks/bin/check-closes-n.js +72 -6
- package/dist/checks/bin/check-coherence.js +77 -7
- package/dist/checks/bin/check-dead-branch-push.js +72 -6
- package/dist/checks/bin/check-dispatch-readiness.js +77 -7
- package/dist/checks/bin/check-doc-coverage-push.js +77 -7
- package/dist/checks/bin/check-doc-coverage.js +80 -11
- package/dist/checks/bin/check-evidence-fresh.js +72 -6
- package/dist/checks/bin/check-first-push-dispatch.js +77 -7
- package/dist/checks/bin/check-issue-assignment.js +72 -6
- package/dist/checks/bin/check-no-disk-state.js +72 -6
- package/dist/checks/bin/check-reader-resolvable-prose.js +72 -6
- package/dist/checks/bin/check-registry-gates.js +72 -6
- package/dist/checks/bin/check-review-gate.js +77 -18
- package/dist/checks/bin/check-single-plan-pr.js +72 -6
- package/dist/checks/bin/check-test-plan.js +72 -6
- package/dist/index.js +591 -206
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +1 -1
- 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 +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +1 -1
- 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 +1 -1
- 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 +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1qpl4m_._.js → [root-of-the-server]__1ccgndq._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1dfzjrd._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0-n77dx._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01605g7._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1u3abhy._.js → _0x999pb._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0ndy563._.js → _1-6rzip._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_087xa25._.js → _1_talrs._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_11vnfnm._.js → _1ar5uqe._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_02drwa_._.js → _1c9b9j-._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_18kqbvs._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_basic_components_index_ts_0xk1vda._.js +1 -1
- 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/{1mexqnauhtbtp.js → 0_jnwbarv-cqk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ek6qlqhokys0.js → 0l_vubs1ephe5.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0ojfbn3yvy-8_.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1jx5_q1g9l-cm.js → 0phqfqwbnt_ey.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0qcu17x3h94a9.js → 17r0szvqeeyhx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ve1-nz-mu_lr.js → 18x3rbqzeutpz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{22y4d5yrovcv-.js → 1bqpfvromtwn1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{39w6jbw3rleez.js → 24ojyzmt_fkg3.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0tmcnl-z8yt2_.js → 30lyvn0yzwdiz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0fm4p9os2lsom.js → 3m0_gk59s-rbx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ywkq74falx-x.js → 40x7f5hm7a5tk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0-08g_jjalnd9.js → 453zhecht_m1q.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -62,7 +62,9 @@ To declare a domain beyond those two — a `migrations/` folder, a codegen outpu
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
**`.aeg/packages`** (the legacy static collision-domain file some repos still carry) is
|
|
65
|
+
**`.aeg/packages`** (the legacy static collision-domain file some repos still carry) is **not read** by `checkBlastRadiusScope` here — no backward-compatibility path, since `apps/cli` has no adopter depending on it. `vinaya doctor` still diagnoses a present file purely as a migration checklist: it names exactly which of its entries (if any) aren't already covered by live derivation, the built-in defaults, or `blastRadius.extraDomains`, so you know what to fold into those before deleting it — but the file itself contributes nothing to the live check.
|
|
66
|
+
|
|
67
|
+
`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.
|
|
66
68
|
|
|
67
69
|
## Where the git hooks live
|
|
68
70
|
|
|
@@ -72,6 +74,10 @@ One thing git cannot version is the config itself: **each fresh clone runs `git
|
|
|
72
74
|
|
|
73
75
|
Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory (husky's `prepare` script owns per-clone wiring), and a repo with its own active raw hooks in `.git/hooks` stays on the legacy append-a-managed-block layout there — re-routing `core.hooksPath` would silently disable the adopter's own hooks. On that legacy layout `vinaya doctor` warns that clones have no hooks, and `vinaya upgrade` migrates to the tracked layout as soon as nothing foreign would be disabled.
|
|
74
76
|
|
|
77
|
+
## Agent skills
|
|
78
|
+
|
|
79
|
+
The emitter that will generate skill pointers under `.agents/skills/vinaya-<role>/SKILL.md`, for tools natively scanning `.agents/skills/` (Codex, Antigravity, Grok Build), is built — `vinaya init` does not call it yet. Each file, once wired, is a 3-line pointer delegating to `vinaya doctrine --role <role>` at read time. User-facing flag, `init`/`upgrade`/`eject`/`doctor` wiring, and the `--agents` selection flag land in #152.
|
|
80
|
+
|
|
75
81
|
Custom checks register under `checks`, one entry per check. **Every key must be namespaced `<yourname>/<id>`** — exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*`, with `vinaya` reserved as a prefix:
|
|
76
82
|
|
|
77
83
|
```json
|
|
@@ -124,7 +130,7 @@ The generated command embeds the member's directory and `bin` path. Both come fr
|
|
|
124
130
|
|
|
125
131
|
`@` is permitted so an npm-scoped member such as `packages/@attalabs/vinaya` resolves normally; it carries no meaning to the shell, to YAML at the position it appears, or to an Actions expression. The path must also resolve inside the repository, which a textual `..` rule cannot guarantee on its own.
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
The generated review-authority workflows run only default-branch code: the required review gate and the Changesets-release exemption on `body-bare-digits` both use `pull_request_target`, and the comment-triggered evaluator also checks out the default branch. None of these authority paths run pull-request code or adopter `ci.setup`; ordinary `pull_request` content checks remain unprivileged. Protect `.github/workflows/**` with required CODEOWNERS review as the complementary change-control layer. The full trust-boundary and bootstrap account is in [`specs/self-hosting.md`](./specs/self-hosting.md) in the source repository — it is not part of the published tarball.
|
|
128
134
|
|
|
129
135
|
## Doc-owners coverage
|
|
130
136
|
|
|
@@ -88,7 +88,7 @@ A ready-to-fill skeleton of the full eight-field rationale lives at `aeg-root/te
|
|
|
88
88
|
A task Issue's body must carry all eight fields in one of these two forms. **Canonical implementation:** `packages/aeg-core/src/issue-validation.ts` (`checkIssueRationale`, `isTaskIssueLabelSet`) — the single grammar/parser, consumed at two enforcement points per `aeg-root/enforcement.md`'s ring model:
|
|
89
89
|
|
|
90
90
|
- **Ring 0 (creation gate):** `packages/aeg-core/bin/open-issue.ts` refuses to create or edit a task Issue (any Issue labeled `vinaya/tranche:<slug>`) whose body fails `checkIssueRationale`. **It also refuses on three content checks, which grade what the fields *say* rather than that they exist:**
|
|
91
|
-
- `checkBlastRadiusScope` — if **Boundary** or **Project(s) + blast radius** names a path under a collision domain that none of the declared projects owns (ownership resolves against `.vinaya/projects.md`), the Issue must declare a second **registered** project or carry a `blast-radius-ack: <why one lens is enough>` line. `Project(s)` drives the review fan-out, so an under-declared blast radius under-governs the change. **Registered is the operative word:** a name with no row in `.vinaya/projects.md` buys no review lens, so it cannot buy the bypass either — the check filters declared names against the registry before counting them. The domain list itself is live-derived (every `packages/*` workspace member — `package.json`’s `workspaces`, or `pnpm-workspace.yaml`’s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks), with
|
|
91
|
+
- `checkBlastRadiusScope` — if **Boundary** or **Project(s) + blast radius** names a path under a collision domain that none of the declared projects owns (ownership resolves against `.vinaya/projects.md`), the Issue must declare a second **registered** project or carry a `blast-radius-ack: <why one lens is enough>` line. `Project(s)` drives the review fan-out, so an under-declared blast radius under-governs the change. **Registered is the operative word:** a name with no row in `.vinaya/projects.md` buys no review lens, so it cannot buy the bypass either — the check filters declared names against the registry before counting them. The domain list itself is live-derived (every `packages/*` workspace member — `package.json`’s `workspaces`, or `pnpm-workspace.yaml`’s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks), with `vinaya.config.json`'s `blastRadius.extraDomains` adding anything beyond those — see `blast-radius-domains.ts`. The legacy static `.aeg/packages` file is retired; it is no longer read. Dormant only when none of those sources yields any domain at all, which a real repo essentially never hits.
|
|
92
92
|
- `checkNoBriefContent` — the body must carry no `## References`, `Technical surface map`, `Premise`, `Step 0`, or `Test Plan` section. Those are Brief-Author artifacts; an Issue is not a brief's home (it would go stale before work starts).
|
|
93
93
|
- `checkRationaleNamesDocs` — **Docs to keep coherent** and/or **Traps to avoid** must name at least one concrete doc path (`aeg-root/…`, `.claude/skills/…`, `.claude/rules/…`, `apps/<x>/CLAUDE.md`, `apps/<x>/specs/…`, a repo-level `*.md`). A genuinely doc-less surface uses the explicit `no-doc-surface` sentinel — the same shape as `Test Plan: unit-tests-only`. This is the only read-obligation signal a forge write leaves: the skill-check hook fires on file edits, and cutting an Issue edits no file.
|
|
94
94
|
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -71,7 +71,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
71
71
|
| `git commit` | Ever pushed code that didn't even compile? | hook | Refuses a commit that does not build or pass its own checks, before the broken state exists at all. | **Managed `.git/hooks/pre-commit` block** (written by `vinaya init`/`upgrade` through the common-dir hook resolver, so every linked worktree shares it) | The ring-0 check suite passes: the hook runs `vinaya check --all --local` (in this vendoring repo, the built `node apps/cli/dist/index.js` invocation, guarded by a build-missing refusal). Build, lint and unit tests are NOT run by this hook here — they run in the repo-own `ci.yml` (ring 1). | `apps/cli/src/checks/runner.ts` |
|
|
72
72
|
| `git push` | Ever had someone accidentally push straight to main? | hook | Refuses a push that would land straight on main, on the machine that attempted it. | **Managed `.git/hooks/pre-push` block** (same generator and common-dir resolver as pre-commit) | No pushing straight to `main` — a direct push cannot be refused for a commit that already landed on `main` some other way (e.g. a repo-admin action outside the hook's reach); that residual class now has a ring-2 detection backstop (see below, task 24 item 1). A task branch's name must exactly match its planned task row in the tranche plan. A task branch whose most recent pull request is already `MERGED` or `CLOSED` is refused outright — closing the class of bug where stale local work lands on an already-resolved branch, silently reopening/confusing forge state (live-fire incident, 2026-07-03: six topology rows landed on a plan PR's branch after that PR had merged); this check is fail-open (hook-dependent), so it too now has a ring-2 detection backstop (see below, task 24 item 2). This check is deliberately **fail-open**: if the forge cannot be reached (network flake, auth issue, rate limit) it is treated the same as "no pull request exists yet" and the push is allowed — a transient reachability issue must never block every push. On a task branch's first push (before its PR exists), `verify-dispatch` also runs itself here — see the dedicated row below (task 25). Any changed code that has a designated owning document must have that document updated in the same branch, OR (task 29) carry an actor-verified `vinaya/waiver:docs` label on the open pull request, OR (task 4) carry a `Doc-neutral: <pointer> — <note>` PR-body declaration whose matched-file diff is verified comment/whitespace-only (`isMechanicallyNeutralDiff`) — a waiver is a forge-authenticated human act, never a parseable string, and a Doc-neutral declaration is never sufficient by itself; only diff-confirmed evidence satisfies it. Before a PR exists (first push) no waiver label can exist yet, so an owned-doc violation here is **warn-with-declared-intent, not a hard block**: the push always succeeds, and the printed message states plainly that ring 1 stays red until a principal applies the label, the doc is updated, or a Doc-neutral declaration with confirming evidence is added — this replaces the earlier first-push `Doc-waiver:` commit-trailer self-service, which is now dead code (that path is superseded). `Doc-ack:` (URL-pointer acknowledgment) is unaffected by the waiver change — still read from the branch's own last commit message before a PR exists, or — for a pre-authoring dry run via `verify-dispatch --simulate`, before any commit exists — from a local drafted-PR-body file. | `apps/cli/src/checks/runner.ts` |
|
|
73
73
|
| Creating a pull request, or editing its title/description | Ever opened a PR that was missing half the info reviewers needed? | event | Refuses to open or edit a pull request until it carries everything a reviewer needs to judge it. | **Forge command gate** — the raw command is refused; the validated wrapper is the only path | The title follows the naming convention. The description carries every required section of the task brief: the impact tier, the executing model, the project(s) touched, a tagged test plan, the file surface the task may touch, the documentation it must update, the isolated-worktree setup step, the stop conditions, the standing autonomy clause, the Issue this work closes,. A *plan* pull request must never close a task Issue. The documentation required by the change's impact tier is present — a doc-coverage finding here is honored ONLY by an actor-verified `vinaya/waiver:docs` label, never a body field. The branch, its planned task row, and the Issue it closes all agree with each other. **Single-plan-PR guard (task 19):** a diff that touches a tranche's topology file (`aeg-root/tranches/*.md`, excluding `completed/`) is refused outright if another OPEN pull request's diff already touches that SAME tranche's topology file — closing the race that produced two concurrent plan pull requests for the same tranche, each cut before the other's newly-added Issue was visible. Ordinary task pull requests never touch a topology file, so this never fires for them. A PR opened via the web UI bypasses this wrapper entirely, so this predicate (extracted to `packages/aeg-core/src/single-plan-pr.ts`, task 24 item 3) now also has a ring-1 CI backstop (see below) — one implementation, two enforcement points. | `packages/aeg-core/bin/open-pr.ts` |
|
|
74
|
-
| Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no per-project state to update. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any
|
|
74
|
+
| Creating a task Issue, or editing its title/description | Ever seen a ticket with zero context on why it exists? | event | Refuses to open or edit a task Issue until it carries the full reasoning behind the task. | **Forge command gate** → validated wrapper | The title follows the naming convention, and the Issue carries the complete planning rationale — what the task is and is not, its sizing, the projects and blast radius it touches, why its dependency edges exist, the known traps, the suggested agent class, the stop-and-escalate conditions, and the documents it must keep coherent. Issues outside the task system pass through untouched. **Beyond presence, four content checks block: every name on the task's line-anchored `Project:` field must resolve to a row in the project registry (`.vinaya/projects.md`), which is the sole authority for valid project names — an unregistered project has no specs to read and no per-project state to update. The names are resolved by the same parser that derives a task's project for the board and dispatch, so the gate and the derivation cannot disagree about what a task declares; the rationale's declared surface must not reach into a shared collision domain (live-derived `packages/*` workspace members, plus a built-in cross-cutting default set, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) that none of its declared projects owns without either a second **registered** project or an explicit `blast-radius-ack:` line — this check reads the same line-anchored field through the same parser, and counts only names that resolve to a registry row, since an unregistered name adds no review lens and so cannot buy the multi-project benefit of the doubt; the body must carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content goes stale before work starts and belongs in the brief; and the rationale must name at least one concrete doc/skill path (or the explicit `no-doc-surface` sentinel), which is the only read-obligation signal a forge write produces, since the skill-check hook fires on file edits and cutting an Issue edits no file. A fourth check warns, never blocks: two open task Issues naming the same collision domain with no mutual `Conflicts-with` edge.** **Label-name validity is checked here too:** a tranche label is the one label whose value the Planner invents, and `vinaya/tranche:` spends 15 of GitHub's 50-character label budget before the slug starts — so a slug that reads fine in prose can be one the forge refuses to create. The wrapper refuses an over-long slug with the exact overshoot, at the point the label is first applied, rather than letting `gh` fail with an opaque 422. **Dependency-edge amendments have exactly one path — the `amend-deps` subcommand, which rewrites the structured `Dependency rationale` field AND appends the `**Amendment (...)**` note in one atomic write, gated on a runtime round-trip parse (`parseRationaleDeps` must read back exactly the requested edges) — no code path changes one representation without the other (closing a five-incident field-vs-amendment drift class this session; modeled on the actor-verified waiver pattern: the sanctioned path is the only path).** | `packages/aeg-core/bin/open-issue.ts` |
|
|
75
75
|
| Writing to pull requests or Issues through the raw API | Ever had a bot silently edit a PR or issue behind your back? | event | Validates forge writes before they reach GitHub — in this repo at the CLI layer, not at a tool-interception hook. | **Validated forge-write commands** (`vinaya issue create/edit`, `vinaya pr create/edit`) | No tool-interception layer is installed in this repo (`ring1_forgeWriteInterception: false` in `vinaya.config.json`), so a raw `gh` write is not refused at the tool call. The sanctioned path is the CLI's validated forge-write commands, which refuse a malformed task Issue (the eight-field Planner rationale) or PR body (the configured brief schema) before anything reaches the forge; ring-1 CI re-runs the same validators as the backstop for writes that bypass them. | `apps/cli/src/lib/forge-write.ts` |
|
|
76
76
|
| Merging | Ever had a broken build get merged anyway? | event | Holds the merge on the forge side while anything the gates check is still failing. | **Required review-gate check + branch protection** | No agent-side merge hook is installed in this repo. The merge gate is ring 1: the required `vinaya review gate` check (a clean code-reviewer APPROVE and a clean security-review PASS verdict from principal-allowlisted authors, or a principal-applied, actor-verified `vinaya/waiver:review` label) plus branch protection marking that check required. | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
77
77
|
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **the managed `.git/hooks/pre-push` block** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
@@ -160,7 +160,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
|
|
|
160
160
|
| Branch-ID must match a topology row | ✅ pre-push hook — delegates to aeg-core (`check-branch-topology.ts` → forge-derived Tranche via `deriveTrancheFromForge`, task 3a; originally `parseTranche`, task 32); formerly a hand-rolled grep fork of the same table-parse | — | — | Ring-0 only | **(a) — still a gap, not addressed by this task** |
|
|
161
161
|
| Code→doc ownership (C5) | ✅ pre-push hook + PR-open gate | ✅ `verify-docs --pr`, now a step of the AEG gate suite job (`verify-docs.yml` deleted — task 31) | — | Paired (pre-existing) | not a gap |
|
|
162
162
|
| Brief-section grammar / title convention | ✅ `open-pr.ts`/`open-issue.ts` | ✅ Brief Validation, now a step of the AEG gate suite job (moved out of `archivist.yml` — task 31) | — | Paired (pre-existing) | not a gap |
|
|
163
|
-
| Planner-rationale **content** — blast radius / brief-content / docs-read | ✅ `open-issue.ts` (A/B/D refuse; C warns) | — (no continuous half yet: the checks derive collision domains live from `package.json` + built-in defaults, plus any
|
|
163
|
+
| Planner-rationale **content** — blast radius / brief-content / docs-read | ✅ `open-issue.ts` (A/B/D refuse; C warns) | — (no continuous half yet: the checks derive collision domains live from `package.json` + built-in defaults, plus any `vinaya.config.json` entries, and read `projects.md`, which CI has, but no oracle check re-runs them over the live Issue stock) | — | Ring-0 only | **gap: no ring-1 half** (narrowed: the project-registry content check, added alongside A/B/D, does have one — it re-runs inside R1 below) |
|
|
164
164
|
| Planner-rationale completeness + project-registry resolution (R1) | ✅ `open-issue.ts` — on `edit`, gate applicability now reads the target Issue's **actual forge labels** (`gh issue view --json labels`, unioned with argv; fetch failure = hard refusal), not just this command's own `--label` flags, which are absent on normal body edits (task 36 — every task-Issue body edit previously bypassed the gate silently) | ✅ coherence oracle's R1 — which re-runs the project-registry check too, so an Issue whose `Project:` names an unregistered project is caught even when it was written by a writer ring 0 cannot reach (the web UI, a raw API call) or predates the gate | ✅ `--json`/audit mode | Paired (pre-existing); **edit-path bypass fixed** (task 36) | not a gap |
|
|
165
165
|
| `verify-dispatch` (pre-work readiness) | ✅ **new** — managed `.git/hooks/pre-push` first-push gate (task branch's first push only; fail-open on forge-unreachable) | — | — | **Forced, ring-0 only (this task, item 1)** | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
|
|
166
166
|
| `verify-task` (pre-PR exit composite) | ✅ **new** — `open-pr.ts` runs it wholesale for task branches (`gatePlanForBranch`); on `edit <n>` the gate set now resolves from the target PR's **actual forge head branch** (`gh pr view --json headRefName`, fetch failure = hard refusal), not the local checkout/`BRANCH` env, which silently skipped `closes-n`/`verify-task` on task-PR body edits from a `main` checkout (task 36 bundled finding) | — | — | **Forced, ring-0 only (this task, item 2)**; **edit-path gate-selection bypass fixed** (task 36) | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
|
|
@@ -169,6 +169,7 @@ field on its own line:>
|
|
|
169
169
|
| Field | Requirement |
|
|
170
170
|
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
171
171
|
| Summary | One paragraph. Closes the Issue with `Closes #<N>` somewhere in the body. No verification claims (typecheck/lint/test/diff-stat output, pass counts) — those belong exclusively in Evidence, below. |
|
|
172
|
+
| **Bare digits (whole body)** | `body-bare-digits` (CI) refuses any bare digit outside a fenced/indented/inline code span or `Closes`/`Project`/`Tier`/`Evidence`'s own anchor, correctly placed under its own documented section — nowhere else. `Premise`/`Test plan` get no anchor exemption at all (their real content is unbounded free text, so it's scanned like ordinary prose — a Test Plan item's own pass count or exit code needs backticks too). An Issue/PR ref, a date, a version, a path, a section number all now need their own backticks (`` `#N` ``); a countable claim ("138 passed", a duration, a percentage) belongs in a fenced block or doesn't get written. |
|
|
172
173
|
| Test plan | Every runtime check tagged `[agent]` or `[principal]`. The brief-authoring skill makes this a **required** field — empty plans use `Test Plan: unit-tests-only` as the sentinel. |
|
|
173
174
|
| `[agent]` items | Items the Developer-agent can run end-to-end before opening the PR. Paste the **actual command output**, not a paraphrase. (This is the `[agent]` half of the Verification phase, see `state-machine.md` § Verification.) |
|
|
174
175
|
| `[principal]` items | Items only the Principal can run (auth-gated, vendor-key-dependent, visual). The agent **does not tick these** — the Principal does, after running in a real browser. |
|
|
@@ -151,7 +151,7 @@ Decide by **verification-coupling** (not by project boundaries):
|
|
|
151
151
|
|
|
152
152
|
**Enumerate consumers by command, not by memory.** Before finalizing a task's **Project(s) + blast radius**, run the consumer enumeration for every shared package the task's Boundary or Project(s) names, and paste its output into your planning pass — in this repo: `git grep -l '@attalabs/<pkg>' -- '*/package.json'` (substitute your repo's package scope and manifest layout). The pasted list is the floor for the consumer set. A consumer list written from memory is a self-attestation, and the forgotten consumer is exactly the shipped regression this rule exists to stop. (One hit is the package's own manifest — its `name` field matches; every other hit is a consumer.)
|
|
153
153
|
|
|
154
|
-
**Mechanized.** This rule is no longer prose you have to remember. `checkBlastRadiusScope` reads a collision-domain list derived live from the repo's `packages/*` workspace members (`package.json`'s `workspaces`, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks) —
|
|
154
|
+
**Mechanized.** This rule is no longer prose you have to remember. `checkBlastRadiusScope` reads a collision-domain list derived live from the repo's `packages/*` workspace members (`package.json`'s `workspaces`, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) plus a built-in cross-cutting default set (lockfile, monorepo config, CI, git hooks) — `vinaya.config.json`'s `blastRadius.extraDomains` adds anything beyond those, if declared; the legacy static `.aeg/packages` file is retired and no longer read — and refuses a task Issue whose **Boundary** or **Project(s) + blast radius** names a path under a domain none of its declared projects owns (ownership resolves against `.vinaya/projects.md` — a task on `Project: aeg-core` editing `packages/aeg-core` owns its surface and passes). Satisfy it by listing the consumers, which is what this rule asks for anyway; or, when one review lens genuinely suffices, by an explicit `blast-radius-ack: <why>` line, which makes the judgment reviewable instead of silent.
|
|
155
155
|
|
|
156
156
|
**Only registered names count, and only the line-anchored project field is read.** The declared set comes from `projectsFromBody` — the same parser that derives a task's project for the board and dispatch — so the gate cannot disagree with the derivation about what a task declares, and a file path written in the surrounding prose can no longer be mistaken for a project. A listed consumer with no row in `.vinaya/projects.md` buys nothing: it resolves to no specs, no state and no reviewer, so it adds no review lens and does not satisfy the rule above (it is separately refused by `checkProjectsRegistered`). Listing a name you have not registered is not a way past this gate — register the project first.
|
|
157
157
|
|
|
@@ -177,7 +177,7 @@ This rationale is the **producer side of the `aeg-root/contracts/planner-brief.m
|
|
|
177
177
|
- **Durable** (goes in the rationale): why this is one task and not three; the dependency rationale; the sizing conclusion; which shared packages and consumers are in the blast radius; known traps to avoid; the suggested agent-class; stop-and-escalate conditions. These do not change before the task runs.
|
|
178
178
|
- **Perishable** (do NOT put in the rationale — it belongs in the just-in-time brief): exact function signatures, precise file lists, line-level specifics. These go stale as earlier tasks merge, so the Brief Author re-derives them at dispatch.
|
|
179
179
|
|
|
180
|
-
**Required fields in every Planner's rationale block** (these are the contract's producer fields — emit all eight). **Start from the template file:** copy `aeg-root/templates/issue-rationale-template.md` into the Issue body and fill each placeholder — it packages all eight fields in the rationale grammar, so you never reconstruct the shape from prose; the field definitions below remain the source of truth for content. **Write them in the rationale grammar** — either `**<Field>** — …` bold-inline or `### <Field>` heading, one of the two, so the field is machine-detectable. See `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for the full format spec; `verify-coherence`'s R1 check re-runs the same grammar continuously against the live Issue stock, and the ring-0 gate (`bin/open-issue.ts`) refuses a task Issue whose body fails it at creation time. **Beyond presence, the same gate grades three things about the content and refuses on each:** the declared surface may not reach a shared collision domain (live-derived `packages/*` workspace members + built-in cross-cutting defaults, plus any
|
|
180
|
+
**Required fields in every Planner's rationale block** (these are the contract's producer fields — emit all eight). **Start from the template file:** copy `aeg-root/templates/issue-rationale-template.md` into the Issue body and fill each placeholder — it packages all eight fields in the rationale grammar, so you never reconstruct the shape from prose; the field definitions below remain the source of truth for content. **Write them in the rationale grammar** — either `**<Field>** — …` bold-inline or `### <Field>` heading, one of the two, so the field is machine-detectable. See `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for the full format spec; `verify-coherence`'s R1 check re-runs the same grammar continuously against the live Issue stock, and the ring-0 gate (`bin/open-issue.ts`) refuses a task Issue whose body fails it at creation time. **Beyond presence, the same gate grades three things about the content and refuses on each:** the declared surface may not reach a shared collision domain (live-derived `packages/*` workspace members + built-in cross-cutting defaults, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) that no declared project owns without a second **registered** project or a `blast-radius-ack:` line — this is the shared-package blast-radius rule below, mechanized; the body may carry no brief-shaped section (`## References`, `Technical surface map`, `Premise`, `Step 0`, `Test Plan`) — brief content belongs in the brief; and **Docs to keep coherent** / **Traps to avoid** must name a concrete doc or skill path, or the explicit `no-doc-surface` sentinel. That last one exists because nothing else makes you read the surface you are planning: the skill-check hook fires on file edits, and cutting an Issue edits no file. A fourth check warns only — an undeclared collision-domain overlap with a sibling open Issue:
|
|
181
181
|
- **Boundary** — what this task is and, crucially, what it is *not* (what was deliberately split out).
|
|
182
182
|
- **Sizing** — that it passed the four "too big?" tests (or how a larger candidate was split).
|
|
183
183
|
- **Project(s) + blast radius** — every project touched, and for shared-package changes, which consumers are in the blast radius and whether each needs re-verification or edits.
|
|
@@ -75,7 +75,7 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
75
75
|
4. **Honest tests.** Do the tests prove real behavior, or do they mock the thing under test? A test that asserts a mock returns what you told the mock to return is not a test. Flag it.
|
|
76
76
|
5. **Spot-check code quality** on 2-3 of the most substantive files: clarity, obvious bugs, error handling, dead code, accidental debug/log leftovers, traces of skipped verification hooks.
|
|
77
77
|
6. **Doc coupling.** Tier 1+ work should carry spec/skill updates. If code changed contracts but no docs moved, flag it. (`verify-docs` also gates this in CI — your job is the judgment CI cannot make: are the docs *correct*, not just *present*.) For every doc named in the brief's documentation-update list: if it is absent from the diff, that is a **BLOCKER** (the list is a definition-of-done obligation, not guidance); if it is present but incorrect, that is also a BLOCKER. Check that compliance before reviewing logic. **Coverage of the `.vinaya/doc-owners` bindings is mechanical (`verify-docs` C5).** You no longer carry the "did the right doc move?" cognitive load — CI does. Your job shrinks to **judging correctness of the covered doc**: did the update actually reflect the code change, or is it a no-op edit / a misleading rewrite that silences C5 without reflecting reality? A passing C5 plus an incorrect doc update is a **BLOCKER**. A doc-coverage waiver is no longer a mechanism you weigh: the waiver body-grammar was removed, so a `doc-owners` obligation is deferred only when a principal applies the actor-verified `vinaya/waiver:docs` label — a Developer cannot self-serve it, and there is no body field for you to judge. What is still yours, because no CI gate can check it: whether a published doc reads complete to a stranger who lands on it cold. Hold every doc the brief surfaces to the reader-readability rule — a reader must resolve every symbol on the page from the page itself. A doc update that satisfies C5 mechanically but leaves a sentence leaning on a decision id or bare section number a stranger can't resolve is a MAJOR finding.
|
|
78
|
-
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention. **Before asserting blast-radius coverage is complete** — required whenever the brief lists more than one `Project:`, or the diff touches a path under a shared collision domain (live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any
|
|
78
|
+
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention. **Before asserting blast-radius coverage is complete** — required whenever the brief lists more than one `Project:`, or the diff touches a path under a shared collision domain (live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any `vinaya.config.json` `blastRadius.extraDomains` entries) even on a single-project brief; see `contracts/planner-brief.md` for the full domain-list derivation — run the consumer check for each touched shared package and quote its output in your review comment: `git grep -l '@attalabs/<pkg>' -- 'package.json' '*/package.json'` (this repo's form — substitute the touched package's published name; the two pathspecs are both needed, since `'*/package.json'` alone skips a repo-root manifest; use `git grep`, not `rg`, which silently skips gitignored doc trees). A consumer list you never generated is a consumer list you guessed. This applies to these two verdict fields only — the other checks keep their existing shape; evidence-on-everything is the "flag everything, get ignored" failure in another costume.
|
|
79
79
|
8. **Register and slop, in any reader-facing prose the diff adds or edits.** Two other defects in this same family — an unresolvable citation, and a coined term used without a definition — are checked mechanically now, not by you. This one still isn't, and it is a checkable property, not a stylistic preference: does a sentence narrate the work episode instead of stating the durable fact ("this fixes the bug from the last review," "I checked every case," a first-person aside) where a stranger reading the page later has no session to place it in? Does a padding adjective ("robust," "seamless," "comprehensive") carry no concrete referent a reader could verify? Flag the sentence and say what's wrong with it, the same way you'd flag a bug — this is not the taste-based-rewrite exclusion below; it fires only on a nameable defect (narration, unearned padding), never on a phrasing you'd merely have chosen differently.
|
|
80
80
|
|
|
81
81
|
## What you do NOT do
|
|
@@ -55,7 +55,7 @@ A tranche (`aeg-root/tranches/<name>.md`) is a **thin topology file**: task→Is
|
|
|
55
55
|
|
|
56
56
|
## 6. Conflicts and the two dispatch gates
|
|
57
57
|
|
|
58
|
-
Conflicts are **package-level, and mostly derived, not hand-declared** (collision domains live-derived from `package.json`'s `packages/*` workspace members, plus a built-in cross-cutting default set —
|
|
58
|
+
Conflicts are **package-level, and mostly derived, not hand-declared** (collision domains live-derived from `package.json`'s `packages/*` workspace members, plus a built-in cross-cutting default set — `vinaya.config.json`'s `blastRadius.extraDomains` adds anything beyond that; the legacy static `.aeg/packages` file is retired) — there is **no dynamic path-overlap scanner**. When unsure two tasks collide, declare the conflict and serialize. Two gates, both forge-answerable with zero stored state:
|
|
59
59
|
- never start a task whose `depends-on` isn't **merged**;
|
|
60
60
|
- never start a task while a `conflicts-with` sibling's **PR is open**.
|
|
61
61
|
|
|
@@ -308,7 +308,7 @@ The lowest-commitment way to run AEG: read-only over a team's existing process.
|
|
|
308
308
|
- **Manifest integrity** — `verify-docs --full` and the coherence oracle validate the doc-owners manifest: pointer existence (M1), glob syntax (M2 advisory), and duplicate globs (M3). There is no decision-number sequencing: the log it validated is gone.
|
|
309
309
|
- **Closes #N gate** — task-branch PRs must declare `Closes #<its-issue>` in the PR body; absence fails CI. Non-task branches bypass this forward direction automatically. Real, installed at `.github/workflows/forge-lifecycle.yml::closes-n-gate`. Same enforcement substrate as above. **Forge-native-tranche fix (`fix/closes-n-forge-native`):** the gate's scoped `loadTrancheFiles(null, onlySlug)` call discovers which tranches exist by listing `aeg-root/tranches/*.md` filenames — a tranche with zero topology file never entered that enumeration, so the forge derivation was never invoked for it despite the tranche already being fully Milestone-capable, and the gate failed every such PR with a misleading "no topology file found." Fixed by falling back to a direct forge derivation (the Milestone-existence check plus the file-and-forge composition, today `indexTrancheMilestonesAsync` + `mergeFileTopology`) when the requested `onlySlug` isn't found via files, gated on an actual open Milestone existing so an unrecognized branch slug still fails honestly — the scoped-path counterpart to `verify-dispatch.ts`'s `otherActiveTrancheSlugs` fix. **Reverse direction (`fix/closes-n-reverse`):** a `Closes #N` that resolves to a real AEG task Issue (title matches the `[<tranche-slug>] <task-id>...` convention and carries a `vinaya/tranche:<slug>` label) now requires the branch to actually be named `task/<tranche-slug>/<task-id>`, regardless of what the branch itself is called — closing the blind spot that let a mis-named branch implement a real task Issue with zero forge-visible status. One batched forge query (`fetchTaskIssueRefs`) resolves each referenced Issue's task identity before `checkClosesN` runs. **Code-span hardening (`fix/closes-gate-hardening`):** both the pre-merge gate (`checkClosesN` in `brief-validation.ts`) and the coherence-side parse (`extractClosesReferences` in `coherence-checks.ts`) now `stripCode` the body before matching the closing keyword, as GitHub's own auto-close parser ignores `Closes #N` inside a code span or fenced block. Inline spans of **any backtick-run length** are covered (`` `x` ``, `` ``x`` ``, …) via CommonMark's `(`+)…\1` matched-run rule — the double-backtick form was a false-green in the first cut (peeled as two empty spans). Fenced blocks are matched by a line scanner that pairs a fence with its own closer by **character and run length**: `~~~` tilde fences, info strings (```` ```js ````), and runs longer than three (```` `````` ````) all strip correctly, and an unclosed fence runs to end of body as GitHub renders it — the earlier `` /```[\s\S]*?```/g `` regex missed all three. The keyword set matches GitHub's own: `close`/`closes`/`closed`, `fix`/`fixes`/`fixed`, `resolve`/`resolves`/`resolved`. Before this, a body whose only `Closes #N` was backticked passed the gate **green** yet merged **without** closing its Issue, then read red on every open PR via A3 `auto-close-misfire`; "verify-docs green" now implies "GitHub will auto-close" for fenced + inline-code forms. 4-space **indented** code blocks are stripped too, conservatively: a ≥4-column-indented run counts as code only when it follows a blank line *and* is not inside a list — since within a list item that indentation is the item's own content indent, which GitHub *does* auto-close (blanking it would be a false-red, the brief's over-strip stop condition). The strip runs on the **whole body, before any region is sliced out of it** — never on a slice. Every rule in it is block-structural (a fence pairs with its own closer; an indented run is code only after a blank line and outside list context), so a fragment strips differently from the same text in place: stripping the sliced `AEG:CLOSES` region blanked an anchor indented inside a list item — list content GitHub *does* auto-close — and the Archivist's `extractIssue` returned no Issue, stranding it on merge exactly as before. Markers are HTML comments and survive the strip, so selecting the region from stripped text loses nothing and subsumes the decoy protection: a decoy anchor inside code never survives to be sliced. The same grammar governs `maskCode`, the index-preserving variant `anchoredRegion` runs to find the `AEG:*` markers themselves — it is **upstream** of every `stripCode` call, so while it stayed on the naive fence/inline regexes a decoy `AEG:CLOSES` anchor inside a tilde fence, a ≥4-backtick fence, a double-backtick span, or an indented block won the region outright and the gate resolved a **wrong** Issue number (worse than the strandings above: the post-merge Archivist's `extractIssue` would explicitly close an unrelated Issue). Both now delegate to one pair of scanners, differing only in what they emit per code line — nothing for `stripCode`, same-length filler for `maskCode` — so a divergence is a compile-level impossibility rather than a convention. This hardens all five anchored fields (`CLOSES`, `PROJECT`, `TIER`, `PREMISE`, `TEST-PLAN`) at once, not just the closing reference. `stripCode` normalises `\r\n`/`\r` to `\n` before any of these scanners run: the fence scanners anchor per line, and JS's `.`/`$` never match `\r`, so a CRLF body opened no fence at all and let a fenced `Closes #N` walk free — the same false-green along a new axis, and it hit exactly the web-UI-authored bodies this CI backstop exists for (HTML normalises textarea newlines to CRLF on submit). The closing-keyword separator is **bounded** (`\s{0,8}:?\s{0,8}`, identical in both parsers): two adjacent unbounded `\s*` groups backtrack quadratically on `closes` + long whitespace + no `#` — ~2.0 s at GitHub's 65,536-char body cap, run twice on the fail path — where the bound is 0.1 ms. **Known residual:** a genuine indented code block *nested inside a list* is therefore left unstripped — the deliberately safe direction of that trade. A `Closes #N` surviving only inside code fails with an actionable message pointing at a bare reference in the `AEG:CLOSES` anchor. `stripCode` is the one shared stripper, exported from `anchored-region.ts` (no duplicated regex).
|
|
310
310
|
- *(The practitioner-facing map of every enforcement mechanism — prevention, detection, audit — lives in `aeg-root/enforcement.md`; this section remains the normative gate registry.)*
|
|
311
|
-
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain — live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any
|
|
311
|
+
- **Tool-layer forge gates** — the earliest enforcement point: a PreToolUse hook (`.claude/hooks/check-forge-gates.sh`, wired in `.claude/settings.json`) denies raw `gh pr create`/`gh pr edit --body*`/`gh issue create`/`gh issue edit --body*` (and `gh api` creation POSTs), directing agents to the validated wrappers `packages/aeg-core/bin/open-pr.ts` (runs verify-brief + verify-docs `--pr` + the Closes #N gate locally, calls `gh` only on green) and `bin/open-issue.ts` (a task Issue with a `vinaya/tranche:*` label must carry the full eight-field Planner rationale — `checkIssueRationale`, planner-brief contract, **and pass three content checks on what those fields say, `checkBlastRadiusScope` (the declared surface may not reach a shared collision domain — live-derived `packages/*` workspace members, built-in cross-cutting defaults, plus any `vinaya.config.json` `blastRadius.extraDomains` entries — that no declared project owns, absent a second **registered** project or a `blast-radius-ack:` line), `checkNoBriefContent` (no brief-shaped section in the Issue), `checkRationaleNamesDocs` (the rationale names a concrete doc/skill path, or the `no-doc-surface` sentinel). `checkConflictCompleteness` warns on an undeclared collision-domain overlap and never blocks**). Prevention, not detection: a malformed PR/Issue body is refused at the agent's own tool call and never reaches the forge; the CI gates below re-run the identical aeg-core checks purely as a backstop for non-hook writers. Also enforced at the same layer: title grammar (`checkForgeTitle`, both wrappers), the single-plan-PR guard (`checkSinglePlanPr` — a diff touching a tranche's topology file is refused when another OPEN PR already touches that same tranche's topology file), `gh api` PATCH/curl write-method denies, and a `.husky/pre-push` gate refusing `task/<tranche>/<id>` pushes whose id has no topology row (mechanical). Operational rule: restart running agent sessions after merging hook/settings changes — hooks load at session start. Same mechanism as skill-check enforcement and the T9 merge gate.
|
|
312
312
|
- **Single-plan-PR CI backstop** — `checkSinglePlanPr` (`packages/aeg-core/src/single-plan-pr.ts`) was extracted from `open-pr.ts` (previously private to that wrapper) so the identical predicate could also run forge-side: `.github/workflows/forge-lifecycle.yml::single-plan-pr-gate` re-fetches this PR's touched files plus every other open PR's touched files and fails CI on the same violation the ring-0 hook refuses locally. Closes the gap where a PR opened directly via the GitHub web UI bypasses `open-pr.ts` entirely. No-ops (never fires) for an ordinary task-branch PR, since its diff never touches a tranche topology file. **Dormant in this repo entirely, as of the forge-native cutover:** a plan is now a Milestone plus labeled Issues, and `aeg-root/tranches/` holds no active topology file for a diff to collide over. The guard is kept, not deleted, because it is correct and live for any repo that does keep plans as files — but nothing here can trip it, and a gate that cannot fire should say so rather than read as active protection. One implementation, two enforcement points — no second copy of the predicate.
|
|
313
313
|
- **Coherence oracle (A1/A2/A3/T1/T2/T3/D1/M1/M3)** — `packages/aeg-core/bin/verify-coherence.ts` runs against every PR and genuinely blocks CI — the CLI's own exit code, non-zero on any `fail`-status check, is what the `coherence-gate` job exits with. Failures in A1 (closed-without-merge), A2 (archived-without-provenance), A3 (auto-close-misfire), T1 (phantom-issue-ref), T2 (orphan-task — **plan PRs only, see below**), T3 (tbd-in-active-tranche), D1 (dispatched-on-unmet-deps), M1 (manifest-dangling), and M3 (manifest-duplicate-glob) fail CI. L1–L5 and M2 are advisory (info-only) — every `checkL*` returns `status: info`, so a premature-archive, lifecycle-hygiene, Milestone-attachment-drift, or open-Milestone-all-closed finding is surfaced for a human to investigate but never fails CI. On A1 failures the relevant Issues receive the `vinaya/incoherent` label (Section 14). Real, installed at `.github/workflows/forge-lifecycle.yml::coherence-gate`. Same enforcement substrate as above. **The job's repo-state inputs (topology files, tranche list) are read from a freshly-fetched `origin/main` — not the checkout's `refs/pull/N/merge`, which GitHub materializes lazily and can lag behind main (5+ false-red cycles, 2026-07-03/04). A plan PR's own topology diff still reads from its head ref.**
|
|
314
314
|
- **Planner→Brief rationale completeness (R1)** — the same coherence oracle's **R1** check re-runs `checkIssueRationale` **and `checkProjectsRegistered`** (`packages/aeg-core`) against every open task Issue's body, batched per active tranche alongside T2. A non-grandfathered Issue missing any of the eight `contracts/planner-brief.md` rationale fields — or whose `Project:` field names a project with no row in the project registry — fails CI. Paired with the ring-0 creation gate (`bin/open-issue.ts`, same tool-layer-forge-gates row above) — R1 is the continuous half, the hook is the point-of-creation half; one grammar, two enforcement points (`aeg-root/enforcement.md`). Pre- Issues are grandfathered by explicit Issue number (`R1_GRANDFATHERED_ISSUES`), reported `info`, never blocking — see Section 15b. **Real.** Moves this seam's rationale-completeness half from Trusted (below) to Enforced; the "role doc matches contract prose" half of contract conformance remains Trusted.
|
|
@@ -9,6 +9,8 @@ sidebar_title: "Template: PR report"
|
|
|
9
9
|
|
|
10
10
|
**The `AEG:PREMISE` anchor is not optional when the brief carried a `Premise:` block.** Without it, `premise-recheck` scans the *whole* body for anything premise-shaped — including the brief's own original pins, pasted verbatim into the `<details>` reference copy at the bottom — and re-asserts those against the code you just changed. A premise pinning the *pre-fix* state will correctly fail once your fix lands, because the pin describes what you just changed away from. Put a fresh, post-fix, currently-true assertion inside `<!-- AEG:PREMISE:START -->` / `<!-- AEG:PREMISE:END -->` so the re-check asserts something true of the shipped diff, not the brief's stale snapshot.
|
|
11
11
|
|
|
12
|
+
**No bare digit outside a fenced block.** `body-bare-digits` (CI) refuses a countable claim — a test count, a file count, a timing figure, "N passed" — written loose in a sentence anywhere in this body. A digit is exempt for exactly one reason: it sits inside an inline code span or a fenced/indented code block (`` `N` `` or a fenced block), or inside `Closes #N`/`Project:` (this header block) / `Tier:` (under `## Scope`) / `Evidence` (under `## Evidence`), correctly placed under its own documented section. **Nowhere else** — including inside `Premise`/`Test plan` (both scanned exactly like ordinary prose, no anchor exemption at all — evidence there, byte counts, exit codes all need their own backticks too), an Issue/PR reference, a date, a version, a file path, or a section number: any of those now needs its own backticks (`` `#N` ``, `` `2026-08-18` ``, `` `0.12.0` ``) the same as any other digit. Write the number inside a fenced block or backticks, or don't write it bare at all.
|
|
13
|
+
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
<!-- AEG:CLOSES:START -->
|
|
@@ -126,7 +126,7 @@ The `Project(s)` column remains the authoritative blast-radius record per task;
|
|
|
126
126
|
Two tasks conflict if they touch the same **collision domain** and therefore must not run in parallel. The rules, after the panel's correction:
|
|
127
127
|
|
|
128
128
|
- **Conflicts are declared by the Planner** as `conflicts-with` edges in the thin file. Declared, not inferred.
|
|
129
|
-
- **Collision domains are packages**, live-derived from the repo's own workspace declaration's `packages/*` members (`package.json`'s `workspaces` array, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) — no file to author or keep in sync. Known cross-cutting collision paths that are detectable by convention — **whichever lockfile exists, monorepo config (`tsconfig.json`/`biome.json`/`turbo.json`), `.github/workflows`, `.husky`** — ship as a built-in default set for the same reason. Paths that couple tasks across package boundaries but have no universal convention — **`migrations/`, codegen outputs (protobuf/GraphQL/OpenAPI)** — are declared as their own collision domains via `vinaya.config.json`'s `blastRadius.extraDomains`.
|
|
129
|
+
- **Collision domains are packages**, live-derived from the repo's own workspace declaration's `packages/*` members (`package.json`'s `workspaces` array, or `pnpm-workspace.yaml`'s `packages:` list on a pnpm repo) — no file to author or keep in sync. Known cross-cutting collision paths that are detectable by convention — **whichever lockfile exists, monorepo config (`tsconfig.json`/`biome.json`/`turbo.json`), `.github/workflows`, `.husky`** — ship as a built-in default set for the same reason. Paths that couple tasks across package boundaries but have no universal convention — **`migrations/`, codegen outputs (protobuf/GraphQL/OpenAPI)** — are declared as their own collision domains via `vinaya.config.json`'s `blastRadius.extraDomains`. The legacy static `.aeg/packages` file is retired — no longer read, zero backward compatibility.
|
|
130
130
|
- **The conflict gate is forge-answerable with zero stored state:** "is a `conflicts-with` sibling's PR currently open?" If yes, don't start. That's it.
|
|
131
131
|
- **There is no dynamic path-overlap check.** (The panel's decisive correction.) Computing "which files is each in-flight task touching right now" would require a live task→changed-files map — exactly the mutable execution state the design eliminates. So it is forbidden (§10).
|
|
132
132
|
|