@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.
- package/CHANGELOG.md +130 -0
- package/README.md +583 -494
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/config.yaml +6 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/context/DECISIONS.md +9 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/prd.yaml +38 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/bench-verify.mjs +15 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/package.json +9 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/src/index.mjs +48 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-1.test.mjs +24 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-2.test.mjs +28 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-3.test.mjs +25 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/config.yaml +6 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/prd.yaml +32 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/bench-verify.mjs +15 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/package.json +9 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/src/index.mjs +3 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-1.test.mjs +24 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-2.test.mjs +28 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-3.test.mjs +25 -0
- package/bench/README.md +42 -0
- package/bench/RESULTS.md +36 -0
- package/bench/fixtures/string-kit/.yoke/config.yaml +6 -0
- package/bench/fixtures/string-kit/.yoke/prd.yaml +32 -0
- package/bench/fixtures/string-kit/bench-verify.mjs +15 -0
- package/bench/fixtures/string-kit/package.json +9 -0
- package/bench/fixtures/string-kit/src/index.mjs +3 -0
- package/bench/fixtures/string-kit/tests/STORY-1.test.mjs +24 -0
- package/bench/fixtures/string-kit/tests/STORY-2.test.mjs +28 -0
- package/bench/fixtures/string-kit/tests/STORY-3.test.mjs +25 -0
- package/bench/results/claude-2026-07-09T22-34-01.json +40 -0
- package/bench/run.mjs +115 -0
- package/canon/loop/loop-spec.md +5 -1
- package/canon/manifest.yaml +3 -0
- package/canon/skills/yoke-retrofit/SKILL.md +1 -0
- package/canon/tools/claude-mem.md +15 -0
- package/canon/tools/ui-ux-pro-max.md +15 -0
- package/dist/cli.js +37 -6
- package/dist/loop/cleanup.js +44 -3
- package/dist/loop/loop.js +67 -2
- package/dist/loop/reporter.js +88 -4
- package/dist/loop/run-command.js +25 -8
- package/dist/loop/runner.js +133 -14
- package/dist/loop/watchdog.js +67 -17
- package/dist/retrofit/apply.js +10 -3
- package/dist/retrofit/config.js +9 -1
- package/dist/retrofit/gitignore.js +4 -0
- package/dist/retrofit/planners/claude.js +4 -1
- package/dist/retrofit/planners/gemini.js +2 -1
- package/dist/retrofit/preserve.js +51 -0
- package/dist/update/check.js +86 -0
- package/dist/update/refresh.js +28 -0
- package/dist/update/upgrade.js +30 -0
- package/docs/PUBLISHING.md +41 -0
- package/docs/superpowers/plans/2026-06-27-forge-a-canon.md +815 -0
- package/docs/superpowers/plans/2026-06-27-forge-b1-retrofit-claude.md +842 -0
- package/docs/superpowers/plans/2026-06-27-forge-b2-codex-gemini-tools.md +884 -0
- package/docs/superpowers/plans/2026-06-27-forge-c1-loop-engine.md +891 -0
- package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -0
- package/docs/superpowers/plans/2026-06-28-forge-b3-settings-merge.md +364 -0
- package/docs/superpowers/plans/2026-06-28-forge-c2-loop-verify.md +502 -0
- package/docs/superpowers/plans/2026-06-28-forge-c3-multi-agent-runners.md +377 -0
- package/docs/superpowers/plans/2026-06-28-forge-c4-worktree-isolation.md +413 -0
- package/docs/superpowers/plans/2026-06-28-forge-c5-review-iteration.md +419 -0
- package/docs/superpowers/plans/2026-06-28-forge-cleanup-dep0190.md +233 -0
- package/docs/superpowers/plans/2026-06-28-forge-d-codegraph-choice-minimal-code.md +523 -0
- package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -0
- package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -0
- package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -0
- package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -0
- package/docs/superpowers/plans/2026-07-02-baustein-j-cross-model-review-gstack-compose.md +645 -0
- package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -0
- package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -0
- package/docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md +144 -0
- package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -0
- package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -0
- package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -0
- package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -0
- package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -0
- package/docs/superpowers/specs/2026-07-02-baustein-j-cross-model-review-gstack-compose-design.md +137 -0
- package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -0
- package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -0
- package/docs/superpowers/specs/2026-07-10-companion-tools-decision.md +33 -0
- package/docs/superpowers/specs/2026-07-10-multi-agent-parallel-loop-design.md +99 -0
- package/package.json +8 -2
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
# Forge — Baustein B1 (Retrofit-Skill, Claude Code) 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:** Add a `forge retrofit` command that, run in a target project, detects it, plans a **non-destructive** retrofit, generates **Claude Code** artifacts from the Canon (`.claude/skills/`, `AGENTS.md`, `CLAUDE.md`), records the loop opt-in in `.forge/config.yaml`, and reports what changed.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Pure planning (`planClaudeRetrofit` reads the Canon → list of `Action`s) is separated from effectful application (`applyActions` does backup-then-write, idempotent). Detection and config IO are isolated modules. The CLI wires them. This is Baustein B1 of the Forge spec (`docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md`) — Claude only; Codex/Gemini + tool-MCP wiring are Baustein B2.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (ESM), TypeScript, vitest, `yaml`. Reuses Baustein A modules (`loadManifest`, `Manifest`). Distribution via `npx`.
|
|
10
|
+
|
|
11
|
+
**Builds on:** Baustein A (already on `main`): `src/canon/manifest.ts` (`loadManifest`, `Manifest`), `src/canon/validate.ts`, `src/cli.ts` (`main` dispatch, import-safe via `isMain` guard).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
retrofit/
|
|
20
|
+
config.ts # ForgeConfig type + loadConfig/saveConfig (.forge/config.yaml)
|
|
21
|
+
detect.ts # detectProject() -> Detection
|
|
22
|
+
plan.ts # Action type + planClaudeRetrofit(canonDir, targetDir) -> Action[]
|
|
23
|
+
apply.ts # AppliedAction type + applyActions() (non-destructive, idempotent)
|
|
24
|
+
report.ts # formatReport(applied, config) -> string
|
|
25
|
+
canon-dir.ts # resolveCanonDir() — find the bundled canon next to package.json
|
|
26
|
+
cli.ts # MODIFY: add `retrofit` subcommand
|
|
27
|
+
tests/
|
|
28
|
+
retrofit/
|
|
29
|
+
config.test.ts
|
|
30
|
+
detect.test.ts
|
|
31
|
+
plan.test.ts
|
|
32
|
+
apply.test.ts
|
|
33
|
+
report.test.ts
|
|
34
|
+
retrofit.integration.test.ts
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each module has one responsibility: config IO, detection, pure planning, effectful apply, reporting, canon resolution. The pure/effectful split keeps planning unit-testable without a filesystem and makes apply’s backup logic independently verifiable.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Task 1: Forge config IO
|
|
42
|
+
|
|
43
|
+
**Files:**
|
|
44
|
+
- Create: `src/retrofit/config.ts`
|
|
45
|
+
- Test: `tests/retrofit/config.test.ts`
|
|
46
|
+
|
|
47
|
+
- [ ] **Step 1: Write the failing test**
|
|
48
|
+
|
|
49
|
+
Create `tests/retrofit/config.test.ts`:
|
|
50
|
+
```ts
|
|
51
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
52
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
53
|
+
import { join } from 'node:path'
|
|
54
|
+
import { tmpdir } from 'node:os'
|
|
55
|
+
import { loadConfig, saveConfig, defaultConfig } from '../../src/retrofit/config.js'
|
|
56
|
+
|
|
57
|
+
let dir: string
|
|
58
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'forge-cfg-')) })
|
|
59
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
60
|
+
|
|
61
|
+
describe('forge config', () => {
|
|
62
|
+
it('returns null when no config exists', () => {
|
|
63
|
+
expect(loadConfig(dir)).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('saves and reloads a config round-trip', () => {
|
|
67
|
+
const cfg = { canonVersion: '0.1.0', agents: ['claude'] as const, loop: { enabled: true } }
|
|
68
|
+
saveConfig(dir, cfg)
|
|
69
|
+
expect(existsSync(join(dir, '.forge', 'config.yaml'))).toBe(true)
|
|
70
|
+
expect(loadConfig(dir)).toEqual(cfg)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('defaultConfig has loop disabled', () => {
|
|
74
|
+
expect(defaultConfig('0.1.0').loop.enabled).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
80
|
+
|
|
81
|
+
Run: `npm test -- retrofit/config`
|
|
82
|
+
Expected: FAIL — cannot find module `src/retrofit/config.js`.
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
85
|
+
|
|
86
|
+
Create `src/retrofit/config.ts`:
|
|
87
|
+
```ts
|
|
88
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
89
|
+
import { join, dirname } from 'node:path'
|
|
90
|
+
import { parse, stringify } from 'yaml'
|
|
91
|
+
|
|
92
|
+
export type Agent = 'claude' | 'codex' | 'gemini'
|
|
93
|
+
|
|
94
|
+
export interface ForgeConfig {
|
|
95
|
+
canonVersion: string
|
|
96
|
+
agents: Agent[]
|
|
97
|
+
loop: { enabled: boolean }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function defaultConfig(canonVersion: string): ForgeConfig {
|
|
101
|
+
return { canonVersion, agents: [], loop: { enabled: false } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function configPath(targetDir: string): string {
|
|
105
|
+
return join(targetDir, '.forge', 'config.yaml')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function loadConfig(targetDir: string): ForgeConfig | null {
|
|
109
|
+
const file = configPath(targetDir)
|
|
110
|
+
if (!existsSync(file)) return null
|
|
111
|
+
return parse(readFileSync(file, 'utf8')) as ForgeConfig
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function saveConfig(targetDir: string, config: ForgeConfig): void {
|
|
115
|
+
const file = configPath(targetDir)
|
|
116
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
117
|
+
writeFileSync(file, stringify(config))
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
122
|
+
|
|
123
|
+
Run: `npm test -- retrofit/config`
|
|
124
|
+
Expected: PASS (3 tests).
|
|
125
|
+
|
|
126
|
+
- [ ] **Step 5: Commit**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git add src/retrofit/config.ts tests/retrofit/config.test.ts
|
|
130
|
+
git commit -m "feat: add forge .forge/config.yaml IO"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Task 2: Project detection
|
|
136
|
+
|
|
137
|
+
**Files:**
|
|
138
|
+
- Create: `src/retrofit/detect.ts`
|
|
139
|
+
- Test: `tests/retrofit/detect.test.ts`
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 1: Write the failing test**
|
|
142
|
+
|
|
143
|
+
Create `tests/retrofit/detect.test.ts`:
|
|
144
|
+
```ts
|
|
145
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
146
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
147
|
+
import { join } from 'node:path'
|
|
148
|
+
import { tmpdir } from 'node:os'
|
|
149
|
+
import { detectProject } from '../../src/retrofit/detect.js'
|
|
150
|
+
|
|
151
|
+
let dir: string
|
|
152
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'forge-detect-')) })
|
|
153
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
154
|
+
|
|
155
|
+
describe('detectProject', () => {
|
|
156
|
+
it('reports no agents in an empty project', () => {
|
|
157
|
+
const d = detectProject(dir)
|
|
158
|
+
expect(d.agents).toEqual([])
|
|
159
|
+
expect(d.hasAgentsMd).toBe(false)
|
|
160
|
+
expect(d.hasForgeConfig).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('detects claude via .claude/ and a CLAUDE.md', () => {
|
|
164
|
+
mkdirSync(join(dir, '.claude'), { recursive: true })
|
|
165
|
+
writeFileSync(join(dir, 'CLAUDE.md'), '# project')
|
|
166
|
+
expect(detectProject(dir).agents).toContain('claude')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('detects codex and gemini directories', () => {
|
|
170
|
+
mkdirSync(join(dir, '.codex'), { recursive: true })
|
|
171
|
+
mkdirSync(join(dir, '.gemini'), { recursive: true })
|
|
172
|
+
const d = detectProject(dir)
|
|
173
|
+
expect(d.agents).toContain('codex')
|
|
174
|
+
expect(d.agents).toContain('gemini')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('flags an existing AGENTS.md and .forge config', () => {
|
|
178
|
+
writeFileSync(join(dir, 'AGENTS.md'), 'x')
|
|
179
|
+
mkdirSync(join(dir, '.forge'), { recursive: true })
|
|
180
|
+
writeFileSync(join(dir, '.forge', 'config.yaml'), 'x')
|
|
181
|
+
const d = detectProject(dir)
|
|
182
|
+
expect(d.hasAgentsMd).toBe(true)
|
|
183
|
+
expect(d.hasForgeConfig).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
189
|
+
|
|
190
|
+
Run: `npm test -- retrofit/detect`
|
|
191
|
+
Expected: FAIL — cannot find module `src/retrofit/detect.js`.
|
|
192
|
+
|
|
193
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
194
|
+
|
|
195
|
+
Create `src/retrofit/detect.ts`:
|
|
196
|
+
```ts
|
|
197
|
+
import { existsSync } from 'node:fs'
|
|
198
|
+
import { join } from 'node:path'
|
|
199
|
+
import type { Agent } from './config.js'
|
|
200
|
+
|
|
201
|
+
export interface Detection {
|
|
202
|
+
agents: Agent[]
|
|
203
|
+
hasAgentsMd: boolean
|
|
204
|
+
hasForgeConfig: boolean
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function detectProject(targetDir: string): Detection {
|
|
208
|
+
const has = (...parts: string[]) => existsSync(join(targetDir, ...parts))
|
|
209
|
+
const agents: Agent[] = []
|
|
210
|
+
if (has('.claude') || has('CLAUDE.md')) agents.push('claude')
|
|
211
|
+
if (has('.codex') || has('AGENTS.md')) agents.push('codex')
|
|
212
|
+
if (has('.gemini') || has('GEMINI.md')) agents.push('gemini')
|
|
213
|
+
return {
|
|
214
|
+
agents,
|
|
215
|
+
hasAgentsMd: has('AGENTS.md'),
|
|
216
|
+
hasForgeConfig: has('.forge', 'config.yaml'),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Note: AGENTS.md implies a Codex-style project because Codex reads AGENTS.md natively; that is acceptable detection noise for B1 (we only generate Claude artifacts here). Refined per-agent gating lands in B2.
|
|
222
|
+
|
|
223
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
224
|
+
|
|
225
|
+
Run: `npm test -- retrofit/detect`
|
|
226
|
+
Expected: PASS (4 tests).
|
|
227
|
+
|
|
228
|
+
- [ ] **Step 5: Commit**
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
git add src/retrofit/detect.ts tests/retrofit/detect.test.ts
|
|
232
|
+
git commit -m "feat: add project/harness detection"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Task 3: Plan Claude retrofit (pure)
|
|
238
|
+
|
|
239
|
+
**Files:**
|
|
240
|
+
- Create: `src/retrofit/plan.ts`
|
|
241
|
+
- Test: `tests/retrofit/plan.test.ts`
|
|
242
|
+
|
|
243
|
+
- [ ] **Step 1: Write the failing test**
|
|
244
|
+
|
|
245
|
+
Create `tests/retrofit/plan.test.ts`:
|
|
246
|
+
```ts
|
|
247
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
248
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
249
|
+
import { join } from 'node:path'
|
|
250
|
+
import { tmpdir } from 'node:os'
|
|
251
|
+
import { planClaudeRetrofit } from '../../src/retrofit/plan.js'
|
|
252
|
+
|
|
253
|
+
let canon: string
|
|
254
|
+
beforeEach(() => {
|
|
255
|
+
canon = mkdtempSync(join(tmpdir(), 'forge-canon-'))
|
|
256
|
+
const w = (rel: string, c: string) => {
|
|
257
|
+
mkdirSync(join(canon, rel, '..'), { recursive: true })
|
|
258
|
+
writeFileSync(join(canon, rel), c)
|
|
259
|
+
}
|
|
260
|
+
w('manifest.yaml', `
|
|
261
|
+
name: forge-canon
|
|
262
|
+
version: 0.1.0
|
|
263
|
+
agents: [claude]
|
|
264
|
+
skills:
|
|
265
|
+
- { id: tdd, path: skills/tdd, kind: methodology }
|
|
266
|
+
policy: []
|
|
267
|
+
loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
|
|
268
|
+
tools: []
|
|
269
|
+
`)
|
|
270
|
+
w('AGENTS.md', '# Forge Harness Baseline\n')
|
|
271
|
+
w('skills/tdd/SKILL.md', '---\nname: tdd\ndescription: d\n---\nbody')
|
|
272
|
+
})
|
|
273
|
+
afterEach(() => { rmSync(canon, { recursive: true, force: true }) })
|
|
274
|
+
|
|
275
|
+
describe('planClaudeRetrofit', () => {
|
|
276
|
+
it('plans a skill, AGENTS.md, and CLAUDE.md', () => {
|
|
277
|
+
const actions = planClaudeRetrofit(canon, '/target')
|
|
278
|
+
const targets = actions.map(a => a.target)
|
|
279
|
+
expect(targets).toContain('.claude/skills/tdd/SKILL.md')
|
|
280
|
+
expect(targets).toContain('AGENTS.md')
|
|
281
|
+
expect(targets).toContain('CLAUDE.md')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('copies the canon skill content verbatim', () => {
|
|
285
|
+
const action = planClaudeRetrofit(canon, '/target').find(a => a.target === '.claude/skills/tdd/SKILL.md')!
|
|
286
|
+
expect(action.content).toContain('name: tdd')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('CLAUDE.md imports AGENTS.md', () => {
|
|
290
|
+
const action = planClaudeRetrofit(canon, '/target').find(a => a.target === 'CLAUDE.md')!
|
|
291
|
+
expect(action.content).toContain('@AGENTS.md')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
297
|
+
|
|
298
|
+
Run: `npm test -- retrofit/plan`
|
|
299
|
+
Expected: FAIL — cannot find module `src/retrofit/plan.js`.
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
302
|
+
|
|
303
|
+
Create `src/retrofit/plan.ts`:
|
|
304
|
+
```ts
|
|
305
|
+
import { readFileSync } from 'node:fs'
|
|
306
|
+
import { join } from 'node:path'
|
|
307
|
+
import { loadManifest } from '../canon/manifest.js'
|
|
308
|
+
|
|
309
|
+
export interface Action {
|
|
310
|
+
kind: 'write'
|
|
311
|
+
target: string
|
|
312
|
+
content: string
|
|
313
|
+
reason: string
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const CLAUDE_MD = `# Project Instructions
|
|
317
|
+
|
|
318
|
+
This project uses the Forge harness. Baseline instructions:
|
|
319
|
+
|
|
320
|
+
@AGENTS.md
|
|
321
|
+
`
|
|
322
|
+
|
|
323
|
+
export function planClaudeRetrofit(canonDir: string, _targetDir: string): Action[] {
|
|
324
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'))
|
|
325
|
+
const actions: Action[] = []
|
|
326
|
+
|
|
327
|
+
for (const skill of manifest.skills) {
|
|
328
|
+
const content = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8')
|
|
329
|
+
actions.push({
|
|
330
|
+
kind: 'write',
|
|
331
|
+
target: `.claude/skills/${skill.id}/SKILL.md`,
|
|
332
|
+
content,
|
|
333
|
+
reason: `skill: ${skill.id}`,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
actions.push({
|
|
338
|
+
kind: 'write',
|
|
339
|
+
target: 'AGENTS.md',
|
|
340
|
+
content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
|
|
341
|
+
reason: 'baseline instructions',
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
actions.push({
|
|
345
|
+
kind: 'write',
|
|
346
|
+
target: 'CLAUDE.md',
|
|
347
|
+
content: CLAUDE_MD,
|
|
348
|
+
reason: 'Claude entry importing AGENTS.md',
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
return actions
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
356
|
+
|
|
357
|
+
Run: `npm test -- retrofit/plan`
|
|
358
|
+
Expected: PASS (3 tests).
|
|
359
|
+
|
|
360
|
+
- [ ] **Step 5: Commit**
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
git add src/retrofit/plan.ts tests/retrofit/plan.test.ts
|
|
364
|
+
git commit -m "feat: plan Claude retrofit actions from the canon"
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
### Task 4: Apply actions (non-destructive, idempotent)
|
|
370
|
+
|
|
371
|
+
**Files:**
|
|
372
|
+
- Create: `src/retrofit/apply.ts`
|
|
373
|
+
- Test: `tests/retrofit/apply.test.ts`
|
|
374
|
+
|
|
375
|
+
- [ ] **Step 1: Write the failing test**
|
|
376
|
+
|
|
377
|
+
Create `tests/retrofit/apply.test.ts`:
|
|
378
|
+
```ts
|
|
379
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
380
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'
|
|
381
|
+
import { join } from 'node:path'
|
|
382
|
+
import { tmpdir } from 'node:os'
|
|
383
|
+
import { applyActions } from '../../src/retrofit/apply.js'
|
|
384
|
+
import type { Action } from '../../src/retrofit/plan.js'
|
|
385
|
+
|
|
386
|
+
let target: string
|
|
387
|
+
const backupDir = () => join(target, '.forge', 'backup', 'test')
|
|
388
|
+
|
|
389
|
+
const actions: Action[] = [
|
|
390
|
+
{ kind: 'write', target: 'AGENTS.md', content: 'NEW', reason: 'baseline' },
|
|
391
|
+
{ kind: 'write', target: '.claude/skills/tdd/SKILL.md', content: 'SKILL', reason: 'skill: tdd' },
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
beforeEach(() => { target = mkdtempSync(join(tmpdir(), 'forge-apply-')) })
|
|
395
|
+
afterEach(() => { rmSync(target, { recursive: true, force: true }) })
|
|
396
|
+
|
|
397
|
+
describe('applyActions', () => {
|
|
398
|
+
it('creates new files', () => {
|
|
399
|
+
const res = applyActions(actions, target, { backupDir: backupDir() })
|
|
400
|
+
expect(res.every(r => r.status === 'created')).toBe(true)
|
|
401
|
+
expect(readFileSync(join(target, 'AGENTS.md'), 'utf8')).toBe('NEW')
|
|
402
|
+
expect(existsSync(join(target, '.claude/skills/tdd/SKILL.md'))).toBe(true)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('is idempotent — second run reports unchanged and writes no backup', () => {
|
|
406
|
+
applyActions(actions, target, { backupDir: backupDir() })
|
|
407
|
+
const res = applyActions(actions, target, { backupDir: backupDir() })
|
|
408
|
+
expect(res.every(r => r.status === 'unchanged')).toBe(true)
|
|
409
|
+
expect(existsSync(backupDir())).toBe(false)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('backs up an existing file before overwriting with different content', () => {
|
|
413
|
+
writeFileSync(join(target, 'AGENTS.md'), 'OLD')
|
|
414
|
+
const res = applyActions(actions, target, { backupDir: backupDir() })
|
|
415
|
+
const agents = res.find(r => r.target === 'AGENTS.md')!
|
|
416
|
+
expect(agents.status).toBe('overwritten')
|
|
417
|
+
expect(agents.backedUp).toBeDefined()
|
|
418
|
+
expect(readFileSync(agents.backedUp!, 'utf8')).toBe('OLD')
|
|
419
|
+
expect(readFileSync(join(target, 'AGENTS.md'), 'utf8')).toBe('NEW')
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
425
|
+
|
|
426
|
+
Run: `npm test -- retrofit/apply`
|
|
427
|
+
Expected: FAIL — cannot find module `src/retrofit/apply.js`.
|
|
428
|
+
|
|
429
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
430
|
+
|
|
431
|
+
Create `src/retrofit/apply.ts`:
|
|
432
|
+
```ts
|
|
433
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
|
434
|
+
import { join, dirname } from 'node:path'
|
|
435
|
+
import type { Action } from './plan.js'
|
|
436
|
+
|
|
437
|
+
export interface AppliedAction {
|
|
438
|
+
target: string
|
|
439
|
+
status: 'created' | 'overwritten' | 'unchanged'
|
|
440
|
+
backedUp?: string
|
|
441
|
+
reason: string
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface ApplyOptions {
|
|
445
|
+
backupDir: string
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function applyActions(actions: Action[], targetDir: string, opts: ApplyOptions): AppliedAction[] {
|
|
449
|
+
const results: AppliedAction[] = []
|
|
450
|
+
|
|
451
|
+
for (const action of actions) {
|
|
452
|
+
const dest = join(targetDir, action.target)
|
|
453
|
+
let status: AppliedAction['status']
|
|
454
|
+
let backedUp: string | undefined
|
|
455
|
+
|
|
456
|
+
if (existsSync(dest)) {
|
|
457
|
+
const current = readFileSync(dest, 'utf8')
|
|
458
|
+
if (current === action.content) {
|
|
459
|
+
results.push({ target: action.target, status: 'unchanged', reason: action.reason })
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
backedUp = join(opts.backupDir, action.target)
|
|
463
|
+
mkdirSync(dirname(backedUp), { recursive: true })
|
|
464
|
+
copyFileSync(dest, backedUp)
|
|
465
|
+
status = 'overwritten'
|
|
466
|
+
} else {
|
|
467
|
+
status = 'created'
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
471
|
+
writeFileSync(dest, action.content)
|
|
472
|
+
results.push({ target: action.target, status, backedUp, reason: action.reason })
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return results
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
480
|
+
|
|
481
|
+
Run: `npm test -- retrofit/apply`
|
|
482
|
+
Expected: PASS (3 tests).
|
|
483
|
+
|
|
484
|
+
- [ ] **Step 5: Commit**
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
git add src/retrofit/apply.ts tests/retrofit/apply.test.ts
|
|
488
|
+
git commit -m "feat: apply retrofit actions non-destructively with backups"
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
### Task 5: Report formatting
|
|
494
|
+
|
|
495
|
+
**Files:**
|
|
496
|
+
- Create: `src/retrofit/report.ts`
|
|
497
|
+
- Test: `tests/retrofit/report.test.ts`
|
|
498
|
+
|
|
499
|
+
- [ ] **Step 1: Write the failing test**
|
|
500
|
+
|
|
501
|
+
Create `tests/retrofit/report.test.ts`:
|
|
502
|
+
```ts
|
|
503
|
+
import { describe, it, expect } from 'vitest'
|
|
504
|
+
import { formatReport } from '../../src/retrofit/report.js'
|
|
505
|
+
import type { AppliedAction } from '../../src/retrofit/apply.js'
|
|
506
|
+
|
|
507
|
+
const applied: AppliedAction[] = [
|
|
508
|
+
{ target: 'AGENTS.md', status: 'created', reason: 'baseline' },
|
|
509
|
+
{ target: 'CLAUDE.md', status: 'overwritten', backedUp: '/b/CLAUDE.md', reason: 'entry' },
|
|
510
|
+
{ target: '.claude/skills/tdd/SKILL.md', status: 'unchanged', reason: 'skill: tdd' },
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
describe('formatReport', () => {
|
|
514
|
+
it('summarizes counts and loop state', () => {
|
|
515
|
+
const out = formatReport(applied, { loopEnabled: false })
|
|
516
|
+
expect(out).toContain('1 created')
|
|
517
|
+
expect(out).toContain('1 overwritten')
|
|
518
|
+
expect(out).toContain('1 unchanged')
|
|
519
|
+
expect(out).toContain('Loop: disabled')
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('lists each target with its status', () => {
|
|
523
|
+
const out = formatReport(applied, { loopEnabled: true })
|
|
524
|
+
expect(out).toContain('AGENTS.md')
|
|
525
|
+
expect(out).toContain('Loop: enabled')
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
531
|
+
|
|
532
|
+
Run: `npm test -- retrofit/report`
|
|
533
|
+
Expected: FAIL — cannot find module `src/retrofit/report.js`.
|
|
534
|
+
|
|
535
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
536
|
+
|
|
537
|
+
Create `src/retrofit/report.ts`:
|
|
538
|
+
```ts
|
|
539
|
+
import type { AppliedAction } from './apply.js'
|
|
540
|
+
|
|
541
|
+
export interface ReportMeta {
|
|
542
|
+
loopEnabled: boolean
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function formatReport(applied: AppliedAction[], meta: ReportMeta): string {
|
|
546
|
+
const count = (s: AppliedAction['status']) => applied.filter(a => a.status === s).length
|
|
547
|
+
const lines: string[] = []
|
|
548
|
+
lines.push('Forge retrofit (Claude Code):')
|
|
549
|
+
for (const a of applied) {
|
|
550
|
+
const note = a.backedUp ? ` (backup: ${a.backedUp})` : ''
|
|
551
|
+
lines.push(` ${a.status.padEnd(11)} ${a.target}${note}`)
|
|
552
|
+
}
|
|
553
|
+
lines.push('')
|
|
554
|
+
lines.push(`Summary: ${count('created')} created, ${count('overwritten')} overwritten, ${count('unchanged')} unchanged`)
|
|
555
|
+
lines.push(`Loop: ${meta.loopEnabled ? 'enabled' : 'disabled'}`)
|
|
556
|
+
return lines.join('\n')
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
561
|
+
|
|
562
|
+
Run: `npm test -- retrofit/report`
|
|
563
|
+
Expected: PASS (2 tests).
|
|
564
|
+
|
|
565
|
+
- [ ] **Step 5: Commit**
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
git add src/retrofit/report.ts tests/retrofit/report.test.ts
|
|
569
|
+
git commit -m "feat: format retrofit report"
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Task 6: Canon directory resolution
|
|
575
|
+
|
|
576
|
+
**Files:**
|
|
577
|
+
- Create: `src/retrofit/canon-dir.ts`
|
|
578
|
+
- Test: `tests/retrofit/canon-dir.test.ts`
|
|
579
|
+
|
|
580
|
+
- [ ] **Step 1: Write the failing test**
|
|
581
|
+
|
|
582
|
+
Create `tests/retrofit/canon-dir.test.ts`:
|
|
583
|
+
```ts
|
|
584
|
+
import { describe, it, expect } from 'vitest'
|
|
585
|
+
import { existsSync } from 'node:fs'
|
|
586
|
+
import { join } from 'node:path'
|
|
587
|
+
import { resolveCanonDir } from '../../src/retrofit/canon-dir.js'
|
|
588
|
+
|
|
589
|
+
describe('resolveCanonDir', () => {
|
|
590
|
+
it('finds the bundled canon (contains manifest.yaml)', () => {
|
|
591
|
+
const dir = resolveCanonDir()
|
|
592
|
+
expect(existsSync(join(dir, 'manifest.yaml'))).toBe(true)
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
598
|
+
|
|
599
|
+
Run: `npm test -- retrofit/canon-dir`
|
|
600
|
+
Expected: FAIL — cannot find module `src/retrofit/canon-dir.js`.
|
|
601
|
+
|
|
602
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
603
|
+
|
|
604
|
+
Create `src/retrofit/canon-dir.ts`:
|
|
605
|
+
```ts
|
|
606
|
+
import { existsSync } from 'node:fs'
|
|
607
|
+
import { dirname, join } from 'node:path'
|
|
608
|
+
import { fileURLToPath } from 'node:url'
|
|
609
|
+
|
|
610
|
+
// Walk up from this module to the package root (the dir containing package.json),
|
|
611
|
+
// then return its `canon/` directory. Works under both tsx (src/) and built dist/.
|
|
612
|
+
export function resolveCanonDir(): string {
|
|
613
|
+
let dir = dirname(fileURLToPath(import.meta.url))
|
|
614
|
+
for (let i = 0; i < 10; i++) {
|
|
615
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
616
|
+
return join(dir, 'canon')
|
|
617
|
+
}
|
|
618
|
+
const parent = dirname(dir)
|
|
619
|
+
if (parent === dir) break
|
|
620
|
+
dir = parent
|
|
621
|
+
}
|
|
622
|
+
throw new Error('could not locate package root to resolve canon/')
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
627
|
+
|
|
628
|
+
Run: `npm test -- retrofit/canon-dir`
|
|
629
|
+
Expected: PASS (1 test).
|
|
630
|
+
|
|
631
|
+
- [ ] **Step 5: Commit**
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
git add src/retrofit/canon-dir.ts tests/retrofit/canon-dir.test.ts
|
|
635
|
+
git commit -m "feat: resolve bundled canon directory"
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
### Task 7: Wire `forge retrofit` CLI + integration test
|
|
641
|
+
|
|
642
|
+
**Files:**
|
|
643
|
+
- Modify: `src/cli.ts`
|
|
644
|
+
- Test: `tests/retrofit/retrofit.integration.test.ts`
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 1: Write the failing integration test**
|
|
647
|
+
|
|
648
|
+
Create `tests/retrofit/retrofit.integration.test.ts`:
|
|
649
|
+
```ts
|
|
650
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
651
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
652
|
+
import { join } from 'node:path'
|
|
653
|
+
import { tmpdir } from 'node:os'
|
|
654
|
+
import { runRetrofit } from '../../src/cli.js'
|
|
655
|
+
import { loadConfig } from '../../src/retrofit/config.js'
|
|
656
|
+
|
|
657
|
+
let target: string
|
|
658
|
+
beforeEach(() => { target = mkdtempSync(join(tmpdir(), 'forge-retro-')) })
|
|
659
|
+
afterEach(() => { rmSync(target, { recursive: true, force: true }) })
|
|
660
|
+
|
|
661
|
+
describe('forge retrofit (integration, Claude)', () => {
|
|
662
|
+
it('generates Claude artifacts and writes config with loop disabled by default', () => {
|
|
663
|
+
const code = runRetrofit(target, { loop: false })
|
|
664
|
+
expect(code).toBe(0)
|
|
665
|
+
expect(existsSync(join(target, 'AGENTS.md'))).toBe(true)
|
|
666
|
+
expect(existsSync(join(target, 'CLAUDE.md'))).toBe(true)
|
|
667
|
+
expect(existsSync(join(target, '.claude/skills/tdd/SKILL.md'))).toBe(true)
|
|
668
|
+
expect(existsSync(join(target, '.claude/skills/eng-review/SKILL.md'))).toBe(true)
|
|
669
|
+
const cfg = loadConfig(target)!
|
|
670
|
+
expect(cfg.agents).toContain('claude')
|
|
671
|
+
expect(cfg.loop.enabled).toBe(false)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
it('records loop enabled when --loop is passed', () => {
|
|
675
|
+
runRetrofit(target, { loop: true })
|
|
676
|
+
expect(loadConfig(target)!.loop.enabled).toBe(true)
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('is idempotent on a second run', () => {
|
|
680
|
+
runRetrofit(target, { loop: false })
|
|
681
|
+
const agentsBefore = readFileSync(join(target, 'AGENTS.md'), 'utf8')
|
|
682
|
+
const code = runRetrofit(target, { loop: false })
|
|
683
|
+
expect(code).toBe(0)
|
|
684
|
+
expect(readFileSync(join(target, 'AGENTS.md'), 'utf8')).toBe(agentsBefore)
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
690
|
+
|
|
691
|
+
Run: `npm test -- retrofit.integration`
|
|
692
|
+
Expected: FAIL — `runRetrofit` is not exported from `src/cli.js`.
|
|
693
|
+
|
|
694
|
+
- [ ] **Step 3: Modify `src/cli.ts` to add `runRetrofit` and the subcommand**
|
|
695
|
+
|
|
696
|
+
Add these imports at the top of `src/cli.ts` (below the existing import):
|
|
697
|
+
```ts
|
|
698
|
+
import { resolveCanonDir } from './retrofit/canon-dir.js'
|
|
699
|
+
import { planClaudeRetrofit } from './retrofit/plan.js'
|
|
700
|
+
import { applyActions } from './retrofit/apply.js'
|
|
701
|
+
import { formatReport } from './retrofit/report.js'
|
|
702
|
+
import { loadConfig, saveConfig, defaultConfig, type ForgeConfig } from './retrofit/config.js'
|
|
703
|
+
import { loadManifest } from './canon/manifest.js'
|
|
704
|
+
import { join } from 'node:path'
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Add the `runRetrofit` function (after `runValidate`):
|
|
708
|
+
```ts
|
|
709
|
+
export function runRetrofit(targetDir: string, opts: { loop: boolean }): number {
|
|
710
|
+
const canonDir = resolveCanonDir()
|
|
711
|
+
const canonVersion = loadManifest(join(canonDir, 'manifest.yaml')).version
|
|
712
|
+
|
|
713
|
+
const actions = planClaudeRetrofit(canonDir, targetDir)
|
|
714
|
+
const backupDir = join(targetDir, '.forge', 'backup', String(Date.now()))
|
|
715
|
+
const applied = applyActions(actions, targetDir, { backupDir })
|
|
716
|
+
|
|
717
|
+
const existing = loadConfig(targetDir)
|
|
718
|
+
const config: ForgeConfig = {
|
|
719
|
+
...(existing ?? defaultConfig(canonVersion)),
|
|
720
|
+
canonVersion,
|
|
721
|
+
agents: ['claude'],
|
|
722
|
+
loop: { enabled: opts.loop },
|
|
723
|
+
}
|
|
724
|
+
saveConfig(targetDir, config)
|
|
725
|
+
|
|
726
|
+
console.log(formatReport(applied, { loopEnabled: config.loop.enabled }))
|
|
727
|
+
return 0
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Extend the `switch` in `main` with a `retrofit` case:
|
|
732
|
+
```ts
|
|
733
|
+
case 'retrofit': {
|
|
734
|
+
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.'
|
|
735
|
+
const loop = rest.includes('--loop')
|
|
736
|
+
return runRetrofit(targetDir, { loop })
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
And update the default usage line:
|
|
741
|
+
```ts
|
|
742
|
+
default:
|
|
743
|
+
console.log('usage: forge <validate [canonDir] | retrofit [targetDir] [--loop]>')
|
|
744
|
+
return cmd ? 1 : 0
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
748
|
+
|
|
749
|
+
Run: `npm test -- retrofit.integration`
|
|
750
|
+
Expected: PASS (3 tests).
|
|
751
|
+
|
|
752
|
+
- [ ] **Step 5: Run the full suite and a manual smoke**
|
|
753
|
+
|
|
754
|
+
Run: `npm test`
|
|
755
|
+
Expected: all tests pass (Baustein A's 17 + B1's new tests).
|
|
756
|
+
|
|
757
|
+
Run (PowerShell), retrofitting a throwaway dir:
|
|
758
|
+
```
|
|
759
|
+
mkdir ../forge-smoke 2>$null; npm run forge -- retrofit ../forge-smoke
|
|
760
|
+
```
|
|
761
|
+
Expected: prints the retrofit report; `../forge-smoke/.claude/skills/tdd/SKILL.md`, `AGENTS.md`, `CLAUDE.md`, and `.forge/config.yaml` exist. Then delete it: `Remove-Item -Recurse -Force ../forge-smoke`.
|
|
762
|
+
|
|
763
|
+
- [ ] **Step 6: Commit**
|
|
764
|
+
|
|
765
|
+
```bash
|
|
766
|
+
git add src/cli.ts tests/retrofit/retrofit.integration.test.ts
|
|
767
|
+
git commit -m "feat: wire forge retrofit command for Claude Code"
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
### Task 8: Retrofit skill wrapper (agent-facing)
|
|
773
|
+
|
|
774
|
+
**Files:**
|
|
775
|
+
- Create: `canon/skills/forge-retrofit/SKILL.md`
|
|
776
|
+
- Test: (validated by Baustein A's `real-canon` test — the new skill must be added to `canon/manifest.yaml`)
|
|
777
|
+
|
|
778
|
+
- [ ] **Step 1: Add the skill to `canon/manifest.yaml`**
|
|
779
|
+
|
|
780
|
+
Add under `skills:`:
|
|
781
|
+
```yaml
|
|
782
|
+
- { id: forge-retrofit, path: skills/forge-retrofit, kind: methodology }
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 2: Create `canon/skills/forge-retrofit/SKILL.md`**
|
|
786
|
+
|
|
787
|
+
```markdown
|
|
788
|
+
---
|
|
789
|
+
name: forge-retrofit
|
|
790
|
+
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.
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
# Forge Retrofit
|
|
794
|
+
|
|
795
|
+
Set up (or update) the Forge harness in the current project.
|
|
796
|
+
|
|
797
|
+
1. Run `forge retrofit .` to generate the Claude Code artifacts (`.claude/skills/`, `AGENTS.md`, `CLAUDE.md`). This is non-destructive — existing files are backed up under `.forge/backup/` before any overwrite.
|
|
798
|
+
2. **Ask the user whether to enable the autonomous Loop** (default: off). If yes, re-run with `forge retrofit . --loop`. The choice is recorded in `.forge/config.yaml` and can be changed any time with `forge loop on|off` (Baustein C).
|
|
799
|
+
3. Show the user the printed report (what was created/overwritten/unchanged) and where backups went.
|
|
800
|
+
|
|
801
|
+
Never overwrite the user's existing instructions without surfacing the backup location in your summary.
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
- [ ] **Step 3: Run the canon validator and the real-canon test**
|
|
805
|
+
|
|
806
|
+
Run: `npm test -- real-canon`
|
|
807
|
+
Expected: PASS — the canon (now with `forge-retrofit`) still validates.
|
|
808
|
+
|
|
809
|
+
Run: `npm run forge -- validate canon`
|
|
810
|
+
Expected: `✓ canon valid (canon)`.
|
|
811
|
+
|
|
812
|
+
- [ ] **Step 4: Commit**
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
git add canon/manifest.yaml canon/skills/forge-retrofit/SKILL.md
|
|
816
|
+
git commit -m "feat: add agent-facing forge-retrofit skill to the canon"
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Self-Review
|
|
822
|
+
|
|
823
|
+
**1. Spec coverage (Baustein B1 scope):**
|
|
824
|
+
- detect → Task 2 ✓
|
|
825
|
+
- plan (non-destructive, additive) → Tasks 3, 4 (backup-before-overwrite) ✓
|
|
826
|
+
- generate Claude artifacts (.claude/skills, AGENTS.md, CLAUDE.md) → Tasks 3, 7 ✓
|
|
827
|
+
- report → Tasks 5, 7 ✓
|
|
828
|
+
- loop opt-in recorded in `.forge/config.yaml`, default off → Tasks 1, 7 ✓
|
|
829
|
+
- agent-facing skill that asks about the loop → Task 8 ✓
|
|
830
|
+
- idempotent + reversible (backups) → Tasks 4, 7 ✓
|
|
831
|
+
- (Deferred to B2: Codex/Gemini generation, rtk/graphify/playwright MCP wiring, rtk WSL detection. Correct — B1 is Claude-only.)
|
|
832
|
+
|
|
833
|
+
**2. Placeholder scan:** No TBD/TODO; every step has complete code/content. ✓
|
|
834
|
+
|
|
835
|
+
**3. Type consistency:** `Agent`, `ForgeConfig`, `Detection`, `Action{kind,target,content,reason}`, `AppliedAction{target,status,backedUp?,reason}`, `ReportMeta{loopEnabled}`, `planClaudeRetrofit`, `applyActions`, `formatReport`, `resolveCanonDir`, `runRetrofit`, `loadConfig/saveConfig/defaultConfig` — names and signatures consistent across tasks. `loadManifest`/`Manifest` reused from Baustein A unchanged. ✓
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
## Next Plans (not this document)
|
|
840
|
+
|
|
841
|
+
- **Plan B2 — Retrofit for Codex + Gemini + tool wiring:** generate `AGENTS.md`/`config.toml`/`RTK.md` (Codex) and `.gemini/commands/*.toml`/`GEMINI.md`/`settings.json` (Gemini); wire rtk (hook/instruction with WSL detection), graphify + Playwright MCP servers per agent.
|
|
842
|
+
- **Plan C — Loop-Engine:** Ralph driver, gates, worktree isolation, `forge loop on|off|status`.
|