@hecer/yoke 0.2.0 → 0.8.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +583 -494
  3. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/config.yaml +6 -0
  4. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/context/DECISIONS.md +9 -0
  5. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/prd.yaml +38 -0
  6. package/bench/.runs/claude-2026-07-09T22-34-01/bench-verify.mjs +15 -0
  7. package/bench/.runs/claude-2026-07-09T22-34-01/package.json +9 -0
  8. package/bench/.runs/claude-2026-07-09T22-34-01/src/index.mjs +48 -0
  9. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-1.test.mjs +24 -0
  10. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-2.test.mjs +28 -0
  11. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-3.test.mjs +25 -0
  12. package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/config.yaml +6 -0
  13. package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/prd.yaml +32 -0
  14. package/bench/.runs/gemini-2026-07-09T22-34-02/bench-verify.mjs +15 -0
  15. package/bench/.runs/gemini-2026-07-09T22-34-02/package.json +9 -0
  16. package/bench/.runs/gemini-2026-07-09T22-34-02/src/index.mjs +3 -0
  17. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-1.test.mjs +24 -0
  18. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-2.test.mjs +28 -0
  19. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-3.test.mjs +25 -0
  20. package/bench/README.md +42 -0
  21. package/bench/RESULTS.md +36 -0
  22. package/bench/fixtures/string-kit/.yoke/config.yaml +6 -0
  23. package/bench/fixtures/string-kit/.yoke/prd.yaml +32 -0
  24. package/bench/fixtures/string-kit/bench-verify.mjs +15 -0
  25. package/bench/fixtures/string-kit/package.json +9 -0
  26. package/bench/fixtures/string-kit/src/index.mjs +3 -0
  27. package/bench/fixtures/string-kit/tests/STORY-1.test.mjs +24 -0
  28. package/bench/fixtures/string-kit/tests/STORY-2.test.mjs +28 -0
  29. package/bench/fixtures/string-kit/tests/STORY-3.test.mjs +25 -0
  30. package/bench/results/claude-2026-07-09T22-34-01.json +40 -0
  31. package/bench/run.mjs +115 -0
  32. package/canon/loop/loop-spec.md +5 -1
  33. package/canon/manifest.yaml +3 -0
  34. package/canon/skills/yoke-retrofit/SKILL.md +1 -0
  35. package/canon/tools/claude-mem.md +15 -0
  36. package/canon/tools/ui-ux-pro-max.md +15 -0
  37. package/dist/cli.js +37 -6
  38. package/dist/loop/cleanup.js +44 -3
  39. package/dist/loop/loop.js +67 -2
  40. package/dist/loop/reporter.js +88 -4
  41. package/dist/loop/run-command.js +25 -8
  42. package/dist/loop/runner.js +133 -14
  43. package/dist/loop/watchdog.js +67 -17
  44. package/dist/retrofit/apply.js +10 -3
  45. package/dist/retrofit/config.js +9 -1
  46. package/dist/retrofit/gitignore.js +4 -0
  47. package/dist/retrofit/planners/claude.js +4 -1
  48. package/dist/retrofit/planners/gemini.js +2 -1
  49. package/dist/retrofit/preserve.js +51 -0
  50. package/dist/update/check.js +86 -0
  51. package/dist/update/refresh.js +28 -0
  52. package/dist/update/upgrade.js +30 -0
  53. package/docs/PUBLISHING.md +41 -0
  54. package/docs/superpowers/plans/2026-06-27-forge-a-canon.md +815 -0
  55. package/docs/superpowers/plans/2026-06-27-forge-b1-retrofit-claude.md +842 -0
  56. package/docs/superpowers/plans/2026-06-27-forge-b2-codex-gemini-tools.md +884 -0
  57. package/docs/superpowers/plans/2026-06-27-forge-c1-loop-engine.md +891 -0
  58. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -0
  59. package/docs/superpowers/plans/2026-06-28-forge-b3-settings-merge.md +364 -0
  60. package/docs/superpowers/plans/2026-06-28-forge-c2-loop-verify.md +502 -0
  61. package/docs/superpowers/plans/2026-06-28-forge-c3-multi-agent-runners.md +377 -0
  62. package/docs/superpowers/plans/2026-06-28-forge-c4-worktree-isolation.md +413 -0
  63. package/docs/superpowers/plans/2026-06-28-forge-c5-review-iteration.md +419 -0
  64. package/docs/superpowers/plans/2026-06-28-forge-cleanup-dep0190.md +233 -0
  65. package/docs/superpowers/plans/2026-06-28-forge-d-codegraph-choice-minimal-code.md +523 -0
  66. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -0
  67. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -0
  68. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -0
  69. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -0
  70. package/docs/superpowers/plans/2026-07-02-baustein-j-cross-model-review-gstack-compose.md +645 -0
  71. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -0
  72. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -0
  73. package/docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md +144 -0
  74. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -0
  75. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -0
  76. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -0
  77. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -0
  78. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -0
  79. package/docs/superpowers/specs/2026-07-02-baustein-j-cross-model-review-gstack-compose-design.md +137 -0
  80. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -0
  81. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -0
  82. package/docs/superpowers/specs/2026-07-10-companion-tools-decision.md +33 -0
  83. package/docs/superpowers/specs/2026-07-10-multi-agent-parallel-loop-design.md +99 -0
  84. package/package.json +8 -2
@@ -0,0 +1,884 @@
1
+ # Forge — Baustein B2 (Codex + Gemini retrofit + tool wiring) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Extend `forge retrofit` to generate idiomatic Codex CLI and Gemini CLI artifacts from the Canon, and wire the cross-agent tools (graphify + Playwright MCP for all three agents; rtk per agent with Windows WSL detection) — driven by an `--agent` selection.
6
+
7
+ **Architecture:** Replace the single `planClaudeRetrofit` call with a per-agent **planner registry** (`PLANNERS: Record<Agent, AgentPlanner>`). Each planner emits idiomatic `Action`s for its harness. A shared `tools` module produces the MCP/instruction wiring per agent so the three planners don't duplicate it. `runRetrofit` selects agents (`--agent`, or detected, or all) and records every applied agent additively in `.forge/config.yaml`. Apply/report/backup from B1 are reused unchanged. Baustein B2 of the Forge spec; loop hardening is C2.
8
+
9
+ **Tech Stack:** Node.js (ESM), TypeScript, vitest, `yaml`. Reuses B1: `Action`, `applyActions`, `formatReport`, `detectProject`, `ForgeConfig`, `resolveCanonDir`, `loadManifest`. The exact MCP launch commands are best-effort templates with honest inline comments; tests assert artifact STRUCTURE, not that external tools run.
10
+
11
+ **Builds on:** Baustein A+B1+C1 (on `main`). Key reuse: `src/retrofit/plan.ts` (`planClaudeRetrofit`, `Action`), `src/retrofit/apply.ts`, `src/retrofit/report.ts`, `src/retrofit/detect.ts`, `src/cli.ts` (`runRetrofit`).
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ ```
18
+ src/retrofit/
19
+ tools.ts # NEW: per-agent MCP server config + rtk wiring snippets (pure)
20
+ wsl.ts # NEW: hasWsl() — Windows WSL availability probe
21
+ planners/
22
+ claude.ts # NEW: planClaude() = B1 skills/AGENTS/CLAUDE + tool wiring (.mcp.json, rtk hook/instruction)
23
+ codex.ts # NEW: planCodex() = AGENTS.md + .codex/config.toml (MCP) + RTK.md
24
+ gemini.ts # NEW: planGemini() = GEMINI.md + .gemini/commands/*.toml + .gemini/settings.json + rtk instruction
25
+ plan.ts # MODIFY: keep planClaudeRetrofit (delegates to planners/claude); add PLANNERS + planRetrofit()
26
+ cli.ts (src/cli.ts) # MODIFY: runRetrofit accepts agent selection; --agent flag
27
+ tests/retrofit/
28
+ tools.test.ts # NEW
29
+ planners-codex.test.ts # NEW
30
+ planners-gemini.test.ts # NEW
31
+ plan-dispatch.test.ts # NEW
32
+ retrofit.integration.test.ts # MODIFY: add multi-agent assertions
33
+ ```
34
+
35
+ The planner registry is the single extension point: adding an agent later means one new `planners/*.ts` + one registry line.
36
+
37
+ ---
38
+
39
+ ### Task 1: Tool wiring module
40
+
41
+ **Files:**
42
+ - Create: `src/retrofit/tools.ts`
43
+ - Test: `tests/retrofit/tools.test.ts`
44
+
45
+ - [ ] **Step 1: Write the failing test**
46
+
47
+ Create `tests/retrofit/tools.test.ts`:
48
+ ```ts
49
+ import { describe, it, expect } from 'vitest'
50
+ import { mcpServers, rtkInstruction } from '../../src/retrofit/tools.js'
51
+
52
+ describe('tools', () => {
53
+ it('mcpServers includes graphify and playwright with command+args', () => {
54
+ const servers = mcpServers()
55
+ expect(Object.keys(servers)).toEqual(expect.arrayContaining(['graphify', 'playwright']))
56
+ expect(servers.playwright.command).toBe('npx')
57
+ expect(servers.playwright.args).toContain('@playwright/mcp@latest')
58
+ expect(servers.graphify.command).toBeTypeOf('string')
59
+ })
60
+
61
+ it('rtkInstruction mentions prefixing commands with rtk', () => {
62
+ expect(rtkInstruction()).toMatch(/rtk/i)
63
+ })
64
+ })
65
+ ```
66
+
67
+ - [ ] **Step 2: Run test to verify it fails**
68
+
69
+ Run: `npm test -- retrofit/tools`
70
+ Expected: FAIL — cannot find module `src/retrofit/tools.js`.
71
+
72
+ - [ ] **Step 3: Write minimal implementation**
73
+
74
+ Create `src/retrofit/tools.ts`:
75
+ ```ts
76
+ export interface McpServerConfig {
77
+ command: string
78
+ args: string[]
79
+ }
80
+
81
+ // Best-effort default launch commands. Users may need to adjust these to match
82
+ // their local install (e.g. graphify installed via `uv tool install graphifyy`).
83
+ export function mcpServers(): Record<string, McpServerConfig> {
84
+ return {
85
+ graphify: { command: 'graphify', args: ['serve'] },
86
+ playwright: { command: 'npx', args: ['@playwright/mcp@latest'] },
87
+ }
88
+ }
89
+
90
+ // rtk has no transparent-rewrite hook on Codex/Gemini; those agents get this
91
+ // instruction instead. On Claude (Windows) it is also the WSL-less fallback.
92
+ export function rtkInstruction(): string {
93
+ return [
94
+ '## Token efficiency (rtk)',
95
+ '',
96
+ 'Prefix shell/dev commands with `rtk` to compress their output before it enters context.',
97
+ 'Example: `rtk git status`, `rtk npm test`. See https://github.com/rtk-ai/rtk.',
98
+ ].join('\n')
99
+ }
100
+ ```
101
+
102
+ - [ ] **Step 4: Run test to verify it passes**
103
+
104
+ Run: `npm test -- retrofit/tools`
105
+ Expected: PASS (2 tests).
106
+
107
+ - [ ] **Step 5: Commit**
108
+
109
+ ```bash
110
+ git add src/retrofit/tools.ts tests/retrofit/tools.test.ts
111
+ git commit -m "feat: add per-agent MCP server + rtk wiring snippets"
112
+ ```
113
+
114
+ ---
115
+
116
+ ### Task 2: WSL detection
117
+
118
+ **Files:**
119
+ - Create: `src/retrofit/wsl.ts`
120
+ - Test: `tests/retrofit/wsl.test.ts`
121
+
122
+ - [ ] **Step 1: Write the failing test**
123
+
124
+ Create `tests/retrofit/wsl.test.ts`:
125
+ ```ts
126
+ import { describe, it, expect } from 'vitest'
127
+ import { hasWsl } from '../../src/retrofit/wsl.js'
128
+
129
+ describe('hasWsl', () => {
130
+ it('returns a boolean and never throws', () => {
131
+ expect(typeof hasWsl()).toBe('boolean')
132
+ })
133
+
134
+ it('is false on non-win32 platforms', () => {
135
+ // hasWsl() short-circuits to false unless process.platform === 'win32'
136
+ if (process.platform !== 'win32') {
137
+ expect(hasWsl()).toBe(false)
138
+ } else {
139
+ expect(typeof hasWsl()).toBe('boolean')
140
+ }
141
+ })
142
+ })
143
+ ```
144
+
145
+ - [ ] **Step 2: Run test to verify it fails**
146
+
147
+ Run: `npm test -- retrofit/wsl`
148
+ Expected: FAIL — cannot find module `src/retrofit/wsl.js`.
149
+
150
+ - [ ] **Step 3: Write minimal implementation**
151
+
152
+ Create `src/retrofit/wsl.ts`:
153
+ ```ts
154
+ import { execFileSync } from 'node:child_process'
155
+
156
+ // True only on Windows where a WSL distribution responds. Used to decide whether
157
+ // rtk can use its transparent PreToolUse hook (needs WSL) or must fall back to
158
+ // instruction mode. Never throws.
159
+ export function hasWsl(): boolean {
160
+ if (process.platform !== 'win32') return false
161
+ try {
162
+ execFileSync('wsl', ['--status'], { stdio: 'pipe', timeout: 5000 })
163
+ return true
164
+ } catch {
165
+ return false
166
+ }
167
+ }
168
+ ```
169
+
170
+ - [ ] **Step 4: Run test to verify it passes**
171
+
172
+ Run: `npm test -- retrofit/wsl`
173
+ Expected: PASS (2 tests).
174
+
175
+ - [ ] **Step 5: Commit**
176
+
177
+ ```bash
178
+ git add src/retrofit/wsl.ts tests/retrofit/wsl.test.ts
179
+ git commit -m "feat: add WSL availability probe for rtk hook fallback"
180
+ ```
181
+
182
+ ---
183
+
184
+ ### Task 3: Claude planner (extract B1 + add tool wiring)
185
+
186
+ **Files:**
187
+ - Create: `src/retrofit/planners/claude.ts`
188
+ - Test: `tests/retrofit/planners-claude.test.ts`
189
+
190
+ - [ ] **Step 1: Write the failing test**
191
+
192
+ Create `tests/retrofit/planners-claude.test.ts`:
193
+ ```ts
194
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
195
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
196
+ import { join } from 'node:path'
197
+ import { tmpdir } from 'node:os'
198
+ import { planClaude } from '../../src/retrofit/planners/claude.js'
199
+
200
+ let canon: string
201
+ beforeEach(() => {
202
+ canon = mkdtempSync(join(tmpdir(), 'forge-canon-'))
203
+ const w = (rel: string, c: string) => { mkdirSync(join(canon, rel, '..'), { recursive: true }); writeFileSync(join(canon, rel), c) }
204
+ w('manifest.yaml', `
205
+ name: forge-canon
206
+ version: 0.1.0
207
+ agents: [claude]
208
+ skills:
209
+ - { id: tdd, path: skills/tdd, kind: methodology }
210
+ policy: []
211
+ loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
212
+ tools: []
213
+ `)
214
+ w('AGENTS.md', '# Baseline\n')
215
+ w('skills/tdd/SKILL.md', '---\nname: tdd\ndescription: d\n---\nbody')
216
+ })
217
+ afterEach(() => { rmSync(canon, { recursive: true, force: true }) })
218
+
219
+ describe('planClaude', () => {
220
+ it('plans skill, AGENTS.md, CLAUDE.md and an .mcp.json with both servers', () => {
221
+ const targets = planClaude(canon, '/t').map(a => a.target)
222
+ expect(targets).toContain('.claude/skills/tdd/SKILL.md')
223
+ expect(targets).toContain('AGENTS.md')
224
+ expect(targets).toContain('CLAUDE.md')
225
+ expect(targets).toContain('.mcp.json')
226
+ })
227
+
228
+ it('the .mcp.json content references graphify and playwright', () => {
229
+ const mcp = planClaude(canon, '/t').find(a => a.target === '.mcp.json')!
230
+ expect(mcp.content).toContain('graphify')
231
+ expect(mcp.content).toContain('playwright')
232
+ })
233
+ })
234
+ ```
235
+
236
+ - [ ] **Step 2: Run test to verify it fails**
237
+
238
+ Run: `npm test -- planners-claude`
239
+ Expected: FAIL — cannot find module `src/retrofit/planners/claude.js`.
240
+
241
+ - [ ] **Step 3: Write minimal implementation**
242
+
243
+ Create `src/retrofit/planners/claude.ts`:
244
+ ```ts
245
+ import { readFileSync } from 'node:fs'
246
+ import { join } from 'node:path'
247
+ import { loadManifest } from '../../canon/manifest.js'
248
+ import type { Action } from '../plan.js'
249
+ import { mcpServers, rtkInstruction } from '../tools.js'
250
+ import { hasWsl } from '../wsl.js'
251
+
252
+ const claudeMd = (rtkNote: string) => `# Project Instructions
253
+
254
+ This project uses the Forge harness. Baseline instructions:
255
+
256
+ @AGENTS.md
257
+ ${rtkNote ? `\n${rtkNote}\n` : ''}`
258
+
259
+ export function planClaude(canonDir: string, _targetDir: string): Action[] {
260
+ const manifest = loadManifest(join(canonDir, 'manifest.yaml'))
261
+ const actions: Action[] = []
262
+
263
+ for (const skill of manifest.skills) {
264
+ actions.push({
265
+ kind: 'write',
266
+ target: `.claude/skills/${skill.id}/SKILL.md`,
267
+ content: readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8'),
268
+ reason: `skill: ${skill.id}`,
269
+ })
270
+ }
271
+
272
+ actions.push({
273
+ kind: 'write',
274
+ target: 'AGENTS.md',
275
+ content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
276
+ reason: 'baseline instructions',
277
+ })
278
+
279
+ // rtk: PreToolUse hook needs WSL on Windows; otherwise fall back to instruction mode.
280
+ const rtkHookable = hasWsl()
281
+ actions.push({
282
+ kind: 'write',
283
+ target: 'CLAUDE.md',
284
+ content: claudeMd(rtkHookable ? '' : rtkInstruction()),
285
+ reason: 'Claude entry importing AGENTS.md',
286
+ })
287
+
288
+ actions.push({
289
+ kind: 'write',
290
+ target: '.mcp.json',
291
+ content: JSON.stringify({ mcpServers: mcpServers() }, null, 2) + '\n',
292
+ reason: 'MCP servers (graphify, playwright)',
293
+ })
294
+
295
+ if (rtkHookable) {
296
+ actions.push({
297
+ kind: 'write',
298
+ target: '.claude/settings.json',
299
+ content: JSON.stringify({
300
+ hooks: {
301
+ PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'rtk hook' }] }],
302
+ },
303
+ }, null, 2) + '\n',
304
+ reason: 'rtk PreToolUse hook (WSL detected)',
305
+ })
306
+ }
307
+
308
+ return actions
309
+ }
310
+ ```
311
+
312
+ - [ ] **Step 4: Run test to verify it passes**
313
+
314
+ Run: `npm test -- planners-claude`
315
+ Expected: PASS (2 tests).
316
+
317
+ - [ ] **Step 5: Commit**
318
+
319
+ ```bash
320
+ git add src/retrofit/planners/claude.ts tests/retrofit/planners-claude.test.ts
321
+ git commit -m "feat: add claude planner with MCP + rtk wiring"
322
+ ```
323
+
324
+ ---
325
+
326
+ ### Task 4: Codex planner
327
+
328
+ **Files:**
329
+ - Create: `src/retrofit/planners/codex.ts`
330
+ - Test: `tests/retrofit/planners-codex.test.ts`
331
+
332
+ - [ ] **Step 1: Write the failing test**
333
+
334
+ Create `tests/retrofit/planners-codex.test.ts`:
335
+ ```ts
336
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
337
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
338
+ import { join } from 'node:path'
339
+ import { tmpdir } from 'node:os'
340
+ import { planCodex } from '../../src/retrofit/planners/codex.js'
341
+
342
+ let canon: string
343
+ beforeEach(() => {
344
+ canon = mkdtempSync(join(tmpdir(), 'forge-canon-'))
345
+ const w = (rel: string, c: string) => { mkdirSync(join(canon, rel, '..'), { recursive: true }); writeFileSync(join(canon, rel), c) }
346
+ w('manifest.yaml', `
347
+ name: forge-canon
348
+ version: 0.1.0
349
+ agents: [codex]
350
+ skills: []
351
+ policy: []
352
+ loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
353
+ tools: []
354
+ `)
355
+ w('AGENTS.md', '# Baseline\n')
356
+ })
357
+ afterEach(() => { rmSync(canon, { recursive: true, force: true }) })
358
+
359
+ describe('planCodex', () => {
360
+ it('plans AGENTS.md, .codex/config.toml, and RTK.md', () => {
361
+ const targets = planCodex(canon, '/t').map(a => a.target)
362
+ expect(targets).toContain('AGENTS.md')
363
+ expect(targets).toContain('.codex/config.toml')
364
+ expect(targets).toContain('RTK.md')
365
+ })
366
+
367
+ it('config.toml has [mcp_servers.graphify] and [mcp_servers.playwright]', () => {
368
+ const toml = planCodex(canon, '/t').find(a => a.target === '.codex/config.toml')!
369
+ expect(toml.content).toContain('[mcp_servers.graphify]')
370
+ expect(toml.content).toContain('[mcp_servers.playwright]')
371
+ })
372
+ })
373
+ ```
374
+
375
+ - [ ] **Step 2: Run test to verify it fails**
376
+
377
+ Run: `npm test -- planners-codex`
378
+ Expected: FAIL — cannot find module `src/retrofit/planners/codex.js`.
379
+
380
+ - [ ] **Step 3: Write minimal implementation**
381
+
382
+ Create `src/retrofit/planners/codex.ts`:
383
+ ```ts
384
+ import { readFileSync } from 'node:fs'
385
+ import { join } from 'node:path'
386
+ import type { Action } from '../plan.js'
387
+ import { mcpServers, rtkInstruction } from '../tools.js'
388
+
389
+ function tomlMcp(): string {
390
+ const servers = mcpServers()
391
+ // Codex reads MCP servers from ~/.codex/config.toml. This project-level file is a
392
+ // ready-to-merge snippet; users append these blocks to their global config.
393
+ return Object.entries(servers)
394
+ .map(([name, cfg]) => {
395
+ const args = cfg.args.map(a => `"${a}"`).join(', ')
396
+ return `[mcp_servers.${name}]\ncommand = "${cfg.command}"\nargs = [${args}]\n`
397
+ })
398
+ .join('\n')
399
+ }
400
+
401
+ export function planCodex(canonDir: string, _targetDir: string): Action[] {
402
+ return [
403
+ {
404
+ kind: 'write',
405
+ target: 'AGENTS.md',
406
+ content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
407
+ reason: 'baseline instructions (Codex reads AGENTS.md natively)',
408
+ },
409
+ {
410
+ kind: 'write',
411
+ target: '.codex/config.toml',
412
+ content: `# Forge: MCP servers for Codex. Merge into ~/.codex/config.toml.\n\n${tomlMcp()}`,
413
+ reason: 'MCP servers (graphify, playwright)',
414
+ },
415
+ {
416
+ kind: 'write',
417
+ target: 'RTK.md',
418
+ content: rtkInstruction() + '\n',
419
+ reason: 'rtk instruction (Codex has no rewrite hook)',
420
+ },
421
+ ]
422
+ }
423
+ ```
424
+
425
+ - [ ] **Step 4: Run test to verify it passes**
426
+
427
+ Run: `npm test -- planners-codex`
428
+ Expected: PASS (2 tests).
429
+
430
+ - [ ] **Step 5: Commit**
431
+
432
+ ```bash
433
+ git add src/retrofit/planners/codex.ts tests/retrofit/planners-codex.test.ts
434
+ git commit -m "feat: add codex planner (AGENTS.md, config.toml MCP, RTK.md)"
435
+ ```
436
+
437
+ ---
438
+
439
+ ### Task 5: Gemini planner
440
+
441
+ **Files:**
442
+ - Create: `src/retrofit/planners/gemini.ts`
443
+ - Test: `tests/retrofit/planners-gemini.test.ts`
444
+
445
+ - [ ] **Step 1: Write the failing test**
446
+
447
+ Create `tests/retrofit/planners-gemini.test.ts`:
448
+ ```ts
449
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
450
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
451
+ import { join } from 'node:path'
452
+ import { tmpdir } from 'node:os'
453
+ import { parse } from 'yaml'
454
+ import { planGemini } from '../../src/retrofit/planners/gemini.js'
455
+
456
+ let canon: string
457
+ beforeEach(() => {
458
+ canon = mkdtempSync(join(tmpdir(), 'forge-canon-'))
459
+ const w = (rel: string, c: string) => { mkdirSync(join(canon, rel, '..'), { recursive: true }); writeFileSync(join(canon, rel), c) }
460
+ w('manifest.yaml', `
461
+ name: forge-canon
462
+ version: 0.1.0
463
+ agents: [gemini]
464
+ skills:
465
+ - { id: tdd, path: skills/tdd, kind: methodology }
466
+ policy: []
467
+ loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
468
+ tools: []
469
+ `)
470
+ w('AGENTS.md', '# Baseline\n')
471
+ w('skills/tdd/SKILL.md', '---\nname: tdd\ndescription: Test-driven development\n---\nbody')
472
+ })
473
+ afterEach(() => { rmSync(canon, { recursive: true, force: true }) })
474
+
475
+ describe('planGemini', () => {
476
+ it('plans GEMINI.md, a command toml per skill, and settings.json', () => {
477
+ const targets = planGemini(canon, '/t').map(a => a.target)
478
+ expect(targets).toContain('GEMINI.md')
479
+ expect(targets).toContain('.gemini/commands/tdd.toml')
480
+ expect(targets).toContain('.gemini/settings.json')
481
+ })
482
+
483
+ it('settings.json wires mcpServers and AGENTS.md context', () => {
484
+ const s = planGemini(canon, '/t').find(a => a.target === '.gemini/settings.json')!
485
+ const cfg = JSON.parse(s.content)
486
+ expect(Object.keys(cfg.mcpServers)).toContain('graphify')
487
+ expect(cfg.context.fileName).toContain('AGENTS.md')
488
+ })
489
+
490
+ it('a command toml carries description and prompt', () => {
491
+ const cmd = planGemini(canon, '/t').find(a => a.target === '.gemini/commands/tdd.toml')!
492
+ expect(cmd.content).toContain('description')
493
+ expect(cmd.content).toContain('prompt')
494
+ expect(cmd.content).toContain('Test-driven development')
495
+ })
496
+ })
497
+ ```
498
+
499
+ - [ ] **Step 2: Run test to verify it fails**
500
+
501
+ Run: `npm test -- planners-gemini`
502
+ Expected: FAIL — cannot find module `src/retrofit/planners/gemini.js`.
503
+
504
+ - [ ] **Step 3: Write minimal implementation**
505
+
506
+ Create `src/retrofit/planners/gemini.ts`:
507
+ ```ts
508
+ import { readFileSync } from 'node:fs'
509
+ import { join } from 'node:path'
510
+ import { stringify } from 'yaml'
511
+ import { loadManifest } from '../../canon/manifest.js'
512
+ import { parseFrontmatter } from '../../canon/frontmatter.js'
513
+ import type { Action } from '../plan.js'
514
+ import { mcpServers, rtkInstruction } from '../tools.js'
515
+
516
+ function tomlString(s: string): string {
517
+ return '"""\n' + s.replace(/"""/g, '\\"\\"\\"') + '\n"""'
518
+ }
519
+
520
+ export function planGemini(canonDir: string, _targetDir: string): Action[] {
521
+ const manifest = loadManifest(join(canonDir, 'manifest.yaml'))
522
+ const actions: Action[] = []
523
+
524
+ // GEMINI.md: baseline + rtk instruction (Gemini has no rewrite hook).
525
+ const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8')
526
+ actions.push({
527
+ kind: 'write',
528
+ target: 'GEMINI.md',
529
+ content: `${baseline}\n${rtkInstruction()}\n`,
530
+ reason: 'baseline + rtk instruction (no hook on Gemini)',
531
+ })
532
+
533
+ // One TOML slash command per skill.
534
+ for (const skill of manifest.skills) {
535
+ const body = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8')
536
+ const fm = parseFrontmatter(body) ?? {}
537
+ const description = String(fm.description ?? skill.id)
538
+ const prompt = `You are using the "${skill.id}" skill.\n\n${description}\n\nFollow it for the current task.`
539
+ actions.push({
540
+ kind: 'write',
541
+ target: `.gemini/commands/${skill.id}.toml`,
542
+ content: `description = "${description.replace(/"/g, '\\"')}"\nprompt = ${tomlString(prompt)}\n`,
543
+ reason: `gemini command: ${skill.id}`,
544
+ })
545
+ }
546
+
547
+ // settings.json: MCP servers + read AGENTS.md as context.
548
+ actions.push({
549
+ kind: 'write',
550
+ target: '.gemini/settings.json',
551
+ content: JSON.stringify({
552
+ mcpServers: mcpServers(),
553
+ context: { fileName: ['AGENTS.md', 'GEMINI.md'] },
554
+ }, null, 2) + '\n',
555
+ reason: 'MCP servers + AGENTS.md context',
556
+ })
557
+
558
+ // Also ship AGENTS.md so the context.fileName entry resolves.
559
+ actions.push({
560
+ kind: 'write',
561
+ target: 'AGENTS.md',
562
+ content: baseline,
563
+ reason: 'baseline instructions (shared)',
564
+ })
565
+
566
+ return actions
567
+ }
568
+ ```
569
+
570
+ Note: `stringify` from `yaml` is imported for parity with other planners but TOML is hand-built here; if your linter flags the unused import, remove it. (Keep `parse`-free.) The `tomlString` helper uses TOML multi-line basic strings.
571
+
572
+ - [ ] **Step 4: Run test to verify it passes**
573
+
574
+ Run: `npm test -- planners-gemini`
575
+ Expected: PASS (3 tests). If the unused `stringify` import causes a TS/lint error, delete that import line.
576
+
577
+ - [ ] **Step 5: Commit**
578
+
579
+ ```bash
580
+ git add src/retrofit/planners/gemini.ts tests/retrofit/planners-gemini.test.ts
581
+ git commit -m "feat: add gemini planner (GEMINI.md, command toml, settings.json)"
582
+ ```
583
+
584
+ ---
585
+
586
+ ### Task 6: Planner registry + dispatch
587
+
588
+ **Files:**
589
+ - Modify: `src/retrofit/plan.ts`
590
+ - Test: `tests/retrofit/plan-dispatch.test.ts`
591
+
592
+ - [ ] **Step 1: Write the failing test**
593
+
594
+ Create `tests/retrofit/plan-dispatch.test.ts`:
595
+ ```ts
596
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
597
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
598
+ import { join } from 'node:path'
599
+ import { tmpdir } from 'node:os'
600
+ import { planRetrofit } from '../../src/retrofit/plan.js'
601
+
602
+ let canon: string
603
+ beforeEach(() => {
604
+ canon = mkdtempSync(join(tmpdir(), 'forge-canon-'))
605
+ const w = (rel: string, c: string) => { mkdirSync(join(canon, rel, '..'), { recursive: true }); writeFileSync(join(canon, rel), c) }
606
+ w('manifest.yaml', `
607
+ name: forge-canon
608
+ version: 0.1.0
609
+ agents: [claude, codex, gemini]
610
+ skills:
611
+ - { id: tdd, path: skills/tdd, kind: methodology }
612
+ policy: []
613
+ loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
614
+ tools: []
615
+ `)
616
+ w('AGENTS.md', '# Baseline\n')
617
+ w('skills/tdd/SKILL.md', '---\nname: tdd\ndescription: d\n---\nbody')
618
+ })
619
+ afterEach(() => { rmSync(canon, { recursive: true, force: true }) })
620
+
621
+ describe('planRetrofit', () => {
622
+ it('dispatches to one planner', () => {
623
+ const targets = planRetrofit(canon, '/t', ['codex']).map(a => a.target)
624
+ expect(targets).toContain('.codex/config.toml')
625
+ expect(targets).not.toContain('.claude/skills/tdd/SKILL.md')
626
+ })
627
+
628
+ it('merges multiple agents and de-dupes shared targets (AGENTS.md once)', () => {
629
+ const actions = planRetrofit(canon, '/t', ['claude', 'codex', 'gemini'])
630
+ const targets = actions.map(a => a.target)
631
+ expect(targets).toContain('.claude/skills/tdd/SKILL.md')
632
+ expect(targets).toContain('.codex/config.toml')
633
+ expect(targets).toContain('.gemini/commands/tdd.toml')
634
+ expect(targets.filter(t => t === 'AGENTS.md')).toHaveLength(1)
635
+ })
636
+ })
637
+ ```
638
+
639
+ - [ ] **Step 2: Run test to verify it fails**
640
+
641
+ Run: `npm test -- plan-dispatch`
642
+ Expected: FAIL — `planRetrofit` is not exported.
643
+
644
+ - [ ] **Step 3: Modify `src/retrofit/plan.ts`**
645
+
646
+ Keep the `Action` interface and `planClaudeRetrofit` (used by B1 tests). Add imports at the top:
647
+ ```ts
648
+ import { planClaude } from './planners/claude.js'
649
+ import { planCodex } from './planners/codex.js'
650
+ import { planGemini } from './planners/gemini.js'
651
+ import type { Agent } from './config.js'
652
+ ```
653
+
654
+ Re-point `planClaudeRetrofit` to the new planner (replace its body) so there is one source of truth:
655
+ ```ts
656
+ export function planClaudeRetrofit(canonDir: string, targetDir: string): Action[] {
657
+ return planClaude(canonDir, targetDir)
658
+ }
659
+ ```
660
+
661
+ Add the registry + dispatcher at the end of the file:
662
+ ```ts
663
+ export type AgentPlanner = (canonDir: string, targetDir: string) => Action[]
664
+
665
+ export const PLANNERS: Record<Agent, AgentPlanner> = {
666
+ claude: planClaude,
667
+ codex: planCodex,
668
+ gemini: planGemini,
669
+ }
670
+
671
+ export function planRetrofit(canonDir: string, targetDir: string, agents: Agent[]): Action[] {
672
+ const seen = new Set<string>()
673
+ const merged: Action[] = []
674
+ for (const agent of agents) {
675
+ for (const action of PLANNERS[agent](canonDir, targetDir)) {
676
+ if (seen.has(action.target)) continue
677
+ seen.add(action.target)
678
+ merged.push(action)
679
+ }
680
+ }
681
+ return merged
682
+ }
683
+ ```
684
+
685
+ Note: the original B1 `planClaudeRetrofit` body (the inline skill/AGENTS/CLAUDE logic) is now superseded by `planClaude` in Task 3, which is a superset (adds `.mcp.json`). The B1 `plan.test.ts` assertions (skill, AGENTS.md, CLAUDE.md, `@AGENTS.md`) still hold because `planClaude` emits all of those. Run `npm test -- retrofit/plan` to confirm B1's plan test still passes after the re-point.
686
+
687
+ - [ ] **Step 4: Run test to verify it passes**
688
+
689
+ Run: `npm test -- plan-dispatch retrofit/plan`
690
+ Expected: PASS — both the new dispatch tests and B1's original plan tests.
691
+
692
+ - [ ] **Step 5: Commit**
693
+
694
+ ```bash
695
+ git add src/retrofit/plan.ts tests/retrofit/plan-dispatch.test.ts
696
+ git commit -m "feat: add planner registry and multi-agent planRetrofit dispatch"
697
+ ```
698
+
699
+ ---
700
+
701
+ ### Task 7: CLI agent selection + multi-agent retrofit
702
+
703
+ **Files:**
704
+ - Modify: `src/cli.ts`
705
+ - Test: `tests/retrofit/retrofit.integration.test.ts` (extend)
706
+
707
+ - [ ] **Step 1: Add failing multi-agent integration tests**
708
+
709
+ Append to `tests/retrofit/retrofit.integration.test.ts` (inside the existing `describe`):
710
+ ```ts
711
+ it('retrofits all three agents when --agent all is selected', () => {
712
+ const code = runRetrofit(target, { loop: false, agents: ['claude', 'codex', 'gemini'] })
713
+ expect(code).toBe(0)
714
+ expect(existsSync(join(target, '.claude/skills/tdd/SKILL.md'))).toBe(true)
715
+ expect(existsSync(join(target, '.codex/config.toml'))).toBe(true)
716
+ expect(existsSync(join(target, '.gemini/settings.json'))).toBe(true)
717
+ const cfg = loadConfig(target)!
718
+ expect(cfg.agents).toEqual(expect.arrayContaining(['claude', 'codex', 'gemini']))
719
+ })
720
+
721
+ it('retrofits only the selected agent', () => {
722
+ runRetrofit(target, { loop: false, agents: ['gemini'] })
723
+ expect(existsSync(join(target, '.gemini/settings.json'))).toBe(true)
724
+ expect(existsSync(join(target, '.codex/config.toml'))).toBe(false)
725
+ })
726
+ ```
727
+
728
+ - [ ] **Step 2: Run test to verify it fails**
729
+
730
+ Run: `npm test -- retrofit.integration`
731
+ Expected: FAIL — `runRetrofit` does not accept an `agents` option yet (type error / undefined behavior).
732
+
733
+ - [ ] **Step 3: Modify `src/cli.ts` `runRetrofit`**
734
+
735
+ Replace the imports line for plan to use the dispatcher:
736
+ ```ts
737
+ import { planRetrofit } from './retrofit/plan.js'
738
+ import type { Agent } from './retrofit/config.js'
739
+ ```
740
+ (remove the now-unused `planClaudeRetrofit` import if present).
741
+
742
+ Change the `runRetrofit` signature and body:
743
+ ```ts
744
+ export function runRetrofit(targetDir: string, opts: { loop: boolean; agents?: Agent[] }): number {
745
+ const canonDir = resolveCanonDir()
746
+ const canonVersion = loadManifest(join(canonDir, 'manifest.yaml')).version
747
+
748
+ const detection = detectProject(targetDir)
749
+ const agents: Agent[] = opts.agents && opts.agents.length > 0
750
+ ? opts.agents
751
+ : (detection.agents.length > 0 ? detection.agents : ['claude'])
752
+
753
+ const actions = planRetrofit(canonDir, targetDir, agents)
754
+ const backupDir = join(targetDir, '.forge', 'backup', String(Date.now()))
755
+ const applied = applyActions(actions, targetDir, { backupDir })
756
+
757
+ const existing = loadConfig(targetDir)
758
+ const priorAgents = existing?.agents ?? []
759
+ const mergedAgents = [...new Set([...priorAgents, ...agents])]
760
+ const config: ForgeConfig = {
761
+ ...(existing ?? defaultConfig(canonVersion)),
762
+ canonVersion,
763
+ agents: mergedAgents,
764
+ loop: { enabled: opts.loop },
765
+ }
766
+ saveConfig(targetDir, config)
767
+
768
+ console.log(formatReport(applied, { loopEnabled: config.loop.enabled, detectedAgents: detection.agents }))
769
+ return 0
770
+ }
771
+ ```
772
+ (Ensure `detectProject` is imported — it was added in B1's fix. Keep that import.)
773
+
774
+ Update the `retrofit` case in `main` to parse `--agent`:
775
+ ```ts
776
+ case 'retrofit': {
777
+ const targetDir = rest.find(a => !a.startsWith('-')) ?? '.'
778
+ const loop = rest.includes('--loop')
779
+ const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length)
780
+ const all: Agent[] = ['claude', 'codex', 'gemini']
781
+ const agents = !agentArg || agentArg === 'all'
782
+ ? (agentArg === 'all' ? all : undefined)
783
+ : agentArg.split(',').filter((a): a is Agent => (all as string[]).includes(a))
784
+ return runRetrofit(targetDir, { loop, agents })
785
+ }
786
+ ```
787
+
788
+ Update the default usage line:
789
+ ```ts
790
+ default:
791
+ console.log('usage: forge <validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--loop] | loop <on|off|status|run>>')
792
+ return cmd ? 1 : 0
793
+ ```
794
+
795
+ - [ ] **Step 4: Run test to verify it passes**
796
+
797
+ Run: `npm test -- retrofit.integration`
798
+ Expected: PASS (the existing B1 integration tests + the two new multi-agent tests).
799
+
800
+ - [ ] **Step 5: Run the full suite + smoke**
801
+
802
+ Run: `npm test`
803
+ Expected: all tests pass.
804
+
805
+ Run (PowerShell):
806
+ ```
807
+ mkdir ../forge-smoke2 2>$null; npm run forge -- retrofit ../forge-smoke2 --agent=all
808
+ ```
809
+ Expected: report lists created files; `../forge-smoke2` contains `.claude/`, `.codex/config.toml`, `.gemini/settings.json`, `AGENTS.md`, `.forge/config.yaml`. Clean up: `Remove-Item -Recurse -Force ../forge-smoke2`.
810
+
811
+ - [ ] **Step 6: Commit**
812
+
813
+ ```bash
814
+ git add src/cli.ts tests/retrofit/retrofit.integration.test.ts
815
+ git commit -m "feat: forge retrofit --agent selection with multi-agent generation"
816
+ ```
817
+
818
+ ---
819
+
820
+ ### Task 8: Update the retrofit skill + canon tool docs
821
+
822
+ **Files:**
823
+ - Modify: `canon/skills/forge-retrofit/SKILL.md`
824
+
825
+ - [ ] **Step 1: Update `canon/skills/forge-retrofit/SKILL.md`**
826
+
827
+ Replace the numbered list step 1 and add an agent note. New content:
828
+ ```markdown
829
+ ---
830
+ name: forge-retrofit
831
+ description: Use when asked to "retrofit", "forge this project", or set up the Forge harness in a project — runs forge retrofit and asks whether to enable the autonomous loop.
832
+ ---
833
+
834
+ # Forge Retrofit
835
+
836
+ Set up (or update) the Forge harness in the current project.
837
+
838
+ 1. Run `forge retrofit .` to generate artifacts for the agents detected in the project, or `forge retrofit . --agent=all` for Claude + Codex + Gemini. This is non-destructive — existing files are backed up under `.forge/backup/` before any overwrite. Generated per agent:
839
+ - Claude: `.claude/skills/`, `AGENTS.md`, `CLAUDE.md`, `.mcp.json` (+ rtk hook when WSL is available).
840
+ - Codex: `AGENTS.md`, `.codex/config.toml` (MCP), `RTK.md`.
841
+ - Gemini: `GEMINI.md`, `.gemini/commands/*.toml`, `.gemini/settings.json`.
842
+ 2. **Ask the user whether to enable the autonomous Loop** (default off). If yes, add `--loop`. Recorded in `.forge/config.yaml`; toggle any time with `forge loop on|off`.
843
+ 3. Show the printed report (created/overwritten/unchanged + detected agents) and where backups went. Note that MCP launch commands in the generated configs may need adjusting to the user's local tool installs.
844
+ ```
845
+
846
+ - [ ] **Step 2: Validate the canon**
847
+
848
+ Run: `npm run forge -- validate canon`
849
+ Expected: `✓ canon valid (canon)`.
850
+
851
+ Run: `npm test -- real-canon`
852
+ Expected: PASS.
853
+
854
+ - [ ] **Step 3: Commit**
855
+
856
+ ```bash
857
+ git add canon/skills/forge-retrofit/SKILL.md
858
+ git commit -m "docs: document multi-agent retrofit in the forge-retrofit skill"
859
+ ```
860
+
861
+ ---
862
+
863
+ ## Self-Review
864
+
865
+ **1. Spec coverage (Baustein B2 scope):**
866
+ - Codex artifact generation (AGENTS.md, config.toml MCP, RTK.md) → Task 4 ✓
867
+ - Gemini artifact generation (GEMINI.md, command TOML, settings.json, AGENTS.md context) → Task 5 ✓
868
+ - Tool wiring: graphify + Playwright MCP per agent → Tasks 1, 3, 4, 5 ✓
869
+ - rtk per agent; Claude hook with WSL detection + instruction fallback → Tasks 1, 2, 3 ✓
870
+ - Gemini asymmetry (no hook → instruction) → Tasks 1, 5 ✓
871
+ - Per-agent planner registry + `--agent` selection, additive agent recording → Tasks 6, 7 ✓
872
+ - Non-destructive/idempotent (reuses B1 apply) → Task 7 ✓
873
+ - (Honest limitation, documented: exact MCP launch commands are best-effort templates; Codex project-vs-global config noted inline. C2 unaffected.)
874
+
875
+ **2. Placeholder scan:** No TBD/TODO. The "remove the unused import if your linter flags it" note in Task 5 is a conditional instruction, not a placeholder — the import is optional. Every code step is complete.
876
+
877
+ **3. Type consistency:** `Action`, `Agent`, `McpServerConfig`, `mcpServers()`, `rtkInstruction()`, `hasWsl()`, `planClaude`/`planCodex`/`planGemini`, `AgentPlanner`, `PLANNERS`, `planRetrofit`, `runRetrofit({loop,agents?})`, `formatReport(...,{loopEnabled,detectedAgents})` — consistent. `planClaudeRetrofit` retained as a thin delegate so B1's `plan.test.ts` stays green. Config merge uses the additive pattern established in B1. ✓
878
+
879
+ ---
880
+
881
+ ## Next Plans (not this document)
882
+
883
+ - **Plan C2 — Loop hardening:** independent test run (replace exit-code trust), per-iteration git-worktree isolation, Codex/Gemini runners, review-iteration with role separation, tool-surface-readiness gate.
884
+ - **Optional B3 — polish:** real per-tool install checks, Codex global-config merge helper, Serena swap option.