@sabaiway/agent-workflow-kit 1.5.1 → 1.6.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,56 @@ 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
+ ## 1.6.0 — Methodology slot reconciliation; engine becomes the canonical methodology home
8
+
9
+ The workflow methodology now has a **single canonical home** in `agent-workflow-engine`
10
+ (`available:false` — content only, not yet published or wired live), and the kit keeps
11
+ **byte-identical mirror copies** so the existing injection + fallback keep working with **no new
12
+ runtime dependency**. A drift-guard test (`tools/methodology-mirror.test.mjs`) pins the mirrors to
13
+ the engine canon: `references/planning.md` and `tools/methodology-slot.md` must equal their engine
14
+ counterparts byte-for-byte.
15
+
16
+ The user-facing win is **stamp-independent slot reconciliation**. A single atomic, idempotent kit
17
+ operation now **ensures the `workflow:methodology` slot exists and is filled** in a deployed
18
+ `AGENTS.md`, on **bootstrap** and on **every upgrade**:
19
+
20
+ - **`tools/inject-methodology.mjs`** gains `METHODOLOGY_ANCHOR`, `EMPTY_SLOT`, `ensureSlot`, and
21
+ `reconcileSlot` (reusing the existing `findSlot` / `injectMethodology` / `extractSlot` marker
22
+ parser — no second parser). `reconcileSlot` = **ensure the slot exists** (insert an empty marker
23
+ pair right after the Session-Protocols anchor when a legacy entry point lacks one) → **inject the
24
+ bounded fragment ONLY IF the slot is empty** (a filled / user-customized slot is preserved
25
+ verbatim) → **cap-check** (`AGENTS.md` ≤ 100 lines). On a malformed slot or a missing / duplicate
26
+ anchor it **STOPs with an error and never edits** — the file is left byte-for-byte unchanged.
27
+ - A new CLI mode — `inject-methodology.mjs reconcile <AGENTS.md>` — runs that policy as **one
28
+ atomic write** (temp + rename); there is no partial state where markers exist but the fill failed.
29
+ - The kit **fallback** entry-point template (`references/templates/AGENTS.md`) now ships the **empty
30
+ methodology slot** (matching memory's template) instead of an inline methodology line, so a fresh
31
+ fallback bootstrap gets a slot the kit fills. A new test (`test/fallback-template-cap.test.mjs`)
32
+ pins that template — empty and filled — under the 100-line cap.
33
+ - **Bootstrap** and **upgrade** (`SKILL.md`) now run `reconcile`. On upgrade it runs
34
+ **before** the lineage short-circuit, so the slot is reconciled on every upgrade — reaching even
35
+ legacy **`1.3.0`** deployments — **without bumping the deployment-lineage head**.
36
+
37
+ The deployment-lineage head **stays `1.3.0`** and `agent-workflow-memory` is **untouched** (no code,
38
+ version, or migration change): reconciliation is stamp-independent, so it needs no head bump (which
39
+ would have forced a memory republish, since the head is hard-coded in memory's stamp module).
40
+ Additive — no user-facing break. The engine's npm packaging, `available:true`, and the live
41
+ `kit → engine` read selector are deferred to the next plan.
42
+
43
+ ## 1.5.2 — README uplift to front-door grade (docs)
44
+
45
+ Docs-only patch. The npm-facing `README.md` is uplifted to match the GitHub family front door's
46
+ pitch and voice while staying the kit's **manual**: a stronger hero, a compact "Part of the
47
+ agent-workflow family" callout, a new **composition-root** section (the kit delegates to the memory
48
+ substrate, injects the methodology, and detects the optional `codex` / `agy` bridges — all on the
49
+ in-repo deploy, never on `npx … init`), a two-tier cross-agent note, and links **up** to the family
50
+ front door instead of re-telling the whole-family story (AD-009). Accuracy passes hold: `init` ≠
51
+ project deploy, the scoped `dependency-free` / `no telemetry` claims, bridges-as-skills, the
52
+ `available:false` engine stub, and the bridge context-file priority. A new dev-only test
53
+ (`test/readme-structure.test.mjs`) enforces fenced-ASCII width ≤ 78, in-page anchor resolution, and
54
+ local-link existence across the published READMEs. No code, schema, or deployed-payload change; the
55
+ deployment-lineage head stays `1.3.0` (no migration).
56
+
7
57
  ## 1.5.1 — README hero fix (docs)
8
58
 
9
59
  Docs-only patch. The hero showed a hardcoded `v1.4.0` chip while the kit was 1.5.0; the chip is
package/README.md CHANGED
@@ -2,24 +2,43 @@
2
2
 
3
3
  # 🧠 agent-workflow-kit
4
4
 
5
- **A portable, cross-agent memory & workflow system for AI coding agents.**
5
+ **Durable, cross-agent memory & workflow for AI coding agents — the one command that installs it.**
6
6
 
7
- *Bootstrap it once — then every future session reconstructs project context in seconds
8
- instead of re-reading your whole repo.*
7
+ *Run it once per machine, deploy it once per project — then every future session boots from a
8
+ small, structured memory instead of re-reading your whole repo and re-deriving yesterday's
9
+ decisions. Works with Claude Code, Codex, Cursor, and any agent that reads `AGENTS.md`.*
9
10
 
10
11
  [![npm version](https://img.shields.io/npm/v/@sabaiway/agent-workflow-kit?logo=npm)](https://www.npmjs.com/package/@sabaiway/agent-workflow-kit)
11
12
  [![npm downloads](https://img.shields.io/npm/dm/@sabaiway/agent-workflow-kit)](https://www.npmjs.com/package/@sabaiway/agent-workflow-kit)
12
13
  [![license](https://img.shields.io/npm/l/@sabaiway/agent-workflow-kit)](./LICENSE)
13
14
  [![node](https://img.shields.io/node/v/@sabaiway/agent-workflow-kit)](https://nodejs.org)
14
15
 
15
- `Node ≥ 18` · `dependency-free` · `kernel-only`
16
+ `Node ≥ 18` · `dependency-free scripts` · `no telemetry in family code`
17
+
18
+ **One command to start:**
19
+
20
+ ```bash
21
+ npx @sabaiway/agent-workflow-kit init
22
+ ```
23
+
24
+ <sub>This installs the **global skill** — deploying into a project is a separate step ([below](#-install)).</sub>
16
25
 
17
26
  **Works with any tool that reads `AGENTS.md`** — Claude Code · Codex · Cursor · Devin Desktop (formerly Windsurf) · GitHub Copilot · Gemini CLI · Cline · Aider · and 20+ more.
18
27
 
28
+ **Quick-jump:** [Install](#-install) · [What it deploys](#-what-it-deploys-into-your-project) · [How it works](#-how-it-works-60-seconds) · [Composition root](#-the-composition-root-of-the-family)
29
+
19
30
  </div>
20
31
 
21
32
  ---
22
33
 
34
+ > **Part of the [`agent-workflow`](https://github.com/sabaiway/agent-workflow) family.** This package
35
+ > is the **composition root** + entry point: it **delegates** memory deployment to the substrate,
36
+ > **injects** the workflow methodology, and **detects** optional execution backends. This page is the
37
+ > kit's **manual** (install · commands · what it deploys) — for the whole-family story, start at the
38
+ > **[family front door](https://github.com/sabaiway/agent-workflow#readme)**.
39
+
40
+ ---
41
+
23
42
  ## ❓ The problem
24
43
 
25
44
  AI coding agents are **stateless between sessions**. Every new chat starts from zero:
@@ -59,22 +78,25 @@ WITH the kit · boots from memory, cost flat
59
78
  s3 ~5k tok █ ← decisions kept
60
79
  ```
61
80
 
62
- <sub>*Illustrative — exact numbers scale with repo size. The point is the **shape**: cold re-reads that grow vs. a flat, cache-warm boot.*</sub>
81
+ <sub>*Illustrative/directional, not a measured guarantee — exact numbers scale with repo size. The point is the **shape**: cold re-reads that grow vs. a flat, cache-warm boot.*</sub>
63
82
 
64
83
  | | 🚫 Without | ✅ With `agent-workflow-kit` |
65
84
  |---|---|---|
66
- | **Session boot** | re-read source + grep to rebuild context | read 4 small docs, ~constant |
85
+ | **Session boot** | re-read source + grep to rebuild context | read a few small docs, ~constant |
67
86
  | **Boot cost** | grows with repo, paid every session | flat; stable layer stays prompt-cache-warm |
68
87
  | **Cross-session memory** | none | `handover` (where we left off) |
69
88
  | **Past decisions** | re-litigated | `decisions.md` (ADRs) — settled once |
70
89
  | **Known bugs** | re-discovered | `known_issues.md` — impact + workaround |
71
90
  | **Doc growth** | unbounded sprawl | frontmatter caps + 3-tier rolling archive |
72
91
  | **Drift** | docs ≠ code over time | pre-commit gate keeps them honest |
92
+ | **Cross-agent** | re-explain the project to each tool | one `AGENTS.md`, read by 20+ agents |
73
93
 
74
94
  ---
75
95
 
76
96
  ## 📦 What it deploys into your project
77
97
 
98
+ Invoking the skill **inside a project** creates a portable memory and its maintenance policy:
99
+
78
100
  ```
79
101
  your-repo/
80
102
  ├── AGENTS.md ← single entry point
@@ -94,24 +116,35 @@ your-repo/
94
116
  ├── tech_reference.md ← configs & patterns
95
117
  ├── pages/ ← one spec per page/route
96
118
  └── history/ ← archive (HOT→WARM→COLD)
97
- + scripts/ ← caps · index · archive (Node)
98
- + pre-commit hook ← keeps it all honest
119
+ + scripts/ ← caps · index · archive (Node path)
120
+ + pre-commit hook ← keeps it all honest (Node path)
99
121
  ```
100
122
 
101
- Two visibility modes, chosen at bootstrap: **visible** (committed) or **hidden**
102
- (in-tree but git-ignored, so the repo "looks normal").
123
+ The Markdown memory is **stack-agnostic**; the `scripts/` + pre-commit hook are the **Node path**
124
+ (dependency-free, `node --test`). Non-Node projects keep the same policy by hand.
125
+
126
+ Two **visibility** modes, chosen at deploy time: **visible** (committed with the repo) or **hidden**
127
+ (same files in-tree but git-ignored via the global `core.excludesFile`, so the repo "looks normal").
128
+ Hidden changes how the files are *tracked*, not where agents find them.
103
129
 
104
130
  ---
105
131
 
106
132
  ## 🚀 Install
107
133
 
108
- **One command** installs the kit into `~/.claude/skills/` and wires any Codex / Devin Desktop you have:
134
+ ### 1. Install the global skill once per machine
109
135
 
110
136
  ```bash
111
137
  npx @sabaiway/agent-workflow-kit init
112
138
  ```
113
139
 
114
- Then invoke it **inside a project** first time vs. already-deployed use different sub-commands:
140
+ `init` installs/refreshes the skill at `~/.claude/skills/agent-workflow-kit/` and wires launchers for
141
+ any Claude Code / Codex / Devin Desktop it finds. It **does not** deploy into a project, and **does
142
+ not** install the optional bridges.
143
+
144
+ ### 2. Deploy into a project — once per repo
145
+
146
+ Invoke the installed skill **inside the target repository** — first time vs. already-deployed use
147
+ different sub-commands:
115
148
 
116
149
  | Agent | First time in the project | Project already has the kit |
117
150
  |-------|---------------------------|-----------------------------|
@@ -119,21 +152,22 @@ Then invoke it **inside a project** — first time vs. already-deployed use diff
119
152
  | **Codex** | `/skills` menu → `agent-workflow-kit` | …→ `agent-workflow-kit upgrade` |
120
153
  | **Devin Desktop** (Windsurf · Devin Local) | `/agent-workflow-kit` | `/agent-workflow-kit upgrade` |
121
154
 
122
- <sub>`/agent-workflow-kit` bootstraps a fresh deployment (and asks your **visibility**, **conversational language**, and whether the agent may **attribute work to itself / AI** — default off); `/agent-workflow-kit upgrade` migrates an existing one to the kit's current version. The `npx … init` above is a third, separate thing — it updates the **kit itself**, not any project.</sub>
155
+ <sub>`/agent-workflow-kit` bootstraps a fresh deployment (and asks your **visibility**, **conversational language**, and whether the agent may **attribute work to itself / AI** — default off); `/agent-workflow-kit upgrade` migrates an existing one to the kit's current version.</sub>
123
156
 
124
- > **New in 1.4.0 — optional memory substrate.** The memory layer is now also published
125
- > standalone as [`@sabaiway/agent-workflow-memory`](https://www.npmjs.com/package/@sabaiway/agent-workflow-memory).
126
- > If it is installed, the kit **delegates** substrate deployment to it and injects the workflow
127
- > methodology; if not, the kit uses its **own bundled copy** the one command above keeps
128
- > working with no new dependency. Same `docs/ai/` either way.
157
+ > **Optional standalone memory substrate.** The memory layer is also published standalone as
158
+ > [`@sabaiway/agent-workflow-memory`](https://www.npmjs.com/package/@sabaiway/agent-workflow-memory).
159
+ > If a **healthy** copy is installed (the kit validates it with its own shipped validator), the kit
160
+ > **delegates** substrate deployment to it and injects the workflow methodology; otherwise it uses
161
+ > its **own bundled copy** — the one command above keeps working with no new dependency. Same
162
+ > `docs/ai/` either way.
129
163
 
130
- **Upgrade the kit itself** later — same command with `@latest`:
164
+ ### Refresh the kit itself — same command with `@latest`
131
165
 
132
166
  ```bash
133
167
  npx @sabaiway/agent-workflow-kit@latest init
134
168
  ```
135
169
 
136
- <sub>That refreshes the **kit's own files** — distinct from `/agent-workflow-kit upgrade`, which migrates a **project's** deployment (see **Use** below).</sub>
170
+ <sub>That refreshes the **kit's own files** in `~/.claude/skills/` — distinct from `/agent-workflow-kit upgrade`, which migrates a **project's** deployment (see **Use** below).</sub>
137
171
 
138
172
  <details>
139
173
  <summary><b>Manual install</b> — no <code>npx</code></summary>
@@ -180,7 +214,7 @@ command is printed).
180
214
  | Command | When | What happens |
181
215
  |---------|------|--------------|
182
216
  | `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data → installs enforcement → **asks before committing** |
183
- | `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, applies migrations, re-stamps |
217
+ | `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps |
184
218
  | `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
185
219
 
186
220
  It **never auto-commits** and **never overwrites** an existing `AGENTS.md` without asking.
@@ -196,18 +230,50 @@ It **never auto-commits** and **never overwrites** an existing `AGENTS.md` witho
196
230
  - **Layered, lazy loading** — *always-loaded* = `AGENTS.md` + `index.md` (~160 lines, cache-warm). *On-demand* = open a `docs/ai/` file only when its "Read When" applies. *Hierarchical* = subdir `AGENTS.md` loads when you work in that folder. *Archive* = old history rolls out of the hot files.
197
231
  - **Caps + freshness** — every doc declares a `maxLines` cap; a pre-commit hook blocks commits that bust a cap or let the auto-generated index go stale.
198
232
  - **3-tier rolling archive** — `changelog.md` (HOT, last days) → `history/recent.md` (WARM) → per-month COLD + a one-line condensed index. Hot files stay small forever.
199
- - **Plan lifecycle** — Plan → Phase → Step → Substep, ephemeral plan files, a mandatory Cleanup phase, and a session-continuity heuristic tuned for large-context models (e.g. Opus 4.8).
233
+ - **Plan lifecycle** — Plan → Phase → Step → Substep, ephemeral plan files, a mandatory Cleanup phase, and a session-continuity heuristic tuned for large-context models (e.g. Claude Opus).
200
234
  - **No silent failures** — every guard that rejects an action logs structured context.
201
235
 
202
236
  Enforcement ships as dependency-free **Node** scripts (`node --test`, no package manager assumed). Non-Node projects follow the same policy by hand.
203
237
 
204
238
  ---
205
239
 
240
+ ## 🧩 The composition root of the family
241
+
242
+ The kit is the member you install — the family's **composition root**. `npx … init` only installs
243
+ the kit globally; the composition happens when you **deploy it in a repo** (`/agent-workflow-kit`):
244
+
245
+ ```
246
+ agent-workflow-kit — the composition root (installed via npx … init)
247
+ on /agent-workflow-kit in a repo, the kit:
248
+ ├─ delegates ─▶ memory substrate (healthy copy, else bundled fallback)
249
+ ├─ injects ─▶ workflow methodology (engine = future supplier; stub)
250
+ ├─ deploys ─▶ AGENTS.md + docs/ai/ + Node scripts + pre-commit hook
251
+ └─ detects ─▶ optional backends (codex / agy — not by init)
252
+ ```
253
+
254
+ - **Delegates** substrate deployment to **`@sabaiway/agent-workflow-memory`** when a healthy
255
+ standalone copy is present, else uses its **bundled fallback** — same `docs/ai/` either way.
256
+ - **Injects** the bounded workflow methodology into the deployed `AGENTS.md`. Its *future* home is
257
+ **`agent-workflow-engine`** — today an `available: false` stub, never one of the shipped backends.
258
+ - **Detects** the optional `codex` / `agy` **bridges** — agent skills with a manual once-per-machine
259
+ setup (not npm, not installed by `init`); `/agent-workflow-kit backends` reports readiness
260
+ read-only. A bridge reads the deployed memory only if it wins that tool's context-file priority,
261
+ and the bridges call third-party services (so "no telemetry" covers family code, not those).
262
+
263
+ > Full member-by-member map + the whole-family story: the
264
+ > **[family front door](https://github.com/sabaiway/agent-workflow#readme)** — this page stays the
265
+ > kit's manual.
266
+
267
+ ---
268
+
206
269
  ## 🤝 Cross-agent by design
207
270
 
208
- One kit, three front doors the *output* (`AGENTS.md` + `docs/ai/`) is read natively by
209
- Codex, Cursor, Devin Desktop, Copilot, Gemini CLI & 20+ tools, and the *bootstrapper* runs from
210
- Claude Code, Codex, or Devin Desktop. No logic is duplicated per tool.
271
+ One kit, two tiers**no logic is duplicated per tool:**
272
+
273
+ - The **output** (`AGENTS.md` + `docs/ai/`) is read natively by Claude Code (via the `CLAUDE.md`
274
+ alias) · Codex · Cursor · Devin Desktop · Copilot · Gemini CLI & 20+ tools.
275
+ - The **bootstrapper** runs from Claude Code · Codex · Devin Desktop — their launchers point at the
276
+ same `SKILL.md`, so deployment logic lives in one place.
211
277
 
212
278
  ---
213
279
 
@@ -215,16 +281,21 @@ Claude Code, Codex, or Devin Desktop. No logic is duplicated per tool.
215
281
 
216
282
  ```
217
283
  agent-workflow-kit/
218
- ├── README.md ← you are here
219
- ├── SKILL.md ← agent-facing algorithm
284
+ ├── README.md ← you are here (the kit's manual)
285
+ ├── SKILL.md ← agent-facing deploy / upgrade algorithm
220
286
  ├── CHANGELOG.md ← version history
221
287
  ├── capability.json ← agent-workflow family manifest (composition-root)
222
288
  ├── references/
223
- ├── templates/ ← AGENTS.md + every docs/ai file
224
- ├── scripts/ ← caps / archive / index + tests
225
- ├── contracts.md ← visibility / language / attribution rules
226
- └── planning.md ← plan lifecycle + continuity
227
- ├── tools/ ← family tooling: manifest schema + validator, methodology-slot injection, backend detector (detect-backends)
289
+ ├── templates/ ← AGENTS.md + every docs/ai file
290
+ ├── scripts/ ← caps / archive / index + tests
291
+ ├── contracts.md ← visibility / language / attribution rules
292
+ └── planning.md ← plan lifecycle + continuity
293
+ ├── tools/ ← family tooling:
294
+ │ ├── manifest/ ← capability-manifest schema + validator
295
+ │ ├── delegation.mjs ← detect substrate · delegate-or-fall-back
296
+ │ ├── inject-methodology.mjs ← write the methodology slot
297
+ │ ├── detect-backends.mjs ← read-only backend detector
298
+ │ └── release-scan.mjs ← attribution / release gate
228
299
  ├── launchers/ ← Codex / Devin Desktop / Cursor entries
229
300
  └── migrations/ ← per-version upgrade steps
230
301
  ```
@@ -232,5 +303,5 @@ agent-workflow-kit/
232
303
  ---
233
304
 
234
305
  <div align="center">
235
- <sub>Kernel-only · stack-agnostic · distilled from a multi-year-verified reference implementation.</sub>
306
+ <sub>Kernel-only · stack-agnostic · no telemetry in family code · distilled from a multi-year-verified reference implementation — <a href="https://github.com/sabaiway/agent-workflow">sabaiway/agent-workflow</a></sub>
236
307
  </div>
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.5.1'
6
+ version: '1.6.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -49,21 +49,25 @@ made: a partial/broken memory install discovered mid-flow must not disable the w
49
49
  **Hand-off contract (explicit; tested independent of agent interpretation).**
50
50
  - **Delegated** (memory valid): the kit passes the **target project dir** + the **three setup
51
51
  answers** (visibility / language / attribution) to `agent-workflow-memory`, which writes
52
- `docs/ai/` + `AGENTS.md` + **`.memory-version`**. The kit then **injects the bounded
53
- methodology slot** (below) and writes the kit-fallback **`.workflow-version`**. → **both
54
- stamps** present.
52
+ `docs/ai/` + `AGENTS.md` (with the empty slot) + **`.memory-version`**. The kit then
53
+ **reconciles the bounded methodology slot** (below) and writes the kit-fallback
54
+ **`.workflow-version`**. → **both stamps** present.
55
55
  - **Fallback** (memory absent/invalid): the kit runs the bootstrap procedure below from its own
56
- bundled assets and writes **`.workflow-version`** only. Softly suggest installing
56
+ bundled assets whose entry-point template now ships the **empty methodology slot** the kit
57
+ reconciles + fills — and writes **`.workflow-version`** only. Softly suggest installing
57
58
  `agent-workflow-memory` — never a prerequisite.
58
59
 
59
- **Methodology injection (the kit is the ONLY writer of memory's slot).** After `AGENTS.md`
60
- exists, inject the bounded methodology fragment into its `workflow:methodology` slot:
61
- `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs <project>/AGENTS.md`. It injects a short
62
- summary + pointer (Phase-1 source: the kit's bundled `tools/methodology-slot.md`) **not** the
63
- full `references/planning.md` and keeps `AGENTS.md` under its ≤100-line cap. Marker contract:
64
- exactly one ordered `start end` pair replace only the bytes between them; markers absent →
65
- no-op; any malformed state (single, reversed, nested, duplicate) no-op **with an error**,
66
- never edit.
60
+ **Methodology slot reconciliation (the kit is the ONLY writer of memory's slot).** After
61
+ `AGENTS.md` exists, reconcile its `workflow:methodology` slot:
62
+ `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md`. Reconcile is
63
+ **one atomic operation**: **ensure the slot exists** (insert an empty marker pair right after the
64
+ Session-Protocols anchor when a legacy entry point lacks one) **inject the bounded fragment ONLY
65
+ IF the slot is empty** (a filled / user-customized slot is preserved verbatim) **cap-check**
66
+ (keeps `AGENTS.md` ≤100 lines). The fragment is a short summary + pointer (source: the kit's bundled
67
+ `tools/methodology-slot.md`, a **byte-identical mirror of the `agent-workflow-engine` canon**) —
68
+ **not** the full `references/planning.md`. Contract: exactly one ordered `start → end` pair; a
69
+ malformed slot (single, reversed, nested, duplicate) or a missing / duplicate anchor → **STOP with
70
+ an error**, never edit (the file is left byte-for-byte unchanged).
67
71
 
68
72
  **One composition-level commit gate.** The delegated memory mode performs **no** commit and
69
73
  raises **no** "ask to commit". There is exactly **one** gate, owned by the kit, **after**
@@ -103,7 +107,8 @@ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to
103
107
  9. **Wire / hide** per visibility (see contract). Install the pre-commit hook (Node projects): `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting.
104
108
  10. **Stamp the deployment lineage.** Write the **deployment-lineage head** into
105
109
  `docs/ai/.workflow-version` (one semver line). The lineage head is **`1.3.0`** — the shared
106
- `agent-workflow` deployment lineage, **NOT** this kit's package version (`1.4.0`). The two are
110
+ `agent-workflow` deployment lineage, **NOT** this kit's npm package version (see
111
+ `package.json` / `CHANGELOG.md`). The two are
107
112
  independent axes: a packaging-only release bumps the package but leaves the lineage head until a
108
113
  migration actually changes the deployed `docs/ai` structure. A stamp greater than the head →
109
114
  STOP (never downgrade).
@@ -122,11 +127,13 @@ Fill strategy:
122
127
  ### Mode: upgrade
123
128
 
124
129
  1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
125
- 2. Compare to the **deployment-lineage head** (`1.3.0` — NOT this kit's package version). If equal → report "up to date" and stop. If the stamp is **greater than the head** or unparseable → **STOP and report** (never downgrade).
126
- 3. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
127
- 4. Apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** safe to re-run.
128
- 5. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`.
129
- 6. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`1.3.0`, not the package version). Report changes; **ask before committing**.
130
+ 2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`1.3.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).
131
+ 3. **Reconcile the methodology slot — stamp-independent, BEFORE the equal-head short-circuit.** Reached only when the stamp **≤ head**. Run `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md`. This ensures the `workflow:methodology` slot exists and is filled on **every** upgrade, idempotently (zero-diff when already present + filled) — so even a legacy / current **`1.3.0`** deployment gains the slot **without a lineage-head bump** (the head stays `1.3.0`; **no `agent-workflow-memory` change**). It inserts an empty slot at the Session-Protocols anchor if absent, preserves a customized slot verbatim, and STOPs (never edits) on a malformed slot or a missing / duplicate anchor. No-Node project: open `AGENTS.md`, and if there is no `<!-- workflow:methodology:start/end -->` pair, paste it right after the *Read it before any code change.* line and fill it from `tools/methodology-slot.md`.
132
+ 4. **Equal-head short-circuit.** If the stamp **equals** the head the lineage is up to date: **stop here** (the slot was already reconciled in step 3).
133
+ 5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
134
+ 6. Apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** safe to re-run.
135
+ 7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`.
136
+ 8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`1.3.0`, not the package version). Report changes; **ask before committing**.
130
137
 
131
138
  ### Mode: backends
132
139
 
@@ -207,6 +214,6 @@ Deploy these into `AGENTS.md`; remove rows that don't apply to the stack.
207
214
  - [`references/scripts/`](references/scripts/) — the Node enforcement scripts (caps + staleness + index-freshness gate, 3-tier archive, hook installer) and their unit tests.
208
215
  - [`migrations/`](migrations/) — per-version upgrade steps; see `migrations/README.md`.
209
216
  - [`launchers/`](launchers/) — run the bootstrapper from non-Claude agents (`SKILL.md` is a native Codex skill; a Devin Desktop workflow launcher + install script). See `launchers/README.md`.
210
- - [`tools/`](tools/) — the family-wide tooling the kit **owns and ships**: `manifest/{schema.md,validate.mjs}` (the `capability.json` schema + the validator the kit runs as the memory detector, and root CI invokes), `delegation.mjs` (the executable delegate/fallback decision + hand-off plan), `inject-methodology.mjs` + `methodology-slot.md` (the bounded slot injection), `detect-backends.mjs` (the read-only **backend detector** behind `/agent-workflow-kit backends`), and `release-scan.mjs` (the attribution-off release gate). See [`tools/manifest/schema.md`](tools/manifest/schema.md).
217
+ - [`tools/`](tools/) — the family-wide tooling the kit **owns and ships**: `manifest/{schema.md,validate.mjs}` (the `capability.json` schema + the validator the kit runs as the memory detector, and root CI invokes), `delegation.mjs` (the executable delegate/fallback decision + hand-off plan), `inject-methodology.mjs` + `methodology-slot.md` (the bounded slot reconciliation — ensure-slot / inject-if-empty / cap; the fragment is a byte-identical mirror of the `agent-workflow-engine` canon, pinned by `methodology-mirror.test.mjs`), `detect-backends.mjs` (the read-only **backend detector** behind `/agent-workflow-kit backends`), and `release-scan.mjs` (the attribution-off release gate). See [`tools/manifest/schema.md`](tools/manifest/schema.md).
211
218
  - [`capability.json`](capability.json) — the kit's own `agent-workflow` family manifest (`kind: composition-root`).
212
219
  - [`CHANGELOG.md`](CHANGELOG.md) — version history of this kernel.
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": "1.5.1",
6
+ "version": "1.6.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
@@ -11,7 +11,7 @@ releases add files/templates, which `upgrade` reconciles without a migration.
11
11
  2. Select every migration whose `<version>` is **strictly newer** than the stamp.
12
12
  3. Apply them in **ascending semver order**.
13
13
  4. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`1.3.0` today — the
14
- shared lineage, **not** this skill's package version `1.4.0`). A stamp greater than the head → STOP.
14
+ shared lineage, **not** this skill's npm package version). A stamp greater than the head → STOP.
15
15
 
16
16
  ## Authoring rules
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.5.1",
3
+ "version": "1.6.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",
@@ -53,7 +53,8 @@ All project knowledge lives in `docs/ai/`. Layered, lazy-loaded context:
53
53
 
54
54
  Start-of-session, during-work, and task-completion procedures live in [`docs/ai/agent_rules.md`](./docs/ai/agent_rules.md) §1. **Read it before any code change.**
55
55
 
56
- Planning (plan files, vocabulary, lifecycle, mandatory Cleanup) → the project's planning skill / `docs/ai/agent_rules.md` §"Planning Workflow".
56
+ <!-- workflow:methodology:start -->
57
+ <!-- workflow:methodology:end -->
57
58
 
58
59
  ---
59
60
 
@@ -83,10 +83,10 @@ export const handoffPlan = (delegate) =>
83
83
  : {
84
84
  mode: 'fallback',
85
85
  memoryWrites: [],
86
- // Fallback ships the kit's OWN AGENTS.md, which carries the methodology INLINE (no slot
87
- // markers) so injection is a deliberate no-op here. Label it as inline, not a "slot"
88
- // (the "slot" mechanism only exists in the delegate branch, on memory's AGENTS.md).
89
- kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md methodology (inline)', 'docs/ai/.workflow-version'],
86
+ // Fallback now ships the kit's OWN AGENTS.md carrying the EMPTY methodology slot (Plan 2);
87
+ // the kit reconciles it (ensure-slot + inject-because-empty) exactly like the delegate
88
+ // path so both paths end with a FILLED slot, not inline methodology.
89
+ kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md methodology slot', 'docs/ai/.workflow-version'],
90
90
  stampsPresent: ['.workflow-version'],
91
91
  memoryRaisesCommitGate: false,
92
92
  commitGate: 'kit-only-after-injection',
@@ -106,10 +106,11 @@ describe('handoffPlan — stamp sets + single commit gate', () => {
106
106
  assert.deepEqual(p.memoryWrites, []);
107
107
  assert.equal(p.memoryRaisesCommitGate, false);
108
108
  assert.equal(p.commitGate, 'kit-only-after-injection');
109
- // Fallback ships the kit's own AGENTS.md with methodology INLINE never a "slot" (no markers).
109
+ // Fallback now ships the kit's own AGENTS.md with the EMPTY methodology slot, which the kit
110
+ // reconciles + fills — the same slot mechanism as the delegate path (Plan 2).
110
111
  assert.ok(
111
- p.kitWrites.some((w) => w.includes('inline')) && !p.kitWrites.some((w) => w.includes('slot')),
112
- 'fallback kitWrites should describe inline methodology, not a slot',
112
+ p.kitWrites.some((w) => w.includes('slot')) && !p.kitWrites.some((w) => w.includes('inline')),
113
+ 'fallback kitWrites should describe the methodology slot, not inline methodology',
113
114
  );
114
115
  });
115
116
  });
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // Methodology slot injection — the composition root's only mutation of memory's AGENTS.md.
2
+ // Methodology slot injection + reconciliation — the composition root's only mutation of a
3
+ // deployed AGENTS.md.
3
4
  //
4
- // memory ships an EMPTY delimited slot in templates/AGENTS.md; the kit (which knows the whole
5
- // family) fills it. The engine only *provides* the methodology text Plan 2 repoints the
6
- // source to it. Phase 1 source = the kit's bundled tools/methodology-slot.md (a BOUNDED summary
7
- // + pointer, NOT the full references/planning.md), so AGENTS.md stays under its line cap.
5
+ // Both templates (memory's + the kit fallback) ship an EMPTY delimited slot; the kit (which knows
6
+ // the whole family) fills it. The bounded fragment (tools/methodology-slot.md) is a BOUNDED summary
7
+ // + pointer, NOT the full references/planning.md, so AGENTS.md stays under its line cap; it is a
8
+ // byte-identical MIRROR of the canonical text in agent-workflow-engine (drift-guarded).
8
9
  //
9
- // Marker contract (shared with memory's upgrade extract-and-reinsert), strictly enforced:
10
- // - exactly one ordered start→end pair → replace only the bytes between them.
11
- // - markers absent (legacy AGENTS.md) gracefully NO-OP (slot migration is Plan 2).
12
- // - any malformed state (single, reversed, nested, duplicate) → NO-OP WITH AN ERROR; never edit.
13
- // Prefix/suffix bytes are preserved exactly. Re-running with the same fragment is idempotent.
10
+ // Two layers over one marker parser:
11
+ // - injectMethodology fill an EXISTING slot. Marker contract, strictly enforced:
12
+ // exactly one ordered start→end pair replace only the bytes between them;
13
+ // markers absent → NO-OP; any malformed state (single, reversed, nested, duplicate) →
14
+ // NO-OP WITH AN ERROR, never edit. Prefix/suffix preserved exactly; re-run is idempotent.
15
+ // - ensureSlot / reconcileSlot — the bootstrap/upgrade policy (Plan 2): ensure the slot EXISTS
16
+ // (insert an empty pair at the Session-Protocols anchor when a legacy file lacks one) →
17
+ // inject ONLY IF empty (preserve a customized slot verbatim) → cap-check. Stamp-independent.
14
18
  //
15
19
  // Pure string functions (testable with byte-preservation fixtures); dependency-free, Node >= 18.
16
20
 
@@ -18,6 +22,10 @@ export const START_MARKER = '<!-- workflow:methodology:start -->';
18
22
  export const END_MARKER = '<!-- workflow:methodology:end -->';
19
23
  export const AGENTS_MD_CAP = 100; // the deployed AGENTS.md line budget (its own footer rule)
20
24
 
25
+ // Count lines independent of a trailing newline (CRLF-safe: split on '\n' — a CRLF line still ends
26
+ // in '\n', so the count is the same as for LF).
27
+ const lineCount = (text) => text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
28
+
21
29
  const countOccurrences = (haystack, needle) => {
22
30
  let count = 0;
23
31
  let from = 0;
@@ -58,14 +66,15 @@ export const injectMethodology = (text, fragment, { maxLines } = {}) => {
58
66
  const slot = findSlot(text);
59
67
  if (slot.state === 'absent') return { status: 'noop-absent', text };
60
68
  if (slot.state === 'malformed') return { status: 'error', text, error: slot.reason };
69
+ // Frame the fragment with the DOCUMENT's newline style (and convert the LF-canonical fragment to
70
+ // it) so injecting into a CRLF file does not leave lone LFs around the slot.
71
+ const nl = text.includes('\r\n') ? '\r\n' : '\n';
61
72
  const before = text.slice(0, slot.startIdx + START_MARKER.length);
62
73
  const after = text.slice(slot.endIdx);
63
- const out = `${before}\n${fragment.trim()}\n${after}`;
64
- if (maxLines != null) {
65
- const lines = out.split('\n').length - (out.endsWith('\n') ? 1 : 0);
66
- if (lines > maxLines) {
67
- return { status: 'error', text, error: `injection would push AGENTS.md to ${lines} lines (cap ${maxLines}) — trim the fragment or the file` };
68
- }
74
+ const body = fragment.trim().replace(/\r?\n/g, nl);
75
+ const out = `${before}${nl}${body}${nl}${after}`;
76
+ if (maxLines != null && lineCount(out) > maxLines) {
77
+ return { status: 'error', text, error: `injection would push AGENTS.md to ${lineCount(out)} lines (cap ${maxLines}) — trim the fragment or the file` };
69
78
  }
70
79
  return { status: 'injected', text: out };
71
80
  };
@@ -78,19 +87,120 @@ export const extractSlot = (text) => {
78
87
  return text.slice(slot.startIdx + START_MARKER.length, slot.endIdx);
79
88
  };
80
89
 
90
+ // The Session-Protocols anchor line both deployed templates carry (the agent_rules.md §1 sentence).
91
+ // ensureSlot inserts an empty slot right after this line when a legacy entry point has no markers.
92
+ // Contract: EXACTLY ONE match required — 0 or >1 → error (never guess where the methodology lives).
93
+ export const METHODOLOGY_ANCHOR = /^.*Read it before any code change\..*$/m;
94
+
95
+ // The canonical empty slot (an ordered start→end pair, nothing between) — what a fresh template
96
+ // ships and what ensureSlot inserts. LF form; ensureSlot rewrites the newline to match the document.
97
+ export const EMPTY_SLOT = `${START_MARKER}\n${END_MARKER}`;
98
+
99
+ const countMatches = (text, re) => (text.match(new RegExp(re.source, 'gm')) || []).length;
100
+
101
+ // Ensure a single, well-formed methodology slot EXISTS — without filling it. Pure; no fs.
102
+ // { status: 'present', text } a well-formed slot already exists → bytes unchanged (idempotent).
103
+ // { status: 'inserted', text } absent + exactly one anchor → an EMPTY slot inserted right after
104
+ // the anchor line, newline style + all other bytes preserved.
105
+ // { status: 'error', text, error } malformed slot, OR (when absent) 0/>1 anchors → bytes unchanged.
106
+ export const ensureSlot = (text) => {
107
+ const slot = findSlot(text);
108
+ if (slot.state === 'ok') return { status: 'present', text };
109
+ if (slot.state === 'malformed') return { status: 'error', text, error: slot.reason };
110
+ // absent → place an empty slot at the one anchor, or refuse rather than guess.
111
+ const anchors = countMatches(text, METHODOLOGY_ANCHOR);
112
+ if (anchors !== 1) {
113
+ return {
114
+ status: 'error',
115
+ text,
116
+ error: `expected exactly one methodology anchor (the "Read it before any code change." Session-Protocols line), found ${anchors} — refusing to guess where the slot belongs; add the slot markers manually`,
117
+ };
118
+ }
119
+ const nl = text.includes('\r\n') ? '\r\n' : '\n';
120
+ const match = text.match(METHODOLOGY_ANCHOR);
121
+ const eol = text.indexOf('\n', match.index);
122
+ const insertAt = eol === -1 ? text.length : eol + 1;
123
+ const block = `${nl}${EMPTY_SLOT.replace(/\n/g, nl)}${nl}`;
124
+ const out = `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
125
+ return { status: 'inserted', text: out };
126
+ };
127
+
128
+ // Bootstrap/upgrade reconciliation policy (pure): ensure the slot exists, then fill it ONLY IF it
129
+ // is empty (a filled/customized slot is preserved verbatim), enforcing the line cap — all as one
130
+ // step the CLI commits with a single atomic write. On ANY error the INPUT bytes are returned
131
+ // unchanged (the intermediate slot-insert is discarded), so there is no partial on-disk state.
132
+ // reconciled-inserted — slot was absent, inserted at the anchor, then filled.
133
+ // reconciled-filled — slot existed but was empty, now filled.
134
+ // present-filled — slot already carried content → preserved verbatim.
135
+ // error — malformed slot, 0/>1 anchors, or cap exceeded → input unchanged.
136
+ export const reconcileSlot = (text, fragment, { maxLines } = {}) => {
137
+ const ensured = ensureSlot(text);
138
+ if (ensured.status === 'error') return { status: 'error', text, error: ensured.error };
139
+ const current = extractSlot(ensured.text);
140
+ const isEmpty = current == null || current.trim() === '';
141
+ if (!isEmpty) {
142
+ // Preserve a filled/customized slot verbatim — but still enforce the cap on the result, so an
143
+ // already-over-cap entry point is surfaced (input unchanged) rather than silently accepted.
144
+ if (maxLines != null && lineCount(ensured.text) > maxLines) {
145
+ return { status: 'error', text, error: `AGENTS.md is ${lineCount(ensured.text)} lines (cap ${maxLines}) — trim the file (a customized methodology slot must still fit the cap)` };
146
+ }
147
+ return { status: 'present-filled', text: ensured.text };
148
+ }
149
+ const injected = injectMethodology(ensured.text, fragment, { maxLines });
150
+ if (injected.status !== 'injected') return { status: 'error', text, error: injected.error };
151
+ const status = ensured.status === 'inserted' ? 'reconciled-inserted' : 'reconciled-filled';
152
+ return { status, text: injected.text };
153
+ };
154
+
81
155
  const main = async (argv) => {
82
- const { readFile, writeFile, rename } = await import('node:fs/promises');
156
+ const { readFile, writeFile, rename, rm } = await import('node:fs/promises');
83
157
  const { dirname, basename, join, resolve } = await import('node:path');
84
158
  const { fileURLToPath } = await import('node:url');
85
159
  const here = dirname(fileURLToPath(import.meta.url));
86
- const agentsPath = argv[0];
160
+
161
+ // `reconcile <AGENTS.md> [fragment.md]` = ensure-slot + inject-if-empty + cap (bootstrap/upgrade);
162
+ // `<AGENTS.md> [fragment.md]` = the legacy inject-into-existing-slot mode.
163
+ const mode = argv[0] === 'reconcile' ? 'reconcile' : 'inject';
164
+ const rest = mode === 'reconcile' ? argv.slice(1) : argv;
165
+ const agentsPath = rest[0];
87
166
  if (!agentsPath) {
88
- console.error('usage: inject-methodology.mjs <path/to/AGENTS.md> [fragment.md]');
167
+ console.error('usage: inject-methodology.mjs [reconcile] <path/to/AGENTS.md> [fragment.md]');
89
168
  process.exit(2);
90
169
  }
91
- const fragmentPath = argv[1] ? resolve(argv[1]) : resolve(here, 'methodology-slot.md');
170
+ const fragmentPath = rest[1] ? resolve(rest[1]) : resolve(here, 'methodology-slot.md');
92
171
  const text = await readFile(resolve(agentsPath), 'utf8');
93
172
  const fragment = await readFile(fragmentPath, 'utf8');
173
+
174
+ const writeAtomic = async (out) => {
175
+ const tmp = join(dirname(resolve(agentsPath)), `.${basename(agentsPath)}.tmp-${process.pid}-${Date.now()}`);
176
+ try {
177
+ await writeFile(tmp, out, 'utf8');
178
+ await rename(tmp, resolve(agentsPath));
179
+ } catch (err) {
180
+ await rm(tmp, { force: true }).catch(() => {}); // never leave a temp file behind on failure
181
+ throw err;
182
+ }
183
+ };
184
+
185
+ if (mode === 'reconcile') {
186
+ const result = reconcileSlot(text, fragment, { maxLines: AGENTS_MD_CAP });
187
+ if (result.status === 'error') {
188
+ console.error(`[inject-methodology] reconcile refused — ${result.error}`);
189
+ process.exit(1);
190
+ }
191
+ if (result.status === 'present-filled') {
192
+ console.log('[inject-methodology] methodology slot already present and filled — nothing to do (zero-diff).');
193
+ return;
194
+ }
195
+ await writeAtomic(result.text);
196
+ const what =
197
+ result.status === 'reconciled-inserted'
198
+ ? 'inserted the methodology slot at the Session-Protocols anchor and filled it'
199
+ : 'filled the empty methodology slot';
200
+ console.log(`[inject-methodology] reconcile: ${what}.`);
201
+ return;
202
+ }
203
+
94
204
  const result = injectMethodology(text, fragment, { maxLines: AGENTS_MD_CAP });
95
205
  if (result.status === 'error') {
96
206
  console.error(`[inject-methodology] malformed slot — refusing to edit: ${result.error}`);
@@ -100,9 +210,7 @@ const main = async (argv) => {
100
210
  console.log('[inject-methodology] no methodology markers found — nothing to inject (legacy AGENTS.md).');
101
211
  return;
102
212
  }
103
- const tmp = join(dirname(resolve(agentsPath)), `.${basename(agentsPath)}.tmp-${process.pid}-${Date.now()}`);
104
- await writeFile(tmp, result.text, 'utf8');
105
- await rename(tmp, resolve(agentsPath));
213
+ await writeAtomic(result.text);
106
214
  console.log('[inject-methodology] injected the bounded methodology fragment into the slot.');
107
215
  };
108
216
 
@@ -1,22 +1,56 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { readFileSync } from 'node:fs';
3
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
7
+ import { execFileSync } from 'node:child_process';
6
8
  import {
7
9
  injectMethodology,
8
10
  findSlot,
9
11
  extractSlot,
12
+ ensureSlot,
13
+ reconcileSlot,
14
+ METHODOLOGY_ANCHOR,
15
+ EMPTY_SLOT,
16
+ AGENTS_MD_CAP,
10
17
  START_MARKER,
11
18
  END_MARKER,
12
19
  } from './inject-methodology.mjs';
13
20
 
14
21
  const HERE = dirname(fileURLToPath(import.meta.url));
22
+ const SCRIPT = join(HERE, 'inject-methodology.mjs');
15
23
  const FRAGMENT = readFileSync(join(HERE, 'methodology-slot.md'), 'utf8');
16
24
 
17
25
  const wrap = (inner) =>
18
26
  `# AGENTS.md\n\nprefix bytes\n\n## Session Protocols\n\nintro line.\n\n${START_MARKER}${inner}${END_MARKER}\n\n## Hard Constraints\n\nsuffix bytes\n`;
19
27
 
28
+ // The exact Session-Protocols line both deployed templates carry — the slot anchor.
29
+ const ANCHOR_LINE =
30
+ 'Start-of-session, during-work, and task-completion procedures live in [`docs/ai/agent_rules.md`](./docs/ai/agent_rules.md) §1. **Read it before any code change.**';
31
+
32
+ // A pre-slot (markerless) entry point that still carries the Session-Protocols anchor line —
33
+ // the realistic shape of a legacy deployment the upgrade reconciliation must add a slot to.
34
+ const legacyWithAnchor = (nl = '\n') =>
35
+ [
36
+ '# AGENTS.md',
37
+ '',
38
+ 'prefix bytes',
39
+ '',
40
+ '## 🚀 Session Protocols',
41
+ '',
42
+ ANCHOR_LINE,
43
+ '',
44
+ '---',
45
+ '',
46
+ '## 🚫 Hard Constraints',
47
+ '',
48
+ 'suffix bytes',
49
+ '',
50
+ ].join(nl);
51
+
52
+ const countMatches = (text, re) => (text.match(new RegExp(re.source, 'gm')) || []).length;
53
+
20
54
  describe('findSlot — marker classification', () => {
21
55
  it('one ordered pair → ok', () => {
22
56
  assert.equal(findSlot(wrap('\n')).state, 'ok');
@@ -122,3 +156,200 @@ describe('post-injection cap — AGENTS.md stays under its line budget', () => {
122
156
  assert.ok(lines <= 100, `AGENTS.md would be ${lines} lines after injection (cap 100)`);
123
157
  });
124
158
  });
159
+
160
+ describe('METHODOLOGY_ANCHOR — locating the Session-Protocols slot position', () => {
161
+ it('matches exactly one line in a deployed-style markerless entry point', () => {
162
+ assert.equal(countMatches(legacyWithAnchor(), METHODOLOGY_ANCHOR), 1);
163
+ });
164
+ it('matches exactly one line in BOTH shipped templates', () => {
165
+ const memTmpl = readFileSync(
166
+ join(HERE, '..', '..', 'agent-workflow-memory', 'references', 'templates', 'AGENTS.md'),
167
+ 'utf8',
168
+ );
169
+ const kitTmpl = readFileSync(join(HERE, '..', 'references', 'templates', 'AGENTS.md'), 'utf8');
170
+ assert.equal(countMatches(memTmpl, METHODOLOGY_ANCHOR), 1, 'memory template has exactly one anchor');
171
+ assert.equal(countMatches(kitTmpl, METHODOLOGY_ANCHOR), 1, 'kit fallback template has exactly one anchor');
172
+ });
173
+ it('does not match an entry point that lacks the Session-Protocols line', () => {
174
+ assert.equal(countMatches('# AGENTS.md\nno session protocols here\n', METHODOLOGY_ANCHOR), 0);
175
+ });
176
+ });
177
+
178
+ describe('EMPTY_SLOT — the canonical empty marker pair', () => {
179
+ it('is exactly the start+end markers joined by a newline', () => {
180
+ assert.equal(EMPTY_SLOT, `${START_MARKER}\n${END_MARKER}`);
181
+ assert.equal(findSlot(EMPTY_SLOT).state, 'ok');
182
+ assert.equal(extractSlot(EMPTY_SLOT).trim(), '');
183
+ });
184
+ });
185
+
186
+ describe('ensureSlot — idempotent slot presence (insert at the anchor only when absent)', () => {
187
+ it('present (one ok pair) → status present, bytes unchanged', () => {
188
+ const input = wrap('\nfilled\n');
189
+ const out = ensureSlot(input);
190
+ assert.equal(out.status, 'present');
191
+ assert.equal(out.text, input);
192
+ });
193
+
194
+ it('malformed slot → status error, never edits', () => {
195
+ const input = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
196
+ const out = ensureSlot(input);
197
+ assert.equal(out.status, 'error');
198
+ assert.equal(out.text, input);
199
+ });
200
+
201
+ it('absent + exactly one anchor → inserts EMPTY_SLOT after the anchor line, preserving bytes', () => {
202
+ const input = legacyWithAnchor();
203
+ const out = ensureSlot(input);
204
+ assert.equal(out.status, 'inserted');
205
+ assert.equal(findSlot(out.text).state, 'ok', 'a well-formed slot now exists');
206
+ assert.equal(extractSlot(out.text).trim(), '', 'the inserted slot is empty');
207
+ // the slot lands right after the anchor line
208
+ assert.match(out.text, new RegExp(`Read it before any code change\\.\\*\\*\\n+${START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
209
+ // every original line survives
210
+ for (const line of ['# AGENTS.md', 'prefix bytes', ANCHOR_LINE, '## 🚫 Hard Constraints', 'suffix bytes']) {
211
+ assert.ok(out.text.includes(line), `original line preserved: ${line}`);
212
+ }
213
+ });
214
+
215
+ it('absent + zero anchor → status error with an actionable message, never edits', () => {
216
+ const input = '# AGENTS.md\n\nno anchor at all\n';
217
+ const out = ensureSlot(input);
218
+ assert.equal(out.status, 'error');
219
+ assert.equal(out.text, input);
220
+ assert.match(out.error, /anchor/i);
221
+ });
222
+
223
+ it('absent + multiple anchors → status error (refuses to guess), never edits', () => {
224
+ const input = `${ANCHOR_LINE}\n\nsome text\n\n${ANCHOR_LINE}\n`;
225
+ const out = ensureSlot(input);
226
+ assert.equal(out.status, 'error');
227
+ assert.equal(out.text, input);
228
+ });
229
+
230
+ it('preserves CRLF newline style when inserting', () => {
231
+ const input = legacyWithAnchor('\r\n');
232
+ const out = ensureSlot(input);
233
+ assert.equal(out.status, 'inserted');
234
+ assert.equal(findSlot(out.text).state, 'ok');
235
+ assert.ok(out.text.includes(`${START_MARKER}\r\n${END_MARKER}`), 'markers use CRLF');
236
+ assert.ok(!/[^\r]\n/.test(out.text), 'no lone LF introduced into a CRLF document');
237
+ });
238
+
239
+ it('is idempotent — a second ensureSlot finds the slot present and changes nothing', () => {
240
+ const once = ensureSlot(legacyWithAnchor()).text;
241
+ const twice = ensureSlot(once);
242
+ assert.equal(twice.status, 'present');
243
+ assert.equal(twice.text, once);
244
+ });
245
+ });
246
+
247
+ describe('reconcileSlot — ensure + inject-if-empty + cap, as one atomic policy', () => {
248
+ it('markerless legacy (with anchor) → reconciled-inserted, slot filled, outside bytes preserved', () => {
249
+ const input = legacyWithAnchor();
250
+ const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
251
+ assert.equal(out.status, 'reconciled-inserted');
252
+ assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim(), 'slot now carries the fragment');
253
+ assert.ok(out.text.includes(ANCHOR_LINE), 'anchor preserved');
254
+ assert.ok(out.text.includes('## 🚫 Hard Constraints'), 'suffix preserved');
255
+ });
256
+
257
+ it('present empty slot → reconciled-filled', () => {
258
+ const out = reconcileSlot(wrap('\n'), FRAGMENT, { maxLines: AGENTS_MD_CAP });
259
+ assert.equal(out.status, 'reconciled-filled');
260
+ assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim());
261
+ });
262
+
263
+ it('present customized/filled slot → present-filled, preserved verbatim (byte-for-byte)', () => {
264
+ const custom = wrap('\nuser-authored methodology notes\nsecond line\n');
265
+ const out = reconcileSlot(custom, FRAGMENT, { maxLines: AGENTS_MD_CAP });
266
+ assert.equal(out.status, 'present-filled');
267
+ assert.equal(out.text, custom, 'a filled slot is never overwritten');
268
+ });
269
+
270
+ it('malformed slot → error, input returned byte-for-byte', () => {
271
+ const input = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
272
+ const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
273
+ assert.equal(out.status, 'error');
274
+ assert.equal(out.text, input);
275
+ });
276
+
277
+ it('markerless with no anchor → error, input unchanged (never guesses placement)', () => {
278
+ const input = '# AGENTS.md\n\nno slot, no anchor\n';
279
+ const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
280
+ assert.equal(out.status, 'error');
281
+ assert.equal(out.text, input);
282
+ });
283
+
284
+ it('over-cap result → error, input unchanged (atomic: discards the intermediate slot insert)', () => {
285
+ const input = legacyWithAnchor();
286
+ const out = reconcileSlot(input, FRAGMENT, { maxLines: 5 });
287
+ assert.equal(out.status, 'error');
288
+ assert.equal(out.text, input);
289
+ });
290
+
291
+ it('produces a clean CRLF document when inserting + filling (no lone LF)', () => {
292
+ // ensureSlot preserves the document newline style for the markers, and injectMethodology frames
293
+ // the LF-canonical fragment with the document's EOL — so a CRLF document stays uniformly CRLF.
294
+ const input = legacyWithAnchor('\r\n');
295
+ const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
296
+ assert.equal(out.status, 'reconciled-inserted');
297
+ assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim());
298
+ assert.ok(out.text.includes(`${ANCHOR_LINE}\r\n`), 'anchor line keeps CRLF');
299
+ assert.ok(!/[^\r]\n/.test(out.text), 'no lone LF introduced into a CRLF document');
300
+ });
301
+
302
+ it('present filled slot over the line cap → error, input returned unchanged', () => {
303
+ const bigSlot = `\n${Array.from({ length: 30 }, (_, i) => `note ${i}`).join('\n')}\n`;
304
+ const filled = wrap(bigSlot);
305
+ const out = reconcileSlot(filled, FRAGMENT, { maxLines: 10 });
306
+ assert.equal(out.status, 'error');
307
+ assert.equal(out.text, filled, 'an over-cap entry point is surfaced, not silently accepted');
308
+ assert.match(out.error, /cap 10/);
309
+ });
310
+ });
311
+
312
+ describe('reconcile CLI — atomic ensure+inject-if-empty+cap on the real filesystem', () => {
313
+ const withTempAgents = (contents, run) => {
314
+ const dir = mkdtempSync(join(tmpdir(), 'reconcile-cli-'));
315
+ const agents = join(dir, 'AGENTS.md');
316
+ writeFileSync(agents, contents);
317
+ try {
318
+ return run(agents);
319
+ } finally {
320
+ rmSync(dir, { recursive: true, force: true });
321
+ }
322
+ };
323
+
324
+ it('markerless legacy (with anchor) → slot inserted and filled (exit 0)', () => {
325
+ withTempAgents(legacyWithAnchor(), (agents) => {
326
+ execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe' });
327
+ const out = readFileSync(agents, 'utf8');
328
+ assert.equal(findSlot(out).state, 'ok');
329
+ assert.equal(extractSlot(out).trim(), FRAGMENT.trim());
330
+ });
331
+ });
332
+
333
+ it('present empty slot → slot filled (exit 0)', () => {
334
+ withTempAgents(wrap('\n'), (agents) => {
335
+ execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe' });
336
+ assert.equal(extractSlot(readFileSync(agents, 'utf8')).trim(), FRAGMENT.trim());
337
+ });
338
+ });
339
+
340
+ it('filled/customized slot → file left byte-for-byte untouched', () => {
341
+ const custom = wrap('\nuser notes\n');
342
+ withTempAgents(custom, (agents) => {
343
+ execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe' });
344
+ assert.equal(readFileSync(agents, 'utf8'), custom);
345
+ });
346
+ });
347
+
348
+ it('malformed slot → STOP with non-zero exit, file byte-unchanged', () => {
349
+ const malformed = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
350
+ withTempAgents(malformed, (agents) => {
351
+ assert.throws(() => execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe' }));
352
+ assert.equal(readFileSync(agents, 'utf8'), malformed);
353
+ });
354
+ });
355
+ });