@sabaiway/agent-workflow-kit 14.37.0 → 14.39.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,61 @@ 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.39.0 — agents find the project rules from a row in the entry file's Memory Map, and you change them through one command, `/agent-workflow-kit rules` (AD-191)
8
+
9
+ **Every upgrade now keeps a Memory Map row in `AGENTS.md` that links `docs/ai/rules/effective/project-layout.md`, so an
10
+ agent reads the layout rules before it creates, moves or names a file.** The new `/agent-workflow-kit rules` changes
11
+ your own rules: it shows the exact text and writes `docs/ai/rules/local/<theme>.md` only on your yes, then rebuilds and
12
+ checks the file agents read.
13
+
14
+ - **The Memory Map row.** `inject-methodology.mjs reconcile` adds one row per rules theme on the line after the
15
+ `docs/ai/agent_rules.md` row, in the same one write as the three pointers, whether or not the effective file exists
16
+ yet. A row already there is kept byte for byte, also when you edited it; a row with a text an earlier release
17
+ shipped is refreshed (none yet). Where it cannot be placed (no `docs/ai/agent_rules.md` row, that row on two lines,
18
+ or the 100-line limit) the row is skipped with its reason and nothing stops. The reconcile's line names the row's
19
+ outcome beside the pointers, the zero-diff line included; a run that adds a row is no longer zero-diff.
20
+ - **`/agent-workflow-kit rules`, a Configure writer.** It starts with the rules ensure, then does one of three things you
21
+ ask: change a rule in your local file; move an edit you made to the kit file `docs/ai/rules/<theme>.md` (its ensure
22
+ line says it carries your edit) into your local file and delete the kit file, which the next ensure seeds again; or
23
+ drop your local file. On `pending` it does the merge, shows what changed and records it. It stops on a refusal, ends
24
+ on `rules-cli.mjs --check` answering `current`, and never writes the effective file by hand.
25
+ - **The upgrade offers the mode** on a kit-file line that says it carries your edit, and its step-4 and step-8 reports
26
+ relay the Memory Map row's outcome.
27
+ - **The architecture template points to the layout rules.** `references/templates/architecture.md` drops the Layers and
28
+ Module Boundaries sections for one section naming `docs/ai/rules/effective/project-layout.md`; a deployed
29
+ `docs/ai/architecture.md` is yours and is not changed.
30
+ - The router budget for a mode read with the router is 53248 B (was 52224 B); the router stays within 12288 B.
31
+
32
+ **Upgrade note.** No step: the next upgrade adds the row to `AGENTS.md`. To change a layout rule, run
33
+ `/agent-workflow-kit rules` and say what to change.
34
+
35
+ ## 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)
36
+
37
+ **`/agent-workflow-kit upgrade` and bootstrap now write the project-layout rules into `docs/ai/rules/`, keep
38
+ `docs/ai/rules/effective/project-layout.md` a copy of them, and record what it was built from, so
39
+ `rules-cli.mjs --check` answers `current`.** Where you keep your own rules in `docs/ai/rules/local/`, no code merges
40
+ them: the upgrade prints the merge the agent does and the command that records it.
41
+
42
+ - **A ninth upgrade ensure, `rules`,** runs after the spec layer and before the navigator, so the navigator lists the
43
+ files it lays. Its outcome is one of `seeded`, `refreshed`, `regenerated`, `pending`, `customized-preserved` or
44
+ `already-current`, or `failed` with its cause; `--dry-run` answers the `would-` form and writes nothing. Bootstrap
45
+ runs it as `ensure-configs.mjs --reconcile --only rules` in step 10, before the navigator re-run.
46
+ - **The kit file `docs/ai/rules/<theme>.md`** is created when absent, kept when it is the bundled template, refreshed
47
+ when it holds a body an earlier release shipped, and kept byte for byte when you edited it: the line then says your
48
+ own rules belong in `docs/ai/rules/local/<theme>.md`, where they win. The catalog of earlier bodies ships empty, so
49
+ nothing is refreshed yet.
50
+ - **With no local file** the effective file is rebuilt as the kit file's copy and its record entry is written; an
51
+ edited kit file with no frontmatter is kept and nothing else is written for that theme.
52
+ - **With a local file** nothing is written to the effective file or the record; until the merge is recorded, the line
53
+ starts `merge needed — `, states how to merge (by meaning, the local file winning a conflict, the frontmatter kept,
54
+ the user shown what changed) and prints the exact `rules-cli.mjs --record` command to run after it.
55
+ - **A `docs/ai/rules/effective/sources.json` that reads as UTF-8 text but is not JSON of the record's schema** is now
56
+ replaced by the record writer instead of stopping it, in `--record` and in the upgrade alike; a directory or a
57
+ symlink on its path still stops it, writing nothing.
58
+
59
+ **Upgrade note.** No step: the next upgrade lays the theme. If it prints `merge needed`, the agent merges, shows you
60
+ what changed and runs the printed command.
61
+
7
62
  ## 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
63
 
9
64
  **The kit now carries default project rules as thematic prose files, starting with project layout, and a command
package/README.md CHANGED
@@ -107,7 +107,7 @@ your-repo/
107
107
  ├── active_plan.md ← current task
108
108
  ├── agent_rules.md ← session protocols + self-review
109
109
  ├── current_state.md ← snapshot of the codebase now
110
- ├── architecture.md ← layers & boundaries
110
+ ├── architecture.md ← layout rules pointer & extension points
111
111
  ├── technical_specification.md
112
112
  ├── decisions.md ← ADRs — the HOT window (newest)
113
113
  ├── adr/ ← one file per archived ADR + log.md navigator
@@ -252,6 +252,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
252
252
  | `/agent-workflow-kit review-state` | any time | **read-only review-receipt checker** — makes "reviewed ≠ shipped" detectable: the bridge review wrappers append a receipt per successful review (into a file inside the git dir — never committable); this checks that every backend your configured `plan-execution.review` recipe names holds a **fresh, grounded receipt for the current uncommitted tree** (any later edit the payload can see moves the fingerprint and stales the receipt; a review continuation never re-attests a folded tree). `--check` gives a gate exit code to declare in `docs/ai/gates.json` **by hand or via the consent-gated seeder** (preview → your explicit yes; never without consent). `--await [--timeout <s>]` (AD-049) BLOCKS until every recipe-named backend has receipted the current tree — the durable completion signal is the receipt, never a process event — so you wait for the bridges instead of hand-polling. Never writes, never commits, never runs a subscription CLI; it spawns read-only `git` queries to fingerprint the tree — and `git commit --no-verify` stays possible (discipline, not a sandbox). |
253
253
  | `/agent-workflow-kit sandbox-masks` | any time | **exclude lane for sandbox masks** — an OS sandbox (Claude Code) masks work-tree paths with a device or, on Linux, a foreign bind mount, as untracked `git status` noise; the review domain already ignores a **device mask** **by construction** (never-committable untracked classes — char/block devices, FIFOs, sockets — are excluded from the fingerprint, the assembled review payload, and the clean checks), while a **mount-only mask** rides the fingerprint and `git add -A` while git does not ignore it (load-bearing there). This mode hides them from `git status` too: flagless = read-only probe (derives the CURRENT mask set from the unfiltered walk + lstat + `/proc/self/mountinfo` — never a frozen list; off Linux the mount signal is not read and it says so, an unreadable or malformed table refuses — and revalidates fenced entries, loudly flagging one that became a real path); `--apply` = consent-gated FULL-BLOCK replace of its own fenced block in `git rev-parse --git-path info/exclude` (stale masks drop by construction; `--clear` always means REMOVE the block — it takes precedence over the derivation). Writes ONLY its fence — never `.gitignore`, never global config; symlinked/non-regular exclude paths and malformed fences fail closed. Watch note: a real file at an excluded path is silently skipped by bulk staging (`git add -A`/`git add .`) — delete the stale line first; the probe flags exactly this case. |
254
254
  | `/agent-workflow-kit mcp` | Claude Code · opt-in | **typed-channel registration** — the kit ships a read-only stdio **MCP server** exposing its two promptless readers as TYPED tools (`path_inventory`: exists / type / size / line count / listing / a small file's text, many paths in ONE call; `repo_search`: literal search, the pattern a JSON string). Their arguments are named JSON fields rather than a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — legal bytes to search for, with no shell to read them as operators. Shipping the server does nothing on its own — a client sees it only once the project declares it, and this mode is that declaration: the `agent-workflow` entry in **`.mcp.json`** (command `node`, args = the absolute path of the RUNNING kit's server) plus `enabledMcpjsonServers` and the two derived tool allow rules in **`.claude/settings.json`**. Preview by default and the **exact entry is printed before consent** (registering a server means your client will run that command); `--apply` writes `.mcp.json` FIRST, then settings, merge-don't-clobber with each file's EOL kept, and a re-apply adds nothing twice. An existing `agent-workflow` entry that **structurally differs** from what this kit copy would write is refused unwritten unless `--replace` is given (the comparison ignores key order, so re-serialized identical bytes are the same registration) — silently changing what an MCP server launches is what consent must not slide past. `--replace` prints the entry standing in `.mcp.json` and the entry it would write, and replaces it only with `--apply`. An existing `.mcp.json` or `.claude/settings.json` is written in place: it keeps its inode, never removed or renamed over; an absent file is created. Where an OS sandbox masks `.mcp.json` with a device node, it writes nothing, exits 0 and names the advisor's `mcp-channel` item, which applies the registration by the consent route. The server is a read-only child of your client (no write, no exec API) running outside the Bash sandbox as the client does. Never writes `settings.local.json`; never commits. |
255
+ | `/agent-workflow-kit rules` | any time | **your project rules** — the kit lays down default rules per theme (the first is project layout: where code, tests and specs live) in `docs/ai/rules/<theme>.md`; your own rules go into `docs/ai/rules/local/<theme>.md` and win where they differ. Tell the agent in plain words what to change: it shows the exact text first and writes your local file only on your yes, then rebuilds the effective file `docs/ai/rules/effective/<theme>.md` that agents read (merging your rules over the kit’s by meaning) and checks it is current. It also moves an edit you made to the kit file into your local file, or drops your local rules on request. Never commits. |
255
256
  | `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's three canon sections (`## Goal and boundary` + `## Module ledger` + `## Verification`, each required; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. `--autonomy` (AD-044) appends the COMPUTED effective autonomy policy from the git-top `docs/ai/autonomy.json` (every red-line + per-activity level, stated source line; absent file → the computed defaults ARE the policy, exit 0; a malformed policy fails CLOSED, exit 1). `--extra <text|@file>` (repeatable) appends your own facts **byte-verbatim** after the mechanical sections, so the merge is a tool input rather than a shell append — an `@file` must sit inside the proven git work tree (never the git dir) or the system temp surface, and is read through a no-follow descriptor (a FIFO cannot block it, a symlink leaf cannot substitute its target). Prints to stdout; `--out` writes **one scratch file only** — system-temp outside the repo ($TMPDIR / /tmp, rewritable) or a **fresh** gitignored in-repo path (create-only, exclusive write; an existing in-repo file, even gitignored, is refused — the `.env` clobber class); tracked, not-ignored-in-repo, other outside-repo, and symlink/non-regular destinations are all refused. Never commits, never runs a subscription CLI. |
256
257
  | `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
257
258
  | `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
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.39.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -163,6 +163,10 @@ guarded — read `${CLAUDE_SKILL_DIR}/references/modes/sandbox-masks.md` before
163
163
 
164
164
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` before acting.
165
165
 
166
+ ### Mode: rules
167
+
168
+ writer — read `${CLAUDE_SKILL_DIR}/references/modes/rules.md` before acting.
169
+
166
170
  ### Mode: uninstall
167
171
 
168
172
  guarded — read `${CLAUDE_SKILL_DIR}/references/modes/uninstall.md` before acting.
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.39.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.39.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
@@ -0,0 +1,23 @@
1
+ ### Mode: rules
2
+
3
+ <!-- opt-in-capability: none — the user's own rules are changed on request, never a capability the advisor offers -->
4
+
5
+ Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
6
+
7
+ **The one place the user changes the project rules.** Each theme (the first is project layout) has three files under `docs/ai/rules/`: the kit file `docs/ai/rules/<theme>.md`, which only the kit writes; the user's local file `docs/ai/rules/local/<theme>.md`, which wins where it differs; and the effective file `docs/ai/rules/effective/<theme>.md`, which agents read. This mode writes the local file on the user's request and yes, and keeps the effective file and its record current with the kit's own tools. It never commits.
8
+
9
+ **1. Start with the ensure.** Run `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only rules --cwd <project>` and relay its line in plain words. The effective file is built, never written by hand: this mode moves no edit out of it, and the ensure's rebuild or the next merge replaces one.
10
+
11
+ **2. Do what the user asks — one of three.**
12
+
13
+ - **Change a rule.** The user says in plain words what to change. Show the exact text you will write to `docs/ai/rules/local/<theme>.md`, its frontmatter included, holding only the user's own rules and never a copy of the kit's, and write it on the user's yes. That request and that yes are the consent for the write of the local file.
14
+ - **Move an edit of the kit file.** When an ensure line for the kit file says it `carries your edit`, whatever token the run printed (`pending` and `regenerated` included), offer the move: compare `docs/ai/rules/<theme>.md` with the bundled `${CLAUDE_SKILL_DIR}/references/rules/<theme>.md`, write the user's changes, by meaning, into the local file (shown first, written on the yes), then delete the kit file. The next ensure seeds it again from the template, so the kit file keeps its one writer.
15
+ - **Drop the local rules.** On the user's request, delete the local file; the next ensure rebuilds the effective file from the kit file and records it.
16
+
17
+ **3. After each of these, run the ensure again.** On `pending`, do the merge its line states, show the user what changed in the final rules, then run the record command the line prints with the sha256 of the merged file (one plain command for the digest, per the shared command-shapes contract).
18
+
19
+ **It stops on a refusal.** When an ensure run answers `failed`, or the record command exits non-zero, relay the line and stop: write, delete and record nothing further, and run no closing check as if the change held.
20
+
21
+ **4. End with the checks.** Run `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`, then `node ${CLAUDE_SKILL_DIR}/tools/project-rules/rules-cli.mjs --check --cwd <project>`, which answers `current` for every theme; relay both in plain words.
22
+
23
+ **What it never does.** It never writes the effective file except with the merge a `pending` line states, never changes the kit file's text (the move deletes the file whole and the ensure seeds it again), and never writes a local file without the user's request.
@@ -10,7 +10,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
10
10
  2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`4.0.0` — NOT this kit's package version). If the stamp is **greater than the head** or unparseable → **STOP and report**; do not touch a newer / unknown deployment at all (not even the methodology slot). This STOP is one of the few places the number is actionable (*Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`): show the user **the `docs/ai` structure version** their deployment carries versus the one this kit expects, plus the plain one-line two-axes note — naming it the structure version, **never** "lineage head".
11
11
  3. **The step-3 run-list — stamp-independent reconciles, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run the **eight operations below in order, on every upgrade** (equal-head too) — no lineage-head bump, no migration file. Each backticked id anchors its rationale block below the checklist; a consent marker is a CONDITIONAL gate — an explicit yes is required exactly where the row's clause states one:
12
12
 
13
- 1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
13
+ 1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP; per Memory Map row of a rules theme: added · already present · refreshed · skipped (reported).
14
14
  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).
15
15
  3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `pending` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `not-placed` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed` — consent (conditional): a specs replace line → ONE yes naming every file the custom lines name and saying their edits are overwritten, asked even on a non-zero exit; on the yes run the printed line, then STOP if that run fails or an op it does not re-run failed; on a no STOP on any failed op.
16
16
  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`.
@@ -19,11 +19,11 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
19
19
  7. `tier` — `node ${CLAUDE_SKILL_DIR}/tools/tier-preview.mjs --cwd <project>` → per gap: present · offered · declined · undecidable · applied · recorded · refused — consent: one ask, only when offered.
20
20
  8. `bridge-settings` — `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` → outcome line(s), pasted verbatim: ok · absent · flagged · duplicates · unusable.
21
21
 
22
- **`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).
22
+ **`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 and the Memory Map rows present), 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). The same call keeps one Memory Map row per project-rules theme, after the `docs/ai/agent_rules.md` row; one it cannot place is skipped and reported, never a STOP.
23
23
 
24
- **Classify the exit — there are THREE non-zero exits + one soft in-band skip; handle each differently:**
24
+ **Classify the exit — there are THREE non-zero exits + soft in-band skips; handle each differently:**
25
25
 
26
- (a) **Soft, reported skip of a CHAINED pointer — orchestration and/or autonomy (CONTINUE the upgrade).** The chained pointers are the less-critical ones; when one can't be added right now `reconcile` exits **zero**, keeps every prior fill, and **reports each skip on stdout** (the `… skipped …` line) — never silent (Hard Constraint). Three reasons: **(i)** the prior pointers fit but adding this one would push the file past the `AGENTS.md` 100-line cap; **(ii)** the installed `agent-workflow-engine` is **present but too old** to ship that fragment (`<1.2.0` lacks the recipes one; every engine before the autonomy release lacks the autonomy one) — it can still supply the earlier pointers, so only that pointer is withheld; or **(iii)** — autonomy only — its **anchor is absent** because the orchestration pair is itself absent or was skipped, so the autonomy pointer has nowhere to chain (add the orchestration pointer first, then re-run). Report it in the successful-exit report (**step 4** equal-head, else **step 8**) in plain language, e.g. *"The orchestration-recipes / autonomy pointer wasn't added — the entry point is at its 100-line limit / your methodology engine is older than that feature. The recipes stay available via `/agent-workflow-kit recipes` and the autonomy policy via `/agent-workflow-kit set-autonomy`; to add the pointer, trim the entry point and/or refresh the engine with `npx @sabaiway/agent-workflow-engine@latest init`, then re-run upgrade."* (The separate case where the **methodology** pointer ITSELF can't fit the cap is a non-zero exit that changes nothing — continue without the pointers, same plain-language framing.)
26
+ (a) **Soft, reported skip of a CHAINED pointer — orchestration and/or autonomy (CONTINUE the upgrade).** The chained pointers are the less-critical ones; when one can't be added right now `reconcile` exits **zero**, keeps every prior fill, and **reports each skip on stdout** (the `… skipped …` line) — never silent (Hard Constraint). Three reasons: **(i)** the prior pointers fit but adding this one would push the file past the `AGENTS.md` 100-line cap; **(ii)** the installed `agent-workflow-engine` is **present but too old** to ship that fragment (`<1.2.0` lacks the recipes one; every engine before the autonomy release lacks the autonomy one) — it can still supply the earlier pointers, so only that pointer is withheld; or **(iii)** — autonomy only — its **anchor is absent** because the orchestration pair is itself absent or was skipped, so the autonomy pointer has nowhere to chain (add the orchestration pointer first, then re-run). Report it in the successful-exit report (**step 4** equal-head, else **step 8**) in plain language, e.g. *"The orchestration-recipes / autonomy pointer wasn't added — the entry point is at its 100-line limit / your methodology engine is older than that feature. The recipes stay available via `/agent-workflow-kit recipes` and the autonomy policy via `/agent-workflow-kit set-autonomy`; to add the pointer, trim the entry point and/or refresh the engine with `npx @sabaiway/agent-workflow-engine@latest init`, then re-run upgrade."* (The separate case where the **methodology** pointer ITSELF can't fit the cap is a non-zero exit that changes nothing — continue without the pointers, same plain-language framing.) A skipped Memory Map row is its own clause, never a pointer skip: report it as step 4 words the rows.
27
27
 
28
28
  (b) **Malformed pair / missing-or-duplicate anchor (any pointer) — a hard STOP (do NOT continue).** A non-zero exit whose message names a marker/anchor problem; never soft-skip it. (The one deliberate exception is (a)(iii): the autonomy pointer's ABSENT anchor is the chained soft skip, exit zero — a reported skip, not this STOP.)
29
29
 
@@ -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, its line saying it carries your edit), 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. A kit-file line saying it carries your edit, whatever the run's token, is offered `/agent-workflow-kit rules` to move the edit into the local file.
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), and each Memory Map row the same way (or *refreshed*); 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 and Memory Map rows, 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.
@@ -31,8 +31,8 @@ installed `agent-workflow-engine`** (`references/methodology-slot.md` + `referen
31
31
  + `references/autonomy-slot.md`,
32
32
  the family's one source of truth) — **not** a bundled mirror, and **not** the full references. The
33
33
  live read is **lazy + fail-loud**: the engine is consulted **only when a slot actually needs filling**,
34
- so a deployment with **all three** pointers filled reconciles to a zero-diff no-op even on a host
35
- without the engine (one still missing the autonomy pair needs the engine to gain it); when a fill
34
+ so a deployment with **all three** pointers filled and its Memory Map rows present reconciles to a
35
+ zero-diff no-op even on a host without the engine (one still missing the autonomy pair needs the engine to gain it); when a fill
36
36
  **is** needed and the engine is **absent/invalid**, reconcile
37
37
  **STOPs** — report it in plain language with the one-line install command
38
38
  `npx @sabaiway/agent-workflow-engine@latest init` (`npx @sabaiway/agent-workflow-kit@latest init`
@@ -9,28 +9,13 @@ maxLines: 350
9
9
 
10
10
  # Architecture
11
11
 
12
- > How the code is organized. Layers, boundaries, where new code goes.
12
+ > How the code is organized: where each module lives and where new code goes.
13
13
 
14
- ## Layers
14
+ ## Layout rules
15
15
 
16
- ```
17
- UI Components ← {{src/components/}}
18
- ↓
19
- Hooks / Composition ← {{src/hooks/}}
20
- ↓
21
- Services (pure) ← {{src/services/}}
22
- ↓
23
- State / Storage ← {{src/store/ or src/data/}}
24
- ↓
25
- Types / Schemas ← {{src/types/}}
26
- ```
27
-
28
- ## Module Boundaries
29
-
30
- - **Components** — render only. No fetching, no business logic.
31
- - **Hooks** — orchestration; combine services + state.
32
- - **Services** — pure functions. Easy to test.
33
- - **State** — single source of truth per domain.
16
+ The code is laid out by the project-layout rules: modules nest by meaning, and a name says where
17
+ code, tests and specs live. In a kit deployment those rules live in
18
+ `docs/ai/rules/effective/project-layout.md`, laid down by the kit's upgrade.
34
19
 
35
20
  ## File Tree (top level)
36
21
 
@@ -196,6 +196,13 @@ const CATALOG = [
196
196
  kind: GUARDED,
197
197
  oneLine: 'Register the kit’s read-only MCP server in this project, so path questions and literal searches run as typed tool calls instead of shell command strings — previews the exact entry it would declare, writes only on your yes, and where a sandbox hides the file it writes nothing and names the advisor’s mcp-channel item, which applies it by the consent route.',
198
198
  },
199
+ {
200
+ key: 'rules',
201
+ invocation: invocationOf('rules'),
202
+ group: 'Configure',
203
+ kind: WRITER,
204
+ oneLine: 'Change your own project rules, such as how files and modules are laid out: your rules go into your local file, which wins over the kit’s defaults; the exact text is shown first and written only on your yes.',
205
+ },
199
206
  {
200
207
  key: 'recipes',
201
208
  invocation: invocationOf('recipes'),
@@ -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
@@ -6,10 +6,9 @@
6
6
  // 2. workflow:orchestration — the recipe-vocabulary pointer (references/orchestration-slot.md).
7
7
  // 3. workflow:autonomy — the autonomy-policy read contract (references/autonomy-slot.md).
8
8
  //
9
- // All share ONE generic marker engine (a slot DESCRIPTOR parameterizes the markers / anchor /
10
- // empty-slot / leading-blank). The methodology exports (findSlot / injectMethodology / ensureSlot /
11
- // reconcileSlot / slotNeedsFill / extractSlot) delegate to it byte-for-byte, so the methodology
12
- // contract is unchanged; the later slots are the SAME engine with different descriptors.
9
+ // All share ONE generic marker engine (a slot DESCRIPTOR parameterizes the markers / anchor / empty-slot
10
+ // / leading-blank); the methodology exports delegate to it byte-for-byte. After the three slots,
11
+ // reconcile keeps one Memory Map row per project-rules theme (project-rules/memory-map-row.mjs).
13
12
  //
14
13
  // Contract per slot, strictly enforced:
15
14
  // exactly one ordered start→end pair → replace only the bytes between them;
@@ -17,17 +16,15 @@
17
16
  // any malformed state (single, reversed, nested, duplicate) → NO-OP WITH AN ERROR, never edit.
18
17
  // The live read is lazy + fail-loud: resolve+read the engine ONLY when a fill is actually needed, and
19
18
  // STOP loudly (never a silent fallback) when the engine is needed but absent/invalid.
20
- //
21
19
  // Pure string functions (testable with byte-preservation fixtures); dependency-free, Node >= 22.
22
20
  //
23
- // Canonical-refresh (AD-025): a slot filled BEFORE a clause existed is normally preserved verbatim
24
- // (reconcile only fills an EMPTY slot). To push a NEW canonical clause to the EXISTING filled base
25
- // without clobbering a user's customization, reconcileMarkerSlot also REFRESHES a filled slot — but
26
- // ONLY when its content normalize-matches a KNOWN PRIOR canonical fragment (drift-guarded, append-only
27
- // per descriptor). A customized slot never matches → preserved verbatim + a read-only upgrade advisory.
21
+ // Canonical-refresh (AD-025): reconcileMarkerSlot REFRESHES a filled slot ONLY when its content
22
+ // normalize-matches a KNOWN PRIOR canonical fragment (drift-guarded, append-only per descriptor); a
23
+ // customized slot never matches → preserved verbatim + a read-only upgrade advisory.
28
24
 
29
25
  import { isDirectRun } from './direct-run.mjs';
30
26
  import { normalizeCanonical } from './orchestration-config.mjs';
27
+ import { reconcileMemoryMapRows } from './project-rules/index.mjs';
31
28
 
32
29
  export const START_MARKER = '<!-- workflow:methodology:start -->';
33
30
  export const END_MARKER = '<!-- workflow:methodology:end -->';
@@ -326,6 +323,7 @@ export const isFillCapRefusal = (errorMessage) =>
326
323
  isCapRefusal(errorMessage) && errorMessage.includes('injection would push');
327
324
 
328
325
  const EXIT = Symbol('inject-methodology.exit');
326
+ const ROW_WORDS = { inserted: 'added', kept: 'already present', refreshed: 'refreshed to the current text' };
329
327
 
330
328
  // The return-code entry point (5.1): argv[] + injected env/home → { code, stdout, stderr }, no
331
329
  // process.argv / process.exit / console inside — an in-process caller is as hermetic as a spawned
@@ -574,16 +572,18 @@ export const runCli = async (argv, deps = {}) => {
574
572
  }
575
573
  }
576
574
 
577
- // ── One atomic write of the final (three-slot) text ──
578
- if (finalText === text) {
575
+ // ── The Memory Map rows on the three-slot text, then one atomic write ──
576
+ const rows = reconcileMemoryMapRows(finalText, { maxLines: AGENTS_MD_CAP });
577
+ const describeRows = rows.outcomes.map((o) => `the ${o.theme} Memory Map row ${ROW_WORDS[o.status] ?? `skipped — ${o.note}`}`).join('; ');
578
+ if (rows.text === text) {
579
579
  // Byte-unchanged. Still report a skip (it is not "nothing to do" — a pointer was withheld).
580
- if (orchSkipped || autSkipped) log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
581
- else log('[inject-methodology] reconcile: all three pointers already present and filled — nothing to do (zero-diff).');
580
+ if (orchSkipped || autSkipped) log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}; ${describeRows}.`);
581
+ else log(`[inject-methodology] reconcile: all three pointers already present and filled; ${describeRows} — nothing to do (zero-diff).`);
582
582
  reportNotes();
583
583
  return result(0);
584
584
  }
585
- await writeAtomic(finalText);
586
- log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
585
+ await writeAtomic(rows.text);
586
+ log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}; ${describeRows}.`);
587
587
  reportNotes();
588
588
  return result(0);
589
589
  }
@@ -1,3 +1,5 @@
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';
5
+ export { reconcileMemoryMapRows } from './memory-map-row.mjs';
@@ -0,0 +1,62 @@
1
+ // The Memory Map row of each theme in a deployed AGENTS.md: a pure text leaf that reads and writes
2
+ // nothing; inject-methodology.mjs reconcile calls it and stays the file's one writer.
3
+ // Contract: docs/ai/specs/kit/project-rules/memory-map-row.md.
4
+
5
+ import { THEMES, themePaths } from './themes.mjs';
6
+
7
+ // The row texts a release shipped, append-only: a release that changes a row's text first appends
8
+ // the outgoing one here.
9
+ export const MEMORY_MAP_PRIOR_ROWS = Object.freeze([]);
10
+
11
+ const ANCHOR_PATH = 'docs/ai/agent_rules.md';
12
+ const opening = (path) => `| [\`${path}\`](./${path}) |`;
13
+ const rowOf = (theme) => {
14
+ const { effective } = themePaths(theme.name);
15
+ return `${opening(effective)} ${theme.readWhen} | Only through \`/agent-workflow-kit rules\` |`;
16
+ };
17
+
18
+ // The lines of a text, each keeping its own ending, so joining them gives the text back byte for byte.
19
+ const splitLines = (text) => text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
20
+ const bodyOf = (line) => line.replace(/\r?\n$/, '');
21
+ const endingOf = (line) => line.slice(bodyOf(line).length);
22
+ // A table line that does not close its last cell goes on over the next line.
23
+ const isWhole = (line) => bodyOf(line).trimEnd().endsWith('|');
24
+
25
+ // placeAfter(lines, i, row) → the lines with row on the line after i, taking line i's ending.
26
+ const placeAfter = (lines, i, row) => {
27
+ const ending = endingOf(lines[i]);
28
+ if (ending !== '') return lines.toSpliced(i + 1, 0, `${row}${ending}`);
29
+ return lines.toSpliced(i, 1, `${lines[i]}\n`, row);
30
+ };
31
+
32
+ // reconcileMemoryMapRows(text, { maxLines, priors }) → { text, outcomes }: one outcome
33
+ // { theme, status, note } per registry theme in registry order, status inserted, kept, refreshed or
34
+ // skipped; a skipped theme changes no byte and its note names the reason.
35
+ export const reconcileMemoryMapRows = (text, { maxLines, priors = MEMORY_MAP_PRIOR_ROWS } = {}) => {
36
+ let lines = splitLines(text);
37
+ let last = -1;
38
+ const outcomes = [];
39
+ for (const theme of THEMES) {
40
+ const row = rowOf(theme);
41
+ const answer = (status, note = '') => outcomes.push({ theme: theme.name, status, note });
42
+ const at = lines.findIndex((line) => line.startsWith(opening(themePaths(theme.name).effective)));
43
+ if (at !== -1) {
44
+ if (priors.includes(bodyOf(lines[at]))) {
45
+ lines = lines.with(at, `${row}${endingOf(lines[at])}`);
46
+ answer('refreshed');
47
+ } else answer('kept');
48
+ continue;
49
+ }
50
+ const anchor = last !== -1 ? last : lines.findIndex((line) => line.startsWith(opening(ANCHOR_PATH)));
51
+ if (anchor === -1) answer('skipped', `the Memory Map has no ${ANCHOR_PATH} row to place it after`);
52
+ else if (!isWhole(lines[anchor])) answer('skipped', `the Memory Map's ${ANCHOR_PATH} row spans more than one line`);
53
+ else if (maxLines != null && lines.length + 1 > maxLines) {
54
+ answer('skipped', `adding it would take the file past its ${maxLines}-line limit`);
55
+ } else {
56
+ lines = placeAfter(lines, anchor, row);
57
+ last = anchor + 1;
58
+ answer('inserted');
59
+ }
60
+ }
61
+ return { text: lines.join(''), outcomes };
62
+ };
@@ -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 }.