@sabaiway/agent-workflow-kit 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 11.1.0 — the bootstrap offers enforcement on every project, and the advisor closes the loop (AD-131)
8
+
9
+ Bootstrap steps 8–9 (the enforcement scripts + the pre-commit hook) used to decide by the step-1
10
+ recon — a Python, Go or Rust project was denied the caps gate, the archivers and the hook for no
11
+ proven reason, then `upgrade` reported `skipped-no-node-evidence` on the very tree the bootstrap left
12
+ bare. The scripts are dependency-free Node the agent host already runs for step 7's navigator.
13
+ Contract: `node-evidence.md` rev 2 (S8–S10 bound).
14
+
15
+ **The offer, on every project.** `references/modes/bootstrap.md` steps 8–9 offer the scripts and the
16
+ hook preview-first with a one-line why: the bootstrap seeds on `package-json` OR
17
+ `deployed-node-scripts` OR the fact that the agent host is running the kit; the only skip condition
18
+ is that the agent host cannot run them; the residual is stated (the hook runs `node scripts/<x>.mjs`,
19
+ so a committer without `node` on PATH gets the hook's own loud failure). `deploy-tail.md` carries the
20
+ same sentence once; the Windsurf launcher's step list names the offer once and copies nothing
21
+ unconditionally; the by-hand "Non-Node projects follow the policy by hand" claims left the READMEs.
22
+ The step-1 recon is a stack fact, never the criterion.
23
+
24
+ **The advisor's `enforcement` item.** On a `docs/ai` tree whose node evidence answers `none` and
25
+ whose `scripts/` is absent or holds no basename of the kit's own bundle (listed at run time through
26
+ the probe's own `scriptsBundleRoot` seam, never a hardcoded list), Recommendations renders ONE
27
+ optional offer: the exact bootstrap tail as a `HAND-APPLY:` line with both paths shell-quoted.
28
+ Every other cell is silence or a stated skip naming the path — `package-json` /
29
+ `deployed-node-scripts` render nothing and never list the bundle; a wrong-kind path, an unreadable
30
+ probe (`<root>: <code> on <rel>`), an existing, linked or unreadable destination each skip by name;
31
+ no `docs/ai` renders nothing. The probe spawns no git; the installer's refusal over a hook it did not
32
+ write is stated in the posture note (pinned). The commit-guard item's installer-absent skip carries
33
+ its own 155-char remedy (deploy the scripts, bootstrap steps 8–9, install the hook, re-run) on ANY
34
+ evidence — never "run upgrade first".
35
+
36
+ **A declined offer leaves no false claim.** Every install claim in the family is conditional on
37
+ the yes — `installed on a yes` in the READMEs and the seeded `changelog.md` / `decisions.md`
38
+ templates, `where installed` in the seeded `agent_rules.md`, `and the enforcement offer was
39
+ accepted` in both visibility contracts — pinned by one (file, literal, count) table in
40
+ `test/enforcement-offer-docs.test.mjs`; the visible wiring of `package.json` scripts is conditional
41
+ on an existing `package.json` AND an accepted offer; on a no the docs command slots of
42
+ `env_commands.md` and `AGENTS.md` carry a note (the caps + index gate run from the agent's own skill
43
+ copy; the archive rotations need the scripts deployed) and never the skill's absolute path.
44
+
45
+ **Pins, unchanged scripts.** The deployed scripts' basename fallback (no or a malformed
46
+ `package.json` → the directory basename) was already there — `install-git-hooks.test.mjs` and
47
+ `archiver-structure.test.mjs` pin it with a package.json-named control; no script changed.
48
+
49
+ **Measured on itself (the first-pass-quality series' acceptance metric).** Executed delegated on ONE
50
+ held codex session, eight accounted dispatches, every fold through it. Diff council: round 1 codex
51
+ revise 0 blockers + 3 majors, agy SHIP, lens 0 + 2 majors; rounds 2–3 resolved the decline-path
52
+ subarea at altitude; round 4 CLEAN from all three — four rounds against the 13-round baseline
53
+ (AD-130). Three fold-induced findings, each caught by a test or gate before a receipt attested it.
54
+
7
55
  ## 11.0.0 — the per-fold walk is a record, the cap is a refusal at the round table's own signal, and the fingerprint argv has one home (AD-130)
8
56
 
9
57
  Plan 2's two attestation rounds on CONVERGED bytes each found one member of the kit's own robustness
package/README.md CHANGED
@@ -117,12 +117,12 @@ your-repo/
117
117
  ├── tech_reference.md ← configs & patterns
118
118
  ├── pages/ ← one spec per page/route
119
119
  └── history/ ← changelog archive (HOT→WARM→COLD)
120
- + scripts/ ← caps · index · archive (Node path)
121
- + pre-commit hook ← keeps it all honest (Node path)
120
+ + scripts/ ← caps · index · archive (installed on a yes)
121
+ + pre-commit hook ← keeps it all honest (installed on a yes)
122
122
  ```
123
123
 
124
124
  The Markdown memory is **stack-agnostic**; the `scripts/` + pre-commit hook are the **Node path**
125
- (dependency-free, `node --test`). Non-Node projects keep the same policy by hand.
125
+ (dependency-free, `node --test`). They are offered on every project because the agent host runs them; a committer without Node on PATH gets a loud hook failure.
126
126
 
127
127
  Two **visibility** modes, chosen at deploy time: **visible** (committed with the repo) or **hidden**
128
128
  (same files in-tree but git-ignored via the project-local `.git/info/exclude`, so the repo "looks
@@ -225,7 +225,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
225
225
 
226
226
  | Command | When | What happens |
227
227
  |---------|------|--------------|
228
- | `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data → installs enforcement → **asks before committing** |
228
+ | `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data → offers enforcement (installed on a yes) → **asks before committing** |
229
229
  | `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps — then prints a **read-only** one-line backend-status line (what's set up vs missing); refreshes the already-placed bridges from the kit's bundled copies (never installs a new one — set one up with `/agent-workflow-kit setup`) |
230
230
  | `/agent-workflow-kit help` | any time | **read-only command index** — every command, grouped (Inspect / Configure / Orchestrate / Lifecycle) and tagged read-only / writer / guarded. The discoverable entry point, and where any unrecognized invocation lands (always read-only — a garbage invocation never writes). Never writes, never commits, never runs a subscription CLI. |
231
231
  | `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
@@ -270,7 +270,7 @@ It **never auto-commits** and **never overwrites** an existing `AGENTS.md` witho
270
270
  - **Plan lifecycle** — a capped plan (goal and boundary, module ledger, verification), ephemeral plan files, and a mandatory Cleanup phase.
271
271
  - **No silent failures** — every guard that rejects an action logs structured context.
272
272
 
273
- Enforcement ships as dependency-free **Node** scripts (`node --test`, no package manager assumed). Non-Node projects follow the same policy by hand.
273
+ The dependency-free **Node** enforcement scripts (`node --test`, no package manager assumed) and their hook are offered on every project because the agent host runs them; a committer without Node on PATH gets a loud hook failure.
274
274
 
275
275
  ---
276
276
 
@@ -284,7 +284,8 @@ agent-workflow-kit — the composition root (installed via npx … init)
284
284
  on /agent-workflow-kit in a repo, the kit:
285
285
  ├─ delegates ─▶ memory substrate (healthy copy, else bundled fallback)
286
286
  ├─ injects ─▶ workflow methodology (live from the installed engine)
287
- ├─ deploys ─▶ AGENTS.md + docs/ai/ + Node scripts + pre-commit hook
287
+ ├─ deploys ─▶ AGENTS.md + docs/ai/
288
+ │ + Node scripts + pre-commit hook (installed on a yes)
288
289
  ├─ detects ─▶ optional backends (codex / agy, read-only)
289
290
  └─ sets up ─▶ a bridge (opt-in) (place skill + link wrappers)
290
291
  ```
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '11.0.0'
6
+ version: '11.1.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "11.0.0",
6
+ "version": "11.1.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
@@ -26,8 +26,8 @@ reinvent any of it here; the kit's `SKILL.md` is the single source of truth.
26
26
  attribution) as ONE structured multi-question prompt; record each answer individually,
27
27
  write nothing until ALL are answered** →
28
28
  create `AGENTS.md` (+ `CLAUDE.md` symlink) from `<KIT_DIR>/references/templates/` →
29
- deploy `docs/ai/` → copy `<KIT_DIR>/references/scripts/*.mjs` (Node projects) →
30
- wire/hide per visibility → install the pre-commit hook → stamp `docs/ai/.workflow-version`.
29
+ deploy `docs/ai/` → offer the enforcement scripts + pre-commit hook (on a yes: copy `<KIT_DIR>/references/scripts/*.mjs`, install the hook) →
30
+ wire/hide per visibility → stamp `docs/ai/.workflow-version`.
31
31
  4. Report what was filled with real data vs left as TODO, then **ask before committing** —
32
32
  never auto-commit.
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "11.0.0",
3
+ "version": "11.1.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -17,7 +17,7 @@ The user chooses at bootstrap whether the AI artifacts are visible in the repo o
17
17
  **explicit up-front question** (bootstrap step 2), never an assumed default. The two modes then
18
18
  diverge:
19
19
 
20
- - **visible** — artifacts are committed. Wire the project's `package.json` scripts (`docs:check` / `docs:index` / `docs:index:check` / `docs:archive` / `docs:archive:check` / `docs:archive:issues` / `docs:archive:issues:check` / `prepare: node scripts/install-git-hooks.mjs`) and add a minimal `.gitignore` (`docs/plans/`, `.claude/settings.local.json`). This is the canonical model.
20
+ - **visible** — artifacts are committed. When a `package.json` exists and the enforcement offer was accepted, wire the project's `package.json` scripts (`docs:check` / `docs:index` / `docs:index:check` / `docs:archive` / `docs:archive:check` / `docs:archive:issues` / `docs:archive:issues:check` / `prepare: node scripts/install-git-hooks.mjs`); add a minimal `.gitignore` (`docs/plans/`, `.claude/settings.local.json`) either way. This is the canonical model.
21
21
  - **hidden** (in-tree) — same files on disk, but the repo "looks normal": the AI/agent footprint is git-ignored via **one managed block in the project-local `.git/info/exclude`** (resolved with `git rev-parse --git-path info/exclude` — never the machine-global `core.excludesFile`, which would affect every repo on the host; **AD-014** amends **AD-006**). The kit's `tools/hide-footprint.mjs` is the single writer: it covers `KIT_OWN_PATHS ∪ the present subset of KNOWN_FOOTPRINT` (the full footprint table below — the kit's own artifacts **and** every other AI/agent tool's files). Per path: a **tracked** file → **ASK** (an exclude does nothing for it; only `git rm --cached` un-tracks it — the tool prints that command and never runs it); an untracked path already covered by a **tracked `.gitignore`** → dropped (redundant); a **present** file whose name is generic enough to be ambiguous (`falsePositiveRisk`) → **ASK**; everything else → **hidden**. `asks` are excluded from the block unless explicitly opted in. **Verify** treats a path as hidden only when it is **untracked AND ignored by our project-local block** (or a tracked `.gitignore`) — being ignored by the global excludes does **not** count. Re-running re-derives the block wholesale (sorted/deduped) → a clean re-run is a **zero-diff** no-op. On an existing global-excludes deployment the tool **detects + reports the residual legacy machine-global block and keeps it by default** (a harmless double-ignore; the local block wins precedence); removal is the explicit `--remove-global` (it prints the removed lines as a restorable backup), which **the agent only runs after asking** — another of the user's hidden repos on the same host may rely on the same root-anchored global lines. **Do not edit `package.json`** — that is a tracked change and would leak; the pre-commit hook (always untracked in `.git/hooks/`) calls the scripts via `node scripts/<x>.mjs` directly. Windows is supported (text edit, no symlinks; CRLF preserved).
22
22
 
23
23
  **Visibility changes what git sees — never how the agent works (the load-bearing invariant).** Both modes carry the **same** working memory and run the **same** session protocol: `docs/ai` and the deployed workflow artifacts (`AGENTS.md`, the added `scripts/`) are read at session start and maintained on task completion **identically** whether visible or hidden. (Hidden mode git-ignores a *wider* footprint — other AI/agent tools' files too — but that is only a **visibility** surface; the working memory the agent actually reads and maintains is `docs/ai` plus those artifacts.) Hidden mode only makes the repo *look* normal to git; it is **never** a license to skip, defer, or down-prioritise a doc/state update. The single difference is the destination: in hidden mode those updates live on disk and never enter a commit (the footprint is excluded), so a commit there carries only the project's own tracked files while `docs/ai` is still kept current locally — **git-ignored ≠ optional-to-maintain**. An agent that reads "git-ignored / `git status` clean" and infers the working memory is optional has the model backwards. (This is why bootstrap/upgrade never wire hidden-mode docs into a commit, yet the session protocol still mandates maintaining them.)
@@ -1,6 +1,6 @@
1
1
  ### Mode: bootstrap
2
2
 
3
- <!-- opt-in-capability: none — the deployment action itself; the advisor only renders inside an existing deployment -->
3
+ <!-- opt-in-capability: enforcement -->
4
4
 
5
5
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
6
6
 
@@ -23,9 +23,9 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
23
23
  5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (step-1 recon), do **not** overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md` (the cross-agent standard — Codex / Cursor / Devin Desktop / Copilot read it natively) from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md`, and symlink `CLAUDE.md -> AGENTS.md` (`ln -s AGENTS.md CLAUDE.md`) for Claude Code — single source, no duplication. For nested context, add a subdir `AGENTS.md` (+ a `CLAUDE.md` symlink beside it for Claude Code).
24
24
  6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` + the seed `adr/` store + the seed `specs/` store from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys every template except `AGENTS.md`, `adr-record.md` and `SPEC_TEMPLATE.md` — the latter two are skill-home authoring references, never deployed). This deploys the HOT ADR window **`docs/ai/decisions.md`** and the seed navigator **`docs/ai/adr/log.md`** (its `adr/` subdir copies in wholesale), the spec store root **`docs/ai/specs/index.md`** (its `specs/` subdir copies in wholesale), the `.md` docs, **and** the three seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — seeded as the no-consent EMPTY list; FILLING it is the consented `gates-init` preview, `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), and **`docs/ai/autonomy.json`** (the per-project autonomy policy — seeded SPARSE with the onboarding note only, so it is exactly the computed defaults until the user declares levels via `/agent-workflow-kit set-autonomy` or by hand). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
25
25
  7. **Fill templates** per the table below. **Then materialize the navigator, as the last `docs/ai` step:** the entry point declares `docs/ai/index.md` always-loaded and it is a GENERATED artifact with no template, so run `node ${CLAUDE_SKILL_DIR}/references/scripts/check-docs-size.mjs --ensure-index --root=<project>` and relay its one outcome line (`ensure-index: regenerated` / `already-current`, or a `write-refused` / `probe-failed` line naming the offending path — either is a STOP: resolve it before reporting the deploy, since the navigator the entry point declares does not exist yet). The generator runs from **this skill's own directory**, so a project with **no Node runtime** is covered too: the agent host runs it, and step 8's copy is not a precondition.
26
- 8. **Install enforcement (Node projects).** Copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs` (+ `*.test.mjs`) into the project's `scripts/`. They self-configure (project name from `package.json`, hierarchical/on-demand sections auto-discovered). **If the project has no Node runtime** (step-1 recon), skip this step and the hook in step 9 — follow the cap/archive/index policy manually, or port the scripts to the project's language.
27
- 9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). Install the pre-commit hook (Node projects): `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting. Once the gate declaration is final-run-capable, the installer's optional consented `--commit-guard <kit commit-guard.mjs path>` arm binds commits to the green `run-gates --final` receipt (the Recommendations advisor surfaces the exact one-liner).
28
- - **visible** — wire the `package.json` scripts + add the minimal `.gitignore`. **Do not run the hide tool.**
26
+ 8. **Offer enforcement on every project, preview-first.** Explain in one line that the dependency-free Node scripts are available because the agent host already runs their step-7 navigator; the only skip condition is that the agent host cannot run them. Preview the source and destination list and ask; on a yes copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs` (+ `*.test.mjs`) into the project's `scripts/`. On a no, skip the hook in step 9 and say so; the docs command slots then name no project-local command — fill `env_commands.md` and the `Docs caps + index freshness` row of `AGENTS.md` with the same note: the caps + index gate run from the agent's own skill copy (the step-7 `--root=<project>` form) and the archive rotations need the scripts deployed. Never write the skill's absolute path into a deployed doc. They self-configure (project name from `package.json` or, without one, the directory basename; hierarchical/on-demand sections auto-discovered). The hook calls `node scripts/<x>.mjs` directly, so a committer without `node` on PATH gets the hook's own loud failure.
27
+ 9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). When the user accepts the enforcement offer, install the pre-commit hook: `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting. Once the gate declaration is final-run-capable, the installer's optional consented `--commit-guard <kit commit-guard.mjs path>` arm binds commits to the green `run-gates --final` receipt (the Recommendations advisor surfaces the exact one-liner).
28
+ - **visible** — when a `package.json` exists and the offer was accepted, wire its scripts (never create one for this); add the minimal `.gitignore` either way. **Do not run the hide tool.**
29
29
  - **hidden** — run the kit's hide writer (one managed block in the **project-local** `.git/info/exclude`, covering the kit's own artifacts **and** every known AI/agent tool's footprint — never the machine-global excludes): `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --dry-run` **first** (changes nothing, prints the plan), then the same without `--dry-run`. Handle what it surfaces, in plain language (never the tool's internal terms):
30
30
  - A path it reports as **already committed** can't be hidden by ignoring it and is **never un-tracked silently** — show the user the printed `git rm --cached <path>`, let them decide, then opt it in with `--include=<path>`.
31
31
  - A **present file with a generic name** it flags (e.g. `GEMINI.md`) → ask before `--include=<path>`.
@@ -58,5 +58,5 @@ Fill strategy:
58
58
  | `tech_reference.md` | Carry over real configs/patterns found in deps. |
59
59
  | `active_plan.md`, `handover.md` | TODO seed (e.g. "Bootstrap session — fill domain sections after first real work"). |
60
60
  | `decisions.md` | The HOT ADR window — seed `AD-001` (adopting this memory system), newest at the bottom. |
61
- | `adr/log.md` | The seed navigator — deployed as-is (the generator over the seeded HOT); regenerated by `archive-decisions.mjs`, never hand-edited. |
61
+ | `adr/log.md` | The seed navigator — deployed as-is (the generator over the seeded HOT); regenerated by `archive-decisions.mjs` where the enforcement scripts are deployed (installed on a yes), otherwise it stays the seed — never hand-edited either way. |
62
62
  | `known_issues.md`, `changelog.md`, `pages/shared-patterns.md` | Empty template / first bootstrap entry. |
@@ -33,6 +33,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
33
33
 
34
34
  - `source-size` — the practice's arms differ by what the declared gate actually IS. **`adopted-elsewhere`** — the gate runs a DIFFERENT real copy of this checker (a vendored deployment): the practice is running, the advisor simply cannot resolve it as its own sibling, and the realpath anchor deliberately does not widen (it is what stops a lookalike from certifying). So the convergence is the ack writer's preview (`--lane source-size-copy` → `sourceSizeCopyAck`), never `--adopt`, which would mint the record and then be refused by the fill on the id already declared. The fingerprint binds the declared claims AS AUTHORED, sorted — not their resolved paths, which are machine-specific and would churn a committed ack between machines — so a second external copy re-fires it. **`id-squatter`** — a gate carries the `source-size` id and is not this checker under any reading: the id claims the practice while nothing measures module size, so it is **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
35
35
  - `gate-hook` — the base arm is the ordinary opt-in wiring offer. The **`marker-stale`** arm is not: the placed hook validates your declaration through its OWN baked copy and goes dark on any key it does not know, so a declaration carrying the `lcovProducer` key under a hook that predates it silently turns auto-approval OFF, and every gate prompts again with no error anywhere. The condition is the key's PRESENCE, not its value — an older hook rejects a key it does not know whatever that key says, so `"lcovProducer": false` darkens it exactly as `true` does. The arm is deliberately marker-scoped — a stale hook is otherwise harmless — and its recovery is the writer's own: `gate-hook --apply` places only an ABSENT target, so converging means deleting the placed hook and re-placing it. That makes it a destructive **HAND-APPLY** (`rm` + `--apply`, absolute path so it can only delete this project's hook), never something the consent flow runs for you. When the read-lane is also enabled, this arm and `read-lane.stale` would report the same file with the same recovery, so exactly one renders — **this one**, because its cause is the true one: a hook that postdates the read-lane and merely predates the marker key reads `lanes.json` perfectly well, and the read-lane arm's wording would be false over it. Risk profile: deleting one placed hook file, then re-placing it from the bundle; the declaration is never touched.
36
+ - `enforcement` — copies the bundled `references/scripts/*.mjs` runnable scripts and their tests, then installs the pre-commit hook. It is HAND-APPLY: the agent never runs it; the offer renders only when `scripts/` is absent or a plain directory contains no bundled basename, so the copy never clobbers an existing file and never follows a link. The hook invokes `node` directly, so a committer without it on PATH gets a loud failure. The installer refuses to overwrite a pre-commit hook it did not write — on that refusal stop and merge the hook by hand (bootstrap step 9); the copied scripts stay in place. Risk profile: local tooling and a Git hook are added only when the maintainer applies the shown command.
36
37
  - `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project, which is a different kind of consent from the other velocity items: a registration is a command your MCP client will RUN, so the apply here is the mode's own **flagless preview**, which prints the exact entry and writes nothing — the `--apply` that follows is a SEPARATE step you run after reading that entry, and the consent flow never carries it. What the registration buys: path questions and literal searches become typed tool calls whose arguments are named JSON fields instead of a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — they are perfectly legal bytes to search for, there is simply no shell to read them as operators. Posture: the server is a **read-only child of your client** (path/type/size/line facts and literal search over this project root; no write and no exec API), and like the client itself it runs **outside the Bash sandbox** — the sandbox is not what bounds it, the server's own root containment is. The two allow rules it adds make those two tools promptless and nothing else. Two arms are **HAND-APPLY** and never run for you: **`.differing`** — an `agent-workflow` entry already stands in `.mcp.json` and **structurally differs** from what this kit copy would write (another kit copy, a hand-edited path, an added `env`; the comparison ignores key order, so re-serialized identical bytes are the same registration), and silently changing what an MCP server launches is exactly what consent must not slide past, so the remedy is your edit; and the **masked** arm, where an OS sandbox hides `.mcp.json` behind a device node — the kit cannot write there, so it hands you the text to paste from outside the sandbox. When the file is masked but the settings half is already complete, the item **does not render at all**: what is unobservable becomes a stated SKIP, so optimality is withheld rather than a registration you already made being offered again. Stated limit: this item does **not** detect a `disabledMcpjsonServers` veto, so a converged `mcp-channel` means *what the mode writes is in place*, not *the client will load it* — see `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` for why that check was subtracted rather than half-built. Risk profile: a new read-only channel your client will launch; no write or exec exposure, and no existing declaration is touched.
37
38
  - ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
38
39
  - `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it writes the reader/checker pairs and the store root, create-only, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries a NAMED **HAND-APPLY alternative** — the DECLINE preview (`ack-write --lane spec-adoption`, a dry-run that prints the exact `--apply`), which records `specAdoptionAck` in the family-owned `docs/ai/acks.json`. **The two are exclusive, and the order is part of the contract:** ask which the maintainer wants BEFORE the confirmation; on "seed" run the apply; on "decline" run NOTHING — hand over the preview line and let them run it and its printed `--apply`; never run the seed after a decline was chosen, and never run the decline preview for them. **`adopting`** (a store with no live contract): an optional item naming the draft count; its apply is that same decline preview (a dry-run; after the SAME confirmation you run the `--apply` it prints), and the honest remedy is authoring a contract from the skill's `SPEC_TEMPLATE.md`. A recorded decline silences BOTH arms and shows on `status` as ` — declined`; one live contract makes the decline moot. An unreadable store is a stated skip, never `flow optimal`. Risk profile: the seed writes only create-only files under `scripts/` and `docs/ai/specs/`; the decline writes one key into the ack store; nothing here authors a spec or blocks a plan.
@@ -1,8 +1,9 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { readFileSync } from 'node:fs';
4
- import { dirname, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
3
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { basename, dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
7
 
7
8
  // Structural pin (green by construction, a tripwire not a proof): all three archivers read
8
9
  // documents through the ONE shared tokenizer. A fourth archiver — or a future edit that reaches
@@ -12,6 +13,50 @@ import { fileURLToPath } from 'node:url';
12
13
 
13
14
  const DIR = dirname(fileURLToPath(import.meta.url));
14
15
  const ARCHIVERS = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-decisions.mjs'];
16
+ const ARCHIVER_CLOSURE = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-caps.mjs', 'markdown-blocks.mjs'];
17
+ const FIXED_DATE = '2026-09-02';
18
+ const MALFORMED_PACKAGE_JSON = '{ not json';
19
+ const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
20
+ const NAMED_FIXTURE = 'named-fixture';
21
+
22
+ const buildArchiveHeadings = async (packageJson) => {
23
+ const root = mkdtempSync(join(tmpdir(), 'archiver-name-'));
24
+ const scripts = join(root, 'scripts');
25
+ mkdirSync(scripts);
26
+ for (const name of ARCHIVER_CLOSURE) cpSync(join(DIR, name), join(scripts, name));
27
+ if (packageJson !== null) writeFileSync(join(root, 'package.json'), packageJson);
28
+ const changelog = await import(pathToFileURL(join(scripts, 'archive-changelog.mjs')).href);
29
+ const issues = await import(pathToFileURL(join(scripts, 'archive-issues.mjs')).href);
30
+ const result = {
31
+ fallback: basename(root),
32
+ changelog: changelog.buildRecent([], FIXED_DATE),
33
+ issues: issues.buildResolvedFile(null, [], FIXED_DATE),
34
+ };
35
+ rmSync(root, { recursive: true, force: true });
36
+ return result;
37
+ };
38
+
39
+ const assertArchiveHeadings = (result, projectName) => {
40
+ assert.ok(result.changelog.includes(`# Changelog WARM Archive \u2014 ${projectName}`));
41
+ assert.ok(result.issues.includes(`# Resolved Issues \u2014 ${projectName}`));
42
+ };
43
+
44
+ describe('the archivers head their archives with the directory basename when no package.json, or a malformed one, names the project', () => {
45
+ it('no package.json names the directory basename', async () => {
46
+ const result = await buildArchiveHeadings(null);
47
+ assertArchiveHeadings(result, result.fallback);
48
+ });
49
+
50
+ it('a malformed package.json names the directory basename', async () => {
51
+ const result = await buildArchiveHeadings(MALFORMED_PACKAGE_JSON);
52
+ assertArchiveHeadings(result, result.fallback);
53
+ });
54
+
55
+ it('a named package.json wins', async () => {
56
+ const result = await buildArchiveHeadings(NAMED_PACKAGE_JSON);
57
+ assertArchiveHeadings(result, NAMED_FIXTURE);
58
+ });
59
+ });
15
60
 
16
61
  describe('every archiver reads through the tokenizer — no raw scan survives', () => {
17
62
  for (const name of ARCHIVERS) {
@@ -8,12 +8,15 @@ import { describe, it } from 'node:test';
8
8
  import assert from 'node:assert/strict';
9
9
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, cpSync, existsSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
- import { join, dirname, resolve } from 'node:path';
11
+ import { basename, join, dirname, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { spawnSync } from 'node:child_process';
14
14
 
15
15
  const HERE = dirname(fileURLToPath(import.meta.url));
16
16
  const INSTALLER_SRC = join(HERE, 'install-git-hooks.mjs');
17
+ const MALFORMED_PACKAGE_JSON = '{ not json';
18
+ const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
19
+ const NAMED_FIXTURE = 'named-fixture';
17
20
 
18
21
  // The installer anchors ROOT at <its dir>/.. — deploy it into a fixture's scripts/ like a consumer.
19
22
  const mkProject = () => {
@@ -39,6 +42,33 @@ const hooksPathOf = (root) => {
39
42
  return resolve(root, r.stdout.trim());
40
43
  };
41
44
 
45
+ const markerFor = (name) => `# ${name}:install-git-hooks.mjs`;
46
+ const readHook = (root) => readFileSync(join(hooksPathOf(root), 'pre-commit'), 'utf8');
47
+
48
+ describe('install-git-hooks \u2014 beside no package.json, or a malformed one, the marker names the directory basename', () => {
49
+ it('no package.json, then a MALFORMED one \u2014 the marker names the directory basename', () => {
50
+ const { root } = mkProject();
51
+ const expected = markerFor(basename(root));
52
+ const absent = runInstaller(root);
53
+ assert.equal(absent.status, 0, absent.stderr);
54
+ assert.ok(readHook(root).split('\n').includes(expected));
55
+ writeFileSync(join(root, 'package.json'), MALFORMED_PACKAGE_JSON);
56
+ const malformed = runInstaller(root);
57
+ assert.equal(malformed.status, 0, malformed.stderr);
58
+ assert.ok(readHook(root).split('\n').includes(expected));
59
+ rmSync(root, { recursive: true, force: true });
60
+ });
61
+
62
+ it('a named package.json wins \u2014 the marker carries its name', () => {
63
+ const { root } = mkProject();
64
+ writeFileSync(join(root, 'package.json'), NAMED_PACKAGE_JSON);
65
+ const result = runInstaller(root);
66
+ assert.equal(result.status, 0, result.stderr);
67
+ assert.ok(readHook(root).split('\n').includes(markerFor(NAMED_FIXTURE)));
68
+ rmSync(root, { recursive: true, force: true });
69
+ });
70
+ });
71
+
42
72
  describe('install-git-hooks — the hooks path comes from git plumbing (C7)', () => {
43
73
  it('a normal repo installs at the git-path hooks location', () => {
44
74
  const { root } = mkProject();
@@ -9,7 +9,7 @@ The non-obvious traps — scan these before bootstrapping or upgrading. Each is
9
9
  - **`CLAUDE.md` is a symlink, not a copy.** `ln -s AGENTS.md CLAUDE.md` — single source, no duplication. A copy drifts; a symlink can't.
10
10
  - **Never overwrite an existing entry point or hook.** If `AGENTS.md` / `CLAUDE.md` already exist, or the installer reports a pre-existing non-marker git hook, **stop and ask** the user to merge vs replace — don't clobber.
11
11
  - **Unrecognized invocations are read-only.** Only a **known** subcommand reaches its mode; the **bare** invocation bootstraps (and an existing `docs/ai/` makes it ask upgrade-vs-bootstrap first, never overwrite); **any other / ambiguous** token routes to `help` (read-only). A garbage invocation never writes. The mapping is pinned by `tools/commands.mjs` `routeInvocation` (unit-tested) — don't hand-route around it.
12
- - **No Node runtime → skip enforcement.** If the project has no Node (recon step 1), skip bootstrap steps 8–9 (scripts + hook) and follow the cap/archive/index policy manually, or port the scripts to the project's language. **A skip names a PROVEN fact, never a proxy:** the upgrade ensures judge Node by EVIDENCE — a root `package.json` OR any kit-seeded `scripts/*.mjs` already deployed — and their `skipped-no-node-evidence` line lists every probe that answered absent; a `skipped-*` outcome whose reason the tool could itself disprove is a tool defect, not an outcome. **A skip line that contradicts the observed tree is raised as a finding in the report, never pasted as neutral** — when the tree you can see disproves a skip's stated reason, report the contradiction with the fact named and do not build on the skip.
12
+ - **Enforcement is offered on every project.** The dependency-free scripts are available because the agent host already runs the navigator; for the offer, the only skip condition is that the agent host cannot run them. The hook calls `node scripts/<x>.mjs`, so a committer without `node` on PATH gets the hook's own loud failure. **A skip names a PROVEN fact, never a proxy:** upgrade ensures judge Node by EVIDENCE — a root `package.json` OR any kit-seeded `scripts/*.mjs` already deployed — and the `skipped-no-node-evidence` line lists every probe that answered absent; a reason the tool could disprove is a tool defect. **A skip line that contradicts the observed tree is raised as a finding in the report, never pasted as neutral** — report the contradiction with the proven fact and do not build on the skip.
13
13
  - **Conversational language never translates artifacts.** It governs *dialogue only*. Code, identifiers, paths, commands, log output, abbreviations, and every deployed `docs/ai/` / `AGENTS.md` file stay in their source language. See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract).
14
14
  - **Never auto-commit.** Report quality-gate results and wait for explicit approval — in both modes.
15
15
  - **Never leak kit internals to the user — and a tool-COMPOSED user-facing line holds the same bar at the source.** No ADR ids, tool / function / operation names (`reconcile`, `inject`, `ensureSlot`), marker / slot / fragment / anchor terminology, or verbatim tool stderr **inside the human sentence** of anything the user reads. Translate every tool outcome into plain language a third-party user — who has never read this `SKILL.md` — can understand and act on (e.g. the cap-refusal report in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3). The composed lines themselves are **user-grade** language: machine tokens and tool self-labels belong to the **machine-line channel** — the `[run-gates] status=…` grammar (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`), a line's leading self-label/prefix, or a runnable command/path the user can act on — never mid-sentence; alarm words (`PARTIALLY`, `incomplete`, `failed`, `broken`, `persists`) render only in outcomes gated on a **detected abnormal condition**. ONE designed exception, stated not implied: the configuration ensures' LEADING outcome token — one closed-vocabulary token, a failure's closed cause word opening its detail line — is that contract's own machine slot, not a leak. The **verbatim**-paste contract stays: the agent pastes tool-composed outcome lines as written and never re-composes their facts — the lines are user-grade at the source, so pasting them verbatim IS the plain language.
@@ -37,7 +37,7 @@ Read in order, then confirm before starting:
37
37
  Before claiming "done":
38
38
  1. Run all quality gates (lint + type-check + tests) — all green.
39
39
  2. Update docs: `current_state.md` (feature ready), `changelog.md` (entry), `handover.md` (**REPLACE** the last-session block — session delta, never append; older deltas live in `changelog.md` → `history/`), `pages/<page>.md` (matches implementation), the governing spec (its revision lands with the code: `draft` -> `live` on the landing row). Only bump "Last Updated" when content actually changed.
40
- 3. Run the docs cap-validator + index-freshness gate (pre-commit also enforces). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
40
+ 3. Run the docs cap-validator + index-freshness gate (the pre-commit hook also enforces it where installed). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
41
41
  4. If the work executed a plan file — run that plan's final **Phase: Cleanup** (see the planning skill / §5). Without it the plan is not done.
42
42
  5. **Ask before committing** (§4): report lint / type-check / test counts + docs status, then wait for explicit approval. DO NOT auto-commit.
43
43
 
@@ -20,7 +20,7 @@ maxLines: 700
20
20
  **Changes:**
21
21
  - Created `AGENTS.md` (entry point) + `CLAUDE.md` symlink.
22
22
  - Created `docs/ai/` with the spec files + `pages/`.
23
- - Installed the docs cap-validator / index-freshness / archive scripts + pre-commit hook.
23
+ - Offered the docs cap-validator / index-freshness / archive scripts + pre-commit hook (installed on a yes).
24
24
  - Recorded the real stack, scripts, and architecture into the relevant files.
25
25
 
26
26
  **Quality gates:** n/a (no code changes).
@@ -13,7 +13,7 @@ maxLines: 500
13
13
  > consequences, newest at the bottom. Link related ADRs with `[[AD-XXX]]`; retrieval is by the
14
14
  > `AD-NNN` id (filename), grep over the flat store, or the `[[AD-NNN]]` supersession chain.
15
15
 
16
- > **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically; no cap is ever raised and there is no monolithic ledger.
16
+ > **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically where the enforcement scripts are deployed (installed on a yes); no cap is ever raised and there is no monolithic ledger.
17
17
 
18
18
  ## AD-001 — Adopt AI-agent memory system (`docs/ai/`)
19
19
 
@@ -22,7 +22,7 @@ maxLines: 500
22
22
 
23
23
  **Context.** Multi-session AI work loses context between runs. Without a structured handover, each new session re-reads code, re-discovers decisions, and repeats past mistakes.
24
24
 
25
- **Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via a pre-commit hook. Deployed via the `agent-workflow-kit` skill.
25
+ **Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via the deployed scripts + pre-commit hook (offered on every project, installed on a yes). Deployed via the `agent-workflow-kit` skill.
26
26
 
27
27
  **Rationale.** Single entry + structured spec files = constant boot-up cost regardless of project size. ADRs prevent litigating the same decision twice. `pages/<page>.md` keeps behaviour canonical (docs > assumptions). Caps + one-file-per-ADR archival keep every record scannable as the decision history grows without bound.
28
28
 
@@ -83,6 +83,7 @@ import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
83
83
  import { assertContainedRealPath } from './fs-safe.mjs';
84
84
  import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
85
85
  import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
86
+ import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
86
87
  // The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
87
88
  import { surveyExecutorVehicle, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
88
89
  // The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
@@ -108,7 +109,12 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
108
109
  const HERE = dirname(fileURLToPath(import.meta.url));
109
110
  const toolPath = (rel) => join(HERE, rel);
110
111
  const q = shellQuoteArg;
111
-
112
+ const ENFORCEMENT_ABSENT_CODE = 'ENOENT';
113
+ const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
114
+ const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
115
+ const ENFORCEMENT_WHAT = 'the enforcement scripts are not deployed beside docs/ai (no Node evidence) \u2014 the offer copies them and installs the pre-commit hook';
116
+ const ENFORCEMENT_APPLY_TEMPLATE = 'HAND-APPLY: mkdir -p {root}/scripts && cp {kit}/references/scripts/*.mjs {root}/scripts/ && node {root}/scripts/install-git-hooks.mjs';
117
+ const COMMIT_GUARD_INSTALLER_REMEDY = 'the hook installer (scripts/install-git-hooks.mjs) is not deployed \u2014 deploy the enforcement scripts (bootstrap steps 8\u20139) and install the hook, then re-run';
112
118
  // ── the section contract tokens (doc-parity-bound in upgrade.md + the mode doc) ────────────────
113
119
  export const RECOMMENDATIONS_SECTION_HEADER = '## Recommendations (agent-workflow)';
114
120
  export const RECOMMENDATIONS_EMPTY_LINE = 'no recommendations — flow optimal.';
@@ -166,6 +172,7 @@ export const SEVERITIES = Object.freeze({
166
172
  // offer to enable something.
167
173
  'gate-hook.marker-stale': SEVERITY_ATTENTION,
168
174
  'commit-guard': SEVERITY_OPTIONAL,
175
+ enforcement: SEVERITY_OPTIONAL,
169
176
  'read-lane': SEVERITY_OPTIONAL,
170
177
  'read-lane.stale': SEVERITY_ATTENTION,
171
178
  'read-lane.missing': SEVERITY_ATTENTION,
@@ -249,6 +256,7 @@ export const WHATS = Object.freeze({
249
256
  'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
250
257
  'gate-hook.marker-stale': 'the declaration carries the lcovProducer key but the placed hook predates it — the hook goes dark and every gate prompts',
251
258
  'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
259
+ enforcement: ENFORCEMENT_WHAT,
252
260
  'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
253
261
  'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
254
262
  'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
@@ -313,6 +321,7 @@ export const BENEFITS = Object.freeze({
313
321
  'source-size': 'maintainability — a module you can hold whole stays reviewable, and size drift becomes recorded, reasoned debt',
314
322
  'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
315
323
  'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
324
+ enforcement: 'enforcement — bundled checks and the pre-commit hook keep caps, archives, and the generated index current',
316
325
  'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
317
326
  'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
318
327
  'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
@@ -361,6 +370,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
361
370
  { id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
362
371
  { id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
363
372
  { id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
373
+ { id: 'enforcement', mode: 'bootstrap', advisorKey: 'enforcement' },
364
374
  { id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
365
375
  { id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
366
376
  { id: 'mcp-channel', mode: 'mcp', advisorKey: 'mcp-channel' },
@@ -843,7 +853,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
843
853
  if (!isFinalCapableDeclaration(gates, root)) return; // no receipt to guard yet — no offer
844
854
  const installer = join(root, 'scripts', 'install-git-hooks.mjs');
845
855
  if (!(deps.exists ?? existsSync)(installer)) {
846
- skip('commit-guard', new Error('the hook installer (scripts/install-git-hooks.mjs) is not deployed — run upgrade first'));
856
+ skip('commit-guard', new Error(COMMIT_GUARD_INSTALLER_REMEDY));
847
857
  return;
848
858
  }
849
859
  const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
@@ -866,6 +876,45 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
866
876
  }
867
877
  };
868
878
 
879
+ const readEnforcementNode = (path, lstat) => {
880
+ try {
881
+ return lstat(path);
882
+ } catch (err) {
883
+ if (err?.code === ENFORCEMENT_ABSENT_CODE) return null;
884
+ throw new Error(`${path}: ${err?.code ?? err?.message ?? err}`);
885
+ }
886
+ };
887
+
888
+ export const probeEnforcement = ({ root, deps, add, skip }) => {
889
+ try {
890
+ const lstat = deps.lstat ?? lstatSync;
891
+ const docsAi = join(root, 'docs', 'ai');
892
+ const docsAiNode = readEnforcementNode(docsAi, lstat);
893
+ if (docsAiNode === null) return;
894
+ if (!docsAiNode.isDirectory()) throw new Error(`${docsAi} is not a plain directory`);
895
+ const evidence = probeNodeEvidence(root, lstat);
896
+ if (evidence.state === NODE_EVIDENCE.PACKAGE_JSON || evidence.state === NODE_EVIDENCE.DEPLOYED_SCRIPTS) return;
897
+ if (evidence.state === NODE_EVIDENCE.UNREADABLE) throw new Error(`${root}: ${evidence.error}`);
898
+ if (evidence.state !== NODE_EVIDENCE.NONE) throw new Error(`unknown Node-evidence state: ${evidence.state}`);
899
+ if (evidence.wrongKind.length > 0) throw new Error(join(root, evidence.wrongKind[0]));
900
+ const readDir = deps.readdir ?? readdirSync;
901
+ const bundleRoot = deps.scriptsBundleRoot ?? ENFORCEMENT_BUNDLE_ROOT;
902
+ const bundleBasenames = readDir(bundleRoot).filter((name) => name.endsWith('.mjs')).sort();
903
+ if (NODE_EVIDENCE_SCRIPTS.some((name) => !bundleBasenames.includes(name))) throw new Error(`${bundleRoot}: a runnable enforcement script is missing`);
904
+ const scripts = join(root, 'scripts');
905
+ const scriptsNode = readEnforcementNode(scripts, lstat);
906
+ if (scriptsNode !== null && !scriptsNode.isDirectory()) throw new Error(`${scripts} is not a plain directory`);
907
+ for (const basename of scriptsNode === null ? [] : bundleBasenames) {
908
+ const destination = join(scripts, basename);
909
+ if (readEnforcementNode(destination, lstat) === null) continue;
910
+ throw new Error(`${destination} already exists; the enforcement copy would clobber it`);
911
+ }
912
+ add('enforcement', ENFORCEMENT_WHAT, fillTemplate(ENFORCEMENT_APPLY_TEMPLATE, { root: q(root), kit: q(ENFORCEMENT_KIT_ROOT) }));
913
+ } catch (err) {
914
+ skip('enforcement', err);
915
+ }
916
+ };
917
+
869
918
  // The read-lane offer (AD-055 Part II, Help-through-Recommendations): once the gate hook is PLACED
870
919
  // and WIRED, offer to enable the opt-in read-only compound lane. Mutually exclusive with the
871
920
  // gate-hook item BY CONSTRUCTION — that item keys on `!wired`, this one on `wired` — so an unplaced
@@ -1242,7 +1291,7 @@ const readReadLaneToggle = (root, deps) => {
1242
1291
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1243
1292
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1244
1293
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1245
- export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption']);
1294
+ export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement']);
1246
1295
 
1247
1296
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1248
1297
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1547,6 +1596,7 @@ const PROBES = Object.freeze([
1547
1596
  probeGatesInert,
1548
1597
  probeSourceSize,
1549
1598
  probeCommitGuard,
1599
+ probeEnforcement,
1550
1600
  probeReadLane,
1551
1601
  probeStateBlockHook,
1552
1602
  probeCheapAgents,