@attalabs/vinaya 0.19.3 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -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 +6 -4
- package/aeg-root/process.md +5 -5
- package/aeg-root/roles/archivist.md +9 -10
- package/aeg-root/roles/brief-author.md +1 -1
- package/aeg-root/roles/developer.md +1 -1
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +1 -1
- package/aeg-root/roles/tranche-archivist.md +3 -16
- 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/pr-report-template.md +4 -0
- package/dist/checks/bin/check-body-bare-digits.js +192 -27
- package/dist/checks/bin/check-branch-topology.js +518 -11
- package/dist/checks/bin/check-brief-shape.js +104 -11
- package/dist/checks/bin/check-closes-n.js +518 -11
- package/dist/checks/bin/check-coherence.js +526 -12
- package/dist/checks/bin/check-dead-branch-push.js +104 -11
- package/dist/checks/bin/check-dispatch-readiness.js +621 -14
- package/dist/checks/bin/check-doc-coverage-push.js +170 -20
- package/dist/checks/bin/check-doc-coverage.js +170 -20
- package/dist/checks/bin/check-doctrine-portability.js +112 -12
- package/dist/checks/bin/check-evidence-fresh.js +213 -20
- package/dist/checks/bin/check-first-push-dispatch.js +526 -12
- package/dist/checks/bin/check-issue-assignment.js +518 -11
- package/dist/checks/bin/check-main-branch-refusal.js +3724 -0
- package/dist/checks/bin/check-no-disk-state.js +104 -11
- package/dist/checks/bin/check-reader-resolvable-prose.js +186 -17
- package/dist/checks/bin/check-registry-gates.js +185 -16
- package/dist/checks/bin/check-retired-vocabulary.js +185 -16
- package/dist/checks/bin/check-review-gate.js +112 -12
- package/dist/checks/bin/check-single-plan-pr.js +104 -11
- package/dist/checks/bin/check-test-plan.js +104 -11
- package/dist/checks/bin/check-workspace-escape.js +104 -11
- package/dist/index.js +664 -56
- package/package.json +1 -1
- package/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.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.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.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.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/{[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]__0o771t1._.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]__03jepya._.js → [root-of-the-server]__16cuydh._.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/_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/_1n0cnq-._.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/{3noqvds-2bvrd.js → 0ea8-8nopz29h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0la0hhmjprdxm.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1lk7h-0buf4ab.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3gehliafvmjo9.js → 2twnwe77d4n-m.js} +1 -1
- 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/static/chunks/2iysedx-n06xc.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 → b2VfZK2AItZwlghK_pUN0}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → b2VfZK2AItZwlghK_pUN0}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → b2VfZK2AItZwlghK_pUN0}/_ssgManifest.js +0 -0
|
@@ -249,7 +249,7 @@ Do not leave documentation as an implication of the tier checklist. **List the e
|
|
|
249
249
|
- **Never list a new file for a one-off report, audit finding, coverage summary, or working brief.** If the task's deliverable is a finding, an audit result, or a coverage report, its home is the PR body (task-scoped) or an Issue/PR comment (not task-scoped) — never a new file under `aeg-root/` or a product's `aeg-project/`. A committed scratch file has already broken AEG Studio once (a coverage report committed as `aeg-root/tranches/<name>.audit.md` was silently parsed as a broken tranche by the Studio loader) and produced a stale permanent brief once (`aeg-project/briefs/<name>-brief.md`, contradicting this skill's own "pasted, not committed" rule above). See `tranche-model.md` §9 rule 4.
|
|
250
250
|
- **Tier 0** — usually none. State "No doc updates required (Tier 0)."
|
|
251
251
|
- **Tier 1** — name each: which spec(s) reflect the new behavior, which skill(s) if a convention shifted, `docs-index.md` if files were added/removed/renamed.
|
|
252
|
-
- **Tier 3** — all Tier 1 items, plus: which state changes (the per-project
|
|
252
|
+
- **Tier 3** — all Tier 1 items, plus: which state changes (the tranche file; per-project backlogs; any non-derivable fact recorded as an ordinary open Issue). **Never** list `roadmap.md`, `now.md`, or a pinned state Issue — all retired. Active-work state is derived from the forge, not written to a file.
|
|
253
253
|
|
|
254
254
|
A Tier 1+ brief with an empty doc-update list is malformed.
|
|
255
255
|
|
|
@@ -371,7 +371,7 @@ These two qualify a task and ride into the PR body. Both are reference-only —
|
|
|
371
371
|
```
|
|
372
372
|
**Project:** [project(s) this task touches — e.g. "web" or "engine, api"]
|
|
373
373
|
```
|
|
374
|
-
Multi-valued. Resolves against `.vinaya/projects.md`. **Required in a multi-project repo; omitted entirely in a single-project repo** (no registry → one project → no field). Routes the Developer to the right specs
|
|
374
|
+
Multi-valued. Resolves against `.vinaya/projects.md`. **Required in a multi-project repo; omitted entirely in a single-project repo** (no registry → one project → no field). Routes the Developer to the right specs; a value that doesn't resolve to a registry row makes the brief malformed (refuse, don't guess). It must match the Planner's `Project(s)` for the task, including every shared-package consumer in the blast radius — see `roles/planner.md` and the planner-brief contract.
|
|
375
375
|
|
|
376
376
|
```
|
|
377
377
|
**Ticket:** [external ticket link(s) — e.g. "SAT-412 — https://…"]
|
|
@@ -125,11 +125,10 @@ Rows = artifact types. Columns = roles. "—" means no authority. The Reviewer i
|
|
|
125
125
|
| **Specs** (`apps/*/specs/*.md`) | Approves PR; ratifies a spec-only change | Coherence review on PR; can open spec-only PRs | Writes in PR per brief scope | Validates cross-references; flags stale specs in drift cron |
|
|
126
126
|
| **Skills** (canonical `aeg-root/skills/*/SKILL.md`) | Approves PR | Coherence review | Writes in PR per brief scope | Flags stale skill references in drift cron |
|
|
127
127
|
| **Agent defs** | Approves PR | Coherence review | Writes in PR per brief scope | Flags stale agent references in drift cron |
|
|
128
|
-
| **`state.md`** | Approves PR | Writes in PR | Flags state changes needed in PR description | Updates per-project state at close-out, for every project the task listed |
|
|
129
128
|
| **Per-project backlogs** (`apps/*/specs/*-backlog.md`) | Approves PR | Writes (held/future items — out of the flow) | — | — |
|
|
130
|
-
| **`
|
|
129
|
+
| **`state-machine.md`** | Approves PR; final authority on system-level rule changes | Proposes changes via PR | — | Flags inconsistencies in drift cron |
|
|
131
130
|
| **`thinking.md`** | Reads | Writes freely in any Brief Author session (best-effort, optional) | Reads | Flags if untouched >7 days |
|
|
132
|
-
| **
|
|
131
|
+
| **Lessons log** (the standing lessons Issue) | Approves/rejects/defers items at windows | Appends items; marks resolved after Principal action | Appends via escalation (`severity: product`) | The **Tranche Archivist** appends the retrospective at Phase 13 close-out (`roles/tranche-archivist.md`) — one new comment per tranche, never an edit. (The hand-edited per-project state Issue this row once also covered is retired: everything it held is either forge-derived — Milestones + `vinaya/tranche:*` labels — or belongs in an ordinary open Issue closed when resolved.) |
|
|
133
132
|
| **Source code** | Merges PR | — | Writes in PR per brief scope; opens PR | — |
|
|
134
133
|
| **Forge labels** (the Section 14 vocabulary) | Applies `vinaya/override:docs` (Principal-only) | Applies `vinaya/tier:*` (Planner, at cut) + `vinaya/needs:*-input` / `vinaya/blocked` (by hand or via automation) | Applies `vinaya/needs:*-input` / `vinaya/blocked` (by hand or via automation) | Applies `vinaya/needs:brief-correction`; asserts `vinaya/tier:*` label == PR-body `Tier:` (drift cron) |
|
|
135
134
|
| **Task status** | — | — | — | — *(nobody writes it — derived from the forge)* |
|
|
@@ -167,7 +166,7 @@ Used when verifying state, resolving contradictions, running verify-docs, post-m
|
|
|
167
166
|
1. Ratified specs (`Status: ratified`)
|
|
168
167
|
2. Shipped code (main — what actually runs)
|
|
169
168
|
3. Aspirational specs (`Status: target`)
|
|
170
|
-
4.
|
|
169
|
+
4. The lessons Issue (append-only retrospectives/calibration) — non-derivable observations; active status always derived from the forge, never from any doc
|
|
171
170
|
5. Skills + `thinking.md`
|
|
172
171
|
6. Briefs / Issues / PR descriptions — intent at time of writing
|
|
173
172
|
7. Conversation logs / tool runtime logs — lowest
|
|
@@ -205,6 +204,8 @@ A spec file exists in one of four states:
|
|
|
205
204
|
|
|
206
205
|
Most specs are `draft` — no deliberate ratification pass has been done. Future PRs ratify as appropriate.
|
|
207
206
|
|
|
207
|
+
**Spec naming (locked):** spec filenames are `{product}-spec.md` or `{component}-spec.md` — no `-v0`/`-v1`/`-draft`/date suffixes. Version state lives in the file's `Status:` block above, never the filename. Renaming to add a version suffix requires a lock challenge.
|
|
208
|
+
|
|
208
209
|
> Note: the Reviewer's spec-conformance check (§3) reads the project spec **as written** — at whatever ratification state it currently holds. A `draft` spec is still the project's stated intent and is checked against; a contradiction with a `ratified` spec is the most serious. The Reviewer never edits the spec; if it's wrong, that's an escalation.
|
|
209
210
|
|
|
210
211
|
---
|
|
@@ -304,7 +305,7 @@ The lowest-commitment way to run AEG: read-only over a team's existing process.
|
|
|
304
305
|
- **Typecheck, tests** — standard CI gates; always blocking.
|
|
305
306
|
- **Issue template / no forbidden fields** — a required Issue template + a CI check reject planning metadata (priority/estimates/points) on task Issues, keeping them execution-only.
|
|
306
307
|
- **Brief validation** — the Archivist's `brief-validation` job runs `packages/aeg-core/bin/verify-brief.ts` against the PR body and checks presence of every required brief section (Tier, `For:` model attribution, `Project:` — both read from the PR body's header block, before the first `##` heading, the same region the post-merge Archivist's provenance assembly reads — tagged Test Plan, surface map, doc-update list, worktree Step 0, stop conditions, autonomy clause, `Closes #N`); flags malformed briefs (`vinaya/needs:brief-correction`) and fails CI. **The trigger is the body, not the branch (`fix/brief-gate-nontask`):** the gate runs when the branch is `task/<tranche>/<n>` **or** the body is brief-shaped (`isBriefShaped` — ≥2 of surface map / doc-update list / stop conditions / autonomy clause, matched on `stripCode`'d text so a brief *quoted inside a fence* stays exempt). Bodies that are neither still bypass — that exemption is for an ordinary non-AEG PR (a one-line dependency bump) which carries no brief and must not be forced to grow one. The old branch-only bypass was the wrong proxy for it: a standalone `fix/*` brief **is** a brief, and it skipped every section check — confirmed live on `fix/studio-tranche-href`, which shipped with no §7 documentation-update list because `checkDocUpdateList`, the checker that exists for exactly that, never ran. `Closes #N` remains **task-branch-only** (`BriefSectionsOptions.requireClosesN`): a standalone fix brief has no task Issue to close, and a `plan/*` PR is *forbidden* to carry one by the plan-PR guard below — requiring it there would make the two gates jointly unsatisfiable. The same validator also runs at **authoring time** as the `brief-shape` check — `PR_BODY="$(cat brief.md)" vinaya check brief-shape` (on this repo's toolchain, also `bun packages/aeg-core/bin/verify-brief.ts --body-file <brief.md>`) — before a PR or even a branch exists, so a Brief Author gates a brief before dispatch rather than after the Developer has done the work (`skills/brief-authoring/SKILL.md` protocol step 4); with no `BRANCH`, the branch is read from the brief's own Step 0 `git worktree add … -b` line. **Includes the plan-PR Closes guard**: a `plan/*` branch whose PR body carries `Closes #N` fails CI before the non-task bypass even runs — a plan PR creates Issues, it does not resolve one (`roles/planner.md`); this closed a confirmed pattern of plan PRs prematurely closing task Issues on merge. **Real**, now the `Brief Validation` step (9/9) of the AEG gate suite job in `.github/workflows/forge-lifecycle.yml`. Presence-only — it cannot judge whether a Test Plan item is truly scriptable or whether a declared `unit-tests-only` is justified by the surface map; those remain Reviewer + Verification judgment. Same enforcement substrate as above.
|
|
307
|
-
- **Per-task Archivist close-out** — `.github/workflows/archivist.yml::post-merge` (`packages/aeg-core/bin/archive-task.ts`) runs on every push to `main`, resolves the merge commit's PR via `gh api repos/{owner}/{repo}/commits/{sha}/pulls`, and — for task-branch PRs only — assembles and posts the Archivist provenance block (`roles/archivist.md` item 8) and explicitly closes the task's Issue, confirming the closed state before exiting. Idempotent **per PR**, not per Issue (an Issue can legitimately accrue multiple merged PRs over its life; idempotency never spans PRs): skips silently if the PR already carries a provenance comment. Skips silently on non-task branches (`plan/*`, `fix/*`, …). Fails loud (non-zero exit) on any `gh`/permission error — never a silent no-op. Real, installed with `contents: read`, `issues: write`, `pull-requests: write` permissions. Does not automate the Tranche Archivist or items 2–7 of per-task close-out (docs coherence,
|
|
308
|
+
- **Per-task Archivist close-out** — `.github/workflows/archivist.yml::post-merge` (`packages/aeg-core/bin/archive-task.ts`) runs on every push to `main`, resolves the merge commit's PR via `gh api repos/{owner}/{repo}/commits/{sha}/pulls`, and — for task-branch PRs only — assembles and posts the Archivist provenance block (`roles/archivist.md` item 8) and explicitly closes the task's Issue, confirming the closed state before exiting. Idempotent **per PR**, not per Issue (an Issue can legitimately accrue multiple merged PRs over its life; idempotency never spans PRs): skips silently if the PR already carries a provenance comment. Skips silently on non-task branches (`plan/*`, `fix/*`, …). Fails loud (non-zero exit) on any `gh`/permission error — never a silent no-op. Real, installed with `contents: read`, `issues: write`, `pull-requests: write` permissions. Does not automate the Tranche Archivist or items 2–7 of per-task close-out (docs coherence, `docs-index.md`, token ledger) — those remain dispatched-Archivist judgment work.
|
|
308
309
|
- **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
310
|
- **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
311
|
- *(The practitioner-facing map of every enforcement mechanism — prevention, detection, audit — lives in `aeg-root/enforcement.md`; this section remains the normative gate registry.)*
|
|
@@ -324,7 +325,7 @@ The lowest-commitment way to run AEG: read-only over a team's existing process.
|
|
|
324
325
|
- **Dispatch gates** (depends-on merged / no conflicting PR open) — read from the forge and complied with; mechanical enforcement arrives when an automation tool runs dispatch (`tranche-model.md` §8).
|
|
325
326
|
- **Label discipline** (Section 14) — `vinaya/tier:*` present on every task Issue and kept in sync with the PR-body `Tier:`; `vinaya/needs:*-input` / `vinaya/blocked` present-when-true *and removed when false*; no `status:*` labels; no `project:*` labels; no label outside the closed set. Trusted discipline; the Archivist drift cron asserts the tier label/field match and flags stale `vinaya/needs:*` labels.
|
|
326
327
|
- **Contract conformance** (a role doc matches its `contracts/*.md` seam) — trusted discipline; the Archivist drift cron flags a role doc that contradicts its contract. The contract is the source of truth; a divergent role doc is the bug. (The Planner→Brief contract's *rationale field-completeness* — every task Issue body carries all eight fields — moved to Enforced via R1 above; only the "role doc text matches contract prose" half remains trusted here.)
|
|
327
|
-
- **Provenance assembly at close-out — items 1/8 moved to Enforced**, see "Per-task Archivist close-out" above; items 2–7 of close-out (docs coherence,
|
|
328
|
+
- **Provenance assembly at close-out — items 1/8 moved to Enforced**, see "Per-task Archivist close-out" above; items 2–7 of close-out (docs coherence, `docs-index.md`, token ledger) remain trusted, dispatched-Archivist judgment work — it records, it never gates.
|
|
328
329
|
- **Decision logging during chat** — Brief Author announces and logs during the conversation; CI cannot verify.
|
|
329
330
|
- **No execution metadata in the tranche file; no dynamic conflict scanner** — the two anti-regression rules (`tranche-model.md` §9); trusted discipline, flagged by the Archivist drift cron and the Planner's gates.
|
|
330
331
|
- **`thinking.md` updates; ratification-window attendance; lock acknowledgment (advisory in V0); spec ratification passes** — all trusted.
|
|
@@ -400,6 +401,7 @@ No label outside this table may be applied to a task Issue or its PR. (The Archi
|
|
|
400
401
|
| `vinaya/tier:0` / `vinaya/tier:1` / `vinaya/tier:3` | Issue (+ mirrors the PR-body `Tier:`) | Impact tier — drives required docs (§9) and whether it merges at a ratification window. The forge has no concept of "impact." | **Planner** sets it at Issue cut (plan-time estimate). The **PR-body `Tier:`** is the binding value at merge; the Developer corrects the field if execution reveals a different tier, and re-syncs the label. | **Always-mandatory** — exactly one per task |
|
|
401
402
|
| `vinaya/type:build` / `vinaya/type:chore` / `vinaya/type:docs` / `vinaya/type:feat` / `vinaya/type:fix` / `vinaya/type:perf` / `vinaya/type:refactor` / `vinaya/type:revert` / `vinaya/type:style` / `vinaya/type:test` | Issue | What kind of change the task is — the same ten commit-type names `roles/developer.md`'s commit conventions use, applied to the Issue instead of the commit. The forge has no concept of a task's shape. | **Planner** sets it at Issue cut. | **Always-mandatory, checked at Issue creation only** — `open-issue.ts` requires exactly one on every `create`; it never re-checks the label on a later `edit`, so this is forward-only from this axis's own merge without retroactively refusing an edit to an Issue that predates it (no backfill) |
|
|
402
403
|
| `vinaya/blocked` | Issue | A block that has **no forge fact** behind it ("waiting on an answer" isn't visible from branch/PR state). | Developer/Brief Author when a task is blocked on an escalation; **removed** the moment it unblocks. | Conditional-mandatory |
|
|
404
|
+
| `vinaya/backlog` | Issue | Marks deliberate unplanned-ness — the Issue belongs to no tranche and is excluded from dispatch, a fact no forge field shows. | Planner/Principal when an Issue is consciously left unplanned; **removed** when a Planner promotes it into a tranche. | Conditional-mandatory |
|
|
403
405
|
| `vinaya/incoherent` | Issue | A `COMPLETED` close with **no merged-PR link** — the forge shows "closed" but cannot show *whether the one law was honored* (done iff a merged `Closes #N`). Marks the anomaly for a human to resolve; AEG never auto-reopens. | Detected by `verify-coherence` (A1) / surfaced in Studio; applied when the incoherence is found, **removed** when a human clears it (link the merge, or re-close `NOT_PLANNED`). | Conditional-mandatory |
|
|
404
406
|
| `vinaya/needs:execution-input` | Issue | Routes an open escalation to the **Brief Author** (§7). | Developer at escalation; removed when answered. | Conditional-mandatory |
|
|
405
407
|
| `vinaya/needs:strategy-input` | Issue | Routes to the **Brief Author** — a design-path question, not an execution one. | Developer at escalation; removed when answered. | Conditional-mandatory |
|
package/aeg-root/task-model.md
CHANGED
|
@@ -36,7 +36,7 @@ This is the operational sequence — distinct from §2's derived-status vocabula
|
|
|
36
36
|
3. **Review** — code-reviewer pass (`roles/reviewer.md`) and security pass (`roles/security.md`), each emitting a structured verdict. A REQUEST CHANGES or FAIL loops back to **Code** — bounded by a revision ceiling so the loop always ends, never open-ended.
|
|
37
37
|
4. **Verify** — the tagged Test Plan runs for real: the `[agent]` items the Developer executes against the running app, the `[principal]` items a human confirms. This is the gap between "CI is green" and "the feature actually works" — static review alone cannot exercise an auth-gated, key-dependent, or browser-rendered path.
|
|
38
38
|
5. **Merge** — the PR merges once Review and Verify are both satisfied. This is the boundary, not a stage with its own actor: merging is the mechanical consequence of the two gates above clearing, not a separate decision.
|
|
39
|
-
6. **Archive** — the Archivist closes out: the Issue is explicitly closed (never left to GitHub's advisory auto-close), docs coherence is confirmed, the
|
|
39
|
+
6. **Archive** — the Archivist closes out: the Issue is explicitly closed (never left to GitHub's advisory auto-close), docs coherence is confirmed, the provenance block is assembled and posted to the merged PR. This is the task's terminal stage — nothing follows it at this altitude. (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.)
|
|
40
40
|
|
|
41
41
|
Orphaned task (a branch exists, no PR, gone stale) is the one named exception to this straight line: a close-out/sweep step flags it and a human deletes the branch, returning the task to `todo` — a real, owned recovery path, not a silent drop.
|
|
42
42
|
|
|
@@ -46,6 +46,10 @@ Closes #[N]
|
|
|
46
46
|
|
|
47
47
|
Run `vinaya pr report --write <this-body-file>` and commit its output — this block is generated, never hand-typed. `check-evidence-fresh` refuses a body whose block doesn't match the head it's attached to.
|
|
48
48
|
|
|
49
|
+
The block opens with `Head:` and a `Summary:` line — a file and line count derived from the same `--numstat` printed two lines below it, so the body never needs a hand-written "four files changed" sentence that a later commit silently falsifies. Its value is emitted inside backticks, which is what makes it legal under the bare-digit rule below — no exemption, just the same inline code span any other digit needs. Do not type or edit the line: `check-evidence-fresh` byte-compares it, backticks included, against a fresh recompute, and a hand-written count fails there.
|
|
50
|
+
|
|
51
|
+
**Keep the anchor pair out of the `<details>` block.** `body-bare-digits` blanks every digit inside a collapsed block, so an evidence block hidden in one is exempt from the digit scan and unverifiable — `check-evidence-fresh` refuses rather than passing silently. Both checks resolve this region from one shared, normalised context, so a zero-width character or an HTML entity in a marker no longer makes one of them see a block the other cannot.
|
|
52
|
+
|
|
49
53
|
<!-- AEG:EVIDENCE:START -->
|
|
50
54
|
[run `vinaya pr report --write` to populate — do not type this block by hand]
|
|
51
55
|
<!-- AEG:EVIDENCE:END -->
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
6
6
|
// ../../packages/aeg-core/src/gate-audience.ts
|
|
7
7
|
var GATE_AUDIENCE = {
|
|
8
|
-
"check-branch-topology": { shippedAs: "branch-topology" },
|
|
9
|
-
"check-first-push-dispatch": { shippedAs: "first-push-dispatch" },
|
|
10
|
-
"check-no-disk-state": { shippedAs: "no-disk-state" },
|
|
11
|
-
"verify-brief": { shippedAs: "brief-shape" },
|
|
12
|
-
"verify-coherence": { shippedAs: "coherence" },
|
|
13
|
-
"verify-dispatch": { shippedAs: "dispatch-readiness" },
|
|
14
|
-
"verify-docs": { shippedAs: ["doc-coverage", "doc-coverage-push"] },
|
|
15
|
-
"verify-registry": { shippedAs: "registry-gates" },
|
|
16
|
-
"verify-review-gate": { shippedAs: "review-gate" },
|
|
17
|
-
"verify-single-plan-pr": { shippedAs: "single-plan-pr" },
|
|
18
|
-
"verify-test-plan": { shippedAs: "test-plan" },
|
|
8
|
+
"check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
|
|
9
|
+
"check-first-push-dispatch": { shippedAs: "first-push-dispatch", ring: 0 },
|
|
10
|
+
"check-no-disk-state": { shippedAs: "no-disk-state", ring: 0 },
|
|
11
|
+
"verify-brief": { shippedAs: "brief-shape", ring: 0 },
|
|
12
|
+
"verify-coherence": { shippedAs: "coherence", ring: 0 },
|
|
13
|
+
"verify-dispatch": { shippedAs: "dispatch-readiness", ring: 0 },
|
|
14
|
+
"verify-docs": { shippedAs: ["doc-coverage", "doc-coverage-push"], ring: 0 },
|
|
15
|
+
"verify-registry": { shippedAs: "registry-gates", ring: 0 },
|
|
16
|
+
"verify-review-gate": { shippedAs: "review-gate", ring: 1 },
|
|
17
|
+
"verify-single-plan-pr": { shippedAs: "single-plan-pr", ring: 0 },
|
|
18
|
+
"verify-test-plan": { shippedAs: "test-plan", ring: 1 },
|
|
19
19
|
"check-direct-main-push": {
|
|
20
20
|
internal: "Reaches adopters through `vinaya audit --only=direct-push`, not the check registry: it is a ring-2 sweep over merge history keyed on `GITHUB_SHA`, run by the generated archivist job on push to the default branch. It has no diff to key on, so there is nothing for `vinaya check` to run it against."
|
|
21
21
|
},
|
|
@@ -327,6 +327,13 @@ var LABELS = [
|
|
|
327
327
|
form: "literal",
|
|
328
328
|
carries: "Execution is halted pending an external unblock; wins over every other derived status."
|
|
329
329
|
},
|
|
330
|
+
{
|
|
331
|
+
key: "backlog",
|
|
332
|
+
id: "vinaya/backlog",
|
|
333
|
+
category: "state",
|
|
334
|
+
form: "literal",
|
|
335
|
+
carries: "The Issue is deliberately unplanned — belongs to no tranche and is excluded from dispatch, until a Planner promotes it."
|
|
336
|
+
},
|
|
330
337
|
{
|
|
331
338
|
key: "tier-0",
|
|
332
339
|
id: "vinaya/tier:0",
|
|
@@ -1985,7 +1992,59 @@ function overrideActive(opts) {
|
|
|
1985
1992
|
return false;
|
|
1986
1993
|
}
|
|
1987
1994
|
// ../../packages/aeg-core/src/premise-check.ts
|
|
1995
|
+
import { createHash } from "node:crypto";
|
|
1988
1996
|
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
1997
|
+
function isPremiseHeader(line) {
|
|
1998
|
+
const stripped = line.replace(/[*#]/g, "").trim();
|
|
1999
|
+
return /^premise\s*:?$/i.test(stripped);
|
|
2000
|
+
}
|
|
2001
|
+
var PREMISE_LINE = /^[-*]\s*(\S+)\s+(contains|absent|sha256)\s*:\s*(.+)$/i;
|
|
2002
|
+
function parsePremiseBlock(prBody) {
|
|
2003
|
+
const searchIn = anchoredRegion(prBody, "PREMISE") ?? prBody;
|
|
2004
|
+
const lines = searchIn.split(/\r?\n/);
|
|
2005
|
+
const assertions = [];
|
|
2006
|
+
let inBlock = false;
|
|
2007
|
+
for (const raw of lines) {
|
|
2008
|
+
const line = raw.trim();
|
|
2009
|
+
if (!inBlock) {
|
|
2010
|
+
if (isPremiseHeader(line))
|
|
2011
|
+
inBlock = true;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (line === "")
|
|
2015
|
+
break;
|
|
2016
|
+
const m = line.match(PREMISE_LINE);
|
|
2017
|
+
if (!m)
|
|
2018
|
+
break;
|
|
2019
|
+
const [, path, kindRaw, value] = m;
|
|
2020
|
+
const kind = kindRaw.toLowerCase();
|
|
2021
|
+
if (!ASSERTION_KINDS.has(kind))
|
|
2022
|
+
continue;
|
|
2023
|
+
assertions.push({ kind, path, value: value.trim() });
|
|
2024
|
+
}
|
|
2025
|
+
return assertions;
|
|
2026
|
+
}
|
|
2027
|
+
function checkPremises(assertions, fileReader) {
|
|
2028
|
+
const failures = [];
|
|
2029
|
+
for (const a of assertions) {
|
|
2030
|
+
const content = fileReader(a.path);
|
|
2031
|
+
if (content === null) {
|
|
2032
|
+
failures.push(`premise-check: ${a.path} does not exist on disk (premise asserted \`${a.kind}: ${a.value}\`) — the surface moved since this brief was authored; re-dig before proceeding.`);
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (a.kind === "contains" && !content.includes(a.value)) {
|
|
2036
|
+
failures.push(`premise-check: ${a.path} no longer contains "${a.value}" — the premise this brief pinned has moved; re-dig before proceeding.`);
|
|
2037
|
+
} else if (a.kind === "absent" && content.includes(a.value)) {
|
|
2038
|
+
failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
|
|
2039
|
+
} else if (a.kind === "sha256") {
|
|
2040
|
+
const actual = createHash("sha256").update(content).digest("hex");
|
|
2041
|
+
if (actual !== a.value.toLowerCase()) {
|
|
2042
|
+
failures.push(`premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return { pass: failures.length === 0, failures };
|
|
2047
|
+
}
|
|
1989
2048
|
|
|
1990
2049
|
// ../../packages/aeg-core/src/test-plan-section.ts
|
|
1991
2050
|
var HEADING_FORM_RE = /^#{1,4}[ \t]*(?:\d+[a-z]?\.[ \t]*)?\*{0,2}Test Plan\*{0,2}[ \t]*$/im;
|
|
@@ -2789,6 +2848,7 @@ function checkG1(rows, existsFn) {
|
|
|
2789
2848
|
}
|
|
2790
2849
|
return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
2791
2850
|
}
|
|
2851
|
+
var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
|
|
2792
2852
|
function checkG2(rows, candidateFiles) {
|
|
2793
2853
|
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
|
|
2794
2854
|
const findings = [];
|
|
@@ -2800,6 +2860,16 @@ function checkG2(rows, candidateFiles) {
|
|
|
2800
2860
|
});
|
|
2801
2861
|
}
|
|
2802
2862
|
}
|
|
2863
|
+
for (const row of rows) {
|
|
2864
|
+
const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
|
|
2865
|
+
if (carriesPlaceholder) {
|
|
2866
|
+
findings.push({
|
|
2867
|
+
row: row.action,
|
|
2868
|
+
path: row.implementation,
|
|
2869
|
+
reason: `${row.ring} row "${row.action}" still carries the scaffold placeholder marker — the why is owed`
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2803
2873
|
return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
|
|
2804
2874
|
}
|
|
2805
2875
|
function checkG3(ring0Rows, crossingFiles) {
|
|
@@ -3319,6 +3389,29 @@ function isNewDiskStateFile(path, status) {
|
|
|
3319
3389
|
return false;
|
|
3320
3390
|
return ANY_DEPTH_TRANCHES_MD.test(path) || TOKENS_FILE.test(path);
|
|
3321
3391
|
}
|
|
3392
|
+
// ../../packages/aeg-core/src/main-branch-refusal.ts
|
|
3393
|
+
function checkMainBranchRefusal(facts) {
|
|
3394
|
+
const { currentSymbolicBranch, defaultBranch } = facts;
|
|
3395
|
+
if (currentSymbolicBranch === null)
|
|
3396
|
+
return null;
|
|
3397
|
+
if (defaultBranch === null) {
|
|
3398
|
+
return {
|
|
3399
|
+
reason: "default-branch-undetermined",
|
|
3400
|
+
severity: "warning",
|
|
3401
|
+
currentBranch: currentSymbolicBranch,
|
|
3402
|
+
defaultBranch: null
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
if (currentSymbolicBranch === defaultBranch) {
|
|
3406
|
+
return {
|
|
3407
|
+
reason: "on-default-branch",
|
|
3408
|
+
severity: "error",
|
|
3409
|
+
currentBranch: currentSymbolicBranch,
|
|
3410
|
+
defaultBranch
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
return null;
|
|
3414
|
+
}
|
|
3322
3415
|
// ../../packages/aeg-core/src/branch-topology-gate.ts
|
|
3323
3416
|
function taskBranchTopologyFields(branch) {
|
|
3324
3417
|
if (!/^task\/.*\/.*$/.test(branch))
|
|
@@ -3665,8 +3758,15 @@ function isSafeRepoRelPath(p) {
|
|
|
3665
3758
|
var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
|
|
3666
3759
|
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
3667
3760
|
});
|
|
3761
|
+
var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
|
|
3762
|
+
function isCanonicalHookBlockPath(p) {
|
|
3763
|
+
return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
|
|
3764
|
+
}
|
|
3765
|
+
var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
|
|
3766
|
+
message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
|
|
3767
|
+
});
|
|
3668
3768
|
var ManagedBlockRecordSchema = z.object({
|
|
3669
|
-
path:
|
|
3769
|
+
path: ManagedHookBlockPath,
|
|
3670
3770
|
marker: z.string(),
|
|
3671
3771
|
comment: z.enum(["hash", "html"])
|
|
3672
3772
|
});
|
|
@@ -3847,6 +3947,81 @@ function emitCheckError(error) {
|
|
|
3847
3947
|
`);
|
|
3848
3948
|
}
|
|
3849
3949
|
|
|
3950
|
+
// src/lib/numstat.ts
|
|
3951
|
+
function summariseNumstat(numstat) {
|
|
3952
|
+
const rows = numstat.split(`
|
|
3953
|
+
`).filter((l) => l.trim() !== "");
|
|
3954
|
+
let added = 0;
|
|
3955
|
+
let deleted = 0;
|
|
3956
|
+
let binary = 0;
|
|
3957
|
+
for (const row of rows) {
|
|
3958
|
+
const [a, d] = row.split("\t");
|
|
3959
|
+
if (a === "-" || d === "-") {
|
|
3960
|
+
binary++;
|
|
3961
|
+
continue;
|
|
3962
|
+
}
|
|
3963
|
+
added += Number(a) || 0;
|
|
3964
|
+
deleted += Number(d) || 0;
|
|
3965
|
+
}
|
|
3966
|
+
const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
|
|
3967
|
+
const bin = binary > 0 ? `, ${binary} binary` : "";
|
|
3968
|
+
return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin}`;
|
|
3969
|
+
}
|
|
3970
|
+
var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
|
|
3971
|
+
|
|
3972
|
+
// src/checks/scan-context.ts
|
|
3973
|
+
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
3974
|
+
var HTML_ENTITIES = {
|
|
3975
|
+
""": '"',
|
|
3976
|
+
"'": "'",
|
|
3977
|
+
"<": "<",
|
|
3978
|
+
">": ">",
|
|
3979
|
+
"&": "&"
|
|
3980
|
+
};
|
|
3981
|
+
function decodeNamedEntities(body) {
|
|
3982
|
+
return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
3983
|
+
}
|
|
3984
|
+
function normalizeBody(rawBody) {
|
|
3985
|
+
return decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
|
|
3986
|
+
}
|
|
3987
|
+
function buildAnchorLookupMask(body) {
|
|
3988
|
+
return maskDetailsBlocks(maskCode(body));
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
class ScanContext {
|
|
3992
|
+
raw;
|
|
3993
|
+
normalised;
|
|
3994
|
+
masked;
|
|
3995
|
+
constructor(raw, normalised, masked) {
|
|
3996
|
+
this.raw = raw;
|
|
3997
|
+
this.normalised = normalised;
|
|
3998
|
+
this.masked = masked;
|
|
3999
|
+
}
|
|
4000
|
+
static from(rawBody) {
|
|
4001
|
+
const normalised = normalizeBody(rawBody);
|
|
4002
|
+
return new ScanContext(rawBody, normalised, buildAnchorLookupMask(normalised));
|
|
4003
|
+
}
|
|
4004
|
+
rawResolutionAgrees(field) {
|
|
4005
|
+
const rawHasPair = anchoredRegionBounds(this.raw, field) !== null;
|
|
4006
|
+
return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
function resolveAnchoredRegion(ctx, field) {
|
|
4010
|
+
const bounds = anchoredRegionBounds(ctx.masked, field);
|
|
4011
|
+
if (bounds === null) {
|
|
4012
|
+
return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
|
|
4013
|
+
}
|
|
4014
|
+
return {
|
|
4015
|
+
region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
|
|
4016
|
+
maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
function summaryLineIndex(maskedRegion) {
|
|
4020
|
+
const index = maskedRegion.split(`
|
|
4021
|
+
`).findIndex((line) => line.startsWith(EVIDENCE_SUMMARY_PREFIX));
|
|
4022
|
+
return index === -1 ? null : index;
|
|
4023
|
+
}
|
|
4024
|
+
|
|
3850
4025
|
// src/checks/body-bare-digits-logic.ts
|
|
3851
4026
|
function blankRange(text, start, end) {
|
|
3852
4027
|
const region = text.slice(start, end);
|
|
@@ -4022,8 +4197,8 @@ var BOUNDED_ANCHOR_BLANK = {
|
|
|
4022
4197
|
PROJECT: blankProjectField,
|
|
4023
4198
|
EVIDENCE: blankEvidenceField
|
|
4024
4199
|
};
|
|
4025
|
-
function buildScanMask(
|
|
4026
|
-
let masked =
|
|
4200
|
+
function buildScanMask(ctx) {
|
|
4201
|
+
let masked = ctx.masked;
|
|
4027
4202
|
masked = blankAnchoredRegions(masked);
|
|
4028
4203
|
masked = blankPremiseValues(masked);
|
|
4029
4204
|
masked = blankTokenReportSection(masked);
|
|
@@ -4032,17 +4207,6 @@ function buildScanMask(body) {
|
|
|
4032
4207
|
}
|
|
4033
4208
|
var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
|
|
4034
4209
|
var LIST_MARKER_TOKEN = /^\p{Nd}{1,9}[.)]$/u;
|
|
4035
|
-
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
4036
|
-
var HTML_ENTITIES = {
|
|
4037
|
-
""": '"',
|
|
4038
|
-
"'": "'",
|
|
4039
|
-
"<": "<",
|
|
4040
|
-
">": ">",
|
|
4041
|
-
"&": "&"
|
|
4042
|
-
};
|
|
4043
|
-
function decodeNamedEntities(body) {
|
|
4044
|
-
return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
4045
|
-
}
|
|
4046
4210
|
function isLineLeadingListMarker(line, matchStart, rawToken) {
|
|
4047
4211
|
if (!LIST_MARKER_TOKEN.test(rawToken))
|
|
4048
4212
|
return false;
|
|
@@ -4050,8 +4214,9 @@ function isLineLeadingListMarker(line, matchStart, rawToken) {
|
|
|
4050
4214
|
return /^ {0,3}$/.test(before);
|
|
4051
4215
|
}
|
|
4052
4216
|
function checkBareDigits(rawBody) {
|
|
4053
|
-
const
|
|
4054
|
-
const
|
|
4217
|
+
const ctx = ScanContext.from(rawBody);
|
|
4218
|
+
const body = ctx.normalised;
|
|
4219
|
+
const masked = buildScanMask(ctx);
|
|
4055
4220
|
const maskedLines = masked.split(`
|
|
4056
4221
|
`);
|
|
4057
4222
|
const origLines = body.split(`
|