@attalabs/vinaya 0.19.0 → 0.19.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 +35 -1
- package/aeg-root/enforcement.md +3 -2
- package/dist/checks/bin/check-body-bare-digits.js +204 -15
- package/dist/checks/bin/check-branch-topology.js +116 -13
- package/dist/checks/bin/check-brief-shape.js +116 -13
- package/dist/checks/bin/check-closes-n.js +116 -13
- package/dist/checks/bin/check-coherence.js +207 -18
- package/dist/checks/bin/check-dead-branch-push.js +116 -13
- package/dist/checks/bin/check-dispatch-readiness.js +204 -15
- package/dist/checks/bin/check-doc-coverage-push.js +208 -19
- package/dist/checks/bin/check-doc-coverage.js +208 -19
- package/dist/checks/bin/check-evidence-fresh.js +116 -13
- package/dist/checks/bin/check-first-push-dispatch.js +204 -15
- package/dist/checks/bin/check-issue-assignment.js +116 -13
- package/dist/checks/bin/check-no-disk-state.js +116 -13
- package/dist/checks/bin/check-reader-resolvable-prose.js +410 -36
- package/dist/checks/bin/check-registry-gates.js +116 -13
- package/dist/checks/bin/check-retired-vocabulary.js +3595 -0
- package/dist/checks/bin/check-review-gate.js +204 -15
- package/dist/checks/bin/check-single-plan-pr.js +116 -13
- package/dist/checks/bin/check-test-plan.js +116 -13
- package/dist/index.js +4740 -3988
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.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/[root-of-the-server]__0e95dv5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0ulzsom._.js → [root-of-the-server]__0xdkvod._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -5
- 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/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/{2r36w820aqwb9.js → 1zaotsjjq5o6o.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2xoclb4g7d9n7.js → 295mri9sjante.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3ocvtlhgckh81.js → 2opd70pfbc5yx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/templates/noop-check.template.ts +18 -0
- package/templates/role.template.md +20 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{RISz5nirLr1mUfMqzSaFS → SG6gXRhit12wl-8HLX9d3}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{RISz5nirLr1mUfMqzSaFS → SG6gXRhit12wl-8HLX9d3}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{RISz5nirLr1mUfMqzSaFS → SG6gXRhit12wl-8HLX9d3}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
23
23
|
| `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. `--port <n>` binds an exact port — see below. |
|
|
24
24
|
| `vinaya milestone create --title <title> --body-file <path>` | Create a GitHub Milestone from a validated body. The title is free text, never parsed for a version — the description's optional `Release:` field is the sole authority for one. Refuses before any `gh` write when the goal is absent, `Release:` is present but malformed, or an `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
|
|
25
25
|
| `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]` | Move one or more existing tranches into a target Milestone: reattaches every Issue carrying each `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact is gathered and refused-or-passed as ONE batch before any write — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone refuses the whole invocation, not just its own slug. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
|
|
26
|
+
| `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
|
|
26
27
|
|
|
27
28
|
## Config
|
|
28
29
|
|
|
@@ -78,9 +79,25 @@ Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory
|
|
|
78
79
|
|
|
79
80
|
On that legacy `.git/hooks` layout, note where the hook actually lives: hooks are never per-worktree, so its real home is the main checkout's shared hooks directory (`git rev-parse --git-common-dir`), which from a linked worktree is outside that worktree's own root. `vinaya eject` follows the hook there and bounds the removal to that directory's `hooks/` subtree, rather than to the worktree root — so ejecting from a linked worktree strips the hook instead of leaving it armed. A canonically spelled block path resolving anywhere else is refused, and the refusal is whole-run: `eject` removes nothing at all rather than making a partial destructive pass. (The `.git/` test is byte-exact, so a non-canonical spelling takes the working-tree branch instead — nothing vinaya generates produces one.)
|
|
80
81
|
|
|
82
|
+
## Agent-native entry points (`--agents`)
|
|
83
|
+
|
|
84
|
+
`vinaya init` writes three agent-native entry points by default, one per vendor: `.agents/skills/vinaya-<role>/SKILL.md` (Agent skills, below), `.claude/commands/vinaya.md` (Claude Code command), and `.gemini/commands/vinaya.toml` (Gemini CLI command). Each one is a thin pointer that shells out to `vinaya doctrine --role <role>` at read time — there is exactly one source of role doctrine, never a copy baked into any of these files.
|
|
85
|
+
|
|
86
|
+
`vinaya init --agents=<comma-list|all|none>` controls which of the three get written — `all` (the default) writes all three, `none` writes none, and a comma-separated list (e.g. `--agents=claude,gemini`) writes exactly the named vendors. There is no interactive prompt: `init` is scriptable/CI-safe today, and prompting would break that. The selection is persisted into `vinaya.config.json`'s `managed.agents` — `vinaya upgrade`/`vinaya doctor` read it back rather than re-deriving a default, so a repo initialized with a narrowed selection never has a later flagless `vinaya upgrade` silently add the other vendors' files, nor silently drop the recorded selection; a vendor left out is simply invisible to `vinaya doctor` rather than reported as "not installed". `vinaya eject` removes whichever of the three are actually owned, the same way it removes every other vinaya-managed artifact.
|
|
87
|
+
|
|
81
88
|
## Agent skills
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
Generates skill pointers under `.agents/skills/vinaya-<role>/SKILL.md`, for tools natively scanning `.agents/skills/` (Codex, Antigravity, Grok Build) — one file per role discovered under `aeg-root/roles/*.md`, excluding `principal` (the one seat this doctrine never grants an agent). Each file is a 3-line pointer delegating to `vinaya doctrine --role <role>` at read time.
|
|
91
|
+
|
|
92
|
+
## Gemini CLI command
|
|
93
|
+
|
|
94
|
+
Generates `.gemini/commands/vinaya.toml`, Gemini CLI's own custom-command surface. It emits ONE parameterized command, invoked `/vinaya <role>`, whose `prompt` field embeds `!{vinaya doctrine --role {{args}}}`: Gemini CLI substitutes the typed role into `{{args}}`, shell-escaping it automatically, before running the shell block — and it always prompts the user to confirm the exact resolved command first, with no documented bypass. Role-argument validation stays in `vinaya doctrine --role` alone (`commands/doctrine.ts`), not duplicated here.
|
|
95
|
+
|
|
96
|
+
## Claude Code command
|
|
97
|
+
|
|
98
|
+
Generates a single parameterized `.claude/commands/vinaya.md`, invoked `/vinaya <role>`. The file uses Claude Code's `$ARGUMENTS` substitution to shell out to `vinaya doctrine --role "$ARGUMENTS"` at read time, scoped by `allowed-tools: Bash(vinaya doctrine *)` so it runs without a permission prompt.
|
|
99
|
+
|
|
100
|
+
**Security note:** Claude Code substitutes `$ARGUMENTS` into the command as a raw, pre-shell text splice — the substitution happens before bash parses the line, so the role token can carry live shell syntax. Double-quoting (`"$ARGUMENTS"`) closes the `;`/`|`/`&&` class the substitution's own documented apostrophe bug demonstrates, but — verified empirically, not assumed — it does **not** close `$(...)`/backtick command substitution (still executes inside double quotes), and no quote character can be made airtight against this kind of splice at all: an attacker's token can always contain that same quote character, close it early, inject a command, then reopen a matching quote so the rest of the line still parses. `vinaya doctrine --role`'s own handling (`doctrine.ts`) validates whatever argument it actually receives against the exact set of role names live-discovered from `aeg-root/roles/*.md` and refuses anything else — that gate is real and load-bearing for its own scope, but an injected `$(...)` or quote-breakout command never reaches it as an argument; it runs at the shell level first. The actual remaining backstop is Claude Code's own `allowed-tools: Bash(vinaya doctrine *)` permission matcher refusing to auto-run a chained/injected command — a platform property outside this repo's control, unverified as of this writing. Do not describe this as fully closed in any future edit here or in the emitter; it is reduced, not closed.
|
|
84
101
|
|
|
85
102
|
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:
|
|
86
103
|
|
|
@@ -101,6 +118,23 @@ The one exception is a key that exactly matches a **core** check id: that is an
|
|
|
101
118
|
|
|
102
119
|
Glob scoping (`include`) is permitted; conditional logic (`if`/`unless`/`except`) is **never** part of this grammar — see the check-contract quick reference below for the full grammar and the error contract every registered `run` executable must honor.
|
|
103
120
|
|
|
121
|
+
## Roles config
|
|
122
|
+
|
|
123
|
+
Per-role override and additive-role registration works the same way `checks` does, under `vinaya.config.json`'s own `roles` key — one entry per role, each naming a `contract`: a markdown file, structurally validated against the same shape a bundled role doc carries (`role_id`, `description`, `actor`, `performs`, `refuses_when`, `summary` in frontmatter, plus `title` and `order`, plus a non-empty `## The short version` body section):
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"roles": {
|
|
128
|
+
"security": { "contract": "./roles/custom-security.md" },
|
|
129
|
+
"acme/qa-lead": { "contract": "./roles/qa-lead.md" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A key that exactly matches a **core** role id (`developer`, `security`, and the rest of the bundled doctrine roles) is an override — a **complete replacement**, never a frontmatter patch — whose contract's own `role_id` must equal that key exactly. Any other key must be namespaced `<yourname>/<id>`, same grammar as `checks`, and is additive — its contract's own `role_id` must equal the key's post-`/` segment exactly, and that render id must not collide with a core role id or another additive role's render id. Unlike `checks`, there is no grace period here: a malformed entry (a shape violation, a `role_id` mismatch, a collision, a bare unnamespaced key) fails closed immediately, since `roles` config has no legacy population a warn window would need to keep working. `contract` is a path, resolved relative to `vinaya.config.json`'s own directory — a bare filename with no `/` is rejected at load, the same discipline `checks.run`'s executable path does not need but a file **read** benefits from. Only registrable from a repo-local `vinaya.config.json`; a global `~/.vinaya/config.json`'s `roles` key is stripped at load time, since a role contract becomes agent-facing doctrine (`vinaya doctrine --role` hands it to a third-party agent tool as operating instructions).
|
|
135
|
+
|
|
136
|
+
Run `vinaya check --plan` to see the resolved role registry before anything renders from it — a `RENDERS AS` column (the registry key and the role's own `role_id` differ once an additive entry is namespaced) and a `GATING` column (`core` for a `default`/`overridden` role, `inert` for an `additive` one — no core `ACTIONS` wiring exists for a render id core doctrine never declared).
|
|
137
|
+
|
|
104
138
|
## Check contract — quick reference
|
|
105
139
|
|
|
106
140
|
Full field-by-field reference: [vinaya.attalabs.dev/docs/cli](https://vinaya.attalabs.dev/docs/cli). The short version — what an executable must do to be a valid check:
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -117,12 +117,13 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
117
117
|
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-0 row above — but the scan's definition of "mutating call" is unchanged.) | `packages/aeg-core/bin/verify-registry.ts` |
|
|
118
118
|
| G4 — cited forge numbers resolve | Ever read a doc that cited a ticket number that didn't exist? | ci | Re-checks that every Issue and PR number cited in the docs resolves to a real one. | Every `#NNN` cited anywhere in this page's body is a real Issue or PR in the forge — a fabricated citation fails the build. **Currently scans nothing by design** (task 14): this page's body carries zero forge citations, since task 3 banned them from `aeg-root/**` as doctrine and the `reader-resolvable-prose` check (task 15) now enforces that ban directly. G4 is a standing guard against reintroduction, not a live proof of ongoing citation correctness — it fires the moment a `#NNN` reappears here and doesn't resolve, but has nothing to scan while the ban holds. Verified live: `packages/aeg-core/src/registry-checks.test.ts` appends a fabricated citation to this page's real content and asserts `checkG4` catches it. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
119
119
|
| G5 — role/contract integrity | Ever had a process doc reference a role that was never actually defined? | ci | Re-checks that every role and contract the doctrine references is really defined. | Every `aeg-root/contracts/*.md`'s `producer`/`consumer` names a real `role_id` from `aeg-root/roles/*.md`; every role's `performs`/`refuses_when` frontmatter is present and non-empty. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
120
|
-
| reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every
|
|
120
|
+
| reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `<doctrineRoot>/**` doc and every configured reader-facing page, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes a sibling suite proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `<doctrineRoot>/glossary.md`'s own entry headings, never hard-coded. Registered in `coreCheckRegistry()` (task 7): its doctrine root, reader-facing globs, and legacy-slug corpus location are adopter-configurable via `vinaya.config.json`'s `proseGates` key, defaulting to this repo's own prior hardcoded shape (`doctrineRoot: "aeg-root"`, dormant reader-facing sweep) when unset. Part of `vinaya check --all` for every adopter; not part of the managed hooks. | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
|
|
121
|
+
| retired-vocabulary | Ever read a doc that described a mechanism as live after it was retired? | ci | Re-checks that no doctrine page claims a retired mechanism is still live. | The genuinely-retired half of a sibling suite's original scan — never the forge-number/tranche-slug citation half, which is `reader-resolvable-prose`'s own job and would double-report the identical match if duplicated here. Scoped to `<doctrineRoot>/**` only (the same `proseGates.doctrineRoot` key `reader-resolvable-prose` reads), not the whole adopter repo — this vocabulary means something only inside the shipped doctrine tree. Registered in `coreCheckRegistry()` (task 7), report-only like `reader-resolvable-prose`. | `apps/cli/src/checks/bin/check-retired-vocabulary.ts` |
|
|
121
122
|
| No new on-disk state | Ever had a file and the forge disagree about the same fact? | ci | Blocks a diff that creates a new on-disk state file duplicating what the forge already derives. | No live per-tranche topology file appears under `aeg-root/tranches/` (a top-level file there fails on add or edit; under `completed/**` only genuinely NEW files are refused, so the legacy archives stay editable), and no new `*.tokens.md` appears anywhere — token figures live in the PR body, and the ledger row is appended post-merge. Status-aware (`git diff --name-status`), never fired by a deletion; a rename counts as an addition at its new path. The registered `no-disk-state` check runs the same `isNewDiskStateFile` predicate at ring 0 (the managed hooks) and here; this row names the standalone CLI shim of that gate. | `packages/aeg-core/bin/check-no-disk-state.ts` |
|
|
122
123
|
|
|
123
124
|
**G-checks rollout (task 3):** G1/G2 ship report-only this tranche — they can only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surface as visible debt without retroactively failing in-flight work. G3/G4/G5 are blocking from this tranche onward. G1 flips to blocking in a later, separately-dispatched task, once the report-only period has let the orphan backlog get cleaned up.
|
|
124
125
|
|
|
125
|
-
**reader-resolvable-prose rollout (task 15):**
|
|
126
|
+
**reader-resolvable-prose / retired-vocabulary rollout (task 15; task 7 registered both in `coreCheckRegistry()`):** both ship report-only, same precedent as G1/G2 above — each can only ever print a `warning` finding, never fail CI (the check's own exit code always stays `0`), so any real backlog surfaces as visible debt rather than a wave of newly-red pull requests (161 findings, all class 2, across `aeg-root` on the day `reader-resolvable-prose` first shipped). A later, separately-dispatched task flips either to blocking once its own backlog is cleared.
|
|
126
127
|
|
|
127
128
|
Red CI is now unmergeable by any actor — the repository ruleset's `required_status_checks` rule carries `bypass_actors: []`, so the Principal has no ambient override either. The Principal's override paths are explicit and auditable instead: edit the ruleset itself, or apply an actor-verified `vinaya/waiver:review` label to skip the review gate specifically.
|
|
128
129
|
|
|
@@ -566,6 +566,12 @@ function factsFromLegacyMilestone(milestone) {
|
|
|
566
566
|
lifecycle: milestone.state === "closed" ? "complete" : "active"
|
|
567
567
|
};
|
|
568
568
|
}
|
|
569
|
+
function resolveLegacyFacts(milestone, labelIssues) {
|
|
570
|
+
if (milestone.state === "closed" && labelIssues.length > 0) {
|
|
571
|
+
return { goal: milestone.description ?? "", lifecycle: lifecycleFromIssues(labelIssues) };
|
|
572
|
+
}
|
|
573
|
+
return factsFromLegacyMilestone(milestone);
|
|
574
|
+
}
|
|
569
575
|
function lifecycleFromIssues(issues) {
|
|
570
576
|
if (issues.length === 0)
|
|
571
577
|
return "planned";
|
|
@@ -582,9 +588,9 @@ function goalFromMilestones(milestones, slug) {
|
|
|
582
588
|
function findMilestoneForSlug(owner, repo, slug) {
|
|
583
589
|
const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
|
|
584
590
|
const legacy = matchesLegacyMilestone(milestones, slug);
|
|
585
|
-
if (legacy)
|
|
586
|
-
return factsFromLegacyMilestone(legacy);
|
|
587
591
|
const issues = ghIssueListByLabel(owner, repo, trancheLabel(slug));
|
|
592
|
+
if (legacy)
|
|
593
|
+
return resolveLegacyFacts(legacy, issues);
|
|
588
594
|
return { goal: goalFromMilestones(milestones, slug), lifecycle: lifecycleFromIssues(issues) };
|
|
589
595
|
}
|
|
590
596
|
|
|
@@ -2771,16 +2777,18 @@ function checkDeadBranchPush(input) {
|
|
|
2771
2777
|
}
|
|
2772
2778
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
2773
2779
|
var SHIPS_PREFIX = "aeg-root/";
|
|
2774
|
-
|
|
2780
|
+
function shipsArchivePrefix(shipsPrefix) {
|
|
2781
|
+
return `${shipsPrefix}tranches/completed/`;
|
|
2782
|
+
}
|
|
2775
2783
|
function isSpecFile2(path) {
|
|
2776
2784
|
return path.startsWith("apps/") && path.includes("/specs/") && path.endsWith(".md");
|
|
2777
2785
|
}
|
|
2778
2786
|
function isClaudeMdFile(path) {
|
|
2779
2787
|
return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
|
|
2780
2788
|
}
|
|
2781
|
-
function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix) {
|
|
2782
|
-
if (path.startsWith(
|
|
2783
|
-
return path.startsWith(
|
|
2789
|
+
function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPrefix = SHIPS_PREFIX) {
|
|
2790
|
+
if (path.startsWith(shipsPrefix)) {
|
|
2791
|
+
return path.startsWith(shipsArchivePrefix(shipsPrefix)) ? "internal" : "ships";
|
|
2784
2792
|
}
|
|
2785
2793
|
if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
|
|
2786
2794
|
return "reader-facing";
|
|
@@ -2815,7 +2823,7 @@ function legacySlugPattern(legacySlugs) {
|
|
|
2815
2823
|
const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
2816
2824
|
return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, "g");
|
|
2817
2825
|
}
|
|
2818
|
-
function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2826
|
+
function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = [], shipsPrefix = SHIPS_PREFIX) {
|
|
2819
2827
|
const findings = [];
|
|
2820
2828
|
const legacyPattern = legacySlugPattern(legacySlugs);
|
|
2821
2829
|
const patterns = [
|
|
@@ -2824,7 +2832,7 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
2824
2832
|
...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
|
|
2825
2833
|
];
|
|
2826
2834
|
for (const file of files) {
|
|
2827
|
-
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2835
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
2828
2836
|
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2829
2837
|
continue;
|
|
2830
2838
|
const scrubbed = stripNonProse(file.path, file.content);
|
|
@@ -2870,10 +2878,10 @@ function definesOrLinksGlossary(content, term) {
|
|
|
2870
2878
|
return true;
|
|
2871
2879
|
return /docs\/glossary|glossary\.md/i.test(content);
|
|
2872
2880
|
}
|
|
2873
|
-
function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix) {
|
|
2881
|
+
function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix = SHIPS_PREFIX) {
|
|
2874
2882
|
const findings = [];
|
|
2875
2883
|
for (const file of files) {
|
|
2876
|
-
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2884
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
2877
2885
|
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2878
2886
|
continue;
|
|
2879
2887
|
const scrubbed = stripNonProse(file.path, file.content);
|
|
@@ -2892,10 +2900,10 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
|
|
|
2892
2900
|
}
|
|
2893
2901
|
return findings;
|
|
2894
2902
|
}
|
|
2895
|
-
function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2903
|
+
function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = [], shipsPrefix = SHIPS_PREFIX) {
|
|
2896
2904
|
return [
|
|
2897
|
-
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs),
|
|
2898
|
-
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix)
|
|
2905
|
+
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs, shipsPrefix),
|
|
2906
|
+
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
|
|
2899
2907
|
];
|
|
2900
2908
|
}
|
|
2901
2909
|
function parseGlossaryTerms(glossaryContent) {
|
|
@@ -2909,6 +2917,101 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2909
2917
|
}
|
|
2910
2918
|
return terms;
|
|
2911
2919
|
}
|
|
2920
|
+
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
2921
|
+
var RETIRED_PATTERNS = [
|
|
2922
|
+
String.raw`\bD-[0-9]{3}\b`,
|
|
2923
|
+
"D-(###|NNN|nnn|xxx)",
|
|
2924
|
+
"decision (entry|logged)",
|
|
2925
|
+
"decision-log entry",
|
|
2926
|
+
"decision log entry",
|
|
2927
|
+
"decision log(s|ged|ging)?([^a-z]|$)",
|
|
2928
|
+
"decision-log",
|
|
2929
|
+
"decision entr",
|
|
2930
|
+
"decisions? (are |is )?logged",
|
|
2931
|
+
"log entries",
|
|
2932
|
+
String.raw`decisions\.md`,
|
|
2933
|
+
"decisions-legacy",
|
|
2934
|
+
"CONTRADICTION",
|
|
2935
|
+
"assumes Tier 3",
|
|
2936
|
+
"Lock: ?YES",
|
|
2937
|
+
"Lock: ?NO",
|
|
2938
|
+
"lock approvals?",
|
|
2939
|
+
"approves locks",
|
|
2940
|
+
"Conforms to lock",
|
|
2941
|
+
"Challenges lock",
|
|
2942
|
+
"checkDecisionNumbersFresh",
|
|
2943
|
+
String.raw`roles/team-leader\.md`,
|
|
2944
|
+
String.raw`\(,`,
|
|
2945
|
+
String.raw`,\)`,
|
|
2946
|
+
String.raw`\(\.\)`
|
|
2947
|
+
];
|
|
2948
|
+
var RETIRED_EXEMPT_SUBSTRINGS = [
|
|
2949
|
+
"docs/decisions-legacy.md",
|
|
2950
|
+
"apps/herald-ai/docs/",
|
|
2951
|
+
"apps/vada-ai/docs/",
|
|
2952
|
+
"aeg-root/tranches/completed/",
|
|
2953
|
+
"packages/aeg-core/src/docs/published-prose",
|
|
2954
|
+
"packages/aeg-core/src/retired-vocabulary.test.ts",
|
|
2955
|
+
"packages/aeg-core/src/retired-vocabulary.ts",
|
|
2956
|
+
"/fixtures/",
|
|
2957
|
+
"/node_modules/",
|
|
2958
|
+
"/.next/",
|
|
2959
|
+
"/.turbo/"
|
|
2960
|
+
];
|
|
2961
|
+
var PATTERN_EXEMPT = {
|
|
2962
|
+
[String.raw`\bD-[0-9]{3}\b`]: ["./CLAUDE.md"],
|
|
2963
|
+
[String.raw`decisions\.md`]: [
|
|
2964
|
+
"docs-index.md",
|
|
2965
|
+
"apps/vada-ai/specs/legacy/README.md",
|
|
2966
|
+
"apps/vada-ai/specs/vada-product-spec.md",
|
|
2967
|
+
"apps/vada-ai/web/CLAUDE.md",
|
|
2968
|
+
"apps/vada-ai/CLAUDE.md"
|
|
2969
|
+
],
|
|
2970
|
+
"decisions-legacy": [
|
|
2971
|
+
"packages/aeg-core/src/file-classify.ts",
|
|
2972
|
+
"packages/aeg-core/src/file-classify.test.ts",
|
|
2973
|
+
"packages/aeg-core/src/pr-tier.test.ts",
|
|
2974
|
+
"docs-index.md",
|
|
2975
|
+
".claude/skills/database/SKILL.md",
|
|
2976
|
+
".claude/skills/vada-architecture/SKILL.md",
|
|
2977
|
+
"apps/vada-ai/specs/vada-byok-principles.md",
|
|
2978
|
+
"apps/vada-ai/web/CLAUDE.md",
|
|
2979
|
+
"apps/vada-ai/CLAUDE.md"
|
|
2980
|
+
],
|
|
2981
|
+
CONTRADICTION: ["apps/vada-ai/specs/vada-reviewers-spec.md"]
|
|
2982
|
+
};
|
|
2983
|
+
function lineNumbers(content, pattern) {
|
|
2984
|
+
const out = [];
|
|
2985
|
+
const lines = content.split(`
|
|
2986
|
+
`);
|
|
2987
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2988
|
+
if (pattern.test(lines[i] ?? ""))
|
|
2989
|
+
out.push(i + 1);
|
|
2990
|
+
}
|
|
2991
|
+
return out;
|
|
2992
|
+
}
|
|
2993
|
+
function scanRetiredVocabulary(files) {
|
|
2994
|
+
const findings = [];
|
|
2995
|
+
for (const file of files) {
|
|
2996
|
+
if (RETIRED_EXEMPT_SUBSTRINGS.some((e) => file.path.includes(e)))
|
|
2997
|
+
continue;
|
|
2998
|
+
for (const pattern of RETIRED_PATTERNS) {
|
|
2999
|
+
const patternExempt = PATTERN_EXEMPT[pattern] ?? [];
|
|
3000
|
+
if (patternExempt.some((e) => file.path.includes(e)))
|
|
3001
|
+
continue;
|
|
3002
|
+
const re = new RegExp(pattern);
|
|
3003
|
+
for (const line of lineNumbers(file.content, re)) {
|
|
3004
|
+
findings.push({
|
|
3005
|
+
file: file.path,
|
|
3006
|
+
line,
|
|
3007
|
+
pattern,
|
|
3008
|
+
message: `claims a retired AEG mechanism is still live (matches /${pattern}/)`
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return findings;
|
|
3014
|
+
}
|
|
2912
3015
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2913
3016
|
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2914
3017
|
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
@@ -3164,9 +3267,15 @@ function evaluateTestPlanGate(body, branch) {
|
|
|
3164
3267
|
}
|
|
3165
3268
|
// src/lib/config.ts
|
|
3166
3269
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3270
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3167
3271
|
import { homedir } from "node:os";
|
|
3168
3272
|
import { dirname, join } from "node:path";
|
|
3169
3273
|
import { z } from "zod";
|
|
3274
|
+
|
|
3275
|
+
// src/lib/agent-vendors.ts
|
|
3276
|
+
var AGENT_VENDORS = ["skills", "claude", "gemini"];
|
|
3277
|
+
|
|
3278
|
+
// src/lib/config.ts
|
|
3170
3279
|
var EnvEntrySchema = z.union([
|
|
3171
3280
|
z.literal(true),
|
|
3172
3281
|
z.object({ optional: z.literal(true) }),
|
|
@@ -3205,6 +3314,11 @@ var CheckEntrySchema = z.object({
|
|
|
3205
3314
|
}
|
|
3206
3315
|
}
|
|
3207
3316
|
});
|
|
3317
|
+
var RoleEntrySchema = z.object({
|
|
3318
|
+
contract: z.string().refine((p) => p.includes("/"), {
|
|
3319
|
+
message: 'must be a path (contain at least one "/"), not a bare filename'
|
|
3320
|
+
})
|
|
3321
|
+
});
|
|
3208
3322
|
var BRIEF_BUILTINS = [
|
|
3209
3323
|
"tier",
|
|
3210
3324
|
"testPlan",
|
|
@@ -3253,7 +3367,8 @@ var ManagedManifestSchema = z.object({
|
|
|
3253
3367
|
version: z.number().int().positive(),
|
|
3254
3368
|
files: z.array(SafeRepoRelPath),
|
|
3255
3369
|
blocks: z.array(ManagedBlockRecordSchema),
|
|
3256
|
-
labels: z.array(z.string())
|
|
3370
|
+
labels: z.array(z.string()),
|
|
3371
|
+
agents: z.array(z.enum(AGENT_VENDORS)).optional()
|
|
3257
3372
|
});
|
|
3258
3373
|
var VinayaConfigSchema = z.object({
|
|
3259
3374
|
rings: z.object({
|
|
@@ -3261,16 +3376,79 @@ var VinayaConfigSchema = z.object({
|
|
|
3261
3376
|
ring2_asyncAudits: z.boolean()
|
|
3262
3377
|
}).optional(),
|
|
3263
3378
|
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
3379
|
+
roles: z.record(z.string(), RoleEntrySchema).optional(),
|
|
3264
3380
|
briefSchema: BriefSchemaSchema.optional(),
|
|
3265
3381
|
managed: ManagedManifestSchema.optional(),
|
|
3266
3382
|
principals: z.array(z.string()).min(1).optional(),
|
|
3267
3383
|
releaseActor: z.string().min(1).optional(),
|
|
3268
3384
|
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
3269
|
-
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
|
|
3385
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
|
|
3386
|
+
proseGates: z.object({
|
|
3387
|
+
doctrineRoot: z.string().min(1).optional(),
|
|
3388
|
+
readerFacingPrefix: z.string().min(1).optional(),
|
|
3389
|
+
readerFacingSuffix: z.string().min(1).optional(),
|
|
3390
|
+
legacySlugDir: z.string().min(1).optional()
|
|
3391
|
+
}).optional()
|
|
3270
3392
|
});
|
|
3271
3393
|
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3272
3394
|
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
3273
3395
|
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
3396
|
+
function findLocalConfig() {
|
|
3397
|
+
let dir = process.cwd();
|
|
3398
|
+
while (true) {
|
|
3399
|
+
const candidate = join(dir, LOCAL_CONFIG_FILENAME);
|
|
3400
|
+
if (existsSync(candidate))
|
|
3401
|
+
return candidate;
|
|
3402
|
+
if (existsSync(join(dir, ".git")))
|
|
3403
|
+
return null;
|
|
3404
|
+
const parent = dirname(dir);
|
|
3405
|
+
if (parent === dir)
|
|
3406
|
+
return null;
|
|
3407
|
+
dir = parent;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
function configPath() {
|
|
3411
|
+
const local = findLocalConfig();
|
|
3412
|
+
if (local)
|
|
3413
|
+
return local;
|
|
3414
|
+
if (existsSync(GLOBAL_CONFIG_PATH))
|
|
3415
|
+
return GLOBAL_CONFIG_PATH;
|
|
3416
|
+
return null;
|
|
3417
|
+
}
|
|
3418
|
+
function globalChecksIgnoredWarning(path) {
|
|
3419
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
3420
|
+
}
|
|
3421
|
+
function globalPrincipalsIgnoredWarning(path) {
|
|
3422
|
+
return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
|
|
3423
|
+
}
|
|
3424
|
+
function globalRolesIgnoredWarning(path) {
|
|
3425
|
+
return `${path}: "roles" registration in the global config is ignored — roles may only be registered from a repo-local vinaya.config.json.`;
|
|
3426
|
+
}
|
|
3427
|
+
function globalReleaseActorIgnoredWarning(path) {
|
|
3428
|
+
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
3429
|
+
}
|
|
3430
|
+
function stripGlobalOnlyKeys(config, path) {
|
|
3431
|
+
if (path !== GLOBAL_CONFIG_PATH)
|
|
3432
|
+
return config;
|
|
3433
|
+
let result = config;
|
|
3434
|
+
if (result.checks && Object.keys(result.checks).length > 0) {
|
|
3435
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
3436
|
+
result = { ...result, checks: undefined };
|
|
3437
|
+
}
|
|
3438
|
+
if (result.roles && Object.keys(result.roles).length > 0) {
|
|
3439
|
+
console.error(`⚠ ${globalRolesIgnoredWarning(path)}`);
|
|
3440
|
+
result = { ...result, roles: undefined };
|
|
3441
|
+
}
|
|
3442
|
+
if (result.principals && result.principals.length > 0) {
|
|
3443
|
+
console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
|
|
3444
|
+
result = { ...result, principals: undefined };
|
|
3445
|
+
}
|
|
3446
|
+
if (result.releaseActor) {
|
|
3447
|
+
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
3448
|
+
result = { ...result, releaseActor: undefined };
|
|
3449
|
+
}
|
|
3450
|
+
return result;
|
|
3451
|
+
}
|
|
3274
3452
|
function resolvePrincipalAllowlist(config) {
|
|
3275
3453
|
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
3276
3454
|
}
|
|
@@ -3343,6 +3521,17 @@ function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
|
3343
3521
|
return null;
|
|
3344
3522
|
}
|
|
3345
3523
|
}
|
|
3524
|
+
function loadConfig() {
|
|
3525
|
+
const path = configPath();
|
|
3526
|
+
if (!path)
|
|
3527
|
+
return null;
|
|
3528
|
+
try {
|
|
3529
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
3530
|
+
return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
|
|
3531
|
+
} catch {
|
|
3532
|
+
return null;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3346
3535
|
|
|
3347
3536
|
// src/checks/contract.ts
|
|
3348
3537
|
var CHECK_SCHEMA_VERSION = 1;
|
|
@@ -565,6 +565,12 @@ function factsFromLegacyMilestone(milestone) {
|
|
|
565
565
|
lifecycle: milestone.state === "closed" ? "complete" : "active"
|
|
566
566
|
};
|
|
567
567
|
}
|
|
568
|
+
function resolveLegacyFacts(milestone, labelIssues) {
|
|
569
|
+
if (milestone.state === "closed" && labelIssues.length > 0) {
|
|
570
|
+
return { goal: milestone.description ?? "", lifecycle: lifecycleFromIssues(labelIssues) };
|
|
571
|
+
}
|
|
572
|
+
return factsFromLegacyMilestone(milestone);
|
|
573
|
+
}
|
|
568
574
|
function lifecycleFromIssues(issues) {
|
|
569
575
|
if (issues.length === 0)
|
|
570
576
|
return "planned";
|
|
@@ -581,9 +587,9 @@ function goalFromMilestones(milestones, slug) {
|
|
|
581
587
|
function findMilestoneForSlug(owner, repo, slug) {
|
|
582
588
|
const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
|
|
583
589
|
const legacy = matchesLegacyMilestone(milestones, slug);
|
|
584
|
-
if (legacy)
|
|
585
|
-
return factsFromLegacyMilestone(legacy);
|
|
586
590
|
const issues = ghIssueListByLabel(owner, repo, trancheLabel(slug));
|
|
591
|
+
if (legacy)
|
|
592
|
+
return resolveLegacyFacts(legacy, issues);
|
|
587
593
|
return { goal: goalFromMilestones(milestones, slug), lifecycle: lifecycleFromIssues(issues) };
|
|
588
594
|
}
|
|
589
595
|
|
|
@@ -2770,16 +2776,18 @@ function checkDeadBranchPush(input) {
|
|
|
2770
2776
|
}
|
|
2771
2777
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
2772
2778
|
var SHIPS_PREFIX = "aeg-root/";
|
|
2773
|
-
|
|
2779
|
+
function shipsArchivePrefix(shipsPrefix) {
|
|
2780
|
+
return `${shipsPrefix}tranches/completed/`;
|
|
2781
|
+
}
|
|
2774
2782
|
function isSpecFile2(path) {
|
|
2775
2783
|
return path.startsWith("apps/") && path.includes("/specs/") && path.endsWith(".md");
|
|
2776
2784
|
}
|
|
2777
2785
|
function isClaudeMdFile(path) {
|
|
2778
2786
|
return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
|
|
2779
2787
|
}
|
|
2780
|
-
function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix) {
|
|
2781
|
-
if (path.startsWith(
|
|
2782
|
-
return path.startsWith(
|
|
2788
|
+
function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPrefix = SHIPS_PREFIX) {
|
|
2789
|
+
if (path.startsWith(shipsPrefix)) {
|
|
2790
|
+
return path.startsWith(shipsArchivePrefix(shipsPrefix)) ? "internal" : "ships";
|
|
2783
2791
|
}
|
|
2784
2792
|
if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
|
|
2785
2793
|
return "reader-facing";
|
|
@@ -2814,7 +2822,7 @@ function legacySlugPattern(legacySlugs) {
|
|
|
2814
2822
|
const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
2815
2823
|
return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, "g");
|
|
2816
2824
|
}
|
|
2817
|
-
function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2825
|
+
function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = [], shipsPrefix = SHIPS_PREFIX) {
|
|
2818
2826
|
const findings = [];
|
|
2819
2827
|
const legacyPattern = legacySlugPattern(legacySlugs);
|
|
2820
2828
|
const patterns = [
|
|
@@ -2823,7 +2831,7 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
2823
2831
|
...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
|
|
2824
2832
|
];
|
|
2825
2833
|
for (const file of files) {
|
|
2826
|
-
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2834
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
2827
2835
|
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2828
2836
|
continue;
|
|
2829
2837
|
const scrubbed = stripNonProse(file.path, file.content);
|
|
@@ -2869,10 +2877,10 @@ function definesOrLinksGlossary(content, term) {
|
|
|
2869
2877
|
return true;
|
|
2870
2878
|
return /docs\/glossary|glossary\.md/i.test(content);
|
|
2871
2879
|
}
|
|
2872
|
-
function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix) {
|
|
2880
|
+
function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix = SHIPS_PREFIX) {
|
|
2873
2881
|
const findings = [];
|
|
2874
2882
|
for (const file of files) {
|
|
2875
|
-
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
|
|
2883
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
2876
2884
|
if (!cls || !SWEPT_CLASSES.has(cls))
|
|
2877
2885
|
continue;
|
|
2878
2886
|
const scrubbed = stripNonProse(file.path, file.content);
|
|
@@ -2891,10 +2899,10 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
|
|
|
2891
2899
|
}
|
|
2892
2900
|
return findings;
|
|
2893
2901
|
}
|
|
2894
|
-
function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
|
|
2902
|
+
function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = [], shipsPrefix = SHIPS_PREFIX) {
|
|
2895
2903
|
return [
|
|
2896
|
-
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs),
|
|
2897
|
-
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix)
|
|
2904
|
+
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs, shipsPrefix),
|
|
2905
|
+
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
|
|
2898
2906
|
];
|
|
2899
2907
|
}
|
|
2900
2908
|
function parseGlossaryTerms(glossaryContent) {
|
|
@@ -2908,6 +2916,101 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
2908
2916
|
}
|
|
2909
2917
|
return terms;
|
|
2910
2918
|
}
|
|
2919
|
+
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
2920
|
+
var RETIRED_PATTERNS = [
|
|
2921
|
+
String.raw`\bD-[0-9]{3}\b`,
|
|
2922
|
+
"D-(###|NNN|nnn|xxx)",
|
|
2923
|
+
"decision (entry|logged)",
|
|
2924
|
+
"decision-log entry",
|
|
2925
|
+
"decision log entry",
|
|
2926
|
+
"decision log(s|ged|ging)?([^a-z]|$)",
|
|
2927
|
+
"decision-log",
|
|
2928
|
+
"decision entr",
|
|
2929
|
+
"decisions? (are |is )?logged",
|
|
2930
|
+
"log entries",
|
|
2931
|
+
String.raw`decisions\.md`,
|
|
2932
|
+
"decisions-legacy",
|
|
2933
|
+
"CONTRADICTION",
|
|
2934
|
+
"assumes Tier 3",
|
|
2935
|
+
"Lock: ?YES",
|
|
2936
|
+
"Lock: ?NO",
|
|
2937
|
+
"lock approvals?",
|
|
2938
|
+
"approves locks",
|
|
2939
|
+
"Conforms to lock",
|
|
2940
|
+
"Challenges lock",
|
|
2941
|
+
"checkDecisionNumbersFresh",
|
|
2942
|
+
String.raw`roles/team-leader\.md`,
|
|
2943
|
+
String.raw`\(,`,
|
|
2944
|
+
String.raw`,\)`,
|
|
2945
|
+
String.raw`\(\.\)`
|
|
2946
|
+
];
|
|
2947
|
+
var RETIRED_EXEMPT_SUBSTRINGS = [
|
|
2948
|
+
"docs/decisions-legacy.md",
|
|
2949
|
+
"apps/herald-ai/docs/",
|
|
2950
|
+
"apps/vada-ai/docs/",
|
|
2951
|
+
"aeg-root/tranches/completed/",
|
|
2952
|
+
"packages/aeg-core/src/docs/published-prose",
|
|
2953
|
+
"packages/aeg-core/src/retired-vocabulary.test.ts",
|
|
2954
|
+
"packages/aeg-core/src/retired-vocabulary.ts",
|
|
2955
|
+
"/fixtures/",
|
|
2956
|
+
"/node_modules/",
|
|
2957
|
+
"/.next/",
|
|
2958
|
+
"/.turbo/"
|
|
2959
|
+
];
|
|
2960
|
+
var PATTERN_EXEMPT = {
|
|
2961
|
+
[String.raw`\bD-[0-9]{3}\b`]: ["./CLAUDE.md"],
|
|
2962
|
+
[String.raw`decisions\.md`]: [
|
|
2963
|
+
"docs-index.md",
|
|
2964
|
+
"apps/vada-ai/specs/legacy/README.md",
|
|
2965
|
+
"apps/vada-ai/specs/vada-product-spec.md",
|
|
2966
|
+
"apps/vada-ai/web/CLAUDE.md",
|
|
2967
|
+
"apps/vada-ai/CLAUDE.md"
|
|
2968
|
+
],
|
|
2969
|
+
"decisions-legacy": [
|
|
2970
|
+
"packages/aeg-core/src/file-classify.ts",
|
|
2971
|
+
"packages/aeg-core/src/file-classify.test.ts",
|
|
2972
|
+
"packages/aeg-core/src/pr-tier.test.ts",
|
|
2973
|
+
"docs-index.md",
|
|
2974
|
+
".claude/skills/database/SKILL.md",
|
|
2975
|
+
".claude/skills/vada-architecture/SKILL.md",
|
|
2976
|
+
"apps/vada-ai/specs/vada-byok-principles.md",
|
|
2977
|
+
"apps/vada-ai/web/CLAUDE.md",
|
|
2978
|
+
"apps/vada-ai/CLAUDE.md"
|
|
2979
|
+
],
|
|
2980
|
+
CONTRADICTION: ["apps/vada-ai/specs/vada-reviewers-spec.md"]
|
|
2981
|
+
};
|
|
2982
|
+
function lineNumbers(content, pattern) {
|
|
2983
|
+
const out = [];
|
|
2984
|
+
const lines = content.split(`
|
|
2985
|
+
`);
|
|
2986
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2987
|
+
if (pattern.test(lines[i] ?? ""))
|
|
2988
|
+
out.push(i + 1);
|
|
2989
|
+
}
|
|
2990
|
+
return out;
|
|
2991
|
+
}
|
|
2992
|
+
function scanRetiredVocabulary(files) {
|
|
2993
|
+
const findings = [];
|
|
2994
|
+
for (const file of files) {
|
|
2995
|
+
if (RETIRED_EXEMPT_SUBSTRINGS.some((e) => file.path.includes(e)))
|
|
2996
|
+
continue;
|
|
2997
|
+
for (const pattern of RETIRED_PATTERNS) {
|
|
2998
|
+
const patternExempt = PATTERN_EXEMPT[pattern] ?? [];
|
|
2999
|
+
if (patternExempt.some((e) => file.path.includes(e)))
|
|
3000
|
+
continue;
|
|
3001
|
+
const re = new RegExp(pattern);
|
|
3002
|
+
for (const line of lineNumbers(file.content, re)) {
|
|
3003
|
+
findings.push({
|
|
3004
|
+
file: file.path,
|
|
3005
|
+
line,
|
|
3006
|
+
pattern,
|
|
3007
|
+
message: `claims a retired AEG mechanism is still live (matches /${pattern}/)`
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return findings;
|
|
3013
|
+
}
|
|
2911
3014
|
// ../../packages/aeg-core/src/dispatch-gate.ts
|
|
2912
3015
|
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
2913
3016
|
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|