@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 +50 -0
- package/README.md +104 -33
- package/SKILL.md +27 -20
- package/capability.json +1 -1
- package/migrations/README.md +1 -1
- package/package.json +1 -1
- package/references/templates/AGENTS.md +2 -1
- package/tools/delegation.mjs +4 -4
- package/tools/delegation.test.mjs +4 -3
- package/tools/inject-methodology.mjs +131 -23
- package/tools/inject-methodology.test.mjs +232 -1
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
|
-
**
|
|
5
|
+
**Durable, cross-agent memory & workflow for AI coding agents — the one command that installs it.**
|
|
6
6
|
|
|
7
|
-
*
|
|
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
|
[](https://www.npmjs.com/package/@sabaiway/agent-workflow-kit)
|
|
11
12
|
[](https://www.npmjs.com/package/@sabaiway/agent-workflow-kit)
|
|
12
13
|
[](./LICENSE)
|
|
13
14
|
[](https://nodejs.org)
|
|
14
15
|
|
|
15
|
-
`Node ≥ 18` · `dependency-free` · `
|
|
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
|
|
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
|
-
|
|
102
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
> **
|
|
125
|
-
>
|
|
126
|
-
> If
|
|
127
|
-
>
|
|
128
|
-
> working with no new dependency. Same
|
|
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
|
-
|
|
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
|
|
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,
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
├── tools/ ← family tooling:
|
|
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
|
|
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.
|
|
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
|
|
53
|
-
methodology slot** (below) and writes the kit-fallback
|
|
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
|
|
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
|
|
60
|
-
exists,
|
|
61
|
-
`node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs <project>/AGENTS.md`.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 (
|
|
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
|
|
126
|
-
3.
|
|
127
|
-
4.
|
|
128
|
-
5.
|
|
129
|
-
6.
|
|
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
|
|
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
package/migrations/README.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
56
|
+
<!-- workflow:methodology:start -->
|
|
57
|
+
<!-- workflow:methodology:end -->
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
package/tools/delegation.mjs
CHANGED
|
@@ -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
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md methodology
|
|
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
|
|
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('
|
|
112
|
-
'fallback kitWrites should describe
|
|
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
|
|
2
|
+
// Methodology slot injection + reconciliation — the composition root's only mutation of a
|
|
3
|
+
// deployed AGENTS.md.
|
|
3
4
|
//
|
|
4
|
-
// memory
|
|
5
|
-
// family) fills it. The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// Prefix/suffix
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
});
|