@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,815 @@
|
|
|
1
|
+
# Forge — Baustein A (Canon + Validator) 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:** Build the harness-agnostic Canon (source-of-truth) and a `forge validate` CLI that lints the Canon for integrity, fully TDD'd.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A Node.js/TypeScript CLI (`forge`) with a `validate` subcommand. It loads `canon/manifest.yaml` (zod-validated), then checks that every referenced skill/policy/loop/tool path exists and that each `SKILL.md` has valid frontmatter. The Canon itself is seeded with minimal-but-real content that passes validation. This is Baustein A of the Forge spec (`docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md`); B (Retrofit-Skill) and C (Loop) get their own plans.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (ESM), TypeScript, vitest (test), `yaml` + `zod` (parsing/schema). Distribution via `npx`/`package.json` `bin`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
package.json # project + forge bin + scripts
|
|
17
|
+
tsconfig.json # TS config (ESM, strict)
|
|
18
|
+
vitest.config.ts # test config
|
|
19
|
+
.gitignore # node_modules, dist
|
|
20
|
+
src/
|
|
21
|
+
cli.ts # entry: dispatch subcommands (validate)
|
|
22
|
+
canon/
|
|
23
|
+
manifest.ts # zod schema + loadManifest()
|
|
24
|
+
frontmatter.ts # parseFrontmatter() for SKILL.md
|
|
25
|
+
validate.ts # validateCanon() -> Issue[]
|
|
26
|
+
canon/ # THE CANON (source-of-truth, harness-agnostic)
|
|
27
|
+
AGENTS.md
|
|
28
|
+
manifest.yaml
|
|
29
|
+
skills/tdd/SKILL.md
|
|
30
|
+
skills/eng-review/SKILL.md
|
|
31
|
+
policy/gates.md
|
|
32
|
+
policy/roles.md
|
|
33
|
+
loop/loop-spec.md
|
|
34
|
+
loop/prd.schema.md
|
|
35
|
+
tools/rtk.md
|
|
36
|
+
tools/graphify.md
|
|
37
|
+
tools/playwright-mcp.md
|
|
38
|
+
tests/
|
|
39
|
+
canon/frontmatter.test.ts
|
|
40
|
+
canon/manifest.test.ts
|
|
41
|
+
canon/validate.test.ts
|
|
42
|
+
canon/real-canon.test.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each `src/canon/*.ts` file has one responsibility: schema, frontmatter parsing, validation orchestration. The CLI stays thin so logic is unit-testable without spawning processes.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### Task 1: Project scaffold
|
|
50
|
+
|
|
51
|
+
**Files:**
|
|
52
|
+
- Create: `package.json`, `tsconfig.json`, `vitest.config.ts`, `.gitignore`
|
|
53
|
+
- Test: `tests/smoke.test.ts`
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 1: Write the failing smoke test**
|
|
56
|
+
|
|
57
|
+
Create `tests/smoke.test.ts`:
|
|
58
|
+
```ts
|
|
59
|
+
import { describe, it, expect } from 'vitest'
|
|
60
|
+
|
|
61
|
+
describe('smoke', () => {
|
|
62
|
+
it('runs', () => {
|
|
63
|
+
expect(1 + 1).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 2: Create `package.json`**
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "forge",
|
|
73
|
+
"version": "0.1.0",
|
|
74
|
+
"type": "module",
|
|
75
|
+
"bin": { "forge": "./dist/cli.js" },
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsc",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"forge": "tsx src/cli.ts"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"yaml": "^2.5.0",
|
|
83
|
+
"zod": "^3.23.8"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@types/node": "^22.7.0",
|
|
87
|
+
"tsx": "^4.19.1",
|
|
88
|
+
"typescript": "^5.6.2",
|
|
89
|
+
"vitest": "^2.1.1"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- [ ] **Step 3: Create `tsconfig.json`**
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"compilerOptions": {
|
|
99
|
+
"target": "ES2022",
|
|
100
|
+
"module": "ESNext",
|
|
101
|
+
"moduleResolution": "Bundler",
|
|
102
|
+
"outDir": "dist",
|
|
103
|
+
"rootDir": "src",
|
|
104
|
+
"strict": true,
|
|
105
|
+
"esModuleInterop": true,
|
|
106
|
+
"skipLibCheck": true,
|
|
107
|
+
"resolveJsonModule": true
|
|
108
|
+
},
|
|
109
|
+
"include": ["src"]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- [ ] **Step 4: Create `vitest.config.ts`**
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { defineConfig } from 'vitest/config'
|
|
117
|
+
|
|
118
|
+
export default defineConfig({
|
|
119
|
+
test: { environment: 'node', include: ['tests/**/*.test.ts'] },
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- [ ] **Step 5: Create `.gitignore`**
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
node_modules/
|
|
127
|
+
dist/
|
|
128
|
+
*.log
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- [ ] **Step 6: Install and run the test**
|
|
132
|
+
|
|
133
|
+
Run: `npm install && npm test`
|
|
134
|
+
Expected: 1 passing test (`smoke > runs`).
|
|
135
|
+
|
|
136
|
+
- [ ] **Step 7: Commit**
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git add package.json tsconfig.json vitest.config.ts .gitignore tests/smoke.test.ts
|
|
140
|
+
git commit -m "chore: scaffold forge TS project with vitest"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### Task 2: SKILL.md frontmatter parser
|
|
146
|
+
|
|
147
|
+
**Files:**
|
|
148
|
+
- Create: `src/canon/frontmatter.ts`
|
|
149
|
+
- Test: `tests/canon/frontmatter.test.ts`
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 1: Write the failing test**
|
|
152
|
+
|
|
153
|
+
Create `tests/canon/frontmatter.test.ts`:
|
|
154
|
+
```ts
|
|
155
|
+
import { describe, it, expect } from 'vitest'
|
|
156
|
+
import { parseFrontmatter } from '../../src/canon/frontmatter.js'
|
|
157
|
+
|
|
158
|
+
describe('parseFrontmatter', () => {
|
|
159
|
+
it('parses name and description from a --- block', () => {
|
|
160
|
+
const md = '---\nname: tdd\ndescription: Test-driven development\n---\n# Body\n'
|
|
161
|
+
expect(parseFrontmatter(md)).toMatchObject({ name: 'tdd', description: 'Test-driven development' })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('tolerates CRLF line endings', () => {
|
|
165
|
+
const md = '---\r\nname: x\r\ndescription: y\r\n---\r\nbody'
|
|
166
|
+
expect(parseFrontmatter(md)).toMatchObject({ name: 'x', description: 'y' })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('returns null when there is no frontmatter', () => {
|
|
170
|
+
expect(parseFrontmatter('# just a heading\n')).toBeNull()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
176
|
+
|
|
177
|
+
Run: `npm test -- frontmatter`
|
|
178
|
+
Expected: FAIL — cannot find module `src/canon/frontmatter.js`.
|
|
179
|
+
|
|
180
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
181
|
+
|
|
182
|
+
Create `src/canon/frontmatter.ts`:
|
|
183
|
+
```ts
|
|
184
|
+
import { parse } from 'yaml'
|
|
185
|
+
|
|
186
|
+
export function parseFrontmatter(content: string): Record<string, unknown> | null {
|
|
187
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
188
|
+
if (!m) return null
|
|
189
|
+
const parsed = parse(m[1])
|
|
190
|
+
return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
195
|
+
|
|
196
|
+
Run: `npm test -- frontmatter`
|
|
197
|
+
Expected: PASS (3 tests).
|
|
198
|
+
|
|
199
|
+
- [ ] **Step 5: Commit**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
git add src/canon/frontmatter.ts tests/canon/frontmatter.test.ts
|
|
203
|
+
git commit -m "feat: add SKILL.md frontmatter parser"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### Task 3: Manifest schema + loader
|
|
209
|
+
|
|
210
|
+
**Files:**
|
|
211
|
+
- Create: `src/canon/manifest.ts`
|
|
212
|
+
- Test: `tests/canon/manifest.test.ts`
|
|
213
|
+
|
|
214
|
+
- [ ] **Step 1: Write the failing test**
|
|
215
|
+
|
|
216
|
+
Create `tests/canon/manifest.test.ts`:
|
|
217
|
+
```ts
|
|
218
|
+
import { describe, it, expect } from 'vitest'
|
|
219
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
220
|
+
import { join } from 'node:path'
|
|
221
|
+
import { tmpdir } from 'node:os'
|
|
222
|
+
import { loadManifest } from '../../src/canon/manifest.js'
|
|
223
|
+
|
|
224
|
+
function withManifest(yaml: string): string {
|
|
225
|
+
const dir = mkdtempSync(join(tmpdir(), 'forge-mani-'))
|
|
226
|
+
writeFileSync(join(dir, 'manifest.yaml'), yaml)
|
|
227
|
+
return join(dir, 'manifest.yaml')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
describe('loadManifest', () => {
|
|
231
|
+
it('parses a valid manifest', () => {
|
|
232
|
+
const file = withManifest(`
|
|
233
|
+
name: forge-canon
|
|
234
|
+
version: 0.1.0
|
|
235
|
+
agents: [claude, codex, gemini]
|
|
236
|
+
skills:
|
|
237
|
+
- { id: tdd, path: skills/tdd, kind: methodology }
|
|
238
|
+
policy:
|
|
239
|
+
- { path: policy/gates.md }
|
|
240
|
+
loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
|
|
241
|
+
tools:
|
|
242
|
+
- { id: rtk, path: tools/rtk.md }
|
|
243
|
+
`)
|
|
244
|
+
const m = loadManifest(file)
|
|
245
|
+
expect(m.name).toBe('forge-canon')
|
|
246
|
+
expect(m.agents).toEqual(['claude', 'codex', 'gemini'])
|
|
247
|
+
expect(m.skills[0]).toMatchObject({ id: 'tdd', kind: 'methodology' })
|
|
248
|
+
rmSync(join(file, '..'), { recursive: true, force: true })
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('rejects an unknown agent', () => {
|
|
252
|
+
const file = withManifest(`
|
|
253
|
+
name: x
|
|
254
|
+
version: 0.1.0
|
|
255
|
+
agents: [claude, mystery]
|
|
256
|
+
skills: []
|
|
257
|
+
policy: []
|
|
258
|
+
loop: { spec: a, prdSchema: b }
|
|
259
|
+
tools: []
|
|
260
|
+
`)
|
|
261
|
+
expect(() => loadManifest(file)).toThrow()
|
|
262
|
+
rmSync(join(file, '..'), { recursive: true, force: true })
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
268
|
+
|
|
269
|
+
Run: `npm test -- manifest`
|
|
270
|
+
Expected: FAIL — cannot find module `src/canon/manifest.js`.
|
|
271
|
+
|
|
272
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
273
|
+
|
|
274
|
+
Create `src/canon/manifest.ts`:
|
|
275
|
+
```ts
|
|
276
|
+
import { z } from 'zod'
|
|
277
|
+
import { parse } from 'yaml'
|
|
278
|
+
import { readFileSync } from 'node:fs'
|
|
279
|
+
|
|
280
|
+
export const AgentSchema = z.enum(['claude', 'codex', 'gemini'])
|
|
281
|
+
|
|
282
|
+
export const SkillEntrySchema = z.object({
|
|
283
|
+
id: z.string().min(1),
|
|
284
|
+
path: z.string().min(1),
|
|
285
|
+
kind: z.enum(['methodology', 'role']),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
export const ToolEntrySchema = z.object({
|
|
289
|
+
id: z.string().min(1),
|
|
290
|
+
path: z.string().min(1),
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
export const ManifestSchema = z.object({
|
|
294
|
+
name: z.string().min(1),
|
|
295
|
+
version: z.string().min(1),
|
|
296
|
+
agents: z.array(AgentSchema).min(1),
|
|
297
|
+
skills: z.array(SkillEntrySchema),
|
|
298
|
+
policy: z.array(z.object({ path: z.string().min(1) })),
|
|
299
|
+
loop: z.object({ spec: z.string().min(1), prdSchema: z.string().min(1) }),
|
|
300
|
+
tools: z.array(ToolEntrySchema),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
export type Manifest = z.infer<typeof ManifestSchema>
|
|
304
|
+
|
|
305
|
+
export function loadManifest(file: string): Manifest {
|
|
306
|
+
const raw = parse(readFileSync(file, 'utf8'))
|
|
307
|
+
return ManifestSchema.parse(raw)
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
312
|
+
|
|
313
|
+
Run: `npm test -- manifest`
|
|
314
|
+
Expected: PASS (2 tests).
|
|
315
|
+
|
|
316
|
+
- [ ] **Step 5: Commit**
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
git add src/canon/manifest.ts tests/canon/manifest.test.ts
|
|
320
|
+
git commit -m "feat: add canon manifest schema and loader"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### Task 4: Validator core
|
|
326
|
+
|
|
327
|
+
**Files:**
|
|
328
|
+
- Create: `src/canon/validate.ts`
|
|
329
|
+
- Test: `tests/canon/validate.test.ts`
|
|
330
|
+
|
|
331
|
+
- [ ] **Step 1: Write the failing test**
|
|
332
|
+
|
|
333
|
+
Create `tests/canon/validate.test.ts`:
|
|
334
|
+
```ts
|
|
335
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
336
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
337
|
+
import { join } from 'node:path'
|
|
338
|
+
import { tmpdir } from 'node:os'
|
|
339
|
+
import { validateCanon } from '../../src/canon/validate.js'
|
|
340
|
+
|
|
341
|
+
let dir: string
|
|
342
|
+
|
|
343
|
+
function write(rel: string, content: string) {
|
|
344
|
+
const full = join(dir, rel)
|
|
345
|
+
mkdirSync(join(full, '..'), { recursive: true })
|
|
346
|
+
writeFileSync(full, content)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function seedValidCanon() {
|
|
350
|
+
write('manifest.yaml', `
|
|
351
|
+
name: c
|
|
352
|
+
version: 0.1.0
|
|
353
|
+
agents: [claude]
|
|
354
|
+
skills:
|
|
355
|
+
- { id: tdd, path: skills/tdd, kind: methodology }
|
|
356
|
+
policy:
|
|
357
|
+
- { path: policy/gates.md }
|
|
358
|
+
loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
|
|
359
|
+
tools:
|
|
360
|
+
- { id: rtk, path: tools/rtk.md }
|
|
361
|
+
`)
|
|
362
|
+
write('skills/tdd/SKILL.md', '---\nname: tdd\ndescription: d\n---\nbody')
|
|
363
|
+
write('policy/gates.md', 'gates')
|
|
364
|
+
write('loop/loop-spec.md', 'loop')
|
|
365
|
+
write('loop/prd.schema.md', 'prd')
|
|
366
|
+
write('tools/rtk.md', 'rtk')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'forge-canon-')) })
|
|
370
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
371
|
+
|
|
372
|
+
describe('validateCanon', () => {
|
|
373
|
+
it('returns no errors for a well-formed canon', () => {
|
|
374
|
+
seedValidCanon()
|
|
375
|
+
const errors = validateCanon(dir).filter(i => i.level === 'error')
|
|
376
|
+
expect(errors).toEqual([])
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('flags a missing manifest', () => {
|
|
380
|
+
const issues = validateCanon(dir)
|
|
381
|
+
expect(issues.some(i => i.message.includes('manifest.yaml not found'))).toBe(true)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('flags a skill whose SKILL.md is missing', () => {
|
|
385
|
+
seedValidCanon()
|
|
386
|
+
rmSync(join(dir, 'skills/tdd/SKILL.md'))
|
|
387
|
+
expect(validateCanon(dir).some(i => i.message.includes('SKILL.md missing'))).toBe(true)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('flags a skill with no frontmatter name', () => {
|
|
391
|
+
seedValidCanon()
|
|
392
|
+
write('skills/tdd/SKILL.md', '---\ndescription: d\n---\nbody')
|
|
393
|
+
expect(validateCanon(dir).some(i => i.message.includes('missing name'))).toBe(true)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('flags a missing policy file', () => {
|
|
397
|
+
seedValidCanon()
|
|
398
|
+
rmSync(join(dir, 'policy/gates.md'))
|
|
399
|
+
expect(validateCanon(dir).some(i => i.message.includes('policy file not found'))).toBe(true)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('flags duplicate skill ids', () => {
|
|
403
|
+
seedValidCanon()
|
|
404
|
+
write('manifest.yaml', `
|
|
405
|
+
name: c
|
|
406
|
+
version: 0.1.0
|
|
407
|
+
agents: [claude]
|
|
408
|
+
skills:
|
|
409
|
+
- { id: tdd, path: skills/tdd, kind: methodology }
|
|
410
|
+
- { id: tdd, path: skills/tdd, kind: role }
|
|
411
|
+
policy:
|
|
412
|
+
- { path: policy/gates.md }
|
|
413
|
+
loop: { spec: loop/loop-spec.md, prdSchema: loop/prd.schema.md }
|
|
414
|
+
tools:
|
|
415
|
+
- { id: rtk, path: tools/rtk.md }
|
|
416
|
+
`)
|
|
417
|
+
expect(validateCanon(dir).some(i => i.message.includes('duplicate skill id'))).toBe(true)
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
423
|
+
|
|
424
|
+
Run: `npm test -- validate`
|
|
425
|
+
Expected: FAIL — cannot find module `src/canon/validate.js`.
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
428
|
+
|
|
429
|
+
Create `src/canon/validate.ts`:
|
|
430
|
+
```ts
|
|
431
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
432
|
+
import { join } from 'node:path'
|
|
433
|
+
import { loadManifest } from './manifest.js'
|
|
434
|
+
import { parseFrontmatter } from './frontmatter.js'
|
|
435
|
+
|
|
436
|
+
export interface Issue {
|
|
437
|
+
level: 'error' | 'warn'
|
|
438
|
+
message: string
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function validateCanon(canonDir: string): Issue[] {
|
|
442
|
+
const issues: Issue[] = []
|
|
443
|
+
const manifestPath = join(canonDir, 'manifest.yaml')
|
|
444
|
+
if (!existsSync(manifestPath)) {
|
|
445
|
+
return [{ level: 'error', message: `manifest.yaml not found in ${canonDir}` }]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let manifest
|
|
449
|
+
try {
|
|
450
|
+
manifest = loadManifest(manifestPath)
|
|
451
|
+
} catch (e) {
|
|
452
|
+
return [{ level: 'error', message: `manifest.yaml invalid: ${(e as Error).message}` }]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const seenSkill = new Set<string>()
|
|
456
|
+
for (const s of manifest.skills) {
|
|
457
|
+
if (seenSkill.has(s.id)) issues.push({ level: 'error', message: `duplicate skill id: ${s.id}` })
|
|
458
|
+
seenSkill.add(s.id)
|
|
459
|
+
const dir = join(canonDir, s.path)
|
|
460
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
461
|
+
issues.push({ level: 'error', message: `skill ${s.id}: path not found: ${s.path}` })
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
const skillMd = join(dir, 'SKILL.md')
|
|
465
|
+
if (!existsSync(skillMd)) {
|
|
466
|
+
issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md missing` })
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
const fm = parseFrontmatter(readFileSync(skillMd, 'utf8'))
|
|
470
|
+
if (!fm) {
|
|
471
|
+
issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md has no frontmatter` })
|
|
472
|
+
} else {
|
|
473
|
+
if (!fm.name) issues.push({ level: 'error', message: `skill ${s.id}: frontmatter missing name` })
|
|
474
|
+
if (!fm.description) issues.push({ level: 'error', message: `skill ${s.id}: frontmatter missing description` })
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const p of manifest.policy) {
|
|
479
|
+
if (!existsSync(join(canonDir, p.path))) {
|
|
480
|
+
issues.push({ level: 'error', message: `policy file not found: ${p.path}` })
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const loopChecks: ReadonlyArray<readonly [string, string]> = [
|
|
485
|
+
['loop.spec', manifest.loop.spec],
|
|
486
|
+
['loop.prdSchema', manifest.loop.prdSchema],
|
|
487
|
+
]
|
|
488
|
+
for (const [label, rel] of loopChecks) {
|
|
489
|
+
if (!existsSync(join(canonDir, rel))) {
|
|
490
|
+
issues.push({ level: 'error', message: `${label} not found: ${rel}` })
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const seenTool = new Set<string>()
|
|
495
|
+
for (const t of manifest.tools) {
|
|
496
|
+
if (seenTool.has(t.id)) issues.push({ level: 'error', message: `duplicate tool id: ${t.id}` })
|
|
497
|
+
seenTool.add(t.id)
|
|
498
|
+
if (!existsSync(join(canonDir, t.path))) {
|
|
499
|
+
issues.push({ level: 'error', message: `tool ${t.id}: path not found: ${t.path}` })
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return issues
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
508
|
+
|
|
509
|
+
Run: `npm test -- validate`
|
|
510
|
+
Expected: PASS (6 tests).
|
|
511
|
+
|
|
512
|
+
- [ ] **Step 5: Commit**
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
git add src/canon/validate.ts tests/canon/validate.test.ts
|
|
516
|
+
git commit -m "feat: add canon validator"
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### Task 5: CLI wiring (`forge validate`)
|
|
522
|
+
|
|
523
|
+
**Files:**
|
|
524
|
+
- Create: `src/cli.ts`
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 1: Write the implementation**
|
|
527
|
+
|
|
528
|
+
Create `src/cli.ts`:
|
|
529
|
+
```ts
|
|
530
|
+
#!/usr/bin/env node
|
|
531
|
+
import { validateCanon } from './canon/validate.js'
|
|
532
|
+
|
|
533
|
+
export function runValidate(canonDir: string): number {
|
|
534
|
+
const issues = validateCanon(canonDir)
|
|
535
|
+
for (const i of issues) {
|
|
536
|
+
console.log(`${i.level === 'error' ? 'ERROR' : 'warn '} ${i.message}`)
|
|
537
|
+
}
|
|
538
|
+
const errors = issues.filter(i => i.level === 'error')
|
|
539
|
+
if (errors.length === 0) {
|
|
540
|
+
console.log(`✓ canon valid (${canonDir})`)
|
|
541
|
+
return 0
|
|
542
|
+
}
|
|
543
|
+
console.log(`✗ ${errors.length} error(s)`)
|
|
544
|
+
return 1
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function main(argv: string[]): number {
|
|
548
|
+
const [cmd, ...rest] = argv
|
|
549
|
+
switch (cmd) {
|
|
550
|
+
case 'validate':
|
|
551
|
+
return runValidate(rest[0] ?? 'canon')
|
|
552
|
+
default:
|
|
553
|
+
console.log('usage: forge validate [canonDir]')
|
|
554
|
+
return cmd ? 1 : 0
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
process.exit(main(process.argv.slice(2)))
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
- [ ] **Step 2: Verify the CLI runs against a temp canon**
|
|
562
|
+
|
|
563
|
+
Run (PowerShell):
|
|
564
|
+
```
|
|
565
|
+
npm run forge -- validate canon
|
|
566
|
+
```
|
|
567
|
+
Expected: prints errors (canon not seeded yet) and exits non-zero. This confirms the command is wired; Task 6 seeds the real canon so it passes.
|
|
568
|
+
|
|
569
|
+
- [ ] **Step 3: Commit**
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
git add src/cli.ts
|
|
573
|
+
git commit -m "feat: wire forge validate CLI command"
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### Task 6: Seed the real Canon
|
|
579
|
+
|
|
580
|
+
**Files:**
|
|
581
|
+
- Create: `canon/AGENTS.md`, `canon/manifest.yaml`, `canon/skills/tdd/SKILL.md`, `canon/skills/eng-review/SKILL.md`, `canon/policy/gates.md`, `canon/policy/roles.md`, `canon/loop/loop-spec.md`, `canon/loop/prd.schema.md`, `canon/tools/rtk.md`, `canon/tools/graphify.md`, `canon/tools/playwright-mcp.md`
|
|
582
|
+
- Test: `tests/canon/real-canon.test.ts`
|
|
583
|
+
|
|
584
|
+
- [ ] **Step 1: Write the failing integration test**
|
|
585
|
+
|
|
586
|
+
Create `tests/canon/real-canon.test.ts`:
|
|
587
|
+
```ts
|
|
588
|
+
import { describe, it, expect } from 'vitest'
|
|
589
|
+
import { fileURLToPath } from 'node:url'
|
|
590
|
+
import { join } from 'node:path'
|
|
591
|
+
import { validateCanon } from '../../src/canon/validate.js'
|
|
592
|
+
|
|
593
|
+
const repoRoot = fileURLToPath(new URL('../../', import.meta.url))
|
|
594
|
+
|
|
595
|
+
describe('real canon', () => {
|
|
596
|
+
it('validates with zero errors', () => {
|
|
597
|
+
const errors = validateCanon(join(repoRoot, 'canon')).filter(i => i.level === 'error')
|
|
598
|
+
expect(errors).toEqual([])
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
604
|
+
|
|
605
|
+
Run: `npm test -- real-canon`
|
|
606
|
+
Expected: FAIL — `manifest.yaml not found in .../canon`.
|
|
607
|
+
|
|
608
|
+
- [ ] **Step 3: Create `canon/manifest.yaml`**
|
|
609
|
+
|
|
610
|
+
```yaml
|
|
611
|
+
name: forge-canon
|
|
612
|
+
version: 0.1.0
|
|
613
|
+
agents: [claude, codex, gemini]
|
|
614
|
+
skills:
|
|
615
|
+
- { id: tdd, path: skills/tdd, kind: methodology }
|
|
616
|
+
- { id: eng-review, path: skills/eng-review, kind: role }
|
|
617
|
+
policy:
|
|
618
|
+
- { path: policy/gates.md }
|
|
619
|
+
- { path: policy/roles.md }
|
|
620
|
+
loop:
|
|
621
|
+
spec: loop/loop-spec.md
|
|
622
|
+
prdSchema: loop/prd.schema.md
|
|
623
|
+
tools:
|
|
624
|
+
- { id: rtk, path: tools/rtk.md }
|
|
625
|
+
- { id: graphify, path: tools/graphify.md }
|
|
626
|
+
- { id: playwright-mcp, path: tools/playwright-mcp.md }
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- [ ] **Step 4: Create `canon/AGENTS.md`**
|
|
630
|
+
|
|
631
|
+
```markdown
|
|
632
|
+
# Forge Harness — Agent Baseline
|
|
633
|
+
|
|
634
|
+
You are operating in a project retrofitted by Forge. Follow these always:
|
|
635
|
+
|
|
636
|
+
- **Quality first:** Test-driven development is the default. No production code without a failing test first. See skill `tdd`.
|
|
637
|
+
- **Stop-the-Line:** Do not start implementation until Definition of Done / Acceptance Criteria are written. See `policy/gates.md`.
|
|
638
|
+
- **Role separation:** The agent that implements does not self-review, self-merge, or self-audit security. See `policy/roles.md`.
|
|
639
|
+
- **Context efficiency:** Prefer the wired tools (rtk for command output, the code-graph for symbol lookup) over reading whole files. See `tools/`.
|
|
640
|
+
|
|
641
|
+
This file is the portable baseline. Agent-specific instructions are generated alongside it (CLAUDE.md, GEMINI.md).
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
- [ ] **Step 5: Create `canon/skills/tdd/SKILL.md`**
|
|
645
|
+
|
|
646
|
+
```markdown
|
|
647
|
+
---
|
|
648
|
+
name: tdd
|
|
649
|
+
description: Use when writing any production code — enforces RED-GREEN-REFACTOR, no production code without a failing test first.
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
# Test-Driven Development
|
|
653
|
+
|
|
654
|
+
The Iron Law: **No production code without a failing test first.**
|
|
655
|
+
|
|
656
|
+
1. **RED** — write the smallest failing test for the next behavior. Run it; confirm it fails for the right reason.
|
|
657
|
+
2. **GREEN** — write the minimal code to make it pass. Run it; confirm it passes.
|
|
658
|
+
3. **REFACTOR** — clean up without changing behavior. Tests stay green.
|
|
659
|
+
4. **COMMIT** — one behavior per commit.
|
|
660
|
+
|
|
661
|
+
Never write multiple tests ahead of implementation. Never skip the failing-run step.
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
- [ ] **Step 6: Create `canon/skills/eng-review/SKILL.md`**
|
|
665
|
+
|
|
666
|
+
```markdown
|
|
667
|
+
---
|
|
668
|
+
name: eng-review
|
|
669
|
+
description: Use before merging a change — engineering-manager review of architecture, edge cases, test coverage, and performance.
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
# Engineering Review
|
|
673
|
+
|
|
674
|
+
Review a change for: architecture fit, data flow, edge cases, test coverage, and performance. Be opinionated. Block on:
|
|
675
|
+
|
|
676
|
+
- Missing or weak tests for the changed behavior.
|
|
677
|
+
- Unhandled error paths or edge cases.
|
|
678
|
+
- Architectural drift from the project's established patterns.
|
|
679
|
+
|
|
680
|
+
Output: a pass/block verdict with specific, actionable findings. A reviewer never reviews their own implementation (see `policy/roles.md`).
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
- [ ] **Step 7: Create `canon/policy/gates.md`**
|
|
684
|
+
|
|
685
|
+
```markdown
|
|
686
|
+
# Stop-the-Line Gates
|
|
687
|
+
|
|
688
|
+
These gates are enforced mechanically by the Loop (Baustein C) and expected of every agent run.
|
|
689
|
+
|
|
690
|
+
- **DoD gate:** Implementation may not begin until Definition of Done / Acceptance Criteria for the unit of work exist and are recorded (e.g., in the PRD story).
|
|
691
|
+
- **Green-tests gate:** A unit of work is not complete until its tests pass.
|
|
692
|
+
- **Clean-worktree gate:** No dispatch into a dirty or conflicted git worktree.
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
- [ ] **Step 8: Create `canon/policy/roles.md`**
|
|
696
|
+
|
|
697
|
+
```markdown
|
|
698
|
+
# Role Separation
|
|
699
|
+
|
|
700
|
+
The agent that performs a role must not also perform a conflicting one:
|
|
701
|
+
|
|
702
|
+
- **Implementer ≠ Reviewer** — implementation is not self-reviewed.
|
|
703
|
+
- **Implementer ≠ Merger** — the implementer does not merge their own change.
|
|
704
|
+
- **Implementer ≠ Security auditor** — security is not self-audited.
|
|
705
|
+
|
|
706
|
+
In the Loop, these map to separate iterations with fresh context.
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
- [ ] **Step 9: Create `canon/loop/loop-spec.md`**
|
|
710
|
+
|
|
711
|
+
```markdown
|
|
712
|
+
# Loop Specification (Ralph + GSD)
|
|
713
|
+
|
|
714
|
+
The autonomous loop is OPTIONAL and toggle-able (`forge loop on|off`). When enabled:
|
|
715
|
+
|
|
716
|
+
1. Pre-dispatch gates: missing tools / dirty worktree / git conflict → `blocked`.
|
|
717
|
+
2. Pick the highest-priority unfinished PRD story.
|
|
718
|
+
3. Stop-the-Line gate: DoD/AC present, else `blocked`.
|
|
719
|
+
4. Spawn a fresh agent (claude -p | codex exec | gemini) in a git worktree.
|
|
720
|
+
5. Implement ONE story → tests green → review iteration (different role).
|
|
721
|
+
6. Update the PRD (`passes: true`) + atomic commit.
|
|
722
|
+
7. Stop when all stories `passes: true`, or the iteration cap is reached.
|
|
723
|
+
|
|
724
|
+
State lives outside the model context: the PRD file + git. Full driver is built in Baustein C.
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
- [ ] **Step 10: Create `canon/loop/prd.schema.md`**
|
|
728
|
+
|
|
729
|
+
```markdown
|
|
730
|
+
# PRD Schema
|
|
731
|
+
|
|
732
|
+
The loop is driven by a versioned PRD file. Each story:
|
|
733
|
+
|
|
734
|
+
```yaml
|
|
735
|
+
- id: STORY-1
|
|
736
|
+
title: Short imperative description
|
|
737
|
+
priority: 1 # lower = higher priority
|
|
738
|
+
acceptance: # Definition of Done (required before implementation)
|
|
739
|
+
- The endpoint returns 200 for a valid request.
|
|
740
|
+
passes: false # set true only when acceptance is met and tests are green
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Stop condition: every story has `passes: true`.
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
- [ ] **Step 11: Create `canon/tools/rtk.md`**
|
|
747
|
+
|
|
748
|
+
```markdown
|
|
749
|
+
# Tool: rtk (token compression)
|
|
750
|
+
|
|
751
|
+
Per-agent wiring (generated by Baustein B):
|
|
752
|
+
|
|
753
|
+
- **Claude Code:** PreToolUse hook auto-rewrites commands (`git status` → `rtk git status`). On Windows this needs WSL; otherwise fall back to instruction mode (this file injected into CLAUDE.md telling the agent to prefix commands with `rtk`).
|
|
754
|
+
- **Codex CLI:** inject `RTK.md` / AGENTS.md instruction.
|
|
755
|
+
- **Gemini CLI:** no hook system — register rtk as an MCP tool or inject the GEMINI.md instruction.
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- [ ] **Step 12: Create `canon/tools/graphify.md`**
|
|
759
|
+
|
|
760
|
+
```markdown
|
|
761
|
+
# Tool: graphify (code-graph)
|
|
762
|
+
|
|
763
|
+
MIT, multimodal code/doc graph. Wired as an MCP server for all three agents (stdio). Prefer symbol/graph lookups over reading whole files. Caveat: heuristic edges (INFERRED/AMBIGUOUS) and a static index that can go stale — rebuild on significant changes.
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
- [ ] **Step 13: Create `canon/tools/playwright-mcp.md`**
|
|
767
|
+
|
|
768
|
+
```markdown
|
|
769
|
+
# Tool: Playwright MCP (browser / dogfooding)
|
|
770
|
+
|
|
771
|
+
Microsoft Playwright MCP, Apache-2.0. Wired as an MCP server for all three agents — the only browser tool with native MCP parity across Claude/Codex/Gemini. Used for QA, dogfooding user flows, screenshots, and deploy verification.
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
- [ ] **Step 14: Run the integration test**
|
|
775
|
+
|
|
776
|
+
Run: `npm test -- real-canon`
|
|
777
|
+
Expected: PASS — the real canon validates with zero errors.
|
|
778
|
+
|
|
779
|
+
- [ ] **Step 15: Run the full suite and the CLI**
|
|
780
|
+
|
|
781
|
+
Run: `npm test`
|
|
782
|
+
Expected: all tests pass.
|
|
783
|
+
|
|
784
|
+
Run (PowerShell): `npm run forge -- validate canon`
|
|
785
|
+
Expected: `✓ canon valid (canon)` and exit 0.
|
|
786
|
+
|
|
787
|
+
- [ ] **Step 16: Commit**
|
|
788
|
+
|
|
789
|
+
```bash
|
|
790
|
+
git add canon tests/canon/real-canon.test.ts
|
|
791
|
+
git commit -m "feat: seed harness-agnostic canon and validate it"
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Self-Review
|
|
797
|
+
|
|
798
|
+
**1. Spec coverage (Baustein A scope):**
|
|
799
|
+
- Canon structure (skills/policy/loop/tools/AGENTS.md/manifest) → Task 6 ✓
|
|
800
|
+
- Manifest as source-of-truth declaration → Tasks 3, 6 ✓
|
|
801
|
+
- Harness-agnostic (no agent paths in canon) → Task 6 content ✓
|
|
802
|
+
- Independently testable software (`forge validate`) → Tasks 2–5 ✓
|
|
803
|
+
- Windows-native, no Make → Node/npm scripts only ✓
|
|
804
|
+
- (Deferred to Plan B: runtime generation/detect/wire. Deferred to Plan C: loop driver. Correct — A is content + validator only.)
|
|
805
|
+
|
|
806
|
+
**2. Placeholder scan:** No TBD/TODO; every file has complete content; every code step shows full code. ✓
|
|
807
|
+
|
|
808
|
+
**3. Type consistency:** `Issue{level,message}`, `Manifest`, `loadManifest()`, `parseFrontmatter()`, `validateCanon()`, `runValidate()` — names used consistently across tasks. Manifest YAML field names (`skills/policy/loop/tools`, `loop.spec`, `loop.prdSchema`, skill `kind`) match between schema (Task 3) and seed (Task 6). ✓
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## Next Plans (not this document)
|
|
813
|
+
|
|
814
|
+
- **Plan B — Retrofit-Skill:** `forge detect|plan|generate|wire|report`, runtime artifact generation per agent, non-destructive, asks about loop opt-in.
|
|
815
|
+
- **Plan C — Loop-Engine:** the Ralph driver, gates, worktree isolation, `forge loop on|off|status`.
|