@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,502 @@
|
|
|
1
|
+
# Forge — Baustein C2 (Loop verification hardening) 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 the autonomous loop production-safe by running the project's real test suite after each agent iteration and only marking a story `passes: true` (and committing) when tests are GREEN — replacing C1's "trust the agent's exit code" behavior. Plus: configurable verify command and a clearer error when the agent run fails for auth/setup reasons.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add an injectable `Verifier` seam (like `AgentRunner`/`GitOps`). `runLoop` gains a `verify` step: after `runner.success`, it runs `verify(targetDir)`; the story is marked/committed only if BOTH the runner succeeded AND verification passed. A red verify → `blocked` (story stays `passes:false`, no commit). The real verifier runs a shell command (default detected `npm test`, or `.forge/config.yaml` `verify.command`). This is Baustein C2; multi-agent runners (codex/gemini) and per-iteration git-worktree isolation are C3.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (ESM), TypeScript, vitest, `node:child_process` (`execSync`, shell-resolved so `npm.cmd` works on Windows). Extends C1 (`runLoop`, `run-command`, `ForgeConfig`).
|
|
10
|
+
|
|
11
|
+
**Builds on:** Baustein A+B1+B2+C1 (on `main`). Key reuse/modify: `src/loop/loop.ts` (`runLoop`), `src/loop/run-command.ts` (`runLoopCommand`), `src/retrofit/config.ts` (`ForgeConfig` + schema).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
loop/
|
|
20
|
+
verify.ts # NEW: VerifyResult, Verifier type, commandVerifier(command)
|
|
21
|
+
loop.ts # MODIFY: LoopOptions gains verify; gate mark+commit on verify.passed
|
|
22
|
+
run-command.ts # MODIFY: resolve verifier (config or detected npm test); auth hint; pass into runLoop
|
|
23
|
+
retrofit/
|
|
24
|
+
config.ts # MODIFY: ForgeConfig gains optional verify:{command}; schema + resolveVerifyCommand()
|
|
25
|
+
canon/loop/loop-spec.md # MODIFY: document the real verify step
|
|
26
|
+
tests/loop/
|
|
27
|
+
verify.test.ts # NEW
|
|
28
|
+
loop.test.ts # MODIFY: add verify-gating cases
|
|
29
|
+
loop-cli.integration.test.ts # MODIFY: verify wiring + refusal when no command
|
|
30
|
+
tests/retrofit/
|
|
31
|
+
config.test.ts # MODIFY: verify field round-trip + resolveVerifyCommand
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### Task 1: Verify module
|
|
37
|
+
|
|
38
|
+
**Files:**
|
|
39
|
+
- Create: `src/loop/verify.ts`
|
|
40
|
+
- Test: `tests/loop/verify.test.ts`
|
|
41
|
+
|
|
42
|
+
- [ ] **Step 1: Write the failing test**
|
|
43
|
+
|
|
44
|
+
Create `tests/loop/verify.test.ts`:
|
|
45
|
+
```ts
|
|
46
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
47
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
48
|
+
import { join } from 'node:path'
|
|
49
|
+
import { tmpdir } from 'node:os'
|
|
50
|
+
import { commandVerifier } from '../../src/loop/verify.js'
|
|
51
|
+
|
|
52
|
+
let dir: string
|
|
53
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'forge-verify-')) })
|
|
54
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
55
|
+
|
|
56
|
+
describe('commandVerifier', () => {
|
|
57
|
+
it('passes when the command exits 0', () => {
|
|
58
|
+
const r = commandVerifier('node -e "process.exit(0)"')(dir)
|
|
59
|
+
expect(r.passed).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('fails when the command exits non-zero', () => {
|
|
63
|
+
const r = commandVerifier('node -e "process.exit(1)"')(dir)
|
|
64
|
+
expect(r.passed).toBe(false)
|
|
65
|
+
expect(r.summary).toMatch(/verify failed/i)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
71
|
+
|
|
72
|
+
Run: `npm test -- loop/verify`
|
|
73
|
+
Expected: FAIL — cannot find module `src/loop/verify.js`.
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
76
|
+
|
|
77
|
+
Create `src/loop/verify.ts`:
|
|
78
|
+
```ts
|
|
79
|
+
import { execSync } from 'node:child_process'
|
|
80
|
+
|
|
81
|
+
export interface VerifyResult {
|
|
82
|
+
passed: boolean
|
|
83
|
+
summary: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type Verifier = (targetDir: string) => VerifyResult
|
|
87
|
+
|
|
88
|
+
// Runs a shell command in the target dir; passed = exit 0. execSync goes through the
|
|
89
|
+
// shell, so `npm test` resolves npm.cmd on Windows. Output is captured (not streamed).
|
|
90
|
+
export function commandVerifier(command: string): Verifier {
|
|
91
|
+
return (targetDir: string): VerifyResult => {
|
|
92
|
+
try {
|
|
93
|
+
execSync(command, { cwd: targetDir, stdio: 'pipe' })
|
|
94
|
+
return { passed: true, summary: `verify passed: ${command}` }
|
|
95
|
+
} catch {
|
|
96
|
+
return { passed: false, summary: `verify failed: ${command}` }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
103
|
+
|
|
104
|
+
Run: `npm test -- loop/verify`
|
|
105
|
+
Expected: PASS (2 tests).
|
|
106
|
+
|
|
107
|
+
- [ ] **Step 5: Commit**
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git add src/loop/verify.ts tests/loop/verify.test.ts
|
|
111
|
+
git commit -m "feat: add command verifier for independent test runs"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Task 2: Config verify command + resolver
|
|
117
|
+
|
|
118
|
+
**Files:**
|
|
119
|
+
- Modify: `src/retrofit/config.ts`
|
|
120
|
+
- Test: `tests/retrofit/config.test.ts` (extend)
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 1: Add failing tests**
|
|
123
|
+
|
|
124
|
+
Append to `tests/retrofit/config.test.ts` (inside the existing `describe`, and add imports `mkdirSync`, `writeFileSync` from `node:fs` if not present, plus `resolveVerifyCommand`):
|
|
125
|
+
|
|
126
|
+
Add to the import line:
|
|
127
|
+
```ts
|
|
128
|
+
import { loadConfig, saveConfig, defaultConfig, resolveVerifyCommand } from '../../src/retrofit/config.js'
|
|
129
|
+
import { writeFileSync } from 'node:fs'
|
|
130
|
+
```
|
|
131
|
+
Add these tests:
|
|
132
|
+
```ts
|
|
133
|
+
it('round-trips an optional verify command', () => {
|
|
134
|
+
const cfg = { canonVersion: '0.1.0', agents: ['claude'] as const, loop: { enabled: false }, verify: { command: 'npm test' } }
|
|
135
|
+
saveConfig(dir, cfg)
|
|
136
|
+
expect(loadConfig(dir)).toEqual(cfg)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('resolveVerifyCommand prefers config.verify.command', () => {
|
|
140
|
+
expect(resolveVerifyCommand(dir, { canonVersion: '0', agents: [], loop: { enabled: true }, verify: { command: 'pytest' } })).toBe('pytest')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('resolveVerifyCommand falls back to npm test when package.json has a test script', () => {
|
|
144
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ scripts: { test: 'vitest run' } }))
|
|
145
|
+
expect(resolveVerifyCommand(dir, { canonVersion: '0', agents: [], loop: { enabled: true } })).toBe('npm test')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('resolveVerifyCommand returns null when nothing is configured or detectable', () => {
|
|
149
|
+
expect(resolveVerifyCommand(dir, { canonVersion: '0', agents: [], loop: { enabled: true } })).toBeNull()
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
154
|
+
|
|
155
|
+
Run: `npm test -- retrofit/config`
|
|
156
|
+
Expected: FAIL — `resolveVerifyCommand` is not exported; `verify` not in schema.
|
|
157
|
+
|
|
158
|
+
- [ ] **Step 3: Modify `src/retrofit/config.ts`**
|
|
159
|
+
|
|
160
|
+
Add `existsSync, readFileSync` to the `node:fs` import (keep existing imports):
|
|
161
|
+
```ts
|
|
162
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Extend the `ForgeConfig` interface:
|
|
166
|
+
```ts
|
|
167
|
+
export interface ForgeConfig {
|
|
168
|
+
canonVersion: string
|
|
169
|
+
agents: Agent[]
|
|
170
|
+
loop: { enabled: boolean }
|
|
171
|
+
verify?: { command: string }
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Extend the zod schema (the `ForgeConfigSchema` added in B1) with the optional field:
|
|
176
|
+
```ts
|
|
177
|
+
const ForgeConfigSchema = z.object({
|
|
178
|
+
canonVersion: z.string().min(1),
|
|
179
|
+
agents: z.array(AgentSchema),
|
|
180
|
+
loop: z.object({ enabled: z.boolean() }),
|
|
181
|
+
verify: z.object({ command: z.string().min(1) }).optional(),
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Add the resolver at the end of the file:
|
|
186
|
+
```ts
|
|
187
|
+
// Decide which command verifies a story is done: explicit config wins; otherwise
|
|
188
|
+
// detect an npm test script; otherwise null (caller must refuse to run blindly).
|
|
189
|
+
export function resolveVerifyCommand(targetDir: string, config: ForgeConfig): string | null {
|
|
190
|
+
if (config.verify?.command) return config.verify.command
|
|
191
|
+
const pkgPath = join(targetDir, 'package.json')
|
|
192
|
+
if (existsSync(pkgPath)) {
|
|
193
|
+
try {
|
|
194
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
195
|
+
if (pkg?.scripts?.test) return 'npm test'
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore malformed package.json
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
205
|
+
|
|
206
|
+
Run: `npm test -- retrofit/config`
|
|
207
|
+
Expected: PASS (existing + 4 new).
|
|
208
|
+
|
|
209
|
+
- [ ] **Step 5: Commit**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
git add src/retrofit/config.ts tests/retrofit/config.test.ts
|
|
213
|
+
git commit -m "feat: add verify command to config with detection fallback"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Task 3: Gate the loop on verification
|
|
219
|
+
|
|
220
|
+
**Files:**
|
|
221
|
+
- Modify: `src/loop/loop.ts`
|
|
222
|
+
- Test: `tests/loop/loop.test.ts` (extend)
|
|
223
|
+
|
|
224
|
+
- [ ] **Step 1: Add failing tests**
|
|
225
|
+
|
|
226
|
+
In `tests/loop/loop.test.ts`, add a verifier import and a passing-verifier default, and new cases. Add the import:
|
|
227
|
+
```ts
|
|
228
|
+
import type { Verifier } from '../../src/loop/verify.js'
|
|
229
|
+
```
|
|
230
|
+
Add near the other test stubs (after `alwaysPass`):
|
|
231
|
+
```ts
|
|
232
|
+
const verifyOk: Verifier = () => ({ passed: true, summary: 'green' })
|
|
233
|
+
```
|
|
234
|
+
Update existing `runLoop` calls in this file to pass `verify: verifyOk` in their options object (the new required field). Then add:
|
|
235
|
+
```ts
|
|
236
|
+
it('does NOT mark a story passed when verification fails after a successful runner', () => {
|
|
237
|
+
const verifyRed: Verifier = () => ({ passed: false, summary: 'tests red' })
|
|
238
|
+
const commits: string[] = []
|
|
239
|
+
const git: GitOps = { isClean: () => true, commitAll: (_d, m) => commits.push(m) }
|
|
240
|
+
const res = runLoop({ prdPath: prd(), targetDir: dir, runner: alwaysPass, git, verify: verifyRed, maxIterations: 10 })
|
|
241
|
+
expect(res.status).toBe('blocked')
|
|
242
|
+
expect(res.reason).toMatch(/verif/i)
|
|
243
|
+
expect(loadPrd(prd()).every(s => !s.passes)).toBe(true)
|
|
244
|
+
expect(commits).toHaveLength(0)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('marks passed and commits only when runner AND verify both succeed', () => {
|
|
248
|
+
const commits: string[] = []
|
|
249
|
+
const git: GitOps = { isClean: () => true, commitAll: (_d, m) => commits.push(m) }
|
|
250
|
+
const res = runLoop({ prdPath: prd(), targetDir: dir, runner: alwaysPass, git, verify: verifyOk, maxIterations: 10 })
|
|
251
|
+
expect(res.status).toBe('complete')
|
|
252
|
+
expect(commits).toHaveLength(2)
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
257
|
+
|
|
258
|
+
Run: `npm test -- loop/loop`
|
|
259
|
+
Expected: FAIL — `verify` is not part of `LoopOptions` (type error) and the verify-red case isn't handled.
|
|
260
|
+
|
|
261
|
+
- [ ] **Step 3: Modify `src/loop/loop.ts`**
|
|
262
|
+
|
|
263
|
+
Add the import:
|
|
264
|
+
```ts
|
|
265
|
+
import type { Verifier } from './verify.js'
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Add `verify` to `LoopOptions`:
|
|
269
|
+
```ts
|
|
270
|
+
export interface LoopOptions {
|
|
271
|
+
prdPath: string
|
|
272
|
+
targetDir: string
|
|
273
|
+
runner: AgentRunner
|
|
274
|
+
git: GitOps
|
|
275
|
+
verify: Verifier
|
|
276
|
+
maxIterations: number
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
In the loop body, after the runner-success check and before marking/committing, insert the verify gate. Replace the success block:
|
|
281
|
+
```ts
|
|
282
|
+
const result = opts.runner({ targetDir: opts.targetDir, story })
|
|
283
|
+
iterations++
|
|
284
|
+
|
|
285
|
+
if (!result.success) {
|
|
286
|
+
return {
|
|
287
|
+
status: 'blocked',
|
|
288
|
+
iterations,
|
|
289
|
+
reason: `story ${story.id} failed: ${result.summary}`,
|
|
290
|
+
finalProgress: progress(stories),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const verdict = opts.verify(opts.targetDir)
|
|
295
|
+
if (!verdict.passed) {
|
|
296
|
+
return {
|
|
297
|
+
status: 'blocked',
|
|
298
|
+
iterations,
|
|
299
|
+
reason: `story ${story.id} did not verify: ${verdict.summary}`,
|
|
300
|
+
finalProgress: progress(stories),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const updated = stories.map(s => (s.id === story.id ? { ...s, passes: true } : s))
|
|
306
|
+
savePrd(opts.prdPath, updated)
|
|
307
|
+
opts.git.commitAll(opts.targetDir, `forge: complete ${story.id} ${story.title}`)
|
|
308
|
+
} catch (e) {
|
|
309
|
+
savePrd(opts.prdPath, stories)
|
|
310
|
+
return {
|
|
311
|
+
status: 'blocked',
|
|
312
|
+
iterations,
|
|
313
|
+
reason: `commit failed for ${story.id}: ${(e as Error).message}`,
|
|
314
|
+
finalProgress: progress(stories),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
(This keeps the C1 commit-integrity try/catch intact and adds the verify gate before it.)
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
321
|
+
|
|
322
|
+
Run: `npm test -- loop/loop`
|
|
323
|
+
Expected: PASS (all existing C1 cases — now passing `verify: verifyOk` — plus the 2 new ones).
|
|
324
|
+
|
|
325
|
+
- [ ] **Step 5: Commit**
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
git add src/loop/loop.ts tests/loop/loop.test.ts
|
|
329
|
+
git commit -m "feat: gate story completion on independent verification"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### Task 4: Wire the verifier + auth hint in run-command
|
|
335
|
+
|
|
336
|
+
**Files:**
|
|
337
|
+
- Modify: `src/loop/run-command.ts`
|
|
338
|
+
- Test: `tests/loop/loop-cli.integration.test.ts` (extend)
|
|
339
|
+
|
|
340
|
+
- [ ] **Step 1: Add failing tests**
|
|
341
|
+
|
|
342
|
+
In `tests/loop/loop-cli.integration.test.ts`, add an import for a Verifier and update existing `runLoopCommand` calls plus add new cases. Add import:
|
|
343
|
+
```ts
|
|
344
|
+
import type { Verifier } from '../../src/loop/verify.js'
|
|
345
|
+
```
|
|
346
|
+
Add stubs near the top:
|
|
347
|
+
```ts
|
|
348
|
+
const verifyOk: Verifier = () => ({ passed: true, summary: 'ok' })
|
|
349
|
+
```
|
|
350
|
+
Update the existing `runLoopCommand(dir, { maxIterations: 5, runner: passRunner, git: stubGit })` calls to also pass `verify: verifyOk`. Then add:
|
|
351
|
+
```ts
|
|
352
|
+
it('refuses to run when no verify command is configured or detectable', () => {
|
|
353
|
+
saveConfig(dir, cfg())
|
|
354
|
+
const code = runLoopCommand(dir, { maxIterations: 5, runner: passRunner, git: stubGit })
|
|
355
|
+
expect(code).toBe(2)
|
|
356
|
+
expect(loadPrd(join(dir, '.forge', 'prd.yaml'))[0].passes).toBe(false)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('runs when a verify command is configured', () => {
|
|
360
|
+
saveConfig(dir, { ...cfg(), verify: { command: 'node -e "process.exit(0)"' } })
|
|
361
|
+
const code = runLoopCommand(dir, { maxIterations: 5, runner: passRunner, git: stubGit })
|
|
362
|
+
expect(code).toBe(0)
|
|
363
|
+
expect(loadPrd(join(dir, '.forge', 'prd.yaml'))[0].passes).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
```
|
|
366
|
+
Note: the earlier "run completes" test must now configure a verify command too (add `verify: { command: 'node -e "process.exit(0)"' }` to its `saveConfig`), since runs without one now refuse. Update that test's `saveConfig(dir, cfg())` to `saveConfig(dir, { ...cfg(), verify: { command: 'node -e "process.exit(0)"' } })`.
|
|
367
|
+
|
|
368
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
369
|
+
|
|
370
|
+
Run: `npm test -- loop-cli`
|
|
371
|
+
Expected: FAIL — `runLoopCommand` does not yet accept/inject a verifier or refuse on missing command.
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 3: Modify `src/loop/run-command.ts`**
|
|
374
|
+
|
|
375
|
+
Add imports:
|
|
376
|
+
```ts
|
|
377
|
+
import { resolveVerifyCommand } from '../retrofit/config.js'
|
|
378
|
+
import { commandVerifier, type Verifier } from './verify.js'
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Extend `RunLoopCommandOptions`:
|
|
382
|
+
```ts
|
|
383
|
+
export interface RunLoopCommandOptions {
|
|
384
|
+
maxIterations: number
|
|
385
|
+
runner?: AgentRunner
|
|
386
|
+
git?: GitOps
|
|
387
|
+
verify?: Verifier
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
In `runLoopCommand`, after the existing PRD-existence check and before calling `runLoop`, resolve the verifier and refuse if none:
|
|
392
|
+
```ts
|
|
393
|
+
let verify = opts.verify
|
|
394
|
+
if (!verify) {
|
|
395
|
+
const command = resolveVerifyCommand(targetDir, config)
|
|
396
|
+
if (!command) {
|
|
397
|
+
console.error('No verify command configured. Set verify.command in .forge/config.yaml (e.g. "npm test") so the loop can confirm tests pass before marking work done.')
|
|
398
|
+
return 2
|
|
399
|
+
}
|
|
400
|
+
verify = commandVerifier(command)
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
Pass `verify` into the `runLoop` call:
|
|
404
|
+
```ts
|
|
405
|
+
const result = runLoop({
|
|
406
|
+
prdPath: path,
|
|
407
|
+
targetDir,
|
|
408
|
+
runner: opts.runner ?? claudeRunner,
|
|
409
|
+
git: opts.git ?? realGitOps,
|
|
410
|
+
verify,
|
|
411
|
+
maxIterations: opts.maxIterations,
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
After computing `result`, add an auth hint when the run was blocked by an agent auth failure (from the dogfood finding):
|
|
416
|
+
```ts
|
|
417
|
+
if (result.reason && /invalid api key|please run \/login|not logged in/i.test(result.reason)) {
|
|
418
|
+
console.log('Hint: the agent CLI has no credentials in this environment. Set ANTHROPIC_API_KEY or log the agent in for headless use.')
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
423
|
+
|
|
424
|
+
Run: `npm test -- loop-cli`
|
|
425
|
+
Expected: PASS (updated existing + 2 new).
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 5: Run full suite + smoke**
|
|
428
|
+
|
|
429
|
+
Run: `npm test`
|
|
430
|
+
Expected: all pass.
|
|
431
|
+
|
|
432
|
+
Run (PowerShell) — confirm the refusal message in a temp project with the loop enabled but no verify command:
|
|
433
|
+
```
|
|
434
|
+
# (covered by tests; optional manual check)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 6: Commit**
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
git add src/loop/run-command.ts tests/loop/loop-cli.integration.test.ts
|
|
441
|
+
git commit -m "feat: wire verifier into forge loop run; refuse without one; auth hint"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
### Task 5: Update canon loop-spec
|
|
447
|
+
|
|
448
|
+
**Files:**
|
|
449
|
+
- Modify: `canon/loop/loop-spec.md`
|
|
450
|
+
|
|
451
|
+
- [ ] **Step 1: Update the verification language**
|
|
452
|
+
|
|
453
|
+
In `canon/loop/loop-spec.md`, change step 5 of the iteration list from trusting the agent to independent verification, and update the "C1 limitations" section. Replace step 5 and the limitations section with:
|
|
454
|
+
```markdown
|
|
455
|
+
5. On success: run the project's verify command (config `verify.command`, or detected `npm test`). Only if it passes, mark the story `passes: true`, save the PRD, and commit atomically. If the agent succeeds but verification fails: `blocked` (story stays open, no commit).
|
|
456
|
+
```
|
|
457
|
+
And replace the `## C1 limitations` section with:
|
|
458
|
+
```markdown
|
|
459
|
+
## Limitations
|
|
460
|
+
- The agent runner is claude-only; Codex/Gemini runners and per-iteration git-worktree isolation are Baustein C3.
|
|
461
|
+
- The loop run refuses to start if no verify command is configured or detectable, so it never marks work done without a green test run.
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- [ ] **Step 2: Validate the canon**
|
|
465
|
+
|
|
466
|
+
Run: `npm run forge -- validate canon`
|
|
467
|
+
Expected: `✓ canon valid (canon)`.
|
|
468
|
+
|
|
469
|
+
Run: `npm test -- real-canon`
|
|
470
|
+
Expected: PASS.
|
|
471
|
+
|
|
472
|
+
- [ ] **Step 3: Commit**
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
git add canon/loop/loop-spec.md
|
|
476
|
+
git commit -m "docs: canon loop-spec now requires independent verification"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Self-Review
|
|
482
|
+
|
|
483
|
+
**1. Spec coverage (Baustein C2 scope):**
|
|
484
|
+
- Independent test run replaces exit-code trust → Tasks 1, 3 ✓
|
|
485
|
+
- Story marked/committed only when runner AND verify both pass → Task 3 ✓
|
|
486
|
+
- Verify failure → blocked, no false `passes:true`, no commit → Task 3 ✓
|
|
487
|
+
- Configurable verify command + npm-test detection → Task 2 ✓
|
|
488
|
+
- Loop refuses to run with no verify command (never blind) → Task 4 ✓
|
|
489
|
+
- Clearer auth/setup error (dogfood finding) → Task 4 ✓
|
|
490
|
+
- Canon doc reflects real verification → Task 5 ✓
|
|
491
|
+
- (Deferred to C3: codex/gemini runners, per-iteration git-worktree isolation, review-iteration with role separation, tool-surface-readiness gate. Correct.)
|
|
492
|
+
|
|
493
|
+
**2. Placeholder scan:** No TBD/TODO. The "update existing runLoop calls to pass verify" instructions are concrete edits, not placeholders. Every code step is complete.
|
|
494
|
+
|
|
495
|
+
**3. Type consistency:** `VerifyResult{passed,summary}`, `Verifier`, `commandVerifier`, `resolveVerifyCommand`, `ForgeConfig.verify?:{command}`, `LoopOptions.verify`, `RunLoopCommandOptions.verify?` — consistent. `runLoop` now REQUIRES `verify` (all call sites updated in Tasks 3 & 4 tests). `commandVerifier` uses `execSync` (shell-resolved) consistent with the Windows lesson from the C1 dogfood runner fix. `ForgeConfig` extension is additive/optional so B1/B2 configs still parse. ✓
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Next Plans (not this document)
|
|
500
|
+
|
|
501
|
+
- **Plan C3 — Multi-agent + isolation:** `codexRunner`/`geminiRunner` (generalize `claudeInvocation`), runner selection by config/flag, per-iteration git-worktree isolation, review-iteration with role separation, tool-surface-readiness gate.
|
|
502
|
+
- **Plan B3 — settings.json merge:** merge into an existing `.claude/settings.json` instead of replacing.
|