@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,377 @@
|
|
|
1
|
+
# Forge — Baustein C3 (multi-agent loop runners) 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:** Let the autonomous loop drive any of the three agents (Claude / Codex / Gemini), selected by config or a `--runner` flag, and refuse to start with a clear message if the chosen agent's CLI is not installed (a tool-surface-readiness gate that turns the dogfood ENOENT into an upfront error).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Generalize the claude-only runner into an agent-parameterized one: `AGENT_SPECS` maps each agent to its headless CLI form, `agentInvocation(agent, prompt, cwd)` builds the cross-platform invocation (prompt via stdin, shell on Windows so `.cmd` shims resolve), and `makeRunner(agent)` returns an `AgentRunner`. `isAgentAvailable(agent)` probes `<cli> --version`. `runLoopCommand` resolves the runner from `--runner`/`config.agents` and gates on availability. Baustein C3; per-iteration git-worktree isolation and review-iteration with role separation are C4.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (ESM), TypeScript, vitest. Extends C1/C2 `runner.ts`, `run-command.ts`, `cli.ts`. Back-compat: `claudeInvocation` and `claudeRunner` stay exported and behave identically.
|
|
10
|
+
|
|
11
|
+
**Builds on:** A+B1+B2+C1+C2+B3 on `main`. Modifies: `src/loop/runner.ts`, `src/loop/run-command.ts`, `src/cli.ts`, `canon/loop/loop-spec.md`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/loop/
|
|
19
|
+
runner.ts # MODIFY: AGENT_SPECS, agentInvocation, makeRunner, isAgentAvailable; keep claudeInvocation/claudeRunner
|
|
20
|
+
run-command.ts # MODIFY: resolve runner by agent + readiness gate (injectable); RunLoopCommandOptions gains agent?, isAvailable?
|
|
21
|
+
cli.ts (src/cli.ts) # MODIFY: loop run parses --runner=<agent>
|
|
22
|
+
canon/loop/loop-spec.md # MODIFY: document multi-agent runner + readiness gate
|
|
23
|
+
tests/loop/
|
|
24
|
+
runner.test.ts # MODIFY: agentInvocation per agent; isAgentAvailable boolean
|
|
25
|
+
loop-cli.integration.test.ts # MODIFY: readiness-gate refusal via stub
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Task 1: Generalize the runner to all three agents
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Modify: `src/loop/runner.ts`
|
|
34
|
+
- Test: `tests/loop/runner.test.ts` (extend)
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1: Add failing tests**
|
|
37
|
+
|
|
38
|
+
In `tests/loop/runner.test.ts`, extend the import and add cases:
|
|
39
|
+
```ts
|
|
40
|
+
import { buildClaudePrompt, claudeInvocation, agentInvocation, makeRunner, isAgentAvailable } from '../../src/loop/runner.js'
|
|
41
|
+
```
|
|
42
|
+
Add after the existing `claudeInvocation` describe block:
|
|
43
|
+
```ts
|
|
44
|
+
describe('agentInvocation', () => {
|
|
45
|
+
it('maps codex to `codex exec` with the prompt on stdin', () => {
|
|
46
|
+
const inv = agentInvocation('codex', 'P', '/w')
|
|
47
|
+
expect(inv.command).toBe('codex')
|
|
48
|
+
expect(inv.args).toEqual(['exec'])
|
|
49
|
+
expect(inv.options.input).toBe('P')
|
|
50
|
+
expect(inv.args).not.toContain('P')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('maps gemini to `gemini -p` with the prompt on stdin', () => {
|
|
54
|
+
const inv = agentInvocation('gemini', 'P', '/w')
|
|
55
|
+
expect(inv.command).toBe('gemini')
|
|
56
|
+
expect(inv.args).toEqual(['-p'])
|
|
57
|
+
expect(inv.options.input).toBe('P')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('claude back-compat: claudeInvocation equals agentInvocation(claude)', () => {
|
|
61
|
+
expect(claudeInvocation('P', '/w')).toEqual(agentInvocation('claude', 'P', '/w'))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('uses shell mode only on Windows for every agent', () => {
|
|
65
|
+
expect(agentInvocation('gemini', 'P', '/w').options.shell).toBe(process.platform === 'win32')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('makeRunner / isAgentAvailable', () => {
|
|
70
|
+
it('makeRunner returns a callable AgentRunner', () => {
|
|
71
|
+
expect(typeof makeRunner('codex')).toBe('function')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('isAgentAvailable returns a boolean and never throws', () => {
|
|
75
|
+
expect(typeof isAgentAvailable('claude')).toBe('boolean')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
81
|
+
|
|
82
|
+
Run: `npm test -- loop/runner`
|
|
83
|
+
Expected: FAIL — `agentInvocation`/`makeRunner`/`isAgentAvailable` not exported.
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 3: Modify `src/loop/runner.ts`**
|
|
86
|
+
|
|
87
|
+
Add the `Agent` type import at the top (keep existing imports):
|
|
88
|
+
```ts
|
|
89
|
+
import type { Agent } from '../retrofit/config.js'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Rename the `ClaudeInvocation` interface to `Invocation` (it is a structural shape; the existing test uses the function, not the type name):
|
|
93
|
+
```ts
|
|
94
|
+
export interface Invocation {
|
|
95
|
+
command: string
|
|
96
|
+
args: string[]
|
|
97
|
+
options: {
|
|
98
|
+
cwd: string
|
|
99
|
+
input: string
|
|
100
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
101
|
+
shell: boolean
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Add the spec map and `agentInvocation` (place above `claudeInvocation`):
|
|
107
|
+
```ts
|
|
108
|
+
const AGENT_SPECS: Record<Agent, { command: string; baseArgs: string[] }> = {
|
|
109
|
+
claude: { command: 'claude', baseArgs: ['-p'] },
|
|
110
|
+
codex: { command: 'codex', baseArgs: ['exec'] },
|
|
111
|
+
gemini: { command: 'gemini', baseArgs: ['-p'] },
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Build a cross-platform headless invocation for an agent. The prompt goes via
|
|
115
|
+
// stdin (so it needs no shell escaping); shell mode is enabled on Windows so the
|
|
116
|
+
// agent's `.cmd` shim resolves via PATHEXT (the only args are safe literal flags).
|
|
117
|
+
export function agentInvocation(agent: Agent, prompt: string, cwd: string): Invocation {
|
|
118
|
+
const spec = AGENT_SPECS[agent]
|
|
119
|
+
return {
|
|
120
|
+
command: spec.command,
|
|
121
|
+
args: spec.baseArgs,
|
|
122
|
+
options: {
|
|
123
|
+
cwd,
|
|
124
|
+
input: prompt,
|
|
125
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
126
|
+
shell: process.platform === 'win32',
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Replace `claudeInvocation` with a thin back-compat wrapper:
|
|
133
|
+
```ts
|
|
134
|
+
export function claudeInvocation(prompt: string, cwd: string): Invocation {
|
|
135
|
+
return agentInvocation('claude', prompt, cwd)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Add `makeRunner` and `isAgentAvailable`, and redefine `claudeRunner` in terms of `makeRunner` (replace the existing `claudeRunner` function):
|
|
140
|
+
```ts
|
|
141
|
+
export function makeRunner(agent: Agent): AgentRunner {
|
|
142
|
+
return (ctx: AgentContext): AgentResult => {
|
|
143
|
+
const inv = agentInvocation(agent, buildClaudePrompt(ctx.story), ctx.targetDir)
|
|
144
|
+
try {
|
|
145
|
+
// NOTE: The loop trusts the agent's exit code as a proxy for "it ran".
|
|
146
|
+
// Independent test verification happens in the loop (Baustein C2), not here.
|
|
147
|
+
execFileSync(inv.command, inv.args, inv.options)
|
|
148
|
+
return { success: true, summary: `${agent} implemented ${ctx.story.id}` }
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return { success: false, summary: `${agent} failed on ${ctx.story.id}: ${(e as Error).message}` }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const claudeRunner: AgentRunner = makeRunner('claude')
|
|
156
|
+
|
|
157
|
+
// Probe whether the agent's CLI is on PATH (so the loop can refuse upfront with a
|
|
158
|
+
// clear message instead of failing mid-run with spawn ENOENT). Never throws.
|
|
159
|
+
export function isAgentAvailable(agent: Agent): boolean {
|
|
160
|
+
const spec = AGENT_SPECS[agent]
|
|
161
|
+
try {
|
|
162
|
+
execFileSync(spec.command, ['--version'], {
|
|
163
|
+
stdio: 'pipe',
|
|
164
|
+
shell: process.platform === 'win32',
|
|
165
|
+
timeout: 5000,
|
|
166
|
+
})
|
|
167
|
+
return true
|
|
168
|
+
} catch {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Remove the now-superseded old `claudeInvocation` body and old `claudeRunner` body (replaced above). Keep `buildClaudePrompt`, `AgentContext`, `AgentResult`, `AgentRunner`.
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
177
|
+
|
|
178
|
+
Run: `npm test -- loop/runner`
|
|
179
|
+
Expected: PASS (existing claude tests + 6 new).
|
|
180
|
+
|
|
181
|
+
- [ ] **Step 5: Commit**
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git add src/loop/runner.ts tests/loop/runner.test.ts
|
|
185
|
+
git commit -m "feat: generalize loop runner to claude/codex/gemini + availability probe"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### Task 2: Runner selection + readiness gate in run-command
|
|
191
|
+
|
|
192
|
+
**Files:**
|
|
193
|
+
- Modify: `src/loop/run-command.ts`
|
|
194
|
+
- Test: `tests/loop/loop-cli.integration.test.ts` (extend)
|
|
195
|
+
|
|
196
|
+
- [ ] **Step 1: Add failing tests**
|
|
197
|
+
|
|
198
|
+
In `tests/loop/loop-cli.integration.test.ts`, add a case for the readiness refusal. Add to the existing `describe`:
|
|
199
|
+
```ts
|
|
200
|
+
it('refuses to run when the selected agent CLI is unavailable', () => {
|
|
201
|
+
saveConfig(dir, { ...cfg(), verify: { command: 'node -e "process.exit(0)"' } })
|
|
202
|
+
const code = runLoopCommand(dir, {
|
|
203
|
+
maxIterations: 5,
|
|
204
|
+
git: stubGit,
|
|
205
|
+
verify: verifyOk,
|
|
206
|
+
agent: 'codex',
|
|
207
|
+
isAvailable: () => false,
|
|
208
|
+
})
|
|
209
|
+
expect(code).toBe(2)
|
|
210
|
+
expect(loadPrd(join(dir, '.forge', 'prd.yaml'))[0].passes).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('does not run the readiness gate when a runner is injected', () => {
|
|
214
|
+
saveConfig(dir, { ...cfg(), verify: { command: 'node -e "process.exit(0)"' } })
|
|
215
|
+
const code = runLoopCommand(dir, {
|
|
216
|
+
maxIterations: 5,
|
|
217
|
+
runner: passRunner,
|
|
218
|
+
git: stubGit,
|
|
219
|
+
verify: verifyOk,
|
|
220
|
+
isAvailable: () => false, // ignored because runner is injected
|
|
221
|
+
})
|
|
222
|
+
expect(code).toBe(0)
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
227
|
+
|
|
228
|
+
Run: `npm test -- loop-cli`
|
|
229
|
+
Expected: FAIL — `RunLoopCommandOptions` has no `agent`/`isAvailable`; no readiness gate.
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 3: Modify `src/loop/run-command.ts`**
|
|
232
|
+
|
|
233
|
+
Update the runner import and add the new symbols + `Agent` type:
|
|
234
|
+
```ts
|
|
235
|
+
import { claudeRunner, makeRunner, isAgentAvailable, type AgentRunner } from './runner.js'
|
|
236
|
+
import type { Agent } from '../retrofit/config.js'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Extend `RunLoopCommandOptions`:
|
|
240
|
+
```ts
|
|
241
|
+
export interface RunLoopCommandOptions {
|
|
242
|
+
maxIterations: number
|
|
243
|
+
runner?: AgentRunner
|
|
244
|
+
git?: GitOps
|
|
245
|
+
verify?: Verifier
|
|
246
|
+
agent?: Agent
|
|
247
|
+
isAvailable?: (agent: Agent) => boolean
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
In `runLoopCommand`, after the verify resolution block and before the `runLoop` call, resolve the runner with the readiness gate:
|
|
252
|
+
```ts
|
|
253
|
+
let runner = opts.runner
|
|
254
|
+
if (!runner) {
|
|
255
|
+
const agent: Agent = opts.agent ?? config.agents[0] ?? 'claude'
|
|
256
|
+
const available = opts.isAvailable ?? isAgentAvailable
|
|
257
|
+
if (!available(agent)) {
|
|
258
|
+
console.error(`Agent CLI "${agent}" was not found on PATH. Install it, or pick another with --runner=<claude|codex|gemini>.`)
|
|
259
|
+
return 2
|
|
260
|
+
}
|
|
261
|
+
runner = makeRunner(agent)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
Then change the `runLoop` call to use the resolved `runner` instead of `opts.runner ?? claudeRunner`:
|
|
265
|
+
```ts
|
|
266
|
+
const result = runLoop({
|
|
267
|
+
prdPath: path,
|
|
268
|
+
targetDir,
|
|
269
|
+
runner,
|
|
270
|
+
git: opts.git ?? realGitOps,
|
|
271
|
+
verify,
|
|
272
|
+
maxIterations: opts.maxIterations,
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
(`claudeRunner` is still imported for back-compat consumers but no longer the inline default; if the unused-import check complains, the `claudeRunner` import may be dropped — but keep `makeRunner`/`isAgentAvailable`.)
|
|
276
|
+
|
|
277
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
278
|
+
|
|
279
|
+
Run: `npm test -- loop-cli`
|
|
280
|
+
Expected: PASS (existing + 2 new).
|
|
281
|
+
|
|
282
|
+
- [ ] **Step 5: Modify `src/cli.ts` to parse `--runner=`**
|
|
283
|
+
|
|
284
|
+
In the `loop` case, in the `run` branch, parse the runner flag and pass it through. Replace the `run` sub-branch body:
|
|
285
|
+
```ts
|
|
286
|
+
if (sub === 'run') {
|
|
287
|
+
const maxArg = rest.find(a => a.startsWith('--max='))
|
|
288
|
+
const rawMax = maxArg ? Number(maxArg.slice('--max='.length)) : 25
|
|
289
|
+
if (!Number.isFinite(rawMax) || rawMax <= 0) {
|
|
290
|
+
console.error(`Invalid --max value: ${maxArg}`)
|
|
291
|
+
return 1
|
|
292
|
+
}
|
|
293
|
+
const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length)
|
|
294
|
+
const valid = ['claude', 'codex', 'gemini']
|
|
295
|
+
const agent = runnerArg && valid.includes(runnerArg) ? (runnerArg as Agent) : undefined
|
|
296
|
+
if (runnerArg && !agent) {
|
|
297
|
+
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`)
|
|
298
|
+
return 1
|
|
299
|
+
}
|
|
300
|
+
return runLoopCommand(targetDir, { maxIterations: rawMax, agent })
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
Add the `Agent` type import near the top of `src/cli.ts` if not already present:
|
|
304
|
+
```ts
|
|
305
|
+
import type { Agent } from './retrofit/config.js'
|
|
306
|
+
```
|
|
307
|
+
(It may already be imported for the `retrofit --agent` parsing — if so, do not duplicate.)
|
|
308
|
+
|
|
309
|
+
- [ ] **Step 6: Run the full suite + build**
|
|
310
|
+
|
|
311
|
+
Run: `npm test`
|
|
312
|
+
Expected: all pass.
|
|
313
|
+
|
|
314
|
+
Run: `npm run build`
|
|
315
|
+
Expected: tsc 0 errors.
|
|
316
|
+
|
|
317
|
+
- [ ] **Step 7: Commit**
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
git add src/loop/run-command.ts src/cli.ts tests/loop/loop-cli.integration.test.ts
|
|
321
|
+
git commit -m "feat: select loop runner by agent with a tool-readiness gate"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### Task 3: Update canon loop-spec
|
|
327
|
+
|
|
328
|
+
**Files:**
|
|
329
|
+
- Modify: `canon/loop/loop-spec.md`
|
|
330
|
+
|
|
331
|
+
- [ ] **Step 1: Update the runner + limitations language**
|
|
332
|
+
|
|
333
|
+
In `canon/loop/loop-spec.md`, update step 4 and the Limitations section. Change step 4 to:
|
|
334
|
+
```markdown
|
|
335
|
+
4. Run a fresh agent to implement ONE story. The runner is selected by `--runner=<claude|codex|gemini>` or the first configured agent (default claude); the loop refuses to start if that agent's CLI is not installed.
|
|
336
|
+
```
|
|
337
|
+
Replace the Limitations section with:
|
|
338
|
+
```markdown
|
|
339
|
+
## Limitations
|
|
340
|
+
- Each iteration runs in the working tree directly; per-iteration git-worktree isolation and a review-iteration with role separation are Baustein C4.
|
|
341
|
+
- The loop verifies via the project's test command, not a per-agent semantic review.
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- [ ] **Step 2: Validate the canon**
|
|
345
|
+
|
|
346
|
+
Run: `npm run forge -- validate canon`
|
|
347
|
+
Expected: `✓ canon valid (canon)`.
|
|
348
|
+
|
|
349
|
+
Run: `npm test -- real-canon`
|
|
350
|
+
Expected: PASS.
|
|
351
|
+
|
|
352
|
+
- [ ] **Step 3: Commit**
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
git add canon/loop/loop-spec.md
|
|
356
|
+
git commit -m "docs: canon loop-spec documents multi-agent runner + readiness gate"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Self-Review
|
|
362
|
+
|
|
363
|
+
**1. Spec coverage (Baustein C3 scope):**
|
|
364
|
+
- Codex + Gemini runners (generalized `agentInvocation`/`makeRunner`) → Task 1 ✓
|
|
365
|
+
- Runner selection by config/flag → Tasks 2 (config + opts), 2 step 5 (`--runner`) ✓
|
|
366
|
+
- Tool-surface-readiness gate (refuse if CLI missing) → Tasks 1 (`isAgentAvailable`), 2 (gate) ✓
|
|
367
|
+
- Back-compat (claudeInvocation/claudeRunner unchanged behavior) → Task 1 ✓
|
|
368
|
+
- Deterministic tests (injectable `isAvailable`, structural invocation asserts — no real codex/gemini needed) → Tasks 1, 2 ✓
|
|
369
|
+
- (Deferred to C4: per-iteration git-worktree isolation, review-iteration with role separation. Correct.)
|
|
370
|
+
|
|
371
|
+
**2. Placeholder scan:** No TBD/TODO. The "drop the claudeRunner import if unused" note is a conditional cleanup, not a placeholder. Every code step is complete.
|
|
372
|
+
|
|
373
|
+
**3. Type consistency:** `Agent` (from config), `Invocation` (renamed from `ClaudeInvocation`), `agentInvocation`, `makeRunner`, `isAgentAvailable`, `claudeInvocation` (wrapper), `claudeRunner` (= `makeRunner('claude')`), `RunLoopCommandOptions` (gains `agent?`, `isAvailable?`). The C1 dogfood Windows-spawn approach (stdin + shell on win32) is preserved and now applied uniformly to all agents. `buildClaudePrompt` is reused for all agents (agent-agnostic content). ✓
|
|
374
|
+
|
|
375
|
+
## Next Plans (not this document)
|
|
376
|
+
|
|
377
|
+
- **Plan C4 — Loop isolation + review:** per-iteration git-worktree isolation (run each story in a fresh worktree, merge on green), review-iteration with role separation (a second agent pass reviews before the story is marked done).
|