@sabaiway/agent-workflow-kit 14.37.0 → 14.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.38.0 — the upgrade and bootstrap lay the project-layout rules into your project and keep the file agents read current; with your own local rules the agent does the merge (AD-190)
8
+
9
+ **`/agent-workflow-kit upgrade` and bootstrap now write the project-layout rules into `docs/ai/rules/`, keep
10
+ `docs/ai/rules/effective/project-layout.md` a copy of them, and record what it was built from, so
11
+ `rules-cli.mjs --check` answers `current`.** Where you keep your own rules in `docs/ai/rules/local/`, no code merges
12
+ them: the upgrade prints the merge the agent does and the command that records it.
13
+
14
+ - **A ninth upgrade ensure, `rules`,** runs after the spec layer and before the navigator, so the navigator lists the
15
+ files it lays. Its outcome is one of `seeded`, `refreshed`, `regenerated`, `pending`, `customized-preserved` or
16
+ `already-current`, or `failed` with its cause; `--dry-run` answers the `would-` form and writes nothing. Bootstrap
17
+ runs it as `ensure-configs.mjs --reconcile --only rules` in step 10, before the navigator re-run.
18
+ - **The kit file `docs/ai/rules/<theme>.md`** is created when absent, kept when it is the bundled template, refreshed
19
+ when it holds a body an earlier release shipped, and kept byte for byte when you edited it: the line then says your
20
+ own rules belong in `docs/ai/rules/local/<theme>.md`, where they win. The catalog of earlier bodies ships empty, so
21
+ nothing is refreshed yet.
22
+ - **With no local file** the effective file is rebuilt as the kit file's copy and its record entry is written; an
23
+ edited kit file with no frontmatter is kept and nothing else is written for that theme.
24
+ - **With a local file** nothing is written to the effective file or the record; until the merge is recorded, the line
25
+ starts `merge needed — `, states how to merge (by meaning, the local file winning a conflict, the frontmatter kept,
26
+ the user shown what changed) and prints the exact `rules-cli.mjs --record` command to run after it.
27
+ - **A `docs/ai/rules/effective/sources.json` that reads as UTF-8 text but is not JSON of the record's schema** is now
28
+ replaced by the record writer instead of stopping it, in `--record` and in the upgrade alike; a directory or a
29
+ symlink on its path still stops it, writing nothing.
30
+
31
+ **Upgrade note.** No step: the next upgrade lays the theme. If it prints `merge needed`, the agent merges, shows you
32
+ what changed and runs the printed command.
33
+
7
34
  ## 14.37.0 — the first project rules theme, project layout, ships with the writer of a record of what each effective rules file was built from and a read-only check; nothing is laid into a project yet (AD-189)
8
35
 
9
36
  **The kit now carries default project rules as thematic prose files, starting with project layout, and a command
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.37.0'
6
+ version: '14.38.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.37.0",
6
+ "version": "14.38.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.37.0",
3
+ "version": "14.38.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -34,8 +34,10 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
34
34
  - A **leftover machine-wide ignore block** from an older deployment is kept and reported (harmless — the project-local rules win); the advisor's `footprint-global` item of step 11 removes it on the user's yes after its check, which prints each line it removes — another of the user's repos may rely on the same machine-wide rules.
35
35
  - Report the result plainly (what is now hidden). **Windows is supported.**
36
36
  - **Do not edit `package.json`** in hidden mode — a tracked change leaks the whole system.
37
- 10. **Stamp the deployment lineage.** **First re-run the step-7 navigator finalizer**
38
- (`--ensure-index`) whenever a later step touched `docs/ai` — the reconciles that follow the
37
+ 10. **Stamp the deployment lineage.** In both paths, delegated and fallback, **first lay down the
38
+ project rules** — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only rules --cwd <project>`,
39
+ relaying its line — **then re-run the step-7 navigator finalizer**
40
+ (`--ensure-index`) whenever a later step touched `docs/ai` — the rules run and the reconciles that follow the
39
41
  template copy do (the agent-rules lens,
40
42
  `${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md`); it is idempotent, so an
41
43
  untouched tree simply reports `already-current`. Then write the **deployment-lineage head** into
@@ -33,21 +33,23 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
33
33
 
34
34
  **`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; **a leftover machine-wide ignore block is kept + reported — the advisor's `footprint-global` item removes it**). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
35
35
 
36
- **`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all eight** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · executor check · 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); a refresh-class op rewrites only what it owns — the outcome tokens by ensure, below, name each arm. 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, autonomy and vehicles → `seeded` / `already-present`; executor → `not-placed` / `already-current` / `pending`; 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` / `customized-preserved`; 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`, `vehicle-settings-unreadable`, `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 — once row 3's replace ask is answered when a specs replace line printed, then as that ask says** — 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 eight reach an equal-head deployment **without a lineage-head bump or a migration file** (all inherently outside the docs cap-validator).
36
+ **`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all nine** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · executor check · enforcement scripts · spec layer · project rules · 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); a refresh-class op rewrites only what it owns — the outcome tokens by ensure, below, name each arm. 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, autonomy and vehicles → `seeded` / `already-present`; executor → `not-placed` / `already-current` / `pending`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node-evidence`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node-evidence`; rules → `seeded` / `refreshed` / `regenerated` / `pending` / `customized-preserved` / `already-current`; index → `regenerated` / `already-current` / `customized-preserved`; 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`, `vehicle-settings-unreadable`, `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 — once row 3's replace ask is answered when a specs replace line printed, then as that ask says** — 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 nine reach an equal-head deployment **without a lineage-head bump or a migration file**.
37
37
 
38
- **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.)
38
+ **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 — `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` decide it (CRLF/whitespace normalized first; 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.)
39
39
 
40
- **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).
40
+ **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 declared gate matrix is authored content; no note-refresh here).
41
41
 
42
42
  **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).
43
43
 
44
44
  **What the executor ensure does.** It never writes: a placed `.claude/agents/executor.md` whose bytes differ from the body derived from `docs/ai/vehicles.json` is `pending` — the advisor's `executor-vehicle.differing` item re-derives it on the user's yes — and an absent one is `not-placed` (the agents mode places it on consent).
45
45
 
46
- **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 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 (the `enforcement` check line and apply, by the consent route, copy nothing and re-run the installer, which refuses 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.
46
+ **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`, writing nothing. 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; only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the 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 deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook is next refreshed (the `enforcement` check line and apply, by the consent route, copy nothing and re-run the installer, which refuses a non-marker hook as always); an OLD hook without the line is safe — the decisions gate is simply not enforced yet.
47
47
 
48
- **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); under a hook module that is not the bundled one, a write the project's own checker rejects is put back → `customized-preserved`. 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: an untouched tree changes nothing.
48
+ **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 a Node project's hook fails its 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); under a hook module that is not the bundled one, a write the project's own checker rejects is put back → `customized-preserved`. 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: an untouched tree changes nothing.
49
49
 
50
- **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`; edited files are replaced only on row 3's yes. 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 the agent records), `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.
50
+ **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`; edited files are replaced only on row 3's yes. 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`). 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 the agent records), `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.
51
+
52
+ **What the project-rules ensure does.** Each theme's kit file `docs/ai/rules/<theme>.md` is seeded, refreshed only from a body a release shipped (an edit is kept → `customized-preserved`), and with no local file its effective copy and record are rebuilt. With a local file it writes neither: `pending` prints the merge to do and its record command — do the merge, show the user what changed, then run that command.
51
53
 
52
54
  **`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. Of step 3's run-list, only this migration and `tools/checker-gates.mjs` under the `tier` ask change an existing gates.json (the FILL preview runs at init).
53
55
 
@@ -111,7 +113,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
111
113
  **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 changes nothing, 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.
112
114
  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:
113
115
  - **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.
114
- - **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 **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and 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) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **full-flow offer** (`tier`) lines; 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). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
116
+ - **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 **nine project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and 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) and the **story-sessions section** outcome (its own set: refreshed / already current / custom edit preserved + note / section absent — noted / over the file's line cap — refused); the **full-flow offer** (`tier`) lines; 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). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
115
117
  - **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*.
116
118
  - **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 route: the user picks items in plain language; surface each picked item's posture note and get the explicit confirm; run its lane's rendered lines — chat: the one-liner; harness: the check line, then on the yes to its result the apply line; user: the user's own step — per the full consent route 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).
117
119
  - **Recommendations lanes — both exits.** The three lanes (`chat`, `harness`, `user`) and the consent route live in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`; `init` stays a route the user may take.
@@ -121,4 +123,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
121
123
  5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
122
124
  6. **Collect the migration answers FIRST, then apply.** Run `node ${CLAUDE_SKILL_DIR}/tools/migration-notes.mjs --cwd <project>` and relay its stdout lines verbatim, in order; a non-zero exit relays its one named line and STOPs, except `stamp-absent`, which routes back to step 1. Then run the preview `node ${CLAUDE_SKILL_DIR}/tools/migration-blocks.mjs --cwd <project>`. If its lines say both blocks are missing (pre-1.1.0), 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; a single missing block keeps its single ask (step 7). Then apply exactly the notes the tool listed, in its order ("only those newer than the project's stamp" is why, never a second selection). Migrations are **idempotent** — safe to re-run.
123
125
  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. For a block the preview names as needing its answer, **ask the user their conversational language** (`--language`) or **whether the agent may attribute work to itself / AI** (`--attribution`, defaulting to `off`), never re-asking a step-6 answer; then re-run the preview with `--apply` and the answers, relaying its lines. Exit 1 `answer-missing`: ask exactly what its lines name and re-run `--apply` once; a second one STOPs. Any other exit 1 is a named refusal: relay it with its recovery (`cap`: trim `AGENTS.md`, re-run) and STOP before the finalizer and the stamp. **Then, before the stamp, re-run the LATE navigator finalizer:** the migrations of steps 6–7 may have changed `docs/ai` files, so run the one op again — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>` — and relay THAT line in the step-8 report (idempotent: an untouched tree changes nothing; a failure STOPs this upgrade like any other ensure).
124
- 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`4.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 **eight project-configuration ensure** (`configs`) lines (enumerated in step 3) — the navigator one being the late `--only index` run at the end of step 7, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** + **story-sessions section** outcomes (same outcome sets as step 4), the **full-flow offer** (`tier`) outcome, 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 route (per picked item: posture note → explicit confirm → chat: the one-liner; harness: the check line, then on the yes to its result the apply line; user: the user's own step — `${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.
126
+ 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`4.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 **nine project-configuration ensure** (`configs`) lines (enumerated in step 3) — the navigator one being the late `--only index` run at the end of step 7, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** + **story-sessions section** outcomes (same outcome sets as step 4), the **full-flow offer** (`tier`) outcome, 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 route (per picked item: posture note → explicit confirm → chat: the one-liner; harness: the check line, then on the yes to its result the apply line; user: the user's own step — `${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.
@@ -10,6 +10,9 @@
10
10
  // specs scripts/<spec layer> reader + checker pairs seeded when absent, refreshed only on
11
11
  // + docs/ai/specs/index.md a shipped prior body, an edited one replaced only on the
12
12
  // user's yes; store root seeded behind a current checker
13
+ // rules docs/ai/rules/<theme>.md the kit file seeded, or refreshed on a shipped prior body; with
14
+ // + effective/ + sources.json no local file the effective copy and its record rebuilt; with
15
+ // one the merge an agent does is named, never done
13
16
  // index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact); under
14
17
  // a project hook module that is not the bundled one, a write
15
18
  // the project's own checker rejects is put back
@@ -40,12 +43,13 @@ import { isDirectRun } from './direct-run.mjs';
40
43
  import { ensureExecutor } from './ensure-executor.mjs';
41
44
  import { ENSURE_OPS, OWN_IMPLEMENTATIONS, failedOutcome } from './ensure-ops.mjs';
42
45
  import { ensureSpecs } from './ensure-specs.mjs';
46
+ import { ensureRules } from './project-rules/index.mjs';
43
47
 
44
48
  const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
45
49
 
46
50
  // The op table this CLI walks — name → implementation, in ENSURE_OPS order. Composed HERE (the one
47
- // consumer) because the executor and spec-layer ensures import ensure-ops.mjs for their outcome door and probes.
48
- export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, executor: ensureExecutor, specs: ensureSpecs });
51
+ // consumer) because the executor, spec-layer and rules ensures import ensure-ops.mjs for their outcome door and probes.
52
+ export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, executor: ensureExecutor, specs: ensureSpecs, rules: ensureRules });
49
53
 
50
54
  const EXIT_OK = 0;
51
55
  const EXIT_FAILED = 1;
@@ -63,13 +67,13 @@ const REPLACE_WITH_ONLY = '--replace-custom runs the spec layer and then the nav
63
67
  const REPLACE_WITH_DRY_RUN = "--replace-custom writes on the user's yes, so it cannot be combined with --dry-run — nothing was run";
64
68
  const INDEX_NOT_RUN = 'the navigator step did not run: a replace run runs it only after the spec layer succeeds';
65
69
 
66
- const HELP = `ensure-configs — the eight stamp-independent upgrade ensures, as ONE command.
70
+ const HELP = `ensure-configs — the nine stamp-independent upgrade ensures, as ONE command.
67
71
 
68
72
  Usage:
69
73
  node ensure-configs.mjs --reconcile [--dry-run] [--only <op>] [--cwd <project>]
70
74
  node ensure-configs.mjs --reconcile --replace-custom <digest> [--cwd <project>]
71
75
 
72
- --reconcile required — run the eight ensures (${ENSURE_OPS.join(', ')})
76
+ --reconcile required — run the nine ensures (${ENSURE_OPS.join(', ')})
73
77
  --dry-run report what each ensure WOULD do; write nothing
74
78
  --only <op> run EXACTLY ONE of them (an unknown, missing or repeated value is a usage error)
75
79
  --replace-custom <digest>
@@ -80,11 +84,13 @@ Usage:
80
84
  --help, -h this help
81
85
 
82
86
  Every SEED is CREATE-ONLY: an existing file is preserved byte-for-byte, never clobbered and never
83
- refreshed in place. Three ops refresh instead: the orchestration onboarding note, only while it still
87
+ refreshed in place. Four ops refresh instead: the orchestration onboarding note, only while it still
84
88
  matches a canonical the kit shipped (your own wording is preserved verbatim); the spec-layer reader
85
89
  and checker pairs, only while a file's bytes are a body a release shipped (an edited file is
86
90
  preserved, the writes that depend on it — the spec store root included — are withheld behind it,
87
- and ONE printed replace line replaces every edited file only on the user's yes); and the navigator index — a GENERATED artifact, regenerated
91
+ and ONE printed replace line replaces every edited file only on the user's yes); the project rules
92
+ kit file, only while its bytes are a body a release shipped (an edited one is kept), with the effective
93
+ file rebuilt as its copy and recorded where no local file exists; and the navigator index — a GENERATED artifact, regenerated
88
94
  whenever it is missing or stale; where a module the project's hook loads is not the bundled one,
89
95
  the project's own checker judges the navigator and a write it rejects is put back byte for byte.
90
96
  The executor ensure writes nothing: a placed .claude/agents/executor.md whose bytes differ from the
@@ -11,7 +11,8 @@
11
11
  // `specs` sits after `scripts` (its reader pair is a scripts/ seed too) and BEFORE `index`: the store
12
12
  // root it seeds is a docs/ai file the navigator must count.
13
13
  // `executor` follows `vehicles`: its body is derived from the settings file that op seeds.
14
- export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'vehicles', 'executor', 'scripts', 'specs', 'index']);
14
+ // `rules` follows `specs` and precedes `index`: the rules files it lays are docs/ai files the navigator counts.
15
+ export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'vehicles', 'executor', 'scripts', 'specs', 'rules', 'index']);
15
16
 
16
17
  // Tokens that assert a WRITE happened. --dry-run may never emit one of these (the CLI's contract test
17
18
  // walks this set), and each has exactly one `would-` counterpart below. `refreshed` is the spec-layer
@@ -1,3 +1,4 @@
1
1
  // The one door of the project-rules module. Contract: docs/ai/specs/kit/project-rules/index.md.
2
2
  export { THEMES } from './themes.mjs';
3
3
  export { checkRules } from './rules-check.mjs';
4
+ export { ensureRules } from './rules-ensure.mjs';
@@ -0,0 +1,216 @@
1
+ // The upgrade op `rules`: it lays every registry theme's kit file, keeps the effective file a copy of
2
+ // the kit file where no local file exists, and names the merge an agent does where one does. It never
3
+ // writes a local file and never merges two texts. Contract: docs/ai/specs/kit/project-rules/rules-ensure.md.
4
+
5
+ import { readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { writeDocsAiFileAtomic, writeProjectFileCreateOnly } from '../atomic-write.mjs';
8
+ import { composeFailure, composeOutcome, tmpNote } from '../ensure-ops.mjs';
9
+ import { shellQuoteArg } from '../repo-lex.mjs';
10
+ import { parseFrontmatter } from '../../references/scripts/check-docs-size.mjs';
11
+ import { digestOf, readRecord, readUnder } from './rules-check.mjs';
12
+ import { writeRecord } from './rules-record.mjs';
13
+ import { RULES_PRIORS, isKnownPrior } from './rules-priors.mjs';
14
+ import { RECORD_PATH, THEMES, themePaths } from './themes.mjs';
15
+
16
+ const OP = 'rules';
17
+ const FILE_ROLES = ['kit', 'local', 'effective'];
18
+ const causeOf = (err) => String((err && err.message) || err);
19
+
20
+ // ── the survey: the template and every path read once, nothing written ───────────────────────────
21
+
22
+ const readTemplate = (readFile, path) => {
23
+ try {
24
+ return { bytes: Buffer.from(readFile(path)) };
25
+ } catch (err) {
26
+ return { error: causeOf(err) };
27
+ }
28
+ };
29
+
30
+ const unreadable = (rel, read) => {
31
+ if (read.outcome === 'foreign') {
32
+ return composeFailure(OP, 'wrong-node-kind', `${rel}: ${read.code ?? `it is a ${read.className}, not a regular file`} — nothing was read or written; resolve it by hand, then re-run`);
33
+ }
34
+ if (read.outcome === 'ok' || read.outcome === 'absent') return null;
35
+ return composeFailure(OP, 'unexpected-error', `${rel}: could not be read (${read.code ?? read.outcome}) — nothing was written`);
36
+ };
37
+
38
+ const survey = ({ themes, cwd, kitRoot, readFile, deps }) => {
39
+ const surveyed = [];
40
+ for (const theme of themes) {
41
+ const paths = themePaths(theme.name);
42
+ const template = readTemplate(readFile, join(kitRoot, theme.template));
43
+ if (template.error !== undefined) {
44
+ return { failure: composeFailure(OP, 'template-unreadable', `${paths.kit}: the bundled template ${theme.template} could not be read, so nothing was written — reinstall the kit. ${template.error}`) };
45
+ }
46
+ const reads = Object.fromEntries(FILE_ROLES.map((role) => [role, readUnder(cwd, paths[role], deps)]));
47
+ const failure = FILE_ROLES.map((role) => unreadable(paths[role], reads[role])).find(Boolean);
48
+ if (failure) return { failure };
49
+ surveyed.push({ theme, paths, template: template.bytes, reads });
50
+ }
51
+ const recordRead = readUnder(cwd, RECORD_PATH, deps);
52
+ const failure = unreadable(RECORD_PATH, recordRead);
53
+ return failure ? { failure } : { surveyed, record: readRecord(recordRead, themes) };
54
+ };
55
+
56
+ // ── the decision per theme (pure over the survey) ────────────────────────────────────────────────
57
+
58
+ const classifyKit = (name, kit, template, priors) => {
59
+ if (kit.outcome === 'absent') return 'absent';
60
+ if (kit.bytes.equals(template)) return 'current';
61
+ return isKnownPrior(name, kit.bytes, priors) ? 'prior' : 'custom';
62
+ };
63
+
64
+ const planTheme = ({ theme, paths, template, reads }, record, priors) => {
65
+ const { kit, local, effective } = reads;
66
+ const kitState = classifyKit(theme.name, kit, template, priors);
67
+ const kitBytes = kitState === 'absent' || kitState === 'prior' ? template : kit.bytes;
68
+ const kitWrite = { absent: 'seed', prior: 'refresh' }[kitState] ?? null;
69
+ const entry = record.ok ? record.themes[theme.name] : undefined;
70
+ const base = { name: theme.name, paths, kitState, kitBytes, kitWrite, effectiveWrite: null, recordWrite: false };
71
+ if (local.outcome === 'ok') {
72
+ const merged = entry !== undefined && effective.outcome === 'ok' && entry.kit === digestOf(kitBytes)
73
+ && entry.local === digestOf(local.bytes) && entry.effective === digestOf(effective.bytes);
74
+ return { ...base, mode: merged ? 'merged' : 'pending', localDigest: digestOf(local.bytes) };
75
+ }
76
+ if (kitState === 'custom' && parseFrontmatter(kitBytes.toString('utf8')) === null) return { ...base, mode: 'unvouched' };
77
+ const effectiveWrite = effective.outcome === 'absent' ? 'seed' : effective.bytes.equals(kitBytes) ? null : 'rebuild';
78
+ const passed = { kit: digestOf(kitBytes), local: 'absent', effective: digestOf(kitBytes) };
79
+ const recorded = entry !== undefined && entry.kit === passed.kit && entry.local === 'absent' && entry.effective === passed.effective;
80
+ const droppedEdit = effectiveWrite === 'rebuild' && entry !== undefined && entry.effective !== digestOf(effective.bytes);
81
+ return { ...base, mode: 'copy', effectiveWrite, recordWrite: effectiveWrite !== null || !recorded, passed, droppedEdit };
82
+ };
83
+
84
+ // ── the lines, composed from what happened ───────────────────────────────────────────────────────
85
+
86
+ const KIT_LINES = {
87
+ absent: { written: 'created from the bundled template', stood: 'appeared while this run was creating it and holds the bundled template — it stands', would: 'absent — would be created from the bundled template' },
88
+ prior: { written: 'held the body an earlier release shipped — refreshed to the bundled template', would: 'holds the body an earlier release shipped — would be refreshed to the bundled template' },
89
+ };
90
+ const kitLine = (plan, fate) => {
91
+ const { kit, local } = plan.paths;
92
+ if (plan.kitState === 'current') return `${kit}: already the bundled template — nothing written`;
93
+ if (plan.mode === 'unvouched') return `${kit}: carries your edit with no frontmatter — kept byte for byte, and nothing else was written for this theme, because the record writer cannot vouch for a file without frontmatter; your own rules belong in ${local}, where they win`;
94
+ if (plan.kitState === 'custom') return `${kit}: carries your edit — kept byte for byte; your own rules belong in ${local}, where they win, and the kit's updates to this file wait while it carries the edit`;
95
+ return `${kit}: ${KIT_LINES[plan.kitState][fate]}`;
96
+ };
97
+
98
+ const effectiveLine = (plan, fate) => {
99
+ const { kit, local, effective } = plan.paths;
100
+ const dropped = plan.droppedEdit ? `, dropping an edit made to it; edits belong in ${local}` : '';
101
+ if (plan.effectiveWrite === null) return `${effective}: already the copy of ${kit} — nothing written`;
102
+ if (plan.effectiveWrite === 'seed') {
103
+ return `${effective}: ${{ written: `created as a copy of ${kit}`, stood: `appeared while this run was creating it and holds the copy of ${kit} — it stands`, would: `absent — would be created as a copy of ${kit}` }[fate]}`;
104
+ }
105
+ if (fate === 'would') return `${effective}: differs from ${kit} — would be rebuilt as its copy${dropped}`;
106
+ return `${effective}: ${plan.droppedEdit ? `rebuilt as a copy of ${kit}${dropped}` : `rebuilt from ${kit}`}`;
107
+ };
108
+
109
+ const recordLine = (plan, fate, replacing) => {
110
+ const { effective } = plan.paths;
111
+ if (!plan.recordWrite) return `${RECORD_PATH}: already records what ${effective} was built from — nothing written`;
112
+ const would = fate === 'would';
113
+ if (replacing !== null) {
114
+ return `${RECORD_PATH}: ${would ? 'is' : 'was'} unreadable (${replacing}) — ${would ? 'would be replaced' : 'replaced'} by a record of what ${effective} ${would ? 'is' : 'was'} built from`;
115
+ }
116
+ return `${RECORD_PATH}: ${would ? `would record what ${effective} is built from` : `records what ${effective} was built from`}`;
117
+ };
118
+
119
+ const mergeLine = ({ plan, kitRoot, cwd, dryRun }) => {
120
+ const { kit, local, effective } = plan.paths;
121
+ const cli = shellQuoteArg(join(kitRoot, 'tools', 'project-rules', 'rules-cli.mjs'));
122
+ const command = `node ${cli} --record --theme ${plan.name} --kit ${digestOf(plan.kitBytes)} --local ${plan.localDigest} --effective <the sha256 of the merged file> --cwd ${shellQuoteArg(cwd)}`;
123
+ const holds = dryRun && plan.kitWrite !== null ? ` — this command holds once the real run has written ${kit}` : '';
124
+ return `merge needed — merge ${kit} and ${local} into ${effective}: by meaning, the local file wins a conflict, each rule appears once, only the final meaning is written, with no mark of where a rule came from, the frontmatter is kept, and the user is shown what changed; then record it: ${command}${holds}`;
125
+ };
126
+
127
+ const themeLines = ({ plan, fateOf, kitRoot, cwd, dryRun, replacing }) => {
128
+ const lines = [kitLine(plan, fateOf(plan, 'kit'))];
129
+ if (plan.mode === 'pending') lines.push(mergeLine({ plan, kitRoot, cwd, dryRun }));
130
+ if (plan.mode === 'merged') lines.push(`${plan.paths.effective}: already the recorded merge of ${plan.paths.kit} and ${plan.paths.local} — nothing written`);
131
+ if (plan.mode === 'copy') lines.push(effectiveLine(plan, fateOf(plan, 'effective')), recordLine(plan, fateOf(plan, 'record'), replacing));
132
+ return lines;
133
+ };
134
+
135
+ // ── the token: one per run, the first that holds ─────────────────────────────────────────────────
136
+
137
+ const DRY = { seeded: 'would-seed', refreshed: 'would-refresh', regenerated: 'would-regenerate' };
138
+ const tokenOf = (plans, wrote, dryRun) => {
139
+ if (plans.some((plan) => plan.mode === 'pending')) return 'pending';
140
+ const token = (word) => (dryRun ? DRY[word] : word);
141
+ if (plans.some((plan) => plan.kitWrite === 'seed' && wrote(plan, 'kit'))) return token('seeded');
142
+ if (plans.some((plan) => plan.kitWrite === 'refresh' && wrote(plan, 'kit'))) return token('refreshed');
143
+ if (plans.some((plan) => wrote(plan, 'effective') || wrote(plan, 'record'))) return token('regenerated');
144
+ return plans.some((plan) => plan.kitState === 'custom') ? 'customized-preserved' : 'already-current';
145
+ };
146
+
147
+ // ── the writes, in order; the first that fails stops the run ─────────────────────────────────────
148
+
149
+ const writtenNote = (written) => (written.length > 0 ? `already written by this run and not rolled back: ${written.join(', ')}` : 'nothing was written before it');
150
+ const RACE = 'appeared while this run was creating it and does not hold the bytes this run carried — something is writing there underneath this run; nothing further written, re-run when the tree is settled';
151
+ const NOUN = { noun: 'a project rules file' };
152
+
153
+ const writeOne = ({ cwd, rel, body, kind, deps }) => (kind === 'seed'
154
+ ? writeProjectFileCreateOnly(cwd, rel, body, deps, NOUN)
155
+ : writeDocsAiFileAtomic(cwd, rel, body, deps, NOUN));
156
+
157
+ const runWrites = ({ plans, cwd, deps, fates, notes }) => {
158
+ const written = [];
159
+ const stop = (cause, line) => ({ failure: composeFailure(OP, cause, `${line} — ${writtenNote(written)}`, ...notes) });
160
+ for (const plan of plans) {
161
+ const steps = [
162
+ plan.kitWrite && { role: 'kit', kind: plan.kitWrite },
163
+ plan.effectiveWrite && { role: 'effective', kind: plan.effectiveWrite },
164
+ ].filter(Boolean);
165
+ for (const { role, kind } of steps) {
166
+ const rel = plan.paths[role];
167
+ const result = (() => {
168
+ try {
169
+ return { ok: writeOne({ cwd, rel, body: plan.kitBytes, kind, deps }) };
170
+ } catch (err) {
171
+ return { error: causeOf(err) };
172
+ }
173
+ })();
174
+ if (result.error !== undefined) return stop('write-refused', `${rel}: ${result.error}`);
175
+ notes.push(...tmpNote(rel, result.ok.tmpLeftBehind));
176
+ if (!result.ok.created) {
177
+ const now = readUnder(cwd, rel, deps);
178
+ if (now.outcome !== 'ok' || !now.bytes.equals(plan.kitBytes)) return stop('race-unresolved', `${rel}: ${RACE}`);
179
+ }
180
+ if (result.ok.created) written.push(rel);
181
+ fates.set(`${plan.name}:${role}`, result.ok.created ? 'written' : 'stood');
182
+ }
183
+ if (plan.recordWrite) {
184
+ const recorded = writeRecord(cwd, plan.name, plan.passed, deps);
185
+ if (!recorded.ok && recorded.condition === 'write') return stop('write-refused', `${RECORD_PATH}: ${recorded.message}`);
186
+ if (!recorded.ok) {
187
+ return stop('race-unresolved', `${RECORD_PATH}: the record could not vouch for the files (${recorded.condition}: ${recorded.message}), so they changed underneath this run; nothing further written, re-run when the tree is settled`);
188
+ }
189
+ written.push(RECORD_PATH);
190
+ fates.set(`${plan.name}:record`, 'written');
191
+ }
192
+ }
193
+ return {};
194
+ };
195
+
196
+ // ensureRules({ cwd, kitRoot, dryRun?, deps? }) → one outcome of the closed vocabulary for every theme.
197
+ export const ensureRules = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
198
+ const themes = deps.themes ?? THEMES;
199
+ const surveyed = survey({ themes, cwd, kitRoot, readFile: deps.readFile ?? readFileSync, deps });
200
+ if (surveyed.failure) return surveyed.failure;
201
+ const plans = surveyed.surveyed.map((one) => planTheme(one, surveyed.record, deps.priors ?? RULES_PRIORS));
202
+ const firstRecordWrite = plans.findIndex((plan) => plan.recordWrite);
203
+ const replacing = (index) => (index === firstRecordWrite && !surveyed.record.ok ? surveyed.record.reason : null);
204
+ const fates = new Map();
205
+ const notes = [];
206
+ const fateOf = (plan, role) => fates.get(`${plan.name}:${role}`) ?? 'would';
207
+ const lines = () => plans.flatMap((plan, index) => themeLines({ plan, fateOf, kitRoot, cwd, dryRun, replacing: replacing(index) }));
208
+ if (dryRun) {
209
+ const wouldWrite = (plan, role) => (role === 'kit' ? plan.kitWrite !== null : role === 'effective' ? plan.effectiveWrite !== null : plan.recordWrite);
210
+ return composeOutcome(OP, tokenOf(plans, wouldWrite, true), lines(), false);
211
+ }
212
+ const stopped = runWrites({ plans, cwd, deps, fates, notes });
213
+ if (stopped.failure) return stopped.failure;
214
+ const wrote = (plan, role) => fates.get(`${plan.name}:${role}`) === 'written';
215
+ return composeOutcome(OP, tokenOf(plans, wrote, false), [...lines(), ...notes], false);
216
+ };
@@ -0,0 +1,12 @@
1
+ // The append-only catalog of the kit-file bodies earlier releases shipped, and its one question.
2
+ // Contract: docs/ai/specs/kit/project-rules/rules-priors.md.
3
+
4
+ import { digestOf } from './rules-check.mjs';
5
+
6
+ export const RULES_PRIORS = Object.freeze([]);
7
+
8
+ // isKnownPrior(theme, bytes, catalog?) → true exactly when the bytes hash to an entry of that theme.
9
+ export const isKnownPrior = (theme, bytes, catalog = RULES_PRIORS) => {
10
+ const digest = digestOf(bytes);
11
+ return catalog.some((entry) => entry.theme === theme && entry.sha256 === digest);
12
+ };
@@ -40,7 +40,7 @@ const judge = (root, theme, passed, deps) => {
40
40
  const recordRead = readUnder(root, RECORD_PATH, deps);
41
41
  if (!['ok', 'absent'].includes(recordRead.outcome)) return fail('record-path', `${RECORD_PATH} is not a regular file`);
42
42
  const record = readRecord(recordRead, themes);
43
- if (!record.ok) return fail('unreadable-record', `${RECORD_PATH} is unreadable: ${record.reason}`);
43
+ const kept = record.ok ? record.themes : {};
44
44
  const paths = themePaths(theme);
45
45
  const files = Object.fromEntries(ROLES.map((role) => [role, readUnder(root, paths[role], deps)]));
46
46
  const unreadable = ROLES.find((role) => !['ok', 'absent'].includes(files[role].outcome));
@@ -60,7 +60,7 @@ const judge = (root, theme, passed, deps) => {
60
60
  } else if (!effective.equals(kit)) {
61
61
  return fail('effective-differs-from-kit', `with no local file ${paths.effective} must equal ${paths.kit}`);
62
62
  }
63
- return { ok: true, themes: record.themes };
63
+ return { ok: true, themes: kept };
64
64
  };
65
65
 
66
66
  // writeRecord(root, theme, passed, deps?) → { ok: true, path, entry } | { ok: false, condition, message }.