@hecer/yoke 0.2.0 → 0.7.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 +112 -0
- package/README.md +557 -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 +32 -6
- package/dist/loop/cleanup.js +44 -3
- package/dist/loop/loop.js +27 -2
- package/dist/loop/reporter.js +21 -3
- package/dist/loop/run-command.js +19 -6
- package/dist/loop/runner.js +130 -13
- package/dist/loop/watchdog.js +67 -17
- package/dist/retrofit/apply.js +10 -3
- package/dist/retrofit/config.js +2 -0
- package/dist/retrofit/gitignore.js +2 -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,364 @@
|
|
|
1
|
+
# Forge — Baustein B3 (non-destructive settings merge) 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:** Make `forge retrofit` MERGE into an existing `.claude/settings.json` (preserving the user's other keys and not duplicating our hook) instead of replacing it.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add an optional `merge` flag to `Action`. `applyActions` gains a JSON deep-merge path: when `merge` is set and the target exists, it parses both, deep-merges (objects recursive; arrays concatenated + de-duplicated), backs up the original, and writes the merged result (status `merged`). The Claude planner marks only `.claude/settings.json` with `merge: true`. A pure `mergeJson` util holds the merge semantics. Baustein B3 of the Forge spec.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (ESM), TypeScript, vitest. Extends B1/B2 `apply.ts`, `report.ts`, `planners/claude.ts`.
|
|
10
|
+
|
|
11
|
+
**Builds on:** A+B1+B2+C1+C2 on `main`. Modifies: `src/retrofit/apply.ts` (`Action` is defined in `plan.ts`; `AppliedAction` here), `src/retrofit/report.ts`, `src/retrofit/planners/claude.ts`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/retrofit/
|
|
19
|
+
merge-json.ts # NEW: mergeJson(base, incoming) deep merge (pure)
|
|
20
|
+
plan.ts # MODIFY: Action gains optional `merge?: boolean`
|
|
21
|
+
apply.ts # MODIFY: AppliedAction status gains 'merged'; merge path in applyActions
|
|
22
|
+
report.ts # MODIFY: count/format 'merged'
|
|
23
|
+
planners/claude.ts # MODIFY: mark .claude/settings.json action with merge: true
|
|
24
|
+
tests/retrofit/
|
|
25
|
+
merge-json.test.ts # NEW
|
|
26
|
+
apply.test.ts # MODIFY: merge-into-existing case
|
|
27
|
+
report.test.ts # MODIFY: merged count
|
|
28
|
+
planners-claude.test.ts # MODIFY: settings.json carries merge:true
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### Task 1: mergeJson util
|
|
34
|
+
|
|
35
|
+
**Files:**
|
|
36
|
+
- Create: `src/retrofit/merge-json.ts`
|
|
37
|
+
- Test: `tests/retrofit/merge-json.test.ts`
|
|
38
|
+
|
|
39
|
+
- [ ] **Step 1: Write the failing test**
|
|
40
|
+
|
|
41
|
+
Create `tests/retrofit/merge-json.test.ts`:
|
|
42
|
+
```ts
|
|
43
|
+
import { describe, it, expect } from 'vitest'
|
|
44
|
+
import { mergeJson } from '../../src/retrofit/merge-json.js'
|
|
45
|
+
|
|
46
|
+
describe('mergeJson', () => {
|
|
47
|
+
it('preserves keys only present in base', () => {
|
|
48
|
+
expect(mergeJson({ model: 'opus', x: 1 }, { y: 2 })).toEqual({ model: 'opus', x: 1, y: 2 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('recursively merges nested objects', () => {
|
|
52
|
+
expect(mergeJson({ a: { p: 1 } }, { a: { q: 2 } })).toEqual({ a: { p: 1, q: 2 } })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('concatenates arrays and de-dupes by structural equality', () => {
|
|
56
|
+
expect(mergeJson({ h: [{ k: 1 }] }, { h: [{ k: 1 }, { k: 2 }] })).toEqual({ h: [{ k: 1 }, { k: 2 }] })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('incoming primitive overrides base on the same key', () => {
|
|
60
|
+
expect(mergeJson({ a: 1 }, { a: 2 })).toEqual({ a: 2 })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('incoming wins when types mismatch', () => {
|
|
64
|
+
expect(mergeJson({ a: { p: 1 } }, { a: 5 })).toEqual({ a: 5 })
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
70
|
+
|
|
71
|
+
Run: `npm test -- merge-json`
|
|
72
|
+
Expected: FAIL — cannot find module `src/retrofit/merge-json.js`.
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
75
|
+
|
|
76
|
+
Create `src/retrofit/merge-json.ts`:
|
|
77
|
+
```ts
|
|
78
|
+
type Json = unknown
|
|
79
|
+
|
|
80
|
+
function isPlainObject(v: Json): v is Record<string, Json> {
|
|
81
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Deep-merge two parsed JSON values. Objects merge by key (recursing on shared
|
|
85
|
+
// keys). Arrays concatenate with structural de-duplication. For any other type
|
|
86
|
+
// mismatch or primitive collision, the incoming value wins.
|
|
87
|
+
export function mergeJson(base: Json, incoming: Json): Json {
|
|
88
|
+
if (isPlainObject(base) && isPlainObject(incoming)) {
|
|
89
|
+
const out: Record<string, Json> = { ...base }
|
|
90
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
91
|
+
out[key] = key in base ? mergeJson(base[key], value) : value
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(base) && Array.isArray(incoming)) {
|
|
96
|
+
const out = [...base]
|
|
97
|
+
for (const item of incoming) {
|
|
98
|
+
if (!out.some(existing => JSON.stringify(existing) === JSON.stringify(item))) {
|
|
99
|
+
out.push(item)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
return incoming
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
109
|
+
|
|
110
|
+
Run: `npm test -- merge-json`
|
|
111
|
+
Expected: PASS (5 tests).
|
|
112
|
+
|
|
113
|
+
- [ ] **Step 5: Commit**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git add src/retrofit/merge-json.ts tests/retrofit/merge-json.test.ts
|
|
117
|
+
git commit -m "feat: add JSON deep-merge utility"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Task 2: Merge path in applyActions
|
|
123
|
+
|
|
124
|
+
**Files:**
|
|
125
|
+
- Modify: `src/retrofit/plan.ts` (Action interface), `src/retrofit/apply.ts`
|
|
126
|
+
- Test: `tests/retrofit/apply.test.ts` (extend)
|
|
127
|
+
|
|
128
|
+
- [ ] **Step 1: Add failing tests**
|
|
129
|
+
|
|
130
|
+
Append to `tests/retrofit/apply.test.ts` (inside the existing `describe`):
|
|
131
|
+
```ts
|
|
132
|
+
it('merges a merge-flagged JSON action into an existing file, preserving user keys', () => {
|
|
133
|
+
writeFileSync(join(target, 'settings.json'), JSON.stringify({ model: 'opus', hooks: { A: [1] } }))
|
|
134
|
+
const mergeAction: Action = {
|
|
135
|
+
kind: 'write', target: 'settings.json', merge: true,
|
|
136
|
+
content: JSON.stringify({ hooks: { A: [1], B: [2] } }), reason: 'settings',
|
|
137
|
+
}
|
|
138
|
+
const res = applyActions([mergeAction], target, { backupDir: backupDir() })
|
|
139
|
+
expect(res[0].status).toBe('merged')
|
|
140
|
+
expect(res[0].backedUp).toBeDefined()
|
|
141
|
+
const written = JSON.parse(readFileSync(join(target, 'settings.json'), 'utf8'))
|
|
142
|
+
expect(written.model).toBe('opus') // user key preserved
|
|
143
|
+
expect(written.hooks.A).toEqual([1]) // de-duped, not [1,1]
|
|
144
|
+
expect(written.hooks.B).toEqual([2]) // ours added
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('treats a merge action as a plain create when the file does not exist', () => {
|
|
148
|
+
const mergeAction: Action = {
|
|
149
|
+
kind: 'write', target: 'fresh.json', merge: true,
|
|
150
|
+
content: JSON.stringify({ a: 1 }), reason: 'x',
|
|
151
|
+
}
|
|
152
|
+
const res = applyActions([mergeAction], target, { backupDir: backupDir() })
|
|
153
|
+
expect(res[0].status).toBe('created')
|
|
154
|
+
expect(JSON.parse(readFileSync(join(target, 'fresh.json'), 'utf8'))).toEqual({ a: 1 })
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
159
|
+
|
|
160
|
+
Run: `npm test -- retrofit/apply`
|
|
161
|
+
Expected: FAIL — `Action` has no `merge`; `applyActions` produces no `merged` status.
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 3: Add `merge` to the `Action` interface in `src/retrofit/plan.ts`**
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
export interface Action {
|
|
167
|
+
kind: 'write'
|
|
168
|
+
target: string
|
|
169
|
+
content: string
|
|
170
|
+
reason: string
|
|
171
|
+
merge?: boolean
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 4: Add the merge path to `src/retrofit/apply.ts`**
|
|
176
|
+
|
|
177
|
+
Add the import at the top:
|
|
178
|
+
```ts
|
|
179
|
+
import { mergeJson } from './merge-json.js'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Extend the `AppliedAction` status union:
|
|
183
|
+
```ts
|
|
184
|
+
export interface AppliedAction {
|
|
185
|
+
target: string
|
|
186
|
+
status: 'created' | 'overwritten' | 'unchanged' | 'merged'
|
|
187
|
+
backedUp?: string
|
|
188
|
+
reason: string
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
In `applyActions`, inside the `if (existsSync(dest))` branch, handle merge before the plain overwrite. Replace the existing `if (existsSync(dest)) { ... }` block with:
|
|
193
|
+
```ts
|
|
194
|
+
if (existsSync(dest)) {
|
|
195
|
+
const current = readFileSync(dest, 'utf8')
|
|
196
|
+
|
|
197
|
+
if (action.merge) {
|
|
198
|
+
const merged = JSON.stringify(mergeJson(JSON.parse(current), JSON.parse(action.content)), null, 2) + '\n'
|
|
199
|
+
if (merged === current) {
|
|
200
|
+
results.push({ target: action.target, status: 'unchanged', reason: action.reason })
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
backedUp = join(opts.backupDir, action.target)
|
|
204
|
+
mkdirSync(dirname(backedUp), { recursive: true })
|
|
205
|
+
copyFileSync(dest, backedUp)
|
|
206
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
207
|
+
writeFileSync(dest, merged)
|
|
208
|
+
results.push({ target: action.target, status: 'merged', backedUp, reason: action.reason })
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (current === action.content) {
|
|
213
|
+
results.push({ target: action.target, status: 'unchanged', reason: action.reason })
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
backedUp = join(opts.backupDir, action.target)
|
|
217
|
+
mkdirSync(dirname(backedUp), { recursive: true })
|
|
218
|
+
copyFileSync(dest, backedUp)
|
|
219
|
+
status = 'overwritten'
|
|
220
|
+
} else {
|
|
221
|
+
status = 'created'
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
(Keep the rest of the loop — the final `mkdirSync(dirname(dest)...)` + `writeFileSync(dest, action.content)` + `results.push` for the non-merge create/overwrite path — unchanged.)
|
|
225
|
+
|
|
226
|
+
- [ ] **Step 5: Run test to verify it passes**
|
|
227
|
+
|
|
228
|
+
Run: `npm test -- retrofit/apply`
|
|
229
|
+
Expected: PASS (existing + 2 new).
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 6: Commit**
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git add src/retrofit/plan.ts src/retrofit/apply.ts tests/retrofit/apply.test.ts
|
|
235
|
+
git commit -m "feat: merge JSON actions into existing files non-destructively"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Task 3: Report counts merged
|
|
241
|
+
|
|
242
|
+
**Files:**
|
|
243
|
+
- Modify: `src/retrofit/report.ts`
|
|
244
|
+
- Test: `tests/retrofit/report.test.ts` (extend)
|
|
245
|
+
|
|
246
|
+
- [ ] **Step 1: Add a failing test**
|
|
247
|
+
|
|
248
|
+
In `tests/retrofit/report.test.ts`, add a `merged` action to the `applied` fixture array and a test:
|
|
249
|
+
```ts
|
|
250
|
+
it('counts merged actions in the summary', () => {
|
|
251
|
+
const out = formatReport(
|
|
252
|
+
[{ target: '.claude/settings.json', status: 'merged', backedUp: '/b', reason: 'settings' }],
|
|
253
|
+
{ loopEnabled: false, detectedAgents: [] },
|
|
254
|
+
)
|
|
255
|
+
expect(out).toContain('1 merged')
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
260
|
+
|
|
261
|
+
Run: `npm test -- retrofit/report`
|
|
262
|
+
Expected: FAIL — summary has no merged count.
|
|
263
|
+
|
|
264
|
+
- [ ] **Step 3: Update `src/retrofit/report.ts`**
|
|
265
|
+
|
|
266
|
+
In `formatReport`, add `merged` to the summary line. Change the summary push to:
|
|
267
|
+
```ts
|
|
268
|
+
lines.push(`Summary: ${count('created')} created, ${count('overwritten')} overwritten, ${count('merged')} merged, ${count('unchanged')} unchanged`)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
272
|
+
|
|
273
|
+
Run: `npm test -- retrofit/report`
|
|
274
|
+
Expected: PASS (existing + 1 new).
|
|
275
|
+
|
|
276
|
+
- [ ] **Step 5: Commit**
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
git add src/retrofit/report.ts tests/retrofit/report.test.ts
|
|
280
|
+
git commit -m "feat: report merged-action count"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
### Task 4: Claude planner marks settings.json as merge
|
|
286
|
+
|
|
287
|
+
**Files:**
|
|
288
|
+
- Modify: `src/retrofit/planners/claude.ts`
|
|
289
|
+
- Test: `tests/retrofit/planners-claude.test.ts` (extend)
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 1: Add a failing test**
|
|
292
|
+
|
|
293
|
+
In `tests/retrofit/planners-claude.test.ts`, add (the WSL-true path emits settings.json):
|
|
294
|
+
```ts
|
|
295
|
+
it('marks .claude/settings.json as a merge action', () => {
|
|
296
|
+
const settings = planClaude(canon, '/t', true).find(a => a.target === '.claude/settings.json')!
|
|
297
|
+
expect(settings.merge).toBe(true)
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
302
|
+
|
|
303
|
+
Run: `npm test -- planners-claude`
|
|
304
|
+
Expected: FAIL — the settings action has no `merge: true`.
|
|
305
|
+
|
|
306
|
+
- [ ] **Step 3: Update `src/retrofit/planners/claude.ts`**
|
|
307
|
+
|
|
308
|
+
In the `if (rtkHookable)` block, add `merge: true` to the `.claude/settings.json` action:
|
|
309
|
+
```ts
|
|
310
|
+
actions.push({
|
|
311
|
+
kind: 'write',
|
|
312
|
+
target: '.claude/settings.json',
|
|
313
|
+
merge: true,
|
|
314
|
+
content: JSON.stringify({
|
|
315
|
+
_forge: 'Generated by forge retrofit. A pre-existing .claude/settings.json (if any) was backed up under .forge/backup/.',
|
|
316
|
+
hooks: {
|
|
317
|
+
PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'rtk hook' }] }],
|
|
318
|
+
},
|
|
319
|
+
}, null, 2) + '\n',
|
|
320
|
+
reason: 'rtk PreToolUse hook (WSL detected)',
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
325
|
+
|
|
326
|
+
Run: `npm test -- planners-claude`
|
|
327
|
+
Expected: PASS (existing + 1 new).
|
|
328
|
+
|
|
329
|
+
- [ ] **Step 5: Run the full suite + build**
|
|
330
|
+
|
|
331
|
+
Run: `npm test`
|
|
332
|
+
Expected: all pass.
|
|
333
|
+
|
|
334
|
+
Run: `npm run build`
|
|
335
|
+
Expected: tsc 0 errors.
|
|
336
|
+
|
|
337
|
+
Run: `npm run forge -- validate canon`
|
|
338
|
+
Expected: `✓ canon valid (canon)`.
|
|
339
|
+
|
|
340
|
+
- [ ] **Step 6: Commit**
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
git add src/retrofit/planners/claude.ts tests/retrofit/planners-claude.test.ts
|
|
344
|
+
git commit -m "feat: mark .claude/settings.json as a non-destructive merge"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Self-Review
|
|
350
|
+
|
|
351
|
+
**1. Spec coverage (B3 scope):**
|
|
352
|
+
- Merge into existing `.claude/settings.json` (preserve user keys, no hook duplication) → Tasks 1, 2, 4 ✓
|
|
353
|
+
- Generalizable merge mechanism (`Action.merge` + `mergeJson`) → Tasks 1, 2 ✓
|
|
354
|
+
- Still non-destructive (backup before merge-write) → Task 2 ✓
|
|
355
|
+
- Idempotent (merge equal → unchanged) → Task 2 ✓
|
|
356
|
+
- Report reflects merges → Task 3 ✓
|
|
357
|
+
|
|
358
|
+
**2. Placeholder scan:** No TBD/TODO; every step has complete code.
|
|
359
|
+
|
|
360
|
+
**3. Type consistency:** `mergeJson`, `Action.merge?`, `AppliedAction.status` (now includes `merged`), `formatReport` — consistent. The merge path reuses the existing backup mechanism. `JSON.stringify(..., null, 2) + '\n'` formatting matches how the planner generates the content so idempotency holds. ✓
|
|
361
|
+
|
|
362
|
+
## Next Plans (not this document)
|
|
363
|
+
|
|
364
|
+
- **Plan C3 — Multi-agent runners + worktree isolation + review-iteration.**
|