@sabaiway/agent-workflow-kit 5.5.0 → 5.7.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/capability.json +1 -1
  5. package/package.json +1 -1
  6. package/references/hooks/gate-approve.mjs +7 -1
  7. package/references/modes/doc-parity.md +1 -1
  8. package/references/modes/gates.md +20 -4
  9. package/references/modes/procedures.md +2 -0
  10. package/references/modes/recommendations.md +4 -1
  11. package/references/modes/review-state.md +1 -1
  12. package/references/modes/setup.md +18 -2
  13. package/references/modes/upgrade.md +38 -18
  14. package/references/modes/velocity.md +1 -0
  15. package/references/scripts/migrate-gates-branches.test.mjs +146 -1
  16. package/references/scripts/migrate-gates.mjs +295 -60
  17. package/references/scripts/migrate-gates.test.mjs +206 -14
  18. package/references/shared/deploy-tail.md +1 -1
  19. package/references/templates/gates.json +1 -1
  20. package/tools/ack-write.mjs +20 -11
  21. package/tools/atomic-write.mjs +71 -18
  22. package/tools/checker-claim.mjs +100 -0
  23. package/tools/coverage-producer.mjs +43 -6
  24. package/tools/direct-run.mjs +76 -0
  25. package/tools/doc-parity.mjs +34 -3
  26. package/tools/engine-source.mjs +12 -8
  27. package/tools/ensure-configs.mjs +141 -0
  28. package/tools/ensure-ops.mjs +284 -0
  29. package/tools/ensure-vocabulary.mjs +71 -0
  30. package/tools/flow-check-cores.mjs +253 -0
  31. package/tools/flow-check-git-lane.mjs +56 -0
  32. package/tools/flow-check-rungs.mjs +330 -0
  33. package/tools/flow-check.mjs +23 -611
  34. package/tools/gates-declaration.mjs +36 -11
  35. package/tools/gates-init.mjs +140 -25
  36. package/tools/hide-footprint.mjs +21 -3
  37. package/tools/lens-region.mjs +74 -23
  38. package/tools/orchestration-config.mjs +5 -3
  39. package/tools/orchestration-write.mjs +7 -0
  40. package/tools/procedures.mjs +64 -5
  41. package/tools/recommendations.mjs +384 -34
  42. package/tools/refresh-parity.mjs +263 -0
  43. package/tools/run-gates.mjs +8 -5
  44. package/tools/setup-backends.mjs +88 -77
  45. package/tools/source-size-check.mjs +310 -0
  46. package/tools/source-size-config.mjs +244 -0
  47. package/tools/source-size-core.mjs +59 -0
  48. package/tools/source-size-gate-cmd.mjs +27 -0
  49. package/tools/source-size-judge.mjs +114 -0
  50. package/tools/source-size-refusal.mjs +70 -0
  51. package/tools/source-size-report.mjs +254 -0
  52. package/tools/source-size-scope.mjs +145 -0
  53. package/tools/tracked-tree-census.mjs +102 -0
  54. package/tools/upgrade-runlist.mjs +92 -0
  55. package/tools/velocity-profile.mjs +24 -3
@@ -6,7 +6,17 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
6
6
 
7
7
  1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
8
8
  2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`3.0.0` — NOT this kit's package version). If the stamp is **greater than the head** or unparseable → **STOP and report**; do not touch a newer / unknown deployment at all (not even the methodology slot). This STOP is one of the few places the number is actionable (*Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`): show the user **the `docs/ai` structure version** their deployment carries versus the one this kit expects, plus the plain one-line two-axes note — naming it the structure version, **never** "lineage head".
9
- 3. **Reconcile the bounded pointers — stamp-independent, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md`. ONE call reconciles **three** pointers — the **workflow-methodology** pointer, the **orchestration-recipes** pointer (Solo / Reviewed / Council / Delegated, routing to `/agent-workflow-kit recipes`) right below it, and the **autonomy-policy** pointer (the `docs/ai/autonomy.json` read contract, routing to `/agent-workflow-kit set-autonomy`) below that — and is filled on **every** upgrade, idempotently (zero-diff when all are already present + filled), so even a legacy or current equal-head deployment gains them **without a lineage-head bump or a migration** (the `agent-workflow-memory` **package** template did get docs-only headroom trims for the chained pointers, but no deployed-`docs/ai` structure changed). Per slot it inserts an empty pair at its anchor if absent, preserves a customized pair verbatim, and STOPs (never edits) on a malformed pair, a missing / duplicate anchor, or **when a fill is needed but the installed `agent-workflow-engine` is absent/invalid** (the fragments are read live from it — see the distinct outcomes below).
9
+ 3. **The step-3 run-list — stamp-independent reconciles, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run the **seven operations below in order, on every upgrade** (equal-head too) no lineage-head bump, no migration file. Each backticked id anchors its rationale block below the checklist; a consent marker is a CONDITIONAL gate an explicit yes is required exactly where the row's clause states one:
10
+
11
+ 1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
12
+ 2. `footprint` — `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` → visibility: visible · ambiguous · hidden — consent (conditional): ambiguous → ask which it is BEFORE anything; hidden → the conditional re-run without `--dry-run` (its surfaced paths ask per bootstrap step 9).
13
+ 3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node` / `old-adr-layout-migration-instructed` / `failed`.
14
+ 4. `gates-migration` — `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` → the preview plan · INERT checker · CUSTOMIZED entries, each named — consent: apply only on an explicit yes, re-run with `--apply`.
15
+ 5. `bridges` — `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` → per-bridge lines: refreshed · already current · skipped — with its stated reason (not placed / newer than the bundle / unsupported host) · `skipped-readonly` · could not refresh.
16
+ 6. `lens` — `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md` → per section: refreshed · already current · custom edit preserved · file absent / engine too old — skipped · over the line cap — refused · section absent — noted · a fully absent/invalid engine → hard STOP.
17
+ 7. `bridge-settings` — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` → outcome line(s), pasted verbatim: ok · absent · flagged · duplicates · unusable.
18
+
19
+ **`pointers` — the bounded pointers.** ONE call reconciles **three** pointers — the **workflow-methodology** pointer, the **orchestration-recipes** pointer (Solo / Reviewed / Council / Delegated, routing to `/agent-workflow-kit recipes`) right below it, and the **autonomy-policy** pointer (the `docs/ai/autonomy.json` read contract, routing to `/agent-workflow-kit set-autonomy`) below that — and is filled on **every** upgrade, idempotently (zero-diff when all are already present + filled), so even a legacy or current equal-head deployment gains them **without a lineage-head bump or a migration** (the `agent-workflow-memory` **package** template did get docs-only headroom trims for the chained pointers, but no deployed-`docs/ai` structure changed). Per slot it inserts an empty pair at its anchor if absent, preserves a customized pair verbatim, and STOPs (never edits) on a malformed pair, a missing / duplicate anchor, or **when a fill is needed but the installed `agent-workflow-engine` is absent/invalid** (the fragments are read live from it — see the distinct outcomes below).
10
20
 
11
21
  **Classify the exit — there are THREE non-zero exits + one soft in-band skip; handle each differently:**
12
22
 
@@ -18,19 +28,21 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
18
28
 
19
29
  **No-Node project:** the fragments live only in the **installed `agent-workflow-engine`** (`references/methodology-slot.md` + `references/orchestration-slot.md` + `references/autonomy-slot.md`, under `~/.claude/skills/agent-workflow-engine` or `$AGENT_WORKFLOW_ENGINE_DIR`) — there is no bundled copy, and a No-Node host cannot run the `npx` engine install. Open `AGENTS.md` and classify **each** pointer by hand: a **filled / customized** pair → leave it verbatim (no engine needed); a **malformed** pair (not exactly one ordered `start → end`) → STOP, do not edit. A pair that needs filling — **absent markers OR a present-but-empty pair** — needs the engine's fragment, so: if the engine is **not installed**, that pointer **cannot be added** — report it plainly (the methodology is already in `docs/ai/agent_rules.md`; the recipes are available via `/agent-workflow-kit recipes`; the autonomy policy via `/agent-workflow-kit set-autonomy`; install the engine to add the pointers). If the engine **is** present, **count the lines first** — if adding/filling would take the file over 100 lines, **skip that pointer and report the skip** (methodology first, then orchestration, then autonomy; each chained pair sits right under the previous pair's end marker). Fill each empty pair from its engine fragment (`methodology-slot.md` / `orchestration-slot.md` / `autonomy-slot.md`) — never inline a copy (that would re-create the retired mirror).
20
30
 
21
- **Hidden-mode footprint reconcile stamp-independent, same gate, BEFORE the equal-head short-circuit (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility**: `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` (writes **zero bytes**). It reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
31
+ **`footprint`hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
22
32
 
23
- **Orchestration config ensure (seed-or-refresh)stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/orchestration.json` exists **and its onboarding note is current**: **create it from the template if missing**; **if it already exists, preserve every activity/slot the user set, and refresh ONLY the `_README` note when the existing one still matches a known prior canonical** the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (normalize CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim; a *malformed* existing config is **preserved + a loud warning**, never clobbered or silently skipped). The current note points at `/agent-workflow-kit set-recipe` (the config is now agent-writable no more "never written for you"). **The refresh helper is kit-owned** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and the **kit** then applies the `_README` refresh; in the **fallback** path the kit seeds-or-refreshes directly from `${CLAUDE_SKILL_DIR}/references/templates/orchestration.json`. (Memory stays standalone it never depends on this helper.) Like the pointer slots + the footprint reconcile, this reaches an equal-head deployment **without a lineage-head bump or a migration file** (it is a `.json`, inherently outside the docs cap-validator). Report it in the step 4 / step 8 success report (config *seeded* / *note refreshed* / *already current* / *customized — preserved*).
33
+ **`configs` the project-configuration ensures, ONE run.** The ONE command performs **all four** ensures described below orchestration config · gate declaration · autonomy declaration · enforcement scripts — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every seed is **create-only** (an existing file is preserved byte-for-byte), the ONE refresh is the orchestration onboarding note while it still matches a canonical this kit shipped, and one ensure failing **never** skips the others: each reports its own outcome and the run exits non-zero when any of them `failed`. The outcome tokens, by ensure: orchestration → `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved`; gates and autonomy `seeded` / `already-present`; scripts `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node`; and any ensure may report `failed`, whose line OPENS with the cause (relay it with that cause never soften it into a skip; an op that copies file by file also states when it stopped partway). **A non-zero exit STOPs this upgrade** report the failed line and stop there, before the equal-head exit, the migrations and the re-stamp. Add `--dry-run` to preview without writing a byte. Like the pointer slots + the footprint reconcile, all four reach an equal-head deployment **without a lineage-head bump or a migration file** (they are `.json` / `scripts/`, inherently outside the docs cap-validator).
24
34
 
25
- **Gate-declaration ensure (seed-if-missing) stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/gates.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/gates.json` if missing** the kit's OWN template twin, so this works even when the installed memory substrate predates the gates feature (a stale memory never silently loses it); **an existing file is preserved byte-for-byte** (a project's declared gate matrix is authored content never clobbered, never refreshed in place; unlike the orchestration `_README` there is no note-refresh here). Report it in the step 4 / step 8 success report (*seeded* / *already present*). Like the config ensure, this reaches an equal-head deployment without a lineage-head bump or a migration file (a `.json`, inherently outside the docs cap-validator).
35
+ **What the orchestration-config ensure does.** `docs/ai/orchestration.json` must exist **and its onboarding note must be current**: created from the canonical seed if missing; if it already exists, **every activity/slot the user set is preserved** and ONLY the `_README` note is refreshed, and only when the existing one still matches a known prior canonical the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (it normalizes CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim → `customized-preserved`; a *malformed* existing config is **preserved untouched + LOUD** `malformed-preserved`, never clobbered and never silently skipped). The current note points at `/agent-workflow-kit set-recipe`. **Kit-owned:** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and this ensure applies the `_README` refresh; in the **fallback** path it does both. (Memory stays standalone.)
26
36
 
27
- **Legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
37
+ **What the gate-declaration ensure does.** `docs/ai/gates.json` must exist: created from `${CLAUDE_SKILL_DIR}/references/templates/gates.json` if missing the kit's OWN template twin, so a memory substrate predating the gates feature never silently loses it; **an existing file is preserved byte-for-byte** (a project's declared gate matrix is authored content; unlike the orchestration `_README` there is no note-refresh here).
28
38
 
29
- **Autonomy-declaration ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/autonomy.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing** (the kit's OWN template twin, mirrored from memory so a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content — never clobbered, never refreshed in place). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand). Report it in the step 4 / step 8 success report (*seeded* / *already present, preserved*). Like the other config ensures, no lineage-head bump or migration file (a `.json`, outside the docs cap-validator).
39
+ **What the autonomy-declaration ensure does.** `docs/ai/autonomy.json` must exist: created from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing (the kit's own template twin — a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand).
30
40
 
31
- **Enforcement-script ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. Ensure the pairs exist in the project's `scripts/`: **copy `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs` from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing**; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Seed nothing else — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECT FIRST, never auto-migrate (AD-051, Decision 13):** if `status` surfaces `adrLayout: "old"` (a `docs/ai/history/decisions-archive*.md` monolith) or `"old-unrotated"` (no monolith, but the deployed rotator predates the store), it is on the RETIRED 3-tier cascade **do NOT seed or refresh the now one-file-per-ADR rotator here** (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Instead surface a LOUD instruct: run the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits). The seed-if-missing above applies ONLY to a clean layout (neither signal). The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (re-run `node scripts/install-git-hooks.mjs` after the ensure and it will refuse a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook. Skip this ensure on a No-Node project (the scripts are Node enforcement). Report it in the step 4 / step 8 success report (*added* / *already present* / *old ADR layout — migration instructed*).
41
+ **What the enforcement-script ensure does.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. The pairs must exist in the project's `scripts/`: `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs`, copied from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Nothing else is seeded — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECTED FIRST, never auto-migrated (AD-051, Decision 13):** on a `docs/ai/history/decisions-archive*.md` monolith (`old`), or no monolith but a deployed rotator predating the store (`old-unrotated`), the project is on the RETIRED 3-tier cascade, so the ensure writes NOTHING and reports `old-adr-layout-migration-instructed` (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Relay it as the LOUD instruct it is: the fix is the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits), and the seed lands on the next upgrade. A layout the ensure cannot READ is `failed`, not a seed — it never writes on an unverifiable tree. The seed applies ONLY to a clean layout (neither signal), and a project with no `package.json` at its root reports `skipped-no-node` (these are Node enforcement scripts) while the three config ensures still run. The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (re-run `node scripts/install-git-hooks.mjs` after the ensure and it will refuse a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook.
32
42
 
33
- **Placed-bridge refreshstamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
43
+ **`gates-migration` legacy gates.json migration (consented preview D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
44
+
45
+ **`bridges` — placed-bridge refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
34
46
  `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` and **paste its per-bridge
35
47
  output lines verbatim** — every outcome line is composed by the tool (*refreshed* / *already
36
48
  current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + its recovery). It
@@ -39,12 +51,21 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
39
51
  (placement stays the opt-in `${CLAUDE_SKILL_DIR}/references/modes/setup.md` — AD-009/AD-011 honesty intact), a placed bridge
40
52
  **newer** than the bundle is a stated skip naming the kit update (**never a downgrade**), and
41
53
  `skipped-readonly` is an equal-version re-sync a **read-only** skills dir blocked this session (a
42
- stated skip, exit 0 — not a failure). Runs on **every** upgrade (equal-head too), no lineage-head
43
- bump; a *could not refresh* line is non-fatal relay it plainly with its recovery.
44
-
45
- **Agent-rules lens refresh stamp-independent, same gate, BEFORE the equal-head short-circuit.**
46
- Run `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md`
47
- and relay its outcome in plain language: the planning/review lens section was **refreshed** to
54
+ stated skip, exit 0 — not a failure). That line never guesses at the tree: it carries one **proven**
55
+ verdict from a read-only re-scan of the bundle-owned files **and** the wrapper links
56
+ `clean-parity` (nothing the refresh manages differs), `drifted` (every item **named** with the
57
+ recovery that applies: a rerun REPAIRS what it can converge, and REFUSES *what it cannot converge
58
+ under the refresh's no-follow and ownership policy* — **non-exhaustively**: a symlink where it must
59
+ write, an incompatible node kind, a foreign wrapper target; those are named apart as
60
+ resolve-by-hand-then-rerun), or `unverifiable` (every uncomparable item **named**, the remaining
61
+ need for repair unknown). The line carries the exact cause per item — relay it as worded, never
62
+ soften `unverifiable` into "already fine", never relay a resolve-by-hand item as rerun-fixable.
63
+ Runs on **every** upgrade
64
+ (equal-head too), no lineage-head bump; a *could not refresh* line is non-fatal — relay it plainly
65
+ with its recovery.
66
+
67
+ **`lens` — agent-rules lens refresh.** Relay the outcome in plain
68
+ language: the planning/review lens section was **refreshed** to
48
69
  the current canon · was **already current** · carries a **custom edit — preserved verbatim** (+
49
70
  its one-line note) · the **file is absent — skipped** · the **engine is too old — skipped**
50
71
  (refresh it with `npx @sabaiway/agent-workflow-engine@latest init`, then re-run) · **over the
@@ -57,13 +78,12 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
57
78
  cap — refused*; an unreadable bundled template canon is its own loud STOP naming the kit
58
79
  reinstall command.
59
80
 
60
- **Bridge settings reconcile stamp-independent, same gate, BEFORE the equal-head short-circuit.**
61
- Run `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` and **paste its outcome line
81
+ **`bridge-settings`bridge settings reconcile.** **Paste its outcome line(s)
62
82
  verbatim**: it validates the deployed host settings file's keys against the bundled manifests and
63
83
  **NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
64
84
  flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
65
85
  4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
66
- - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the seed pairs to `scripts/`, found them *already present*, or found an *old ADR layout migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
86
+ - **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **four project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (orchestration config, gate declaration, autonomy declaration, enforcement scripts their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line what stopped it; the **`gates-migration`** result *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
67
87
  - **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
68
88
  - **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
69
89
  - **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
@@ -72,4 +92,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
72
92
  5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
73
93
  6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
74
94
  7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.)
75
- 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** + **Communication-section** outcomes (same outcome sets as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
95
+ 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **four project-configuration ensure** (`configs`) lines, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** outcomes (same outcome sets as step 4), and the **bridge-settings reconcile** (`bridge-settings`) outcome (line(s) pasted verbatim) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
@@ -36,6 +36,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
36
36
  - `node ${CLAUDE_SKILL_DIR}/tools/repo-search.mjs --pattern <literal> --path <p>` (wildcard — the LITERAL search lane; for a pattern carrying a shell-significant byte use `--pattern-file <p>` instead, and for a TARGET PATH carrying one use `--paths-file <p>` — either way the bytes never enter the command string. one of the TWO tier tools that carry hook residual coverage — see the honesty note below)
37
37
  - `node ${CLAUDE_SKILL_DIR}/tools/path-inventory.mjs --path <p>` (wildcard — the read-only inventory lane: exists / type / size / `wc -l` lines / one-level listing / small-file contents for N paths in ONE call, so small path questions never need a composed shell. `--paths-file <p>` is its out-of-band target lane; hook residual coverage as with repo-search)
38
38
  - `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs --cwd ${PROJECT_ROOT}` — **EXACT byte-string only**, and honestly **project-exec, not read-only**: it runs YOUR declared `docs/ai/gates.json` commands — the same trust boundary the opt-in hook grants byte-exact per-cmd. A wildcard would be BROADER than that boundary (`--cwd <dir>` executes another project's declared gates), so the bare cwd-defaulting form, any other `--cwd`, `--only`, and **`--final`** forms all still prompt (`--final` WRITES the final-run receipt into the core-evidence store — a recording run is never auto-approved).
39
+ - `node ${CLAUDE_SKILL_DIR}/tools/source-size-check.mjs --check` — **EXACT byte-string only**, and only this READ-ONLY mode: the tool's `--write-baseline` and `--adopt` modes WRITE (the recorded ratchet, and a gate declaration) and are deliberately uncovered — they still prompt, always. Every other form, `--cwd` included, keeps prompting. **What it covers, precisely:** the agent's own direct invocation. It is **not** the byte-string a DECLARED gate carries — the fill emits that path double-quoted (a kit path with a space must survive), and a seedable allow rule may carry no quotes at all, so the two spellings cannot be one string; a declared gate is auto-approved by the opt-in hook instead, byte-exact against `docs/ai/gates.json`.
39
40
  - Writer previews, **exact arg-free dry-run byte-strings only** (the SEEDED tier byte-string is the arg-free preview of each): `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs` — every `--apply`/`--write`/`--yes` still prompts, always. (`gate-hook` also has a **`--read-lane`** flagged preview — the opt-in read-only compound lane, `${CLAUDE_SKILL_DIR}/references/modes/hook.md`; that flagged form is NOT the seeded arg-free byte-string, so it may **prompt once** — it IS a consent flow, stated, no silent cap.)
40
41
 
41
42
  Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with TWO deliberate exceptions, `repo-search.mjs` and `path-inventory.mjs`**, whose prefixes are in the hook's scanned list because they take caller-supplied argument bytes: a redirection or substitution on THEIR invocation raises the ask, whose reason names that tool's out-of-band lane (`--pattern-file` / `--paths-file`) — context for the HUMAN deciding, not a message to the caller. Neither is in the seeded core, so neither inherits the read-lane allow. Every other tier entry: the opt-in hook's residual ask-net guards only the seeded read-only CORE prefixes, so the tier rides the same settings-level residual posture as the core (redirection / command substitution are not inspectable at the settings layer; see the residual notice). A skill or project path that cannot survive UNQUOTED in a byte-exact rule (spaces, metacharacters, non-POSIX) **STOPs the tier up front with a clear error** — nothing is seeded. Anything you want covered beyond the tier — such paths, this repo's own relative-path spellings, other tools — stays a **BY-HAND add** to your settings, with the path your project actually reaches the kit by. Pre-existing `node …` allow entries that do NOT match the seeded tier byte-forms stay flagged by the advisory for hand review.
@@ -10,10 +10,13 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, lstat
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { spawnSync } from 'node:child_process';
13
- import { UNIT_TESTS_COVERAGE_FLAGS, RETIRED_STORE_BASENAMES, main } from './migrate-gates.mjs';
13
+ import { CHECKER_CLAIM, UNIT_TESTS_COVERAGE_FLAGS, RETIRED_STORE_BASENAMES, checkerClaimTool, classifyCheckerClaim, main } from './migrate-gates.mjs';
14
14
 
15
+ // Both core checks exist as real files — canonicity is a realpath anchor, so a check whose file is
16
+ // absent resolves to nothing and is no claim at all (the fail-closed answer run-gates gives too).
15
17
  const KIT_TOOLS = mkdtempSync(join(tmpdir(), 'migrate-branches-kit-'));
16
18
  writeFileSync(join(KIT_TOOLS, 'coverage-check.mjs'), '// the installed checker the migration points at\n');
19
+ writeFileSync(join(KIT_TOOLS, 'review-state.mjs'), '// the installed review-state check\n');
17
20
 
18
21
  const mkProject = (gates) => {
19
22
  const root = mkdtempSync(join(tmpdir(), 'migrate-branches-'));
@@ -34,6 +37,8 @@ const CHECKER = { id: 'coverage-check', title: 'CC', cmd: `node "${join(KIT_TOOL
34
37
  const REVIEW_STATE = { id: 'review-state', title: 'RS', cmd: `node "${join(KIT_TOOLS, 'review-state.mjs')}" --check` };
35
38
  const LEGACY = { id: 'review-ledger', title: 'L', cmd: 'node "/kit/tools/review-ledger.mjs" --check' };
36
39
  const UNIT = { id: 'unit-tests', title: 'U', cmd: 'node --test tools/*.test.mjs' };
40
+ // A suite the closed producer world cannot express, declaring itself with the optional marker.
41
+ const MARKED_SUITE = { id: 'suite', title: 'S', cmd: 'pnpm vitest run --coverage', lcovProducer: true };
37
42
 
38
43
  describe('migrate-gates — refusal and no-op branches', () => {
39
44
  it('--help prints the contract and exits 0', () => {
@@ -141,6 +146,146 @@ describe('migrate-gates — refusal and no-op branches', () => {
141
146
  rmSync(root, { recursive: true, force: true });
142
147
  });
143
148
 
149
+ it('a marker-carrying entry survives an apply UNCHANGED — the loader is lenient, the writer opaque', () => {
150
+ // The declaration this tool rewrites may carry keys it knows nothing about. The loader accepts
151
+ // any `{ gates: [...] }` shape and the writer re-serializes the ENTRY, not a reconstruction of
152
+ // it, so an upgrade over a marker-carrying deployment never silently drops the claim.
153
+ const root = mkProject([LEGACY, MARKED_SUITE, REVIEW_STATE]);
154
+ const io = quiet();
155
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io), 0, io.err.join('\n'));
156
+ const raw = readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8');
157
+ const written = JSON.parse(raw).gates;
158
+ assert.deepEqual(written.map((g) => g.id), ['suite', 'review-state', 'coverage-check'], 'the checker is ADDED over a marker-claimed producer');
159
+ assert.deepEqual(written[0], MARKED_SUITE, 'the marked entry round-trips key for key');
160
+ assert.match(raw, /"lcovProducer": true/, 'and the marker is really in the written bytes');
161
+ assert.doesNotMatch(io.out.join('\n'), /WARNING/, 'nothing is withheld over a declared producer');
162
+ rmSync(root, { recursive: true, force: true });
163
+ });
164
+
165
+ it('a marker on the CHECKER ITSELF never self-pairs — the declared pair stays INERT', () => {
166
+ // The producer question is POSITIONAL: the checker always ends up last, so it can never be its
167
+ // own producer. Asking it over the whole kept set would let this declaration certify itself
168
+ // into final-run-capability with nothing writing the lcov.
169
+ const root = mkProject([{ id: 'lint', title: 'L', cmd: 'eslint .' }, REVIEW_STATE, { ...CHECKER, lcovProducer: true }]);
170
+ const io = quiet();
171
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
172
+ const text = io.out.join('\n');
173
+ assert.match(text, /INERT/, 'the dead pair is named');
174
+ assert.doesNotMatch(text, /already final-run-capable/, 'and never claimed capable');
175
+ rmSync(root, { recursive: true, force: true });
176
+ });
177
+
178
+ it('a MARKED unit-tests entry is a zero-diff keep — never extended, never reported customized', () => {
179
+ // Both arms the marker settles at once: `npm test` is a cmd this tool cannot verify (customized
180
+ // without the marker), and rewriting a cmd whose owner declared it the producer would change
181
+ // bytes the byte-exact hook approval binds.
182
+ const marked = { id: 'unit-tests', title: 'U', cmd: 'npm test', lcovProducer: true };
183
+ const root = mkProject([marked, REVIEW_STATE]);
184
+ const io = quiet();
185
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
186
+ const text = io.out.join('\n');
187
+ assert.match(text, /ADD coverage-check/, 'the claimed producer unlocks the checker');
188
+ assert.doesNotMatch(text, /EXTEND unit-tests/, 'a claimed producer cmd is never rewritten');
189
+ assert.doesNotMatch(text, /CUSTOMIZED/, 'nor reported as a cmd the tool cannot verify');
190
+ rmSync(root, { recursive: true, force: true });
191
+
192
+ // The SAME entry unmarked is the customized/withheld path — the marker is what settles it.
193
+ const bare = mkProject([{ id: 'unit-tests', title: 'U', cmd: 'npm test' }, REVIEW_STATE]);
194
+ const io2 = quiet();
195
+ assert.equal(main(['--cwd', bare, '--kit-tools', KIT_TOOLS], io2), 0, io2.err.join('\n'));
196
+ const text2 = io2.out.join('\n');
197
+ assert.match(text2, /CUSTOMIZED/);
198
+ assert.doesNotMatch(text2, /ADD coverage-check/, 'the checker stays withheld with no producer');
199
+ rmSync(bare, { recursive: true, force: true });
200
+ });
201
+
202
+ it('a marker over an UNRUNNABLE cmd never unlocks the checker — the lenient loader has no validator', () => {
203
+ // This tool accepts any `{ gates: [...] }` shape, so an entry the strict validator would refuse
204
+ // reaches the plan builder intact. A marker on such an entry must not make the migration ADD the
205
+ // canonical checker: the result would be the dead pair the withhold exists to prevent, and the
206
+ // written declaration would then fail run-gates outright.
207
+ for (const cmd of [' ', 'echo a\nrm -rf b']) {
208
+ const root = mkProject([{ id: 'suite', title: 'S', cmd, lcovProducer: true }, REVIEW_STATE]);
209
+ const io = quiet();
210
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
211
+ const text = io.out.join('\n');
212
+ assert.doesNotMatch(text, /ADD coverage-check/, `an unrunnable cmd must not unlock the checker: ${JSON.stringify(cmd)}`);
213
+ assert.match(text, /WARNING: the canonical coverage-check gate was NOT added/, 'and the withhold is stated');
214
+ rmSync(root, { recursive: true, force: true });
215
+ }
216
+ });
217
+
218
+ it('a marker on a DUPLICATE canonical checker never produces for the other — nor claims capability', () => {
219
+ // `--final` accepts exactly ONE canonical checker, and a checker cannot write the lcov it reads.
220
+ // Excluding only the LAST checker row from the producer search let a marker on the first one pair
221
+ // with the second, and the preview then called the result final-run-capable over a declaration
222
+ // --final rejects outright, with nothing writing the file.
223
+ const root = mkProject([{ ...CHECKER, id: 'coverage-check', lcovProducer: true }, REVIEW_STATE, { ...CHECKER, id: 'coverage-check-2' }]);
224
+ const io = quiet();
225
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
226
+ const text = io.out.join('\n');
227
+ assert.doesNotMatch(text, /already final-run-capable/, 'two checkers are never a final-run-capable result');
228
+ assert.match(text, /2 declared gates are the canonical coverage checker/, 'the duplication is NAMED');
229
+ assert.match(text, /INERT/, 'and the pair is still reported inert — nothing writes the lcov');
230
+ rmSync(root, { recursive: true, force: true });
231
+ });
232
+
233
+ it('the tool-claim twin RUNS in this module — three outcomes, fail-closed on the unresolvable', () => {
234
+ // The text drift guard (beside the kit's own copy) proves the two owners are byte-equal; it
235
+ // cannot prove this copy WORKS, because the region is byte-equal inside a DIFFERENT host with
236
+ // different imports. Executing it here is what proves the twin resolves everything it uses.
237
+ const canonical = join(KIT_TOOLS, 'coverage-check.mjs');
238
+ const root = mkProject([]);
239
+ try {
240
+ const elsewhere = join(root, 'vendor-coverage-check.mjs');
241
+ writeFileSync(elsewhere, '// a vendored copy\n');
242
+ const tool = checkerClaimTool('coverage-check.mjs', canonical);
243
+ assert.equal(classifyCheckerClaim(tool, `node "${canonical}" --check`, KIT_TOOLS), CHECKER_CLAIM.CANONICAL);
244
+ const vendored = checkerClaimTool('vendor-coverage-check.mjs', canonical);
245
+ assert.equal(classifyCheckerClaim(vendored, `node "${elsewhere}" --check`, KIT_TOOLS), CHECKER_CLAIM.ELSEWHERE);
246
+ assert.equal(classifyCheckerClaim(tool, `node "${canonical}" --check || true`, KIT_TOOLS), CHECKER_CLAIM.NOT_THE_TOOL, 'a masked form is no claim');
247
+ assert.equal(classifyCheckerClaim(tool, `node "${join(KIT_TOOLS, 'nowhere', 'coverage-check.mjs')}" --check`, KIT_TOOLS), CHECKER_CLAIM.NOT_THE_TOOL, 'unresolvable fails closed');
248
+ assert.equal(classifyCheckerClaim(tool, 'node $(pwd)/coverage-check.mjs --check', KIT_TOOLS), CHECKER_CLAIM.NOT_THE_TOOL, 'a shell-active bare token is no claim');
249
+ } finally {
250
+ rmSync(root, { recursive: true, force: true }); // every other case here cleans up; this one held its root only for a path
251
+ }
252
+ });
253
+
254
+ it('a VENDORED deployment previews at exit 0 and its --apply writes ZERO bytes', () => {
255
+ // The upgrade path this fixes: every preview AND every apply over a deployment that declared the
256
+ // checker through its own vendored copy used to exit 1 on an id collision, so such a deployment
257
+ // could not be upgraded at all.
258
+ const vendoredTools = mkdtempSync(join(tmpdir(), 'migrate-branches-vendored-'));
259
+ writeFileSync(join(vendoredTools, 'coverage-check.mjs'), '// a vendored copy of the checker\n');
260
+ const vendored = { id: 'coverage-check', title: 'CC', cmd: `node "${join(vendoredTools, 'coverage-check.mjs')}" --check` };
261
+ const root = mkProject([UNIT_DONE, REVIEW_STATE, vendored]);
262
+ const before = readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8');
263
+
264
+ const io = quiet();
265
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
266
+ const preview = io.out.join('\n');
267
+ assert.match(preview, /VERIFY \(preserved exactly as declared\): coverage-check/);
268
+ assert.doesNotMatch(preview, /ADD coverage-check/, 'nothing is added over a checker that is already declared');
269
+ assert.doesNotMatch(io.err.join('\n'), /id collision/, 'a vendored copy is not a squatter');
270
+
271
+ const io2 = quiet();
272
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS, '--apply'], io2), 0, io2.err.join('\n'));
273
+ assert.equal(readFileSync(join(root, 'docs', 'ai', 'gates.json'), 'utf8'), before, 'the apply is a ZERO-DIFF write');
274
+ assert.match(io2.out.join('\n'), /NOT final-run-capable/, 'and the withheld claim survives the no-op apply');
275
+ rmSync(vendoredTools, { recursive: true, force: true });
276
+ rmSync(root, { recursive: true, force: true });
277
+ });
278
+
279
+ it('a vendored copy named by a RELATIVE path resolves against the PROJECT root, as the runner resolves it', () => {
280
+ const root = mkProject([UNIT_DONE, REVIEW_STATE, { id: 'coverage-check', title: 'CC', cmd: 'node "vendor/coverage-check.mjs" --check' }]);
281
+ mkdirSync(join(root, 'vendor'), { recursive: true });
282
+ writeFileSync(join(root, 'vendor', 'coverage-check.mjs'), '// a vendored copy inside the project\n');
283
+ const io = quiet();
284
+ assert.equal(main(['--cwd', root, '--kit-tools', KIT_TOOLS], io), 0, io.err.join('\n'));
285
+ assert.match(io.out.join('\n'), /VERIFY \(preserved exactly as declared\): coverage-check/, 'a relative token is resolved, not dismissed');
286
+ rmSync(root, { recursive: true, force: true });
287
+ });
288
+
144
289
  it('an un-unlinkable retired store is reported LOUDLY and never fails the migration', () => {
145
290
  const root = mkProject([LEGACY, UNIT]);
146
291
  spawnSync('git', ['init', '-q'], { cwd: root, encoding: 'utf8' });