@sabaiway/agent-workflow-kit 10.2.0 → 10.4.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 (45) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +7 -7
  3. package/SKILL.md +1 -1
  4. package/capability.json +1 -1
  5. package/package.json +1 -1
  6. package/references/agents/executor.md +40 -0
  7. package/references/modes/agents.md +9 -4
  8. package/references/modes/procedures.md +17 -8
  9. package/references/modes/recipes.md +7 -4
  10. package/references/modes/recommendations.md +4 -1
  11. package/references/modes/set-recipe.md +22 -5
  12. package/references/modes/status.md +3 -3
  13. package/references/modes/upgrade.md +7 -5
  14. package/references/shared/composition-handoff.md +1 -1
  15. package/references/shared/deploy-tail.md +2 -2
  16. package/references/templates/agent_rules.md +3 -2
  17. package/references/templates/orchestration.json +1 -1
  18. package/tools/ack-store.mjs +57 -0
  19. package/tools/ack-write.mjs +1 -1
  20. package/tools/autonomy-config.mjs +1 -1
  21. package/tools/carriers.mjs +140 -0
  22. package/tools/cheap-agents-read.mjs +172 -0
  23. package/tools/cheap-agents.mjs +57 -105
  24. package/tools/commands.mjs +3 -3
  25. package/tools/direct-run.mjs +6 -0
  26. package/tools/doc-parity.mjs +8 -0
  27. package/tools/ensure-ops.mjs +18 -9
  28. package/tools/ensure-specs.mjs +3 -4
  29. package/tools/ensure-vocabulary.mjs +5 -2
  30. package/tools/family-registry.mjs +70 -21
  31. package/tools/flow-check.mjs +2 -7
  32. package/tools/inject-methodology.mjs +4 -0
  33. package/tools/lens-region.mjs +4 -1
  34. package/tools/node-evidence.mjs +77 -0
  35. package/tools/orchestration-config.mjs +34 -13
  36. package/tools/procedures.mjs +65 -52
  37. package/tools/recipes.mjs +156 -184
  38. package/tools/recommendations.mjs +145 -78
  39. package/tools/renderers.mjs +36 -7
  40. package/tools/review-state.mjs +10 -11
  41. package/tools/set-recipe.mjs +63 -24
  42. package/tools/spec-adoption.mjs +71 -0
  43. package/tools/spec-check.mjs +2 -2
  44. package/tools/upgrade-runlist.mjs +1 -1
  45. package/tools/view-model.mjs +19 -3
@@ -1,6 +1,7 @@
1
1
  ### Mode: upgrade
2
2
 
3
3
  <!-- opt-in-capability: family-freshness -->
4
+ <!-- opt-in-capability: spec-adoption -->
4
5
 
5
6
  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
7
 
@@ -10,13 +11,13 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
10
11
 
11
12
  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
13
  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` / `refreshed` / `regenerated` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node` / `old-adr-layout-migration-instructed` / `failed`.
14
+ 3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed`.
14
15
  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
16
  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
17
  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
18
  7. `bridge-settings` — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` → outcome line(s), pasted verbatim: ok · absent · flagged · duplicates · unusable.
18
19
 
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).
20
+ **`pointers` — the bounded pointers.** ONE call reconciles **three** pointers — the **workflow-methodology** pointer, the **orchestration-recipes** pointer (Solo / Reviewed / Council / Delegated / Subagent, 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).
20
21
 
21
22
  **Classify the exit — there are THREE non-zero exits + one soft in-band skip; handle each differently:**
22
23
 
@@ -30,7 +31,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
30
31
 
31
32
  **`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.
32
33
 
33
- **`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy declaration · enforcement scripts · spec layer · navigator index — 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 three refresh-class ops are named apart — the orchestration onboarding note (refreshed only while it still matches a canonical this kit shipped), the spec-layer reader and checker pairs (below) and the **navigator index**, a GENERATED artifact regenerated whenever it is missing or stale (never authored content, so there is nothing to preserve). 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`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node`; index → `regenerated` / `already-current`; 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). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `wrong-node-kind`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **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 six reach an equal-head deployment **without a lineage-head bump or a migration file** (they are `.json` / `scripts/` / a generated artifact / a seeded store root, inherently outside the docs cap-validator).
34
+ **`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy declaration · enforcement scripts · spec layer · navigator index — 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 three refresh-class ops are named apart — the orchestration onboarding note (refreshed only while it still matches a canonical this kit shipped), the spec-layer reader and checker pairs (below) and the **navigator index**, a GENERATED artifact regenerated whenever it is missing or stale (never authored content, so there is nothing to preserve). 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-evidence`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node-evidence`; index → `regenerated` / `already-current`; 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). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `node-evidence-unverifiable`, `wrong-node-kind`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **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 six reach an equal-head deployment **without a lineage-head bump or a migration file** (they are `.json` / `scripts/` / a generated artifact / a seeded store root, inherently outside the docs cap-validator).
34
35
 
35
36
  **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.)
36
37
 
@@ -38,11 +39,11 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
38
39
 
39
40
  **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).
40
41
 
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.
42
+ **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). **Whether Node runs here is PROVEN, never proxied:** the ensure seeds on a regular `package.json` at the root OR on any kit-seeded `scripts/*.mjs` already deployed (a bootstrap places them into projects that never carry a `package.json`); only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the three config ensures still run; a probe that cannot be read is `failed` with the cause `node-evidence-unverifiable`, nothing written. **A `skipped-*` outcome whose stated reason this tool could itself disprove may not exist** — the retired `skipped-no-node` was exactly that, printed beside twenty deployed Node scripts. 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.
42
43
 
43
44
  **What the navigator ensure does.** `docs/ai/index.md` is the always-loaded navigator the entry point declares, and it is GENERATED — no template ships it, so a deployment that never ran the generator boots from a broken entry point (and, on a Node project, carries a pre-commit hook that fails its own index check). The ensure runs the bundled generator's finalizer and reports `regenerated` (it was missing or stale — it was written) or `already-current` (nothing written). It never skips a No-Node project: the generator runs from `${CLAUDE_SKILL_DIR}/references/scripts/` on the agent host, not from the project's `scripts/`. **Its position in the run-list is EARLY and therefore NOT authoritative** — `lens` (and, on the migrated path, steps 6–7) still change `docs/ai` afterwards — so the authoritative run is the LATE `--only index` rung documented at both exits; the early one is idempotent and costs at most an `already-current` line.
44
45
 
45
- **What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`, so behind a custom checker the store root is NOT seeded and the line names the remedy (copy the pair from `${CLAUDE_SKILL_DIR}/references/scripts/` by hand, re-run). One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node` without a `package.json`; every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it.
46
+ **What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`, so behind a custom checker the store root is NOT seeded and the line names the remedy (copy the pair from `${CLAUDE_SKILL_DIR}/references/scripts/` by hand, re-run). One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node-evidence` only when neither a root `package.json` nor any kit-seeded `scripts/*.mjs` is present (the same Node-evidence probe the enforcement-script ensure uses; an unreadable probe is `failed` / `node-evidence-unverifiable`); every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it. **The adoption state this seed leaves behind is reported, never assumed:** the Recommendations section (step 4 / 8) carries the `spec-adoption` item — `not adopted` (the store is absent: the seed offered, the decline a named hand-apply alternative), `adopting` (a store with no live contract: the decline offered), silenced by a recorded decline (`ack-write --lane spec-adoption`), and a store the probe cannot read is a stated skip — and `status` prints the same state on its own line.
46
47
 
47
48
  **`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).
48
49
 
@@ -88,6 +89,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
88
89
  flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
89
90
  **The LATE navigator finalizer — the AUTHORITATIVE run, after the last `docs/ai` mutation.** `lens` above rewrites `docs/ai/agent_rules.md`, so the step-3 index ensure is already behind by the time the run-list ends. Re-run that ONE op here, before the step-4 report: `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`. Relay **this** line in the report (it supersedes the early one; an untouched tree reports `already-current`, a failure STOPs the upgrade like any other ensure). On the migrated path the same rung runs again at the END of step 7 — after the migrations, before the step-8 re-stamp.
90
91
  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:
92
+ - **A skip line that contradicts the observed tree is a FINDING, never pasted as neutral.** Every ensure skip names the fact it proved (the probes that answered absent, the layout it read); if the tree you can see disproves that reason — a "no Node" skip beside deployed Node scripts, a "not deployed" skip beside a stamped `docs/ai` — do not relay the line as an outcome: raise it in the report as a defect of the tool, with the contradicting fact named, and stop short of any step that would build on the skip.
91
93
  - **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 **six project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (orchestration config, gate declaration, autonomy declaration, enforcement scripts, spec layer, navigator index — 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; for the navigator, relay the **late** `--only index` line, not the early one; 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`).
92
94
  - **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*.
93
95
  - **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).
@@ -19,7 +19,7 @@
19
19
  exists, run ONE command — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile
20
20
  <project>/AGENTS.md` — which reconciles **three** bounded pointers in a single atomic write: the
21
21
  **workflow-methodology** pointer (the plan → execute → review summary), the
22
- **orchestration-recipes** pointer (the Solo / Reviewed / Council / Delegated vocabulary, routing to
22
+ **orchestration-recipes** pointer (the Solo / Reviewed / Council / Delegated / Subagent vocabulary, routing to
23
23
  `/agent-workflow-kit recipes`) right below it, and the **autonomy-policy** pointer (the
24
24
  `docs/ai/autonomy.json` read contract) below that. Each is **one atomic operation per slot**:
25
25
  **ensure the slot exists**
@@ -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.
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.
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 @@ The three setup choices — **visibility** (step 2), **conversational language**
37
37
  10. **Honest `known_issues.md`.** Every bug with a workaround gets Impact + Plan so it isn't re-discovered later.
38
38
  11. **One conversational language.** Talk to the user in the language chosen at bootstrap; keep code, paths, commands, and abbreviations in their source language. See *Communication contract*.
39
39
  12. **Attribution is opt-in.** Honour the *Attribution* block: by default no agent/AI/model mention anywhere (commits, PRs, code, comments, docs), and no `Co-Authored-By` trailer. See *Attribution contract*.
40
- 13. **Orchestrate via a named recipe.** Compose execution through a named recipe (Solo / Reviewed / Council / Delegated) and **always commit yourself** — backends are advisory or delegated, never autonomous. Encoded via the reconciled `workflow:orchestration` pointer (it routes to `/agent-workflow-kit recipes` + the engine canon), not by bloating the entry point.
40
+ 13. **Orchestrate via a named recipe.** Compose execution through a named recipe (Solo / Reviewed / Council / Delegated / Subagent) and **always commit yourself** — every other carrier is advisory or delegated, never autonomous. Encoded via the reconciled `workflow:orchestration` pointer (it routes to `/agent-workflow-kit recipes` + the engine canon), not by bloating the entry point.
41
41
 
42
42
  ---
43
43
 
@@ -23,7 +23,7 @@ Read in order, then confirm before starting:
23
23
  4. Confirm with the user: *"I'm taking task X. Confirm?"*
24
24
 
25
25
  ### 1.2. During Work
26
- **Before any feature:** name its governing spec(s) — the feature spec under `docs/ai/specs/` for every touched spec-covered slice (zero is legal during adoption; each cited spec's Out of scope bounds that slice's work). Where only a page spec (`docs/ai/pages/<page>.md`) exists it governs as an ADOPTION SHIM: state Out of scope + Revision inline in the plan. If the contract changes, the spec revision is authored WITH the plan (visible at review) and lands with the code, so docs and code never diverge.
26
+ **Before any feature:** name its governing spec(s) — the feature spec under `docs/ai/specs/` for every touched spec-covered slice (a zero names the adoption state it relies on — not adopted, adopting, or nothing spec-covered touched; each cited spec's Out of scope bounds that slice's work). Where only a page spec (`docs/ai/pages/<page>.md`) exists it governs as an ADOPTION SHIM: state Out of scope + Revision inline in the plan. If the contract changes, the spec revision is authored WITH the plan (visible at review) and lands with the code, so docs and code never diverge.
27
27
 
28
28
  **For every code change:**
29
29
  1. Grep for similar implementations — reuse existing patterns.
@@ -72,13 +72,14 @@ Apply this as part of §2 before any user-facing summary:
72
72
  - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
73
73
  - **Live host/session facts are tool-composed only.** Any claim 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**; 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.
74
74
  - **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.
75
+ - **A skip that contradicts the tree is a finding.** A tool-composed `skipped-*` line whose stated reason the observed tree disproves (a "no Node" skip beside deployed Node scripts) is raised as a FINDING in the report, never pasted as a neutral outcome — and a tool may not emit a skip whose reason it could itself disprove.
75
76
 
76
77
  ### 2.6. Planning, review & process-fidelity invariants
77
78
  Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
78
79
  - **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
79
80
  - **Finding scope (plan-execution) — name the invariant BEFORE the edit.** During EXECUTION only — a plan under authoring has no shipped behaviour to call a live defect in, so plan-review carries none of this. Every finding names the invariant its fix would enforce, and where that invariant already lives decides the disposition: already an acceptance criterion of the phase → **fold here**; it would have to be ADDED → ship the **narrow fix** for the found site (red first, then green) and queue ONLY the generalization — a deferral row carries the invariant, the origin `file:line`, the narrow fix, its proof and a residual exposure declared NOT live; no correct narrow fix → **blocking**: the phase does not close, and it is **never queued**. Two bars declared before each round: a finding counts only if it changes a **WRITE/REMOVE decision** or is a false statement in shipped text; a repeat finding in one subarea **routes to SUBTRACTION**, not a fourth patch.
80
81
  - **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
81
- - **Spec-first.** A plan names its GOVERNING spec(s) — zero, one or many, one per touched spec-covered slice (the feature spec under `docs/ai/specs/`; page-only coverage governs as an ADOPTION SHIM, with Out of scope + Revision stated inline in the plan). Each cited spec's Out of scope bounds that slice's work and the plan's non-goals restate it per slice — no global union; a cross-spec conflict is resolved by a spec revision BEFORE approval, never by silent precedence. A NEW feature's draft spec exists AT plan review (a `create` row); a change to a governed contract rides the plan as its proposed revision (a `modify` row); approval confirms plan and contract atomically, and the revision lands with the code. Scenario bindings are per scenario: a new scenario is `unbound` until its test lands in the same plan, and a status never regresses for an extension.
82
+ - **Spec-first.** A plan names its GOVERNING spec(s) — zero, one or many, one per touched spec-covered slice (the feature spec under `docs/ai/specs/`; page-only coverage governs as an ADOPTION SHIM, with Out of scope + Revision stated inline in the plan). A ZERO names the adoption state it relies on — `not adopted` (no store) or `adopting` (a store with no live contract), either with a recorded decline, or `nothing spec-covered touched` (a store with live contracts) — a bare zero is never a licence; the store's own state is what `status` and the upgrade advisor report. Each cited spec's Out of scope bounds that slice's work and the plan's non-goals restate it per slice — no global union; a cross-spec conflict is resolved by a spec revision BEFORE approval, never by silent precedence. A NEW feature's draft spec exists AT plan review (a `create` row); a change to a governed contract rides the plan as its proposed revision (a `modify` row); approval confirms plan and contract atomically, and the revision lands with the code. Scenario bindings are per scenario: a new scenario is `unbound` until its test lands in the same plan, and a status never regresses for an extension.
82
83
  - **No code-mechanics in the plan.** A ledger row carries its path and anchor, and Verification carries the exact commands (the plan-shape canon) — checked syntax: the plan's own Verification runs them against an explicit expected outcome or gate; the only other syntax a plan may carry is a literal fixture/schema fragment a named test copies or validates. Un-run, logic-bearing syntax — control-flow, a regex, a glob, a grammar, an algorithm body, a mini-DSL — never lives in plan prose, however plausible or shell-verified it looks: a fold or draft that wants one is the trigger to write the test instead.
83
84
  - **Test-as-spec.** Fold a code-touching finding into a red→green TEST, not a prose paragraph — the gate is the only deterministic checker; a paragraph cannot self-check.
84
85
  - **Characterize-first.** Before editing UNCOVERED code, pin its current behavior in a green test, then edit — any unintended change goes red. Never edit what has no checker; first give it one. Keep edits atomic/reversible; prefer SUBTRACTIVE folds.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Each activity is configured independently (e.g. plan-authoring, plan-execution), and so is each slot within it. A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | delegated (you implement / a backend runs a bounded sub-task). The default below is 'solo' everywhere — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. Remove a slot's line (or run `set-recipe --unset <activity>.<slot>`) to fall back to the computed default (reviewed when a review backend is ready, otherwise solo). Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
2
+ "_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Three activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, review), 'plan-execution' (slots execute, review) and 'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is 'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
3
3
  "plan-authoring": {
4
4
  "review": "solo"
5
5
  },
@@ -0,0 +1,57 @@
1
+ // ack-store.mjs — the family-owned neutral acknowledgement store: its path, its closed lane->key registry,
2
+ // the one fact fingerprint and the one guarded reader. Contract: docs/ai/specs/kit/ack-store.md.
3
+ // A READ-ONLY leaf (the writer is ack-write.mjs). Dependency-free, Node >= 22; no side effects on import.
4
+
5
+ import { createHash } from 'node:crypto';
6
+ import { lstatSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { assertContainedRealPath } from './fs-safe.mjs';
9
+ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
10
+
11
+ export const ACKS_FILE = 'docs/ai/acks.json';
12
+ export const ACKS_LANE_KEY = 'sandboxLaneAck';
13
+ export const ACKS_WORKTREES_DIR_KEY = 'worktreesDirAck';
14
+ export const ACKS_COVERAGE_DOMAIN_KEY = 'coverageDomainAck';
15
+ export const ACKS_SOURCE_SIZE_COPY_KEY = 'sourceSizeCopyAck';
16
+ export const ACKS_SPEC_ADOPTION_KEY = 'specAdoptionAck';
17
+
18
+ // The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's command line ->
19
+ // the store key that writer sets. A lane the registry does not name is a usage refusal at the writer.
20
+ export const ACK_LANES = Object.freeze({
21
+ 'sandbox-lane': ACKS_LANE_KEY,
22
+ 'worktrees-dir': ACKS_WORKTREES_DIR_KEY,
23
+ 'coverage-domain': ACKS_COVERAGE_DOMAIN_KEY,
24
+ 'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
25
+ 'spec-adoption': ACKS_SPEC_ADOPTION_KEY,
26
+ });
27
+
28
+ export const FINGERPRINT_LENGTH = 16;
29
+
30
+ // The one fingerprint over an acknowledged FACT (a canonical string the caller composed).
31
+ export const factFingerprint = (fact) => createHash('sha256').update(fact).digest('hex').slice(0, FINGERPRINT_LENGTH);
32
+
33
+ // readAckValue(root, deps, key) -> the recorded string at `key`, or null for the not-yet-acked states
34
+ // (an absent file or docs/ai, a non-string value). The path chain is guarded no-follow and the leaf is
35
+ // read descriptor-bound (`deps.nofollow` injects that door), so a leaf swapped after the guard cannot
36
+ // change the bytes read. A symlinked ancestor/leaf, an escape, a non-regular target, an IO error, a
37
+ // malformed or non-object store all THROW (the caller's stated-skip lane).
38
+ export const readAckValue = (root, deps = {}, ackKey) => {
39
+ const lstat = deps.lstat ?? lstatSync;
40
+ const absPath = join(root, ACKS_FILE);
41
+ try {
42
+ assertContainedRealPath(root, absPath, { lstat });
43
+ } catch (err) {
44
+ if (err?.code === 'ENOENT') return null;
45
+ throw err;
46
+ }
47
+ const read = readRegularFileNoFollow(absPath, deps.nofollow ?? {});
48
+ if (read.outcome === 'absent') return null;
49
+ if (read.outcome === 'foreign') throw new Error(`${ACKS_FILE} is a ${read.className}, not a regular file — refusing to read it`);
50
+ if (read.outcome !== 'ok') throw new Error(`${ACKS_FILE} cannot be read (${read.code})`);
51
+ const parsed = JSON.parse(read.content);
52
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
53
+ throw new Error(`${ACKS_FILE}: expected a JSON object`);
54
+ }
55
+ const value = parsed[ackKey];
56
+ return typeof value === 'string' ? value : null;
57
+ };
@@ -29,7 +29,7 @@ import { lstatSync, readFileSync } from 'node:fs';
29
29
  import { dirname, join, resolve } from 'node:path';
30
30
  import { fileURLToPath } from 'node:url';
31
31
  import { isDirectRun } from './direct-run.mjs';
32
- import { ACKS_FILE, ACK_LANES } from './recommendations.mjs';
32
+ import { ACKS_FILE, ACK_LANES } from './ack-store.mjs';
33
33
  import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
34
34
  import { shellQuoteArg } from './review-state.mjs';
35
35
 
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { readFileSync, lstatSync } from 'node:fs';
22
22
  import { join } from 'node:path';
23
- import { ACTIVITIES } from './recipes.mjs';
23
+ import { POLICY_ACTIVITIES as ACTIVITIES } from './recipes.mjs';
24
24
 
25
25
  // The hand-editable / agent-writable, per-project policy (strict JSON). cwd-relative — the error prefix
26
26
  // uses this rel path so a user sees a path they can open, never an absolute temp/host path.
@@ -0,0 +1,140 @@
1
+ // carriers.mjs — the activity/slot registry plus the subagent carrier's readiness composition.
2
+ // ACTIVITIES + SLOT_RECIPES are the ONE table: recipes.mjs re-exports them, so no importer's path
3
+ // moves. The carrier half declares the `subagent` recipe, appends the executor vehicle to a
4
+ // readiness array as the single provider of the `carry` role, words the degrade a missing or
5
+ // unusable vehicle causes, and holds the dispatch-form wording every render prints (one source, a
6
+ // red line where it is a red line). Governing contract: docs/ai/specs/kit/carriers.md.
7
+ // Leaf — imports only the direct-run guard (recipes.mjs imports THIS), no fs, nothing on import.
8
+ import { refuseDirectRun } from './direct-run.mjs';
9
+
10
+ // The slot VALUE is the slot's TYPE; SLOT_RECIPES lists the values each type accepts. A `switch`
11
+ // slot is a flag, not a recipe: it resolves outside the recipe lattice and never degrades.
12
+ // `policy` marks an activity that is a SESSION with an autonomy level of its own; a routine chore
13
+ // runs inside such a session and carries none.
14
+ export const ACTIVITIES = {
15
+ 'plan-authoring': { slots: { author: 'carrier', review: 'review' }, policy: true },
16
+ 'plan-execution': { slots: { execute: 'execute', review: 'review' }, policy: true },
17
+ routine: { slots: { carrier: 'carrier', parallel: 'switch' }, policy: false },
18
+ };
19
+
20
+ export const POLICY_ACTIVITIES = Object.fromEntries(
21
+ Object.entries(ACTIVITIES).filter(([, activity]) => activity.policy),
22
+ );
23
+
24
+ export const SLOT_RECIPES = {
25
+ review: ['solo', 'reviewed', 'council'],
26
+ execute: ['solo', 'delegated', 'subagent'],
27
+ carrier: ['solo', 'subagent'],
28
+ switch: ['on', 'off'],
29
+ };
30
+
31
+ export const SWITCH_SLOT = 'switch';
32
+ export const SWITCH_DEFAULT = 'on';
33
+
34
+ export const isSwitchSlot = (slotType) => slotType === SWITCH_SLOT;
35
+
36
+ export const CARRY_ROLE = 'carry';
37
+ export const EXECUTOR_PROVIDER = 'executor';
38
+
39
+ export const SUBAGENT_RECIPE = {
40
+ id: 'subagent',
41
+ title: 'Subagent',
42
+ role: CARRY_ROLE,
43
+ minBackends: 1,
44
+ degradesTo: 'solo',
45
+ summary:
46
+ 'a full-tool frontier subagent from the placed executor vehicle carries a bounded, file-disjoint slice; the orchestrator verifies it and commits.',
47
+ };
48
+
49
+ // The readiness tokens the planner judges on. Mirrored from detect-backends.mjs rather than
50
+ // imported: this module is the leaf the planner itself imports.
51
+ const READY = 'ready';
52
+ const MISSING = 'missing';
53
+
54
+ const VEHICLE_READY_STATES = ['placed', 'customized'];
55
+
56
+ // withVehicle(readiness, survey) → a NEW array carrying the executor as the ONE provider of `carry`.
57
+ // Every caller that hands readiness to the resolver composes it this way; a role-filtered readiness
58
+ // computation is what keeps a placed executor from counting as a ready reviewer.
59
+ export const withVehicle = (readiness = [], survey = null) => [
60
+ ...readiness.filter((entry) => entry?.name !== EXECUTOR_PROVIDER),
61
+ {
62
+ name: EXECUTOR_PROVIDER,
63
+ readiness: VEHICLE_READY_STATES.includes(survey?.state) ? READY : MISSING,
64
+ vehicle: survey,
65
+ },
66
+ ];
67
+
68
+ export const EXECUTOR_APPLY = '/agent-workflow-kit agents';
69
+
70
+ // A survey reason may quote a file the user wrote; it is collapsed to one safe line before it rides
71
+ // the one-line render contracts: no escape sequences, control bytes or line breaks, and none of the
72
+ // characters those renders use as cell structure (the separator, parentheses, the equals sign).
73
+ export const safeLine = (text) => String(text ?? '')
74
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/gu, '')
75
+ .replace(/[·()=]/gu, ' ')
76
+ .replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\s]+/gu, ' ')
77
+ .trim();
78
+
79
+ export const vehicleDegradeReason = (survey, applyHint = EXECUTOR_APPLY) => {
80
+ const state = survey?.state ?? MISSING;
81
+ const reason = safeLine(survey?.reason);
82
+ const detail = reason ? ` (${reason})` : '';
83
+ const remedy = state === 'unusable'
84
+ ? `fix or remove ${safeLine(survey?.rel) || '.claude/agents/executor.md'}, then place it with: ${applyHint}`
85
+ : `place it with: ${applyHint}`;
86
+ return `the executor vehicle is ${state}${detail} — ${remedy}`;
87
+ };
88
+
89
+ // ── the dispatch form: the ONE wording source every render prints ───────────────────
90
+ // Pure constants. `procedures.mjs` prints them for a slot resolved to `subagent`; the wording is a
91
+ // red line, so a render composes these strings and never re-words one.
92
+
93
+ // The slice noun is per ACTIVITY — what a bounded slice IS differs for execution, authoring and a
94
+ // routine chore, while the four dispatch lines below are shared by all three.
95
+ export const SLICE_BY_ACTIVITY = {
96
+ 'plan-authoring': 'a slice is a brief naming the goal, the governing spec(s) and the ledger constraints; the subagent drafts the plan or the contract from it, and the orchestrator reviews the draft as its own',
97
+ 'plan-execution': 'a slice is a set of file-disjoint ledger rows; wording is copied verbatim where wording is a red line',
98
+ routine: "a slice is a bounded mechanical task; a read-only one (a sweep, gate triage) rides its placed read-only vehicle, or is carried solo with a stated reason when that vehicle is absent; a write-capable one (a regeneration, a fixture build) rides the executor; the changelog stays the orchestrator's",
99
+ };
100
+
101
+ export const VEHICLE_STATE_TOKEN = '<state>';
102
+
103
+ export const DISPATCH_LINES = [
104
+ `dispatch: the executor vehicle (.claude/agents/executor.md — ${VEHICLE_STATE_TOKEN}), in the background`,
105
+ 'the orchestrator verifies every returned slice by running its suites itself',
106
+ 'the subagent is never told to commit, never a review backend, never a bridge substitute',
107
+ 'honest limit: a Claude Code lane — on a host that cannot dispatch the vehicle, follow this form by hand and say so',
108
+ ];
109
+
110
+ export const PARALLEL_SOLO_NOTE = '(no effect while the carrier is solo)';
111
+
112
+ export const PARALLEL_LINES = {
113
+ on: 'parallel: on — file-disjoint slices dispatch concurrently',
114
+ off: 'parallel: off — one slice at a time',
115
+ };
116
+
117
+ // The slot TYPES a subagent can carry — computed from the one value table, never a second list.
118
+ export const SUBAGENT_SLOT_TYPES = Object.entries(SLOT_RECIPES)
119
+ .filter(([, values]) => values.includes(SUBAGENT_RECIPE.id))
120
+ .map(([slotType]) => slotType);
121
+
122
+ // dispatchForm({ activity, slot, state }) → the lines a `subagent`-resolved slot renders: the
123
+ // activity's slice sentence, then the four shared lines with the surveyed vehicle state filled in.
124
+ // A slot whose type cannot hold `subagent` (a review slot) and an unknown activity render nothing.
125
+ export const dispatchForm = ({ activity, slot, state } = {}) => {
126
+ const slice = SLICE_BY_ACTIVITY[activity];
127
+ const slotType = ACTIVITIES[activity]?.slots?.[slot];
128
+ if (!slice || !SUBAGENT_SLOT_TYPES.includes(slotType)) return [];
129
+ return [slice, ...DISPATCH_LINES.map((line) => line.replace(VEHICLE_STATE_TOKEN, state ?? MISSING))];
130
+ };
131
+
132
+ // parallelLine({ value, carrier }) → the `routine` switch line. The concurrency claim is TRUE only
133
+ // while the effective carrier is `subagent`; under a solo carrier the flag states its own inertness.
134
+ export const parallelLine = ({ value, carrier } = {}) => {
135
+ const inert = carrier !== SUBAGENT_RECIPE.id ? ` ${PARALLEL_SOLO_NOTE}` : '';
136
+ if (value === 'on') return inert ? `parallel: on${inert}` : PARALLEL_LINES.on;
137
+ return `${PARALLEL_LINES.off}${inert}`;
138
+ };
139
+
140
+ refuseDirectRun(import.meta.url);
@@ -0,0 +1,172 @@
1
+ // cheap-agents-read.mjs — the READ-ONLY core of the subagent-vehicle surface: the bundle, the
2
+ // placement plan and the executor vehicle's readiness. Split from cheap-agents.mjs (the writer) so
3
+ // the read-only advisor graph (procedures -> recipes) reaches these facts WITHOUT importing a module
4
+ // that can create `.claude/agents/` — by construction, pinned by the read-graph purity walk.
5
+
6
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { refuseDirectRun } from './direct-run.mjs';
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+
13
+ export const AGENTS_DIR = '.claude/agents';
14
+ export const CLAUDE_DIR = '.claude';
15
+ export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
16
+ export const EXPECTED_WORKFLOW_VERSION = '3.0.0';
17
+ export const BUNDLED_AGENTS_DIR = resolve(HERE, '..', 'references', 'agents');
18
+
19
+ export const UTF8 = 'utf8';
20
+ const ERROR_PREFIX = '[agent-workflow-kit]';
21
+ const EXIT_PRECONDITION = 1;
22
+
23
+ export const CHEAP_AGENTS_STAMP = 'CHEAP_AGENTS_STAMP';
24
+ export const CHEAP_AGENTS_SYMLINK = 'CHEAP_AGENTS_SYMLINK';
25
+ export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
26
+
27
+ export const makeCheapAgentsError = (code, message) =>
28
+ Object.assign(new Error(`${ERROR_PREFIX} ${message}`), { name: 'CheapAgentsError', code, exitCode: EXIT_PRECONDITION });
29
+
30
+ // The injectable READ surface — four functions, none of which can create or modify a file. The
31
+ // writer shell adds its own mkdir/writeFile pair rather than widening this one.
32
+ export const readFsDeps = (deps = {}) => ({
33
+ exists: deps.exists ?? existsSync,
34
+ lstat: deps.lstat ?? lstatSync,
35
+ readFile: deps.readFile ?? readFileSync,
36
+ readdir: deps.readdir ?? readdirSync,
37
+ });
38
+
39
+ export const lstatNoFollow = (absPath, fs) => {
40
+ try {
41
+ return fs.lstat(absPath);
42
+ } catch (err) {
43
+ if (err && err.code === 'ENOENT') return null;
44
+ throw err;
45
+ }
46
+ };
47
+
48
+ // ── the bundle (the kit's own references/agents/) ─────────────────────────────────────
49
+
50
+ export const readBundledAgents = (deps = {}) => {
51
+ const fs = readFsDeps(deps);
52
+ const bundleDir = deps.bundleDir ?? BUNDLED_AGENTS_DIR;
53
+ let names;
54
+ try {
55
+ names = fs.readdir(bundleDir);
56
+ } catch (err) {
57
+ throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `bundled agents dir unreadable (${err.code ?? err.message}): ${bundleDir}`);
58
+ }
59
+ const templates = names
60
+ .filter((name) => name.endsWith('.md'))
61
+ .sort()
62
+ .map((name) => ({ name, content: fs.readFile(join(bundleDir, name), UTF8) }));
63
+ if (templates.length === 0) {
64
+ throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `no bundled agent templates found in ${bundleDir} — the kit install is incomplete`);
65
+ }
66
+ return templates;
67
+ };
68
+
69
+ // ── the preflight reads (velocity discipline: symlink-safe, stamp read, no writes) ────
70
+
71
+ export const readStamp = (absPath, fs) => {
72
+ try {
73
+ if (!fs.exists(absPath)) return null;
74
+ const stamp = String(fs.readFile(absPath, UTF8)).trim();
75
+ return stamp.length ? stamp : null;
76
+ } catch {
77
+ return null; // unreadable stamp == not a valid deployment stamp (apply STOPs; dry-run reports)
78
+ }
79
+ };
80
+
81
+ export const assertDirSafe = (absPath, relPath, fs) => {
82
+ const stat = lstatNoFollow(absPath, fs);
83
+ if (stat === null) return { absent: true };
84
+ if (stat.isSymbolicLink()) throw makeCheapAgentsError(CHEAP_AGENTS_SYMLINK, `${relPath} is a symlink — refusing to write through it`);
85
+ if (!stat.isDirectory()) throw makeCheapAgentsError(CHEAP_AGENTS_SYMLINK, `${relPath} exists but is not a directory — refusing to write through it`);
86
+ return { absent: false };
87
+ };
88
+
89
+ // Per-template placement plan: place | already-current | customized-preserved (never clobbered).
90
+ export const planPlacement = (templates, projectDir, deps = {}) => {
91
+ const fs = readFsDeps(deps);
92
+ return templates.map((template) => {
93
+ const rel = `${AGENTS_DIR}/${template.name}`;
94
+ const abs = join(projectDir, AGENTS_DIR, template.name);
95
+ const stat = lstatNoFollow(abs, fs);
96
+ if (stat === null) return { ...template, rel, abs, action: 'place' };
97
+ if (stat.isSymbolicLink() || !stat.isFile()) {
98
+ throw makeCheapAgentsError(CHEAP_AGENTS_SYMLINK, `${rel} exists but is not a regular file — refusing to touch it`);
99
+ }
100
+ const existing = fs.readFile(abs, UTF8);
101
+ if (existing === template.content) return { ...template, rel, abs, action: 'already-current' };
102
+ return { ...template, rel, abs, action: 'customized-preserved', existing };
103
+ });
104
+ };
105
+
106
+ // ── the executor vehicle's readiness (the subagent carrier's one instrument) ──────────
107
+
108
+ export const EXECUTOR_VEHICLE = 'executor.md';
109
+ export const EXECUTOR_VEHICLE_REL = `${AGENTS_DIR}/${EXECUTOR_VEHICLE}`;
110
+
111
+ // The YAML subset a vehicle's frontmatter is read with: a bare scalar, a single- or double-quoted
112
+ // scalar, or a flow sequence; an unquoted ` #comment` and surrounding whitespace are dropped first.
113
+ const cleanValue = (raw) => {
114
+ const noComment = String(raw ?? '').replace(/^((?:[^"'#]|"[^"]*"|'[^']*')*?)\s+#.*$/u, '$1').trim();
115
+ return noComment.replace(/^(["'])(.*)\1$/u, '$2').replace(/^\[(.*)\]$/u, '$1').trim();
116
+ };
117
+
118
+ // The block-sequence items under the `tools:` key: `- item` lines indented deeper than the key,
119
+ // with blank and comment lines allowed between them; the first other line ends the list.
120
+ const blockItems = (frontmatter) => {
121
+ const lines = frontmatter.split('\n');
122
+ const start = lines.findIndex((line) => /^tools:/u.test(line));
123
+ if (start === -1) return [];
124
+ const keyIndent = lines[start].match(/^[ \t]*/u)[0].length;
125
+ const items = [];
126
+ for (const line of lines.slice(start + 1)) {
127
+ if (/^[ \t]*(#.*)?$/u.test(line)) continue;
128
+ const item = line.match(/^([ \t]*)-[ \t]*(.*)$/u);
129
+ if (!item || item[1].length <= keyIndent) break;
130
+ const value = cleanValue(item[2]);
131
+ if (value) items.push(value);
132
+ }
133
+ return items;
134
+ };
135
+
136
+ const executorFrontmatterRefusal = (content) => {
137
+ const frontmatter = String(content).replace(/\r\n/gu, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/u)?.[1] ?? '';
138
+ for (const key of ['name', 'tools']) {
139
+ if ((frontmatter.match(new RegExp(`^${key}:`, 'gmu')) ?? []).length > 1) return `duplicate \`${key}:\` key in the frontmatter`;
140
+ }
141
+ if (cleanValue(frontmatter.match(/^name:(.*)$/mu)?.[1]) !== 'executor') return 'frontmatter does not declare `name: executor`';
142
+ if (!/^tools:/mu.test(frontmatter)) return null;
143
+ const inline = cleanValue(frontmatter.match(/^tools:(.*)$/mu)?.[1]);
144
+ const listed = inline || blockItems(frontmatter).join(', ');
145
+ if (!listed) return 'tools: is empty — grant a list that includes Bash, or drop the line';
146
+ const granted = listed.split(',').map((tool) => cleanValue(tool));
147
+ return granted.includes('Bash') ? null : `tools: ${listed} is read-only`;
148
+ };
149
+
150
+ // A symlinked, non-regular or unreadable vehicle is a STATE the carrier degrades on, so this survey
151
+ // answers it instead of throwing: its callers compose the answer into a readiness array.
152
+ export const surveyExecutorVehicle = (projectDir, deps = {}) => {
153
+ const rel = EXECUTOR_VEHICLE_REL;
154
+ try {
155
+ const template = readBundledAgents(deps).find((item) => item.name === EXECUTOR_VEHICLE);
156
+ if (!template) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
157
+ const fs = readFsDeps(deps);
158
+ assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
159
+ assertDirSafe(join(projectDir, AGENTS_DIR), AGENTS_DIR, fs);
160
+ const [placement] = planPlacement([template], projectDir, deps);
161
+ if (placement.action === 'place') return { state: 'missing', reason: null, rel };
162
+ if (placement.action === 'already-current') return { state: 'placed', reason: null, rel };
163
+ const refusal = executorFrontmatterRefusal(placement.existing);
164
+ return refusal === null
165
+ ? { state: 'customized', reason: null, rel }
166
+ : { state: 'unusable', reason: refusal, rel };
167
+ } catch (err) {
168
+ return { state: 'unusable', reason: err?.message ?? String(err), rel };
169
+ }
170
+ };
171
+
172
+ refuseDirectRun(import.meta.url);