@izkac/forgekit 0.1.6 → 0.2.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/bin/forge.mjs +2 -0
- package/package.json +1 -1
- package/src/e2e.mjs +172 -0
- package/src/init.mjs +13 -6
- package/src/init.test.mjs +32 -1
- package/src/integrity-check.mjs +5 -3
- package/src/integrity.mjs +310 -20
- package/src/integrity.test.mjs +169 -17
- package/src/set-phase.test.mjs +9 -1
- package/vendor/skills/forge/SKILL.md +1 -1
- package/vendor/skills/forge/docs/forge.md +53 -31
- package/vendor/skills/forge/phases/finish.md +1 -1
- package/vendor/skills/forge/phases/implement.md +1 -0
- package/vendor/skills/forge/phases/plan-openspec.md +8 -2
- package/vendor/skills/forge/phases/plan-specs.md +8 -2
- package/vendor/skills/forge/phases/verify.md +7 -3
- package/vendor/skills/forge/references/runtime-integrity.md +28 -10
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +9 -6
- package/vendor/skills/forge/subagents/task-reviewer-prompt.md +1 -0
- package/vendor/templates/project/claude/commands/forge.md +1 -1
- package/vendor/templates/project/claude/rules/forge.md +20 -16
- package/vendor/templates/project/codex/rules/forge.md +12 -10
- package/vendor/templates/project/cursor/rules/forge.mdc +25 -21
package/src/integrity.test.mjs
CHANGED
|
@@ -6,16 +6,24 @@ import { tmpdir } from 'node:os';
|
|
|
6
6
|
import {
|
|
7
7
|
JOBS_SIGNAL_RE,
|
|
8
8
|
addDeferral,
|
|
9
|
+
checkE2eGate,
|
|
10
|
+
e2ePath,
|
|
11
|
+
e2eStepsHash,
|
|
12
|
+
e2eTemplate,
|
|
13
|
+
initE2e,
|
|
9
14
|
initSpine,
|
|
10
15
|
loadDeferrals,
|
|
11
16
|
openDeferrals,
|
|
12
17
|
resolveChangeDir,
|
|
13
18
|
resolveDeferral,
|
|
19
|
+
runE2eSteps,
|
|
14
20
|
runIntegrityChecks,
|
|
15
21
|
sessionJobsSignalText,
|
|
16
22
|
spinePath,
|
|
17
23
|
spineTemplate,
|
|
24
|
+
validateE2e,
|
|
18
25
|
validateSpine,
|
|
26
|
+
writeE2eResults,
|
|
19
27
|
} from './integrity.mjs';
|
|
20
28
|
|
|
21
29
|
function tmp(prefix) {
|
|
@@ -215,40 +223,184 @@ test('runIntegrityChecks: empty slug without spine still fails', () => {
|
|
|
215
223
|
}
|
|
216
224
|
});
|
|
217
225
|
|
|
218
|
-
|
|
226
|
+
function greenStep(overrides = {}) {
|
|
227
|
+
return {
|
|
228
|
+
name: 'produce',
|
|
229
|
+
cmd: 'node -e "console.log(\'proposals: 3\')"',
|
|
230
|
+
...overrides,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeSpineWithRows(sessionDir) {
|
|
235
|
+
fs.writeFileSync(
|
|
236
|
+
path.join(sessionDir, 'spine.json'),
|
|
237
|
+
`${JSON.stringify({ change: null, notApplicable: null, rows: [validRow()] }, null, 2)}\n`,
|
|
238
|
+
'utf8',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function writeE2eDoc(sessionDir, doc) {
|
|
243
|
+
fs.writeFileSync(path.join(sessionDir, 'e2e.json'), `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
test('validateE2e: filled steps pass; template placeholders rejected', () => {
|
|
247
|
+
assert.equal(validateE2e({ notApplicable: null, steps: [greenStep()] }).ok, true);
|
|
248
|
+
const scaffold = validateE2e(e2eTemplate({ change: 'x' }));
|
|
249
|
+
assert.equal(scaffold.ok, false);
|
|
250
|
+
assert.match(scaffold.problems.join('\n'), /scaffold placeholder/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('validateE2e: missing cmd, bad regex, bad timeout, empty steps', () => {
|
|
254
|
+
assert.match(
|
|
255
|
+
validateE2e({ steps: [{ name: 'x', cmd: '' }] }).problems.join('\n'),
|
|
256
|
+
/missing cmd/,
|
|
257
|
+
);
|
|
258
|
+
assert.match(
|
|
259
|
+
validateE2e({ steps: [greenStep({ expect: '(' })] }).problems.join('\n'),
|
|
260
|
+
/not a valid regex/,
|
|
261
|
+
);
|
|
262
|
+
assert.match(
|
|
263
|
+
validateE2e({ steps: [greenStep({ timeoutMs: -5 })] }).problems.join('\n'),
|
|
264
|
+
/timeoutMs/,
|
|
265
|
+
);
|
|
266
|
+
assert.match(validateE2e({ steps: [] }).problems.join('\n'), /steps is empty/);
|
|
267
|
+
assert.equal(validateE2e({ steps: [], notApplicable: 'no headless env — manual device loop' }).ok, true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('e2e init writes template and refuses overwrite without force', () => {
|
|
271
|
+
const dir = tmp('forge-e2e-init-');
|
|
272
|
+
try {
|
|
273
|
+
const file = path.join(dir, 'e2e.json');
|
|
274
|
+
initE2e({ file, change: 'my-change' });
|
|
275
|
+
assert.equal(JSON.parse(fs.readFileSync(file, 'utf8')).change, 'my-change');
|
|
276
|
+
assert.throws(() => initE2e({ file, change: 'my-change' }), /already exists/);
|
|
277
|
+
initE2e({ file, change: 'other', force: true });
|
|
278
|
+
assert.equal(JSON.parse(fs.readFileSync(file, 'utf8')).change, 'other');
|
|
279
|
+
assert.equal(
|
|
280
|
+
e2ePath({ cwd: dir, session: { openspecChange: null }, sessionDir: dir }),
|
|
281
|
+
file,
|
|
282
|
+
);
|
|
283
|
+
} finally {
|
|
284
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('runE2eSteps: green run with expect match', () => {
|
|
289
|
+
const results = runE2eSteps({ steps: [greenStep({ expect: 'proposals: \\d+' })] });
|
|
290
|
+
assert.equal(results.ok, true);
|
|
291
|
+
assert.equal(results.steps[0].exitCode, 0);
|
|
292
|
+
assert.equal(results.steps[0].expectMatched, true);
|
|
293
|
+
assert.equal(results.stepsHash, e2eStepsHash([greenStep({ expect: 'proposals: \\d+' })]));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('runE2eSteps: non-zero exit fails and skips later steps', () => {
|
|
297
|
+
const results = runE2eSteps({
|
|
298
|
+
steps: [
|
|
299
|
+
{ name: 'boom', cmd: 'node -e "process.exit(3)"' },
|
|
300
|
+
greenStep({ name: 'never' }),
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
assert.equal(results.ok, false);
|
|
304
|
+
assert.equal(results.steps[0].exitCode, 3);
|
|
305
|
+
assert.equal(results.steps[1].skipped, true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('runE2eSteps: exit 0 but expect mismatch fails', () => {
|
|
309
|
+
const results = runE2eSteps({ steps: [greenStep({ expect: 'ratified: \\d+' })] });
|
|
310
|
+
assert.equal(results.ok, false);
|
|
311
|
+
assert.equal(results.steps[0].expectMatched, false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('checkE2eGate: missing file, missing results, stale hash, failed run, green, notApplicable', () => {
|
|
315
|
+
const dir = tmp('forge-e2e-gate-');
|
|
316
|
+
try {
|
|
317
|
+
const e2eFile = path.join(dir, 'e2e.json');
|
|
318
|
+
|
|
319
|
+
let gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
320
|
+
assert.match(gate.problems.join('\n'), /e2e\.json required/);
|
|
321
|
+
|
|
322
|
+
writeE2eDoc(dir, { notApplicable: null, steps: [greenStep()] });
|
|
323
|
+
gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
324
|
+
assert.match(gate.problems.join('\n'), /e2e-results\.json missing/);
|
|
325
|
+
|
|
326
|
+
const results = runE2eSteps({ steps: [greenStep()] });
|
|
327
|
+
writeE2eResults(dir, results);
|
|
328
|
+
gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
329
|
+
assert.deepEqual(gate.problems, []);
|
|
330
|
+
|
|
331
|
+
writeE2eDoc(dir, { notApplicable: null, steps: [greenStep({ name: 'edited' })] });
|
|
332
|
+
gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
333
|
+
assert.match(gate.problems.join('\n'), /stale/);
|
|
334
|
+
|
|
335
|
+
writeE2eDoc(dir, { notApplicable: null, steps: [{ name: 'boom', cmd: 'node -e "process.exit(1)"' }] });
|
|
336
|
+
writeE2eResults(dir, runE2eSteps({ steps: [{ name: 'boom', cmd: 'node -e "process.exit(1)"' }] }));
|
|
337
|
+
gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
338
|
+
assert.match(gate.problems.join('\n'), /failed at step "boom"/);
|
|
339
|
+
|
|
340
|
+
writeE2eDoc(dir, { notApplicable: 'loop needs a physical device', steps: [] });
|
|
341
|
+
gate = checkE2eGate({ e2eFile, sessionDir: dir });
|
|
342
|
+
assert.deepEqual(gate.problems, []);
|
|
343
|
+
assert.equal(gate.notApplicable, true);
|
|
344
|
+
} finally {
|
|
345
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('runIntegrityChecks: spine rows demand an executed green e2e run', () => {
|
|
219
350
|
const cwd = tmp('forge-int-loop-');
|
|
220
351
|
try {
|
|
221
352
|
const sessionDir = makeSessionDir(cwd);
|
|
222
|
-
|
|
223
|
-
fs.writeFileSync(
|
|
224
|
-
spineFile,
|
|
225
|
-
`${JSON.stringify({ change: null, notApplicable: null, rows: [validRow()] }, null, 2)}\n`,
|
|
226
|
-
'utf8',
|
|
227
|
-
);
|
|
353
|
+
writeSpineWithRows(sessionDir);
|
|
228
354
|
const session = { slug: 'wire-worker-jobs', openspecChange: null };
|
|
229
355
|
|
|
230
356
|
let result = runIntegrityChecks({ cwd, sessionDir, session });
|
|
231
357
|
assert.equal(result.ok, false);
|
|
232
|
-
assert.match(result.problems.join('\n'), /
|
|
358
|
+
assert.match(result.problems.join('\n'), /e2e\.json required/);
|
|
233
359
|
|
|
234
|
-
|
|
235
|
-
fs.writeFileSync(evidenceFile, '# Verify\n\ntier 3 green\n', 'utf8');
|
|
360
|
+
writeE2eDoc(sessionDir, { notApplicable: null, steps: [greenStep()] });
|
|
236
361
|
result = runIntegrityChecks({ cwd, sessionDir, session });
|
|
237
362
|
assert.equal(result.ok, false);
|
|
238
|
-
assert.match(result.problems.join('\n'), /
|
|
363
|
+
assert.match(result.problems.join('\n'), /e2e-results\.json missing/);
|
|
239
364
|
|
|
240
|
-
|
|
365
|
+
writeE2eResults(sessionDir, runE2eSteps({ steps: [greenStep()] }));
|
|
241
366
|
result = runIntegrityChecks({ cwd, sessionDir, session });
|
|
242
|
-
assert.equal(result.ok,
|
|
243
|
-
assert.
|
|
367
|
+
assert.equal(result.ok, true);
|
|
368
|
+
assert.equal(result.e2eFile, path.join(sessionDir, 'e2e.json'));
|
|
244
369
|
|
|
370
|
+
// prose "## Product loop" alone no longer satisfies the gate
|
|
371
|
+
fs.rmSync(path.join(sessionDir, 'e2e-results.json'));
|
|
245
372
|
fs.writeFileSync(
|
|
246
|
-
|
|
247
|
-
'# Verify\n\n## Product loop\n\ningest
|
|
373
|
+
path.join(sessionDir, 'verify-evidence.md'),
|
|
374
|
+
'# Verify\n\n## Product loop\n\ningest -> analyze -> ratify: output differs\n',
|
|
248
375
|
'utf8',
|
|
249
376
|
);
|
|
250
377
|
result = runIntegrityChecks({ cwd, sessionDir, session });
|
|
251
|
-
assert.equal(result.ok,
|
|
378
|
+
assert.equal(result.ok, false);
|
|
379
|
+
assert.match(result.problems.join('\n'), /e2e-results\.json missing/);
|
|
380
|
+
} finally {
|
|
381
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('runIntegrityChecks: BLOCKED in verify-evidence blocks even with green e2e', () => {
|
|
386
|
+
const cwd = tmp('forge-int-blocked-');
|
|
387
|
+
try {
|
|
388
|
+
const sessionDir = makeSessionDir(cwd);
|
|
389
|
+
writeSpineWithRows(sessionDir);
|
|
390
|
+
writeE2eDoc(sessionDir, { notApplicable: null, steps: [greenStep()] });
|
|
391
|
+
writeE2eResults(sessionDir, runE2eSteps({ steps: [greenStep()] }));
|
|
392
|
+
fs.writeFileSync(
|
|
393
|
+
path.join(sessionDir, 'verify-evidence.md'),
|
|
394
|
+
'# Verify\n\nBLOCKED: ratify UI unreachable in CI\n',
|
|
395
|
+
'utf8',
|
|
396
|
+
);
|
|
397
|
+
const result = runIntegrityChecks({
|
|
398
|
+
cwd,
|
|
399
|
+
sessionDir,
|
|
400
|
+
session: { slug: 'wire-worker-jobs', openspecChange: null },
|
|
401
|
+
});
|
|
402
|
+
assert.equal(result.ok, false);
|
|
403
|
+
assert.match(result.problems.join('\n'), /BLOCKED/);
|
|
252
404
|
} finally {
|
|
253
405
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
254
406
|
}
|
package/src/set-phase.test.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { runE2eSteps, writeE2eResults } from './integrity.mjs';
|
|
8
9
|
|
|
9
10
|
const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'set-phase.mjs');
|
|
10
11
|
|
|
@@ -253,7 +254,7 @@ test('phase done refuses any session without spine.json', () => {
|
|
|
253
254
|
}
|
|
254
255
|
});
|
|
255
256
|
|
|
256
|
-
test('phase done accepts jobs-scoped session with wired spine +
|
|
257
|
+
test('phase done accepts jobs-scoped session with wired spine + green e2e run', () => {
|
|
257
258
|
const dir = tmp('forge-set-phase-spine-ok-');
|
|
258
259
|
try {
|
|
259
260
|
const sessionFile = makeForgeFixture(dir, 'sess-spine-ok');
|
|
@@ -289,6 +290,13 @@ test('phase done accepts jobs-scoped session with wired spine + product-loop evi
|
|
|
289
290
|
'# Verify\n\n## Product loop\n\ningest -> analyze -> ratify -> run: output differs\n',
|
|
290
291
|
'utf8',
|
|
291
292
|
);
|
|
293
|
+
const e2eSteps = [{ name: 'loop', cmd: 'node -e "console.log(\'ratified: 1\')"' }];
|
|
294
|
+
fs.writeFileSync(
|
|
295
|
+
path.join(sessionDir, 'e2e.json'),
|
|
296
|
+
`${JSON.stringify({ change: null, notApplicable: null, steps: e2eSteps }, null, 2)}\n`,
|
|
297
|
+
'utf8',
|
|
298
|
+
);
|
|
299
|
+
writeE2eResults(sessionDir, runE2eSteps({ steps: e2eSteps }));
|
|
292
300
|
|
|
293
301
|
runSetPhase(dir, ['done']);
|
|
294
302
|
const session = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
|
@@ -117,7 +117,7 @@ Testing: [references/test-strategy.md](./references/test-strategy.md) — tier 1
|
|
|
117
117
|
- Tests required for behavior changes
|
|
118
118
|
- Trace ecosystem consumers when contracts change
|
|
119
119
|
- Honor `openspec/config.yaml` prefixes when the project uses them (OpenSpec engine)
|
|
120
|
-
- **Runtime integrity** — [references/runtime-integrity.md](./references/runtime-integrity.md): **spine.json mandatory every change** (rows or `notApplicable` — not keyword-gated); no stubs / false success; capability specs beat narrow task wording; every claimed capability needs a named production caller;
|
|
120
|
+
- **Runtime integrity** — [references/runtime-integrity.md](./references/runtime-integrity.md): **spine.json mandatory every change** (rows or `notApplicable` — not keyword-gated); no stubs / false success; capability specs beat narrow task wording; every claimed capability needs a named production caller; when spine has rows the product loop must be **executed** — `e2e.json` steps + green `forge e2e run` (or BLOCKED), prose does not satisfy the gate; deferred wiring only via `forge defer` — `forge phase done` mechanically refuses on `forge integrity-check` failures
|
|
121
121
|
|
|
122
122
|
## Agent surfaces
|
|
123
123
|
|
|
@@ -115,7 +115,7 @@ User request
|
|
|
115
115
|
└─────────────┬─────────────┘
|
|
116
116
|
▼
|
|
117
117
|
Verify: audit tier 2 + tier 3 (scope from pace)
|
|
118
|
-
+
|
|
118
|
+
+ forge e2e run (green, or BLOCKED)
|
|
119
119
|
+ forge integrity-check
|
|
120
120
|
│
|
|
121
121
|
▼
|
|
@@ -130,8 +130,9 @@ User request
|
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
**Jobs / workers / queues:** spine is mandatory for *every* change (`forge spine
|
|
133
|
-
init` — rows or `notApplicable`).
|
|
134
|
-
|
|
133
|
+
init` — rows or `notApplicable`). Spine rows also require executable acceptance
|
|
134
|
+
steps (`forge e2e init` at plan, green `forge e2e run` before done). Async work
|
|
135
|
+
also needs wiring + product-loop tasks. See [Runtime integrity](#runtime-integrity).
|
|
135
136
|
|
|
136
137
|
### Triage (top of tree)
|
|
137
138
|
|
|
@@ -178,10 +179,10 @@ See the Forge skill’s [references/plan-routing.md](../references/plan-routing.
|
|
|
178
179
|
| ----- | ------------ | ----------------- |
|
|
179
180
|
| **triage** | Substantial? Skip allowed? Bootstrap session | `forge` skill |
|
|
180
181
|
| **brainstorm** | Explore intent, approaches, approval | `skills/brainstorming` |
|
|
181
|
-
| **plan** | Tracked-change propose; **`forge spine init` every change** (rows or `notApplicable`); wiring + product-loop tasks when async | [plan-routing.md](../references/plan-routing.md) |
|
|
182
|
+
| **plan** | Tracked-change propose; **`forge spine init` every change** (rows or `notApplicable`); rows → `forge e2e init` (steps are a plan deliverable); wiring + product-loop tasks when async | [plan-routing.md](../references/plan-routing.md) |
|
|
182
183
|
| **implement** | Subagent per task, TDD, tier 2 evidence; update spine rows; `forge defer` for deferred wiring | **`/forge:apply`** (OpenSpec) or `/forge:build` + `skills/subagent-driven-development` + `skills/test-driven-development` + [test-strategy](../references/test-strategy.md) |
|
|
183
|
-
| **verify** | Audit tier 2; tier 3;
|
|
184
|
-
| **review** | Combined task reviewer (spec + quality) per task; final review (spine +
|
|
184
|
+
| **verify** | Audit tier 2; tier 3; green `forge e2e run`; `forge integrity-check` | `skills/verification-before-completion` + `verify-evidence.md` |
|
|
185
|
+
| **review** | Combined task reviewer (spec + quality) per task; final review (spine + executed e2e) | `skills/requesting-code-review` |
|
|
185
186
|
| **finish** | Archive (+ ADR if the project uses that); `forge phase done` (integrity gate); cleanup | `/opsx:archive`, `forge cleanup` |
|
|
186
187
|
|
|
187
188
|
**Standalone deep review (outside Forge):** for pre-merge audits with adversarial false-positive filtering, use the **thorough code review** skill — see [thorough-code-review.md](https://github.com/izkac/forgekit/blob/main/docs/thorough-code-review.md). Forge's `requesting-code-review` stays the per-task checkpoint during `/forge:build`.
|
|
@@ -206,9 +207,11 @@ Cursor, Claude Code, and Codex without requiring a chat ID.
|
|
|
206
207
|
notes.md
|
|
207
208
|
decisions.md
|
|
208
209
|
plan.md ← legacy throwaway plans only (deprecated)
|
|
209
|
-
verify-evidence.md ← tier 3 +
|
|
210
|
+
verify-evidence.md ← tier 3 + loop narrative (or BLOCKED)
|
|
211
|
+
e2e-results.json ← forge e2e run results (steps hash + per-step outcomes)
|
|
210
212
|
deferrals.json ← forge defer registry (when used)
|
|
211
213
|
spine.json ← fallback if no tracked change dir
|
|
214
|
+
e2e.json ← fallback if no tracked change dir
|
|
212
215
|
scorecard.md / scorecard.json ← L2 session score (written at done/finish)
|
|
213
216
|
tasks/
|
|
214
217
|
01-first-task/
|
|
@@ -219,8 +222,9 @@ Cursor, Claude Code, and Codex without requiring a chat ID.
|
|
|
219
222
|
final-review.md
|
|
220
223
|
```
|
|
221
224
|
|
|
222
|
-
For OpenSpec / specs-engine changes, the canonical **spine matrix**
|
|
223
|
-
the plan: `openspec/changes/<name>/spine.json`
|
|
225
|
+
For OpenSpec / specs-engine changes, the canonical **spine matrix** and **e2e
|
|
226
|
+
steps** live next to the plan: `openspec/changes/<name>/spine.json` + `e2e.json`
|
|
227
|
+
(or `<specsDir>/changes/<name>/…`).
|
|
224
228
|
|
|
225
229
|
**Session ID:** `<UTC-compact>-<kebab-slug>-<6-hex>`
|
|
226
230
|
|
|
@@ -275,8 +279,9 @@ forge prefs --session-set lite # pin active session only
|
|
|
275
279
|
forge doctor # plan-engine readiness (OpenSpec or specs layout)
|
|
276
280
|
forge doctor --install # attempt npm install -g @fission-ai/openspec
|
|
277
281
|
forge spine init|check # capability→runtime spine matrix (spine.json in change dir)
|
|
282
|
+
forge e2e init|run|check # executable product-loop acceptance (e2e.json + e2e-results.json)
|
|
278
283
|
forge defer add|resolve|list # deferral registry — deferred wiring is tracked debt
|
|
279
|
-
forge integrity-check # mechanical gate: spine + deferrals +
|
|
284
|
+
forge integrity-check # mechanical gate: spine + deferrals + executed e2e
|
|
280
285
|
forge score [--write] [--md] # L2 session scorecard (also auto-written at phase done)
|
|
281
286
|
forge overlay # re-apply OpenSpec vendor overlays in this project
|
|
282
287
|
forge init […] # wire project commands / hooks / rules
|
|
@@ -399,7 +404,7 @@ Integrity upgrades Forge from “no false job success” to **product-loop accep
|
|
|
399
404
|
2. **Runtime owner required** — a library alone does not satisfy a capability; name the production caller (job, endpoint, CLI).
|
|
400
405
|
3. **Tests must fail on a no-op** — asserting “job status became succeeded” is not enough.
|
|
401
406
|
4. **Specs beat narrow tasks** — capability specs win when they conflict with a thin task reading.
|
|
402
|
-
5. **E2E = product loop** — produce → consume → decision changes output. A single job slice (ingest → Parquet) is **not** platform E2E.
|
|
407
|
+
5. **E2E = executed product loop** — produce → consume → decision changes output, run as `e2e.json` steps via `forge e2e run` (prose does not count). A single job slice (ingest → Parquet) is **not** platform E2E.
|
|
403
408
|
6. **Job-kind closure** — every product-surface job kind is wired end-to-end **or deleted** before complete. “Fail closed” is only a temporary `BLOCKED` state.
|
|
404
409
|
7. **Consumer–producer** — if UI/API reads it, production must write it (proven in evidence).
|
|
405
410
|
8. **Deferrals are tracked** — “wiring later” only via `forge defer`; unresolved deferrals block `done`.
|
|
@@ -409,6 +414,7 @@ Integrity upgrades Forge from “no false job success” to **product-loop accep
|
|
|
409
414
|
| Tool | Purpose |
|
|
410
415
|
|------|---------|
|
|
411
416
|
| `forge spine init\|check` | **Mandatory every change.** `spine.json`: rows **or** `notApplicable`. Not keyword-gated. |
|
|
417
|
+
| `forge e2e init\|run\|check` | **Mandatory when the spine has rows.** `e2e.json` step list executed by `forge e2e run`; results (`e2e-results.json`) carry a steps hash, so edits after a green run go stale |
|
|
412
418
|
| `forge defer add\|resolve\|list` | Deferred wiring as tracked debt in the session |
|
|
413
419
|
| `forge integrity-check` | Combined gate — also run automatically by `forge phase done\|finish` |
|
|
414
420
|
|
|
@@ -425,9 +431,9 @@ You do **not** paste a long definition-of-done prompt. After
|
|
|
425
431
|
|
|
426
432
|
| Automatic (CLI / hooks) | Agent-driven (skill phases — required) |
|
|
427
433
|
| ----------------------- | -------------------------------------- |
|
|
428
|
-
| Integrity reminder on every session/prompt hook | Plan: **`forge spine init` every change** — fill rows or `notApplicable` |
|
|
434
|
+
| Integrity reminder on every session/prompt hook | Plan: **`forge spine init` every change** — fill rows or `notApplicable`; rows → also `forge e2e init` |
|
|
429
435
|
| Pace `auto` fail-closed to **standard**; task-count escalation at ≥15 | Implement: update spine rows; `forge defer add` if wiring is deferred |
|
|
430
|
-
| `forge phase done\|finish` requires valid spine + writes L2 scorecard | Verify:
|
|
436
|
+
| `forge phase done\|finish` requires valid spine + green current e2e run + writes L2 scorecard | Verify: green `forge e2e run` when spine has rows (sync-only → prefer `notApplicable`) |
|
|
431
437
|
| `forge status` surfaces `integrity.*` defaults | After done: answer L3 ship-check in `scorecard.md` |
|
|
432
438
|
|
|
433
439
|
**Gates are automatic. Filling evidence is part of the normal phase flow.**
|
|
@@ -471,35 +477,51 @@ forge spine init
|
|
|
471
477
|
|
|
472
478
|
Docs-only / no-runtime changes may set `"notApplicable": "docs-only change"` instead of rows.
|
|
473
479
|
|
|
474
|
-
|
|
480
|
+
Spine rows → also author the executable acceptance steps:
|
|
475
481
|
|
|
476
482
|
```bash
|
|
477
|
-
forge
|
|
478
|
-
#
|
|
479
|
-
forge defer resolve --task 9.7
|
|
483
|
+
forge e2e init
|
|
484
|
+
# edit openspec/changes/<name>/e2e.json — the closed loop as commands
|
|
480
485
|
```
|
|
481
486
|
|
|
482
|
-
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"change": "etl-surveydb-pipeline-closure",
|
|
490
|
+
"notApplicable": null,
|
|
491
|
+
"steps": [
|
|
492
|
+
{ "name": "ingest", "cmd": "node scripts/e2e/ingest-fixture.mjs OP1086" },
|
|
493
|
+
{ "name": "analyze", "cmd": "node scripts/e2e/run-analyze.mjs", "expect": "proposals: [1-9]" },
|
|
494
|
+
{ "name": "ratify", "cmd": "node scripts/e2e/ratify-subset.mjs" },
|
|
495
|
+
{ "name": "run-assert", "cmd": "node scripts/e2e/assert-output-differs.mjs", "timeoutMs": 600000 }
|
|
496
|
+
]
|
|
497
|
+
}
|
|
498
|
+
```
|
|
483
499
|
|
|
484
|
-
|
|
485
|
-
|
|
500
|
+
Steps must assert domain side effects — a list that would pass against a
|
|
501
|
+
stubbed handler is invalid. `"notApplicable": "<reason>"` only when no command
|
|
502
|
+
can drive the loop.
|
|
486
503
|
|
|
487
|
-
|
|
488
|
-
- **Exit code:** 0
|
|
504
|
+
**If wiring must wait for a later task**
|
|
489
505
|
|
|
490
|
-
|
|
506
|
+
```bash
|
|
507
|
+
forge defer add --task 9.7 --reason "analyze_study handler lands in 9.7"
|
|
508
|
+
# … when 9.7 is done:
|
|
509
|
+
forge defer resolve --task 9.7
|
|
510
|
+
```
|
|
491
511
|
|
|
492
|
-
|
|
512
|
+
**Verify** (required when spine has rows):
|
|
493
513
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
3. ratify subset via API → decisions tip at revision R
|
|
497
|
-
4. harmonization_run @R → .sav + Master QML + BI
|
|
498
|
-
5. Assert: output at R differs from unratified baseline
|
|
514
|
+
```bash
|
|
515
|
+
forge e2e run # executes the steps, writes e2e-results.json (session dir)
|
|
499
516
|
```
|
|
500
517
|
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
Green run required; results go stale if `e2e.json` changes afterwards (steps
|
|
519
|
+
hash). Keep a short loop narrative under `## Product loop` in
|
|
520
|
+
`verify-evidence.md` as reviewer context — the gate checks the executed
|
|
521
|
+
results, not the heading.
|
|
522
|
+
|
|
523
|
+
Or an explicit `BLOCKED: …` line in `verify-evidence.md` — then `forge phase
|
|
524
|
+
done` refuses until unblocked or the user passes `--allow-incomplete`.
|
|
503
525
|
|
|
504
526
|
**Finish**
|
|
505
527
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Before marking done, integrity must pass (or the user must approve an incomplete finish):
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
forge integrity-check # spine + deferrals +
|
|
6
|
+
forge integrity-check # spine + deferrals + executed e2e (green, current) / BLOCKED
|
|
7
7
|
forge score # preview L2 scorecard (optional)
|
|
8
8
|
forge phase done # runs integrity checks + writes scorecard.md/json
|
|
9
9
|
# escape hatch only with an honest reason:
|
|
@@ -38,6 +38,7 @@ Honor [../references/runtime-integrity.md](../references/runtime-integrity.md) i
|
|
|
38
38
|
- Do not mark a section complete if libraries exist but nothing in the production path calls them.
|
|
39
39
|
- **Deferrals:** if wiring genuinely lands in a later task, register it — `forge defer add --task <id> --reason "…"` — and resolve it when that task lands. Unregistered "later" is a REJECT; unresolved deferrals block `forge phase done`.
|
|
40
40
|
- **Spine:** when a task wires a capability into production, update its `spine.json` row (runtimeOwner / writes / evidence). `forge spine check` must pass before verify ends.
|
|
41
|
+
- **E2E:** the product-loop acceptance task (last implement task) delivers working `e2e.json` steps and a green `forge e2e run` — steps that would pass against a stubbed handler are invalid.
|
|
41
42
|
|
|
42
43
|
## Per-task loop
|
|
43
44
|
|
|
@@ -16,14 +16,20 @@ Thin wrapper around the project **`openspec-propose`** skill (or `/opsx:propose`
|
|
|
16
16
|
Sync-only / docs-only: `"notApplicable": "<reason>"`. Capability work: one row
|
|
17
17
|
per REQ cluster (library → runtime owner → writes → evidence).
|
|
18
18
|
|
|
19
|
+
When the spine has real rows, also `forge e2e init` — the executable
|
|
20
|
+
product-loop steps (`e2e.json`) are a **plan deliverable**: author them (or
|
|
21
|
+
task out their authoring) so verify can `forge e2e run` them.
|
|
22
|
+
|
|
19
23
|
If the change also involves workers, job queues, handlers, or cross-runtime
|
|
20
24
|
calls, `tasks.md` MUST include:
|
|
21
25
|
|
|
22
26
|
- Explicit **wiring** tasks per job kind / entry point → domain pipeline
|
|
23
|
-
- One **product-loop acceptance** task (last implement task, before
|
|
27
|
+
- One **product-loop acceptance** task (last implement task, before
|
|
28
|
+
verify) — its output is a green `forge e2e run`
|
|
24
29
|
|
|
25
30
|
Missing spine = plan **not** ready. (`forge phase done` refuses without a
|
|
26
|
-
valid spine
|
|
31
|
+
valid spine and, when the spine has rows, a green current e2e run —
|
|
32
|
+
keyword sniffing does not decide.)
|
|
27
33
|
|
|
28
34
|
5. Update session:
|
|
29
35
|
```bash
|
|
@@ -64,14 +64,20 @@ OpenSpec propose flow without the vendor CLI. Change lives under
|
|
|
64
64
|
Sync-only / docs-only: `"notApplicable": "<reason>"`. Capability work: one row
|
|
65
65
|
per REQ cluster (library → runtime owner → writes → evidence).
|
|
66
66
|
|
|
67
|
+
When the spine has real rows, also `forge e2e init` — the executable
|
|
68
|
+
product-loop steps (`e2e.json`) are a **plan deliverable**: author them (or
|
|
69
|
+
task out their authoring) so verify can `forge e2e run` them.
|
|
70
|
+
|
|
67
71
|
If the change also involves workers, job queues, handlers, or cross-runtime
|
|
68
72
|
calls, `tasks.md` MUST include:
|
|
69
73
|
|
|
70
74
|
- Explicit **wiring** tasks per job kind / entry point → domain pipeline
|
|
71
|
-
- One **product-loop acceptance** task (last implement task, before
|
|
75
|
+
- One **product-loop acceptance** task (last implement task, before
|
|
76
|
+
verify) — its output is a green `forge e2e run`
|
|
72
77
|
|
|
73
78
|
Missing spine = plan **not** ready. (`forge phase done` refuses without a
|
|
74
|
-
valid spine
|
|
79
|
+
valid spine and, when the spine has rows, a green current e2e run —
|
|
80
|
+
keyword sniffing does not decide.)
|
|
75
81
|
|
|
76
82
|
5. Update session:
|
|
77
83
|
|
|
@@ -73,12 +73,15 @@ For each requirement in the change's **capability specs** (not only `tasks.md`):
|
|
|
73
73
|
|
|
74
74
|
Record the trace in `verify-evidence.md` (a short REQ → caller table is enough).
|
|
75
75
|
|
|
76
|
-
### 4. Product-loop E2E or BLOCKED
|
|
76
|
+
### 4. Product-loop E2E — executed, or BLOCKED
|
|
77
77
|
|
|
78
78
|
Before leaving verify / claiming the change complete:
|
|
79
79
|
|
|
80
|
-
1.
|
|
81
|
-
2.
|
|
80
|
+
1. Confirm `e2e.json` (scaffolded at plan time via `forge e2e init`) drives the **closed product loop** — not a single job slice. When the design has a producer/consumer split (analyze vs execute, proposals vs ratify), the loop is: produce artifact → consumer reads it → decision/state change → **next run's output differs from baseline**. Steps must assert domain side effects; a step list that would pass against a stubbed handler is invalid.
|
|
81
|
+
2. `forge e2e run` — executes the steps sequentially and writes `e2e-results.json` (per-step exit codes, output tails, steps hash) into the session dir. A **green run** is required; results go stale if `e2e.json` changes afterwards (re-run). Prose in `verify-evidence.md` no longer satisfies the done gate. **Or**
|
|
82
|
+
3. Leave an explicit **`BLOCKED`** list in `verify-evidence.md` explaining why E2E cannot run here — the done gate then refuses `done` until unblocked or the user signs `--allow-incomplete`. (`e2e.json` `notApplicable` is only for loops no command can drive — reviewers police the reason.)
|
|
83
|
+
|
|
84
|
+
Keep a short loop narrative under `## Product loop` in `verify-evidence.md` as reviewer context — the gate checks the executed results, not the heading.
|
|
82
85
|
|
|
83
86
|
Also enforce **job-kind closure**: every product-surface job kind is wired end-to-end or deleted from enums/API/UI before complete. And the **consumer–producer rule**: anything the UI/API reads must be proven written by the production path.
|
|
84
87
|
|
|
@@ -88,6 +91,7 @@ Do **not** mark the change complete or advance to `done` while a critical path i
|
|
|
88
91
|
|
|
89
92
|
```bash
|
|
90
93
|
forge spine check # every capability row wired (library → runtime owner → writes → evidence)
|
|
94
|
+
forge e2e check # green, current e2e-results.json (steps hash matches e2e.json)
|
|
91
95
|
forge defer list # no unresolved deferrals
|
|
92
96
|
forge integrity-check # combined; forge phase done runs the same checks
|
|
93
97
|
```
|
|
@@ -45,12 +45,13 @@ task.
|
|
|
45
45
|
- To shrink scope: stop and ask the user. Do not silently checkbox around gaps.
|
|
46
46
|
- Prefer `DONE_WITH_CONCERNS` / incomplete tasks over green checkboxes.
|
|
47
47
|
|
|
48
|
-
## 5. E2E means product loop, not
|
|
48
|
+
## 5. E2E means an executed product loop, not prose
|
|
49
49
|
|
|
50
50
|
Before claiming the change complete:
|
|
51
51
|
|
|
52
|
-
1.
|
|
53
|
-
live entry points
|
|
52
|
+
1. Author the **closed product loop** as executable steps in `e2e.json`
|
|
53
|
+
(`forge e2e init`), run it against the live entry points
|
|
54
|
+
(`forge e2e run`), and get a **green run** — **or**
|
|
54
55
|
2. Leave an explicit **`BLOCKED`** list in `verify-evidence.md` — and do **not**
|
|
55
56
|
mark the change complete / advance to `done`.
|
|
56
57
|
|
|
@@ -65,8 +66,19 @@ next run's OUTPUT DIFFERS from the baseline
|
|
|
65
66
|
|
|
66
67
|
Example: ingest→Parquet plus a thin run→`.sav` does **not** verify a
|
|
67
68
|
governance loop; analyze→proposals→ratify→run-applies-decisions does.
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
The mechanical gate requires a green, **current** `e2e-results.json` (its
|
|
71
|
+
steps hash must match `e2e.json`) whenever the spine has real rows.
|
|
72
|
+
Describing the loop in `verify-evidence.md` no longer satisfies the gate —
|
|
73
|
+
prose cannot prove wiring. Each step is `{ name, cmd, expect?, timeoutMs? }`:
|
|
74
|
+
exit code 0 required, `expect` (regex) matched against the output. Steps must
|
|
75
|
+
assert **domain side effects** — a step list that would pass against a
|
|
76
|
+
stubbed handler is invalid (rule 3 applies to e2e steps too).
|
|
77
|
+
|
|
78
|
+
`e2e.json` may set `"notApplicable": "<reason>"` only when the loop cannot be
|
|
79
|
+
driven by any command (e.g. requires a physical device). Reviewers police the
|
|
80
|
+
reason; "no time" or "covered by unit tests" is a REJECT. Keep a short loop
|
|
81
|
+
narrative under `## Product loop` in `verify-evidence.md` as reviewer context.
|
|
70
82
|
|
|
71
83
|
## 6. Job-kind closure
|
|
72
84
|
|
|
@@ -126,9 +138,9 @@ Then either:
|
|
|
126
138
|
(e.g. `"sync HTTP only — no async producer/consumer"`). That is the honest
|
|
127
139
|
opt-out; missing `spine.json` is not.
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
real rows. Prefer `notApplicable` for sync-only
|
|
131
|
-
fake loop.
|
|
141
|
+
An executed product loop (`forge e2e run`, green + current results) is
|
|
142
|
+
required when the spine has real rows. Prefer `notApplicable` for sync-only
|
|
143
|
+
changes instead of inventing a fake loop.
|
|
132
144
|
|
|
133
145
|
## Reviewer REJECT checklist (mandatory)
|
|
134
146
|
|
|
@@ -141,6 +153,8 @@ REJECT the task (or final review → `NOT READY`) if any of:
|
|
|
141
153
|
- Spec requirement has a library but no named runtime owner
|
|
142
154
|
- Deferred wiring without a registered open deferral (`forge defer list`)
|
|
143
155
|
- Spine row for this capability missing or library-only
|
|
156
|
+
- E2E steps would pass against a stubbed handler (no domain side-effect
|
|
157
|
+
assertions), or `e2e.json` opts out via `notApplicable` without a real reason
|
|
144
158
|
|
|
145
159
|
## Plan seam (every change)
|
|
146
160
|
|
|
@@ -148,10 +162,14 @@ Before apply-ready:
|
|
|
148
162
|
|
|
149
163
|
1. `forge spine init` — **always**. Fill rows for each capability, or set
|
|
150
164
|
`notApplicable` with a reason (sync-only / docs-only).
|
|
151
|
-
2.
|
|
165
|
+
2. When the spine has real rows, `forge e2e init` — the acceptance **steps are
|
|
166
|
+
a plan deliverable**: author (or task out) the `e2e.json` step list that
|
|
167
|
+
drives the loop and asserts its side effects.
|
|
168
|
+
3. If the change involves workers, job queues, handlers, or cross-runtime
|
|
152
169
|
calls, `tasks.md` MUST also include:
|
|
153
170
|
- Explicit **wiring** tasks per job kind / entry point → domain pipeline
|
|
154
|
-
- One **product-loop acceptance** task (last implement task, before
|
|
171
|
+
- One **product-loop acceptance** task (last implement task, before
|
|
172
|
+
verify) — its output is a green `forge e2e run`
|
|
155
173
|
|
|
156
174
|
Missing spine = plan not ready. Keyword guesses about “jobs in scope” are not
|
|
157
175
|
an excuse to skip the spine.
|
|
@@ -34,14 +34,17 @@ job, …) that invokes the implementing code. Cross-check against `spine.json`
|
|
|
34
34
|
- UI/API reads a collection or artifact nothing in the production path writes → **`NOT READY`**
|
|
35
35
|
- Missing E2E fixture path with no explicit `BLOCKED` in `verify-evidence.md` → **`NOT READY`**
|
|
36
36
|
|
|
37
|
-
## Product-loop
|
|
37
|
+
## Product-loop acceptance (required — executed, not described)
|
|
38
38
|
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
does **not** count as platform E2E.
|
|
39
|
+
`forge e2e check` must be green: `e2e.json` steps drive the **closed loop**
|
|
40
|
+
(produce → consume → decision changes output) and `e2e-results.json` records a
|
|
41
|
+
green, current run (steps hash matches). A single job slice (e.g. ingest→file)
|
|
42
|
+
or a library-level E2E does **not** count as platform E2E. Read the steps —
|
|
43
|
+
would they pass against a stubbed handler? If yes, they prove nothing.
|
|
43
44
|
|
|
44
|
-
- No
|
|
45
|
+
- No green, current e2e run and no `BLOCKED` in `verify-evidence.md` → **`NOT READY`**
|
|
46
|
+
- E2E steps assert no domain side effects (would pass on a stub) → **`NOT READY`**
|
|
47
|
+
- `e2e.json` `notApplicable` without a reason no command could overcome → **`NOT READY`**
|
|
45
48
|
- `BLOCKED` present → **`NOT READY`** (honest, but not READY)
|
|
46
49
|
- Unresolved deferrals in `forge defer list` → **`NOT READY`**
|
|
47
50
|
|