@sabaiway/agent-workflow-kit 1.10.0 → 1.12.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.
@@ -0,0 +1,372 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { join, resolve } from 'node:path';
4
+ import {
5
+ buildPlan,
6
+ executePlan,
7
+ formatPlan,
8
+ parseArgs,
9
+ SAFE_REMOVE,
10
+ MANAGED_MARKER,
11
+ REPORT_ONLY,
12
+ STOP,
13
+ UNINSTALL_STOP,
14
+ } from './uninstall.mjs';
15
+ import { OK } from './family-registry.mjs';
16
+
17
+ // ── synthetic family rows (the surveyFamily shape) ─────────────────────────────
18
+ const row = (name, kind, over = {}) => ({
19
+ name, kind, installed: true, skillDir: `/skills/${name}`, manifestState: OK, version: '1.0.0', ...over,
20
+ });
21
+
22
+ const KIT = row('agent-workflow-kit', 'composition-root');
23
+ const MEMORY = row('agent-workflow-memory', 'memory-substrate');
24
+ const ENGINE = row('agent-workflow-engine', 'methodology-engine');
25
+ const CODEX = row('codex-cli-bridge', 'execution-backend');
26
+ const ANTIGRAVITY = row('antigravity-cli-bridge', 'execution-backend');
27
+
28
+ const find = (items, surface, member) => items.find((i) => i.surface === surface && (member ? i.member === member : true));
29
+
30
+ // A path-keyed mock fs. `symlinks` maps a path → its (absolute) link target; `files` maps path →
31
+ // contents; `dirs` is a set of present directories. realpath is identity (no symlinked bindir here).
32
+ const mockFs = ({ symlinks = {}, files = {}, dirs = [], manifests = {} } = {}) => {
33
+ const enoent = (p) => Object.assign(new Error(`ENOENT: ${p}`), { code: 'ENOENT' });
34
+ const present = (p) => p in symlinks || p in files || dirs.includes(p);
35
+ return {
36
+ exists: (p) => present(p),
37
+ stat: (p) => ({ isFile: () => p in files, isDirectory: () => dirs.includes(p) }),
38
+ lstat: (p) => {
39
+ if (p in symlinks) return { isSymbolicLink: () => true, isFile: () => false };
40
+ if (present(p)) return { isSymbolicLink: () => false, isFile: () => p in files };
41
+ throw enoent(p);
42
+ },
43
+ readlink: (p) => { if (p in symlinks) return symlinks[p]; throw enoent(p); },
44
+ readFile: (p) => { if (p in files) return files[p]; throw enoent(p); },
45
+ realpath: (p) => p,
46
+ readManifest: (skillDir) => { if (skillDir in manifests) return manifests[skillDir]; throw enoent(skillDir); },
47
+ };
48
+ };
49
+
50
+ const CODEX_MANIFEST = {
51
+ name: 'codex-cli-bridge', kind: 'execution-backend',
52
+ roles: {
53
+ execute: { cmd: 'codex-exec', source: 'bin/codex-exec.sh' },
54
+ review: { cmd: 'codex-review', source: 'bin/codex-review.sh' },
55
+ },
56
+ };
57
+
58
+ // ── buildPlan: SKILL axis ──────────────────────────────────────────────────────
59
+
60
+ describe('buildPlan — skill axis', () => {
61
+ it('plans a proven-managed composition-root for removal, with no shared-global warning', () => {
62
+ const { items } = buildPlan({ family: [KIT] }, mockFs());
63
+ const skill = find(items, 'skill');
64
+ assert.equal(skill.class, SAFE_REMOVE);
65
+ assert.equal(skill.path, '/skills/agent-workflow-kit');
66
+ assert.equal(skill.warn, null);
67
+ });
68
+
69
+ it('warns that a shared global (memory/engine/bridge) may be used by other projects', () => {
70
+ const { items } = buildPlan({ family: [MEMORY, ENGINE] }, mockFs());
71
+ assert.match(find(items, 'skill', 'agent-workflow-memory').warn, /GLOBAL skill/);
72
+ assert.match(find(items, 'skill', 'agent-workflow-engine').warn, /GLOBAL skill/);
73
+ });
74
+
75
+ it('STOPs (never removes) a present-but-not-ours skill dir', () => {
76
+ const foreign = row('agent-workflow-kit', 'composition-root', { manifestState: 'foreign' });
77
+ const skill = find(buildPlan({ family: [foreign] }, mockFs()).items, 'skill');
78
+ assert.equal(skill.class, STOP);
79
+ assert.match(skill.reason, /not provably ours/);
80
+ });
81
+
82
+ it('skips a member that is not installed', () => {
83
+ const { items } = buildPlan({ family: [row('agent-workflow-engine', 'methodology-engine', { installed: false, skillDir: null, manifestState: 'not-installed' })] }, mockFs());
84
+ assert.equal(items.length, 0);
85
+ });
86
+
87
+ it('limits to a single member when `member` is given', () => {
88
+ const { items } = buildPlan({ family: [KIT, MEMORY, ENGINE], member: 'agent-workflow-memory' }, mockFs());
89
+ assert.deepEqual(items.map((i) => i.member), ['agent-workflow-memory']);
90
+ });
91
+ });
92
+
93
+ // ── buildPlan: bridge wrappers ─────────────────────────────────────────────────
94
+
95
+ describe('buildPlan — bridge wrappers', () => {
96
+ const bindir = '/home/u/.local/bin';
97
+ const skillDir = '/skills/codex-cli-bridge';
98
+
99
+ it('reverses a wrapper symlink that points at our source (managed-marker)', () => {
100
+ const fs = mockFs({
101
+ manifests: { [skillDir]: CODEX_MANIFEST },
102
+ symlinks: {
103
+ [join(bindir, 'codex-exec')]: join(skillDir, 'bin/codex-exec.sh'),
104
+ [join(bindir, 'codex-review')]: join(skillDir, 'bin/codex-review.sh'),
105
+ },
106
+ });
107
+ const { items } = buildPlan({ family: [CODEX], bindir }, fs);
108
+ const wrappers = items.filter((i) => i.surface === 'wrapper');
109
+ assert.equal(wrappers.length, 2);
110
+ assert.ok(wrappers.every((w) => w.class === MANAGED_MARKER));
111
+ assert.equal(find(wrappers, 'wrapper').expectedSrc, join(skillDir, 'bin/codex-exec.sh'));
112
+ });
113
+
114
+ it('STOPs on a foreign wrapper symlink (points elsewhere) — never removed', () => {
115
+ const fs = mockFs({
116
+ manifests: { [skillDir]: CODEX_MANIFEST },
117
+ symlinks: {
118
+ [join(bindir, 'codex-exec')]: '/somewhere/else/codex-exec',
119
+ [join(bindir, 'codex-review')]: join(skillDir, 'bin/codex-review.sh'),
120
+ },
121
+ });
122
+ const wrappers = buildPlan({ family: [CODEX], bindir }, fs).items.filter((i) => i.surface === 'wrapper');
123
+ assert.equal(wrappers.find((w) => w.path.endsWith('codex-exec')).class, STOP);
124
+ assert.equal(wrappers.find((w) => w.path.endsWith('codex-review')).class, MANAGED_MARKER);
125
+ });
126
+
127
+ it('emits no wrapper item when the link is absent', () => {
128
+ const fs = mockFs({ manifests: { [skillDir]: CODEX_MANIFEST } }); // no symlinks present
129
+ const wrappers = buildPlan({ family: [CODEX], bindir }, fs).items.filter((i) => i.surface === 'wrapper');
130
+ assert.equal(wrappers.length, 0);
131
+ });
132
+ });
133
+
134
+ // ── buildPlan: project deploy axis ─────────────────────────────────────────────
135
+
136
+ describe('buildPlan — project deploy axis', () => {
137
+ const dir = '/proj';
138
+ const project = { dir, deployed: true, docsAiPresent: true, hiddenFence: true, stamps: [] };
139
+
140
+ const projectFs = (extra = {}) => mockFs({
141
+ files: {
142
+ [join(dir, '.git/hooks/pre-commit')]: '#!/usr/bin/env bash\n# myproj:install-git-hooks.mjs\nset -e\n',
143
+ [join(dir, '.claude/settings.json')]: '{ "includeCoAuthoredBy": false }',
144
+ ...extra.files,
145
+ },
146
+ dirs: [join(dir, 'docs/ai'), join(dir, 'docs/plans'), ...(extra.dirs ?? [])],
147
+ });
148
+
149
+ it('plans the hidden fence + marker hook as managed-marker reversals', () => {
150
+ const { items } = buildPlan({ family: [], project, projectDir: dir }, projectFs());
151
+ assert.equal(find(items, 'fence').class, MANAGED_MARKER);
152
+ assert.equal(find(items, 'hook').class, MANAGED_MARKER);
153
+ });
154
+
155
+ it('reports (never removes) an UNMARKED pre-commit hook', () => {
156
+ const fs = mockFs({ files: { [join(dir, '.git/hooks/pre-commit')]: '#!/bin/sh\necho mine\n' } });
157
+ const hook = find(buildPlan({ family: [], project: { ...project, hiddenFence: false }, projectDir: dir }, fs).items, 'hook');
158
+ assert.equal(hook.class, REPORT_ONLY);
159
+ });
160
+
161
+ it('reports the settings.json includeCoAuthoredBy edit (never auto-edits)', () => {
162
+ const settings = buildPlan({ family: [], project, projectDir: dir }, projectFs()).items.find((i) => i.surface === 'settings');
163
+ assert.equal(settings.class, REPORT_ONLY);
164
+ });
165
+
166
+ it('reports docs/ai, AGENTS.md, CLAUDE.md, docs/plans as never-deleted', () => {
167
+ const fs = projectFs({ files: { [join(dir, 'AGENTS.md')]: 'x', [join(dir, 'CLAUDE.md')]: 'x' } });
168
+ const docs = buildPlan({ family: [], project, projectDir: dir }, fs).items.filter((i) => i.surface === 'docs');
169
+ const paths = docs.map((d) => d.path).sort();
170
+ assert.deepEqual(paths, [join(dir, 'AGENTS.md'), join(dir, 'CLAUDE.md'), join(dir, 'docs/ai'), join(dir, 'docs/plans')].sort());
171
+ assert.ok(docs.every((d) => d.class === REPORT_ONLY));
172
+ });
173
+
174
+ it('formatPlan prints rm + git rm guidance for the report-only set', () => {
175
+ const plan = buildPlan({ family: [], project, projectDir: dir }, projectFs());
176
+ const out = formatPlan(plan);
177
+ assert.match(out, /KEEP \(do by hand\)/);
178
+ assert.match(out, /git rm -r --cached/);
179
+ });
180
+ });
181
+
182
+ // ── executePlan: guarded mutation ──────────────────────────────────────────────
183
+
184
+ describe('executePlan — guarded', () => {
185
+ const okClassify = (reg) => ({ installed: true, manifestState: OK, skillDir: `/skills/${reg.name}` });
186
+
187
+ const spyDeps = (over = {}) => {
188
+ const calls = { removeTree: [], unlink: [], unhide: [], rmFile: [] };
189
+ return {
190
+ calls,
191
+ deps: {
192
+ classify: over.classify ?? okClassify,
193
+ removeTree: (p) => { calls.removeTree.push(p); return 'removed'; },
194
+ unlink: (p) => { calls.unlink.push(p); return 'unlinked'; },
195
+ hideFootprint: (opts) => { calls.unhide.push(opts); return { action: 'unhidden' }; },
196
+ rmFile: (p) => { calls.rmFile.push(p); },
197
+ // fs for the wrapper preflight inspect (report 'ours') + the hook marker re-check (present + marked).
198
+ lstat: () => ({ isSymbolicLink: () => true, isFile: () => false }),
199
+ readlink: (p) => p.replace('/home/u/.local/bin/codex-exec', '/skills/codex-cli-bridge/bin/codex-exec.sh'),
200
+ realpath: (p) => p,
201
+ exists: () => true,
202
+ readFile: () => '#!/usr/bin/env bash\n# myproj:install-git-hooks.mjs\nset -e\n',
203
+ ...over.deps,
204
+ },
205
+ };
206
+ };
207
+
208
+ const fullPlan = () => ({
209
+ projectDir: '/proj',
210
+ items: [
211
+ { surface: 'skill', member: 'agent-workflow-kit', path: '/skills/agent-workflow-kit', class: SAFE_REMOVE },
212
+ { surface: 'wrapper', member: 'codex-cli-bridge', path: '/home/u/.local/bin/codex-exec', expectedSrc: '/skills/codex-cli-bridge/bin/codex-exec.sh', class: MANAGED_MARKER },
213
+ { surface: 'fence', path: '/proj/.git/info/exclude', class: MANAGED_MARKER },
214
+ { surface: 'hook', path: '/proj/.git/hooks/pre-commit', class: MANAGED_MARKER },
215
+ { surface: 'docs', path: '/proj/docs/ai', class: REPORT_ONLY },
216
+ ],
217
+ });
218
+
219
+ it('--dry-run mutates nothing', () => {
220
+ const { calls, deps } = spyDeps();
221
+ const r = executePlan(fullPlan(), { dryRun: true }, deps);
222
+ assert.equal(r.applied, false);
223
+ assert.deepEqual([calls.removeTree, calls.unlink, calls.unhide, calls.rmFile], [[], [], [], []]);
224
+ });
225
+
226
+ it('without --yes mutates nothing (awaiting consent)', () => {
227
+ const { calls, deps } = spyDeps();
228
+ const r = executePlan(fullPlan(), {}, deps);
229
+ assert.equal(r.applied, false);
230
+ assert.equal(calls.removeTree.length, 0);
231
+ });
232
+
233
+ it('with --yes applies the auto-removable set and never touches report-only', () => {
234
+ const { calls, deps } = spyDeps();
235
+ const r = executePlan(fullPlan(), { yes: true }, deps);
236
+ assert.equal(r.applied, true);
237
+ assert.deepEqual(calls.removeTree, ['/skills/agent-workflow-kit']);
238
+ assert.deepEqual(calls.unlink, ['/home/u/.local/bin/codex-exec']);
239
+ // The fence is unhidden once for real (mutate) after being validated by a dry-run unhide (preflight).
240
+ assert.ok(calls.unhide.some((o) => o.dryRun === true), 'fence validated by a dry-run unhide in preflight');
241
+ assert.ok(calls.unhide.some((o) => !o.dryRun), 'fence unhidden for real in the mutate phase');
242
+ assert.deepEqual(calls.rmFile, ['/proj/.git/hooks/pre-commit']);
243
+ assert.equal(r.unhidden, true);
244
+ assert.equal(r.hookRemoved, true);
245
+ assert.equal(r.reported.length, 1); // the docs item, untouched
246
+ });
247
+
248
+ it('preflight STOPs (zero mutation) when a skill dir is no longer provably ours', () => {
249
+ const { calls, deps } = spyDeps({ classify: () => ({ installed: true, manifestState: 'foreign', skillDir: '/skills/agent-workflow-kit' }) });
250
+ assert.throws(() => executePlan(fullPlan(), { yes: true }, deps), (err) => err.code === UNINSTALL_STOP);
251
+ assert.deepEqual([calls.removeTree, calls.unlink], [[], []]); // nothing mutated
252
+ });
253
+
254
+ it('preflight STOPs (zero mutation) when a wrapper turned foreign', () => {
255
+ const { calls, deps } = spyDeps({ deps: { readlink: () => '/somewhere/foreign' } });
256
+ assert.throws(() => executePlan(fullPlan(), { yes: true }, deps), (err) => err.code === UNINSTALL_STOP);
257
+ assert.equal(calls.removeTree.length, 0);
258
+ });
259
+
260
+ it('a wrapper that merely VANISHED is benign — teardown proceeds (no abort)', () => {
261
+ const enoent = () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); };
262
+ const { calls, deps } = spyDeps({ deps: { lstat: (p) => (p.endsWith('codex-exec') ? enoent() : { isSymbolicLink: () => true, isFile: () => false }) } });
263
+ const r = executePlan(fullPlan(), { yes: true }, deps);
264
+ assert.equal(r.applied, true);
265
+ assert.deepEqual(calls.removeTree, ['/skills/agent-workflow-kit']); // skill still removed
266
+ });
267
+
268
+ it('refuses (zero mutation) when the pre-commit hook lost OUR marker since the plan', () => {
269
+ const { calls, deps } = spyDeps({ deps: { readFile: () => '#!/bin/sh\n# the user rewrote this hook\n' } }); // no marker
270
+ assert.throws(() => executePlan(fullPlan(), { yes: true }, deps), (err) => err.code === UNINSTALL_STOP);
271
+ assert.deepEqual([calls.removeTree, calls.rmFile], [[], []]); // nothing mutated
272
+ });
273
+
274
+ const planWithStop = () => ({
275
+ projectDir: '/proj',
276
+ items: [
277
+ { surface: 'skill', member: 'agent-workflow-kit', path: '/skills/agent-workflow-kit', class: SAFE_REMOVE },
278
+ { surface: 'wrapper', member: 'codex-cli-bridge', path: '/home/u/.local/bin/codex-exec', class: STOP, reason: 'foreign symlink' },
279
+ ],
280
+ });
281
+
282
+ it('a plan-time STOP (a not-ours surface) is reported + LEFT; the teardown still removes what IS ours (per-item, not global-abort)', () => {
283
+ const { calls, deps } = spyDeps();
284
+ const r = executePlan(planWithStop(), { yes: true }, deps);
285
+ assert.equal(r.applied, true);
286
+ assert.deepEqual(calls.removeTree, ['/skills/agent-workflow-kit']); // ours removed
287
+ assert.deepEqual(calls.unlink, []); // the foreign wrapper (STOP) is never touched
288
+ assert.ok(r.reported.some((i) => i.class === STOP), 'the STOP surface is surfaced, not silently dropped');
289
+ });
290
+
291
+ it('--dry-run never mutates, even with a STOP present', () => {
292
+ const { calls, deps } = spyDeps();
293
+ const r = executePlan(planWithStop(), { dryRun: true }, deps);
294
+ assert.equal(r.applied, false);
295
+ assert.deepEqual([calls.removeTree, calls.unlink], [[], []]);
296
+ });
297
+
298
+ it('a malformed fence is caught by the preflight dry-run unhide → abort before any mutation (codex #2)', () => {
299
+ const { calls, deps } = spyDeps({ deps: { hideFootprint: (opts) => { if (opts.dryRun) throw new Error('malformed managed block'); return { action: 'unhidden' }; } } });
300
+ assert.throws(() => executePlan(fullPlan(), { yes: true }, deps), (err) => err.code === UNINSTALL_STOP);
301
+ assert.deepEqual([calls.removeTree, calls.unlink, calls.rmFile], [[], [], []]); // fence threw in preflight → nothing mutated
302
+ });
303
+ });
304
+
305
+ // ── formatPlan: report-only guidance (codex #4) ─────────────────────────────────
306
+
307
+ describe('formatPlan — report-only guidance', () => {
308
+ it('settings.json gets EDIT guidance (remove the key), never `rm`', () => {
309
+ const plan = { projectDir: '/proj', items: [{ surface: 'settings', path: '/proj/.claude/settings.json', class: REPORT_ONLY, reason: 'x' }] };
310
+ const out = formatPlan(plan);
311
+ assert.match(out, /edit .*settings\.json.* remove the "includeCoAuthoredBy"/);
312
+ assert.ok(!/rm -rf .*settings\.json/.test(out), 'settings.json is never rm-ed');
313
+ });
314
+
315
+ it('paths are shell-quoted in the printed rm/git-rm commands', () => {
316
+ const plan = { projectDir: '/p', items: [{ surface: 'docs', path: '/p/docs/ai', class: REPORT_ONLY, reason: 'x' }] };
317
+ const out = formatPlan(plan);
318
+ assert.match(out, /rm -rf '\/p\/docs\/ai'/);
319
+ assert.match(out, /git rm -r --cached '\/p\/docs\/ai'/);
320
+ });
321
+ });
322
+
323
+ // ── buildPlan: an underivable bridge manifest → STOP, not a silent half-removal (codex #3) ──────────
324
+
325
+ describe('buildPlan — underivable bridge', () => {
326
+ it('emits a STOP for the skill (not SAFE_REMOVE) when deriveLinks throws on the bridge manifest', () => {
327
+ const throwingFs = {
328
+ readManifest: () => { throw new Error('corrupt manifest'); },
329
+ };
330
+ const codex = row('codex-cli-bridge', 'execution-backend');
331
+ const items = buildPlan({ family: [codex], bindir: '/home/u/.local/bin' }, throwingFs).items;
332
+ const skill = items.find((i) => i.surface === 'skill');
333
+ assert.equal(skill.class, STOP);
334
+ assert.ok(!items.some((i) => i.surface === 'skill' && i.class === SAFE_REMOVE));
335
+ assert.ok(!items.some((i) => i.surface === 'wrapper'));
336
+ });
337
+ });
338
+
339
+ // ── parseArgs: strict validation (codex #6) ─────────────────────────────────────
340
+
341
+ describe('parseArgs — strict', () => {
342
+ it('accepts a clean whole-family teardown', () => {
343
+ const a = parseArgs(['--dir', '/proj', '--dry-run']);
344
+ assert.equal(a.bad, null);
345
+ assert.equal(a.dir, '/proj');
346
+ assert.equal(a.dryRun, true);
347
+ assert.equal(a.member, undefined);
348
+ });
349
+
350
+ it('accepts a valid <member>', () => {
351
+ assert.equal(parseArgs(['agent-workflow-memory', '--yes']).bad, null);
352
+ assert.equal(parseArgs(['agent-workflow-memory']).member, 'agent-workflow-memory');
353
+ });
354
+
355
+ it('rejects an unknown flag (a typo cannot silently slip past)', () => {
356
+ assert.match(parseArgs(['--yes', '--frce']).bad, /unknown option/);
357
+ });
358
+
359
+ it('rejects an unknown member name', () => {
360
+ assert.match(parseArgs(['memory']).bad, /unknown member "memory"/);
361
+ });
362
+
363
+ it('rejects --dir / --bindir without a value', () => {
364
+ assert.match(parseArgs(['--dir']).bad, /--dir requires/);
365
+ assert.match(parseArgs(['--dir', '--yes']).bad, /--dir requires/);
366
+ assert.match(parseArgs(['--bindir']).bad, /--bindir requires/);
367
+ });
368
+
369
+ it('rejects more than one positional', () => {
370
+ assert.match(parseArgs(['agent-workflow-kit', 'agent-workflow-memory']).bad, /at most one/);
371
+ });
372
+ });
@@ -1,105 +0,0 @@
1
- # Planning Workflow
2
-
3
- Source of truth for **how plans are written, stored, executed, and torn down**. Overrides the generic `writing-plans` skill — if both trigger, this one wins. Runtime series status (which plan is Current / Pending) lives in `docs/plans/queue.md`.
4
-
5
- ---
6
-
7
- ## 1. Plan vocabulary
8
-
9
- Strict four-level hierarchy, used in plan files (`docs/plans/*.md`) and in verbal summaries:
10
-
11
- - **Plan** — top-level container = the plan file itself. One file = one Plan. A series of related plans is not grouped under any wrapper noun; refer to them as "Plan 1 of N", "the next plan". Series order lives in `queue.md` (§3).
12
- - **Phase** — a large block inside the Plan. Exactly one execution session. Ends with its own verification block. `## Phase 1: …`, `## Phase 2: …`.
13
- - **Step** — an atomic change inside a Phase. Numbered `<phase>.<step>`: `### 1.1. …`. One Step → one logical commit.
14
- - **Substep** — optional split of a complex Step. Lettered: `**1.2.a**`, `**1.2.b**`. Use only when a Step cannot be one command.
15
-
16
- Reserve the word "task" for the todo list and `active_plan.md` — not for plan structure.
17
-
18
- ## 2. Plan directory & lifecycle
19
-
20
- Plan files are **ephemeral, machine-local scratch space**, gitignored (`.gitignore` contains `docs/plans/`).
21
-
22
- **Lifecycle:** Creation (untracked file) → Execution (Phases 1..N-1) → mandatory **Phase N: Cleanup** (§4) → Post-deletion (only `changelog.md` + ADRs remain). Plans are **NEVER committed** — full stop. Even if a plan looks load-bearing (referenced by an ADR), inline the load-bearing content into a persistent doc and delete the plan file.
23
-
24
- **Forbidden:** `git add` of any plan file; plan-file paths in committed docs; leaving plan files on disk after Cleanup. If the user says "commit the plan" — ask back: "the plan is ephemeral — what exactly should I inline into `decisions.md` / `changelog.md`?".
25
-
26
- ## 3. Series & queue.md
27
-
28
- A **series** = 2+ related plans that share a roadmap. The index lives at `docs/plans/queue.md` (gitignored, machine-local):
29
-
30
- ```markdown
31
- ## Series: <name>
32
-
33
- ### Current
34
- - **Plan N / M** — <slug> — <one-line description>
35
-
36
- ### Pending
37
- - **Plan N+1 / M** — <slug or TBD> — <description>
38
-
39
- ### Done
40
- - **Plan K / M** — <slug> — done YYYY-MM-DD. Outputs: <pointers>.
41
- ```
42
-
43
- `queue.md` is initialised when the **first** plan of a series is written, not during its Cleanup — without an upfront index the execution agent has no map of the series. Each plan's Cleanup then marks itself Done (with outputs) and promotes the next plan to Current. A single, unrelated plan does not need a series entry.
44
-
45
- ## 4. Required Cleanup phase
46
-
47
- Every Plan MUST end with a final **Phase N: Cleanup** — the last numbered Phase. Without it the Plan is not done.
48
-
49
- Minimum content:
50
-
51
- - **Migrate outputs** → `docs/ai/decisions.md` (AD-XXX), `changelog.md`, `known_issues.md` (Issue-XXX), `current_state.md`, `pages/<page>.md`.
52
- - **Inline cross-references** — `grep -rn "<plan-slug>" docs/` must be empty. Every pointer is rewritten inline or removed.
53
- - **Update `queue.md`** — if part of a series, mark Done + promote next.
54
- - **Delete the plan file** — `rm docs/plans/<slug>.md`.
55
- - **Verification** — `grep -rn "<slug>" .` empty; `ls docs/plans/<slug>.md` → No such file; docs cap-validator green.
56
-
57
- If a Plan is aborted mid-flight, Cleanup still runs — partial outputs land in `known_issues.md`, then the file is deleted.
58
-
59
- ## 5. All work in plans
60
-
61
- Anything required for the task is a **Step inside the Plan**. Nothing "before the plan", "between plans", or "don't forget" — those evaporate at execution time because the execution agent reads only the plan file, not chat scrollback. Every dependency, check, and install is its own Step or Substep. The final "Next steps" section contains **only user-actionable** items.
62
-
63
- ## 6. Plan-then-execute split
64
-
65
- Default workflow for non-trivial features (multi-file change, new service + hook + UI, architectural choices): write a **self-contained Plan** and stop. Implementation runs in a fresh session via the `executing-plans` skill.
66
-
67
- - Triggers: any feature, refactor, or change touching more than ~1 file, or non-obvious architectural choices.
68
- - Does NOT apply to typos, one-line fixes, doc-only edits, or pure "where is X" research — those run inline.
69
- - The Plan must be readable cold by a fresh agent: file paths, contracts, execution order, verification, gotchas — all inside the file.
70
-
71
- This split is a token-efficiency strategy: exploration context stays out of the execution window.
72
-
73
- ### Session-continuity heuristic (split vs continue)
74
-
75
- The volume trigger above (files / LoC / tokens) is necessary but not sufficient. The deeper question is whether the planning context is the execution **payload** or **noise**:
76
-
77
- - **Split** (fresh session) when planning exploration was *broad fan-out* — many files skimmed, sub-agent dumps, wide searches to *locate* things. That context is noise for execution; discard it.
78
- - **Continue** in the current session when ALL hold: (1) exploration was *targeted-deep* — you read the exact files to be created/modified/copied, so execution would just re-read them; (2) no new heavy exploration is needed to execute; (3) the context budget is healthy (far from the window limit / Lost-in-the-Middle).
79
- - When continuing, each Phase's Verification block is a natural checkpoint. If different Phases need different cold context, continue only through the warm Phases, then split.
80
-
81
- ## 7. Plan-document structure
82
-
83
- ```
84
- # Plan: <human-readable title>
85
-
86
- ## Context ← why this Plan exists, current state, why now (reads cold)
87
- ## Approach ← chosen design + an explicit "What we are NOT doing"
88
- ## Phase 1: <name>
89
- ### 1.1. <step> ← exact paths + commands
90
- ## Phase 2: <name>
91
- ...
92
- ## Phase N: Cleanup ← mandatory (§4)
93
- ## Critical files ← table: file → change kind (new / modify / delete / move)
94
- ## Reuse ← pointers to existing patterns/snippets to copy, not re-derive
95
- ## Verification ← full check sequence (mechanical + behavioural)
96
- ## Next steps ← user-actionable only (§5)
97
- ```
98
-
99
- ## 8. Self-review checklist (before finalizing a Plan)
100
-
101
- - Every Step has exact file paths and exact commands.
102
- - Every recommendation that used to live outside the Plan is now a Step (§5).
103
- - Vocabulary is strict (§1); the Plan ends with **Phase N: Cleanup** (§4).
104
- - If part of a series: `queue.md` is initialised / updated (§3).
105
- - No `git add <plan>` and no "commit the plan" wording in the final report.
@@ -1 +0,0 @@
1
- > **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. Full vocabulary, lifecycle, and the plan-then-execute split live in the project's **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5.