@haposoft/cafekit 0.18.0 → 0.18.1
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/README.md +1 -1
- package/bin/__tests__/codex-hooks.test.js +169 -236
- package/bin/__tests__/codex-native.test.js +54 -114
- package/bin/__tests__/develop-contract.test.js +101 -1361
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +6 -3
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +170 -330
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +26 -0
- package/src/claude/hooks/spec-gate.cjs +8 -63
- package/src/claude/hooks/spec-state.cjs +47 -38
- package/src/claude/migration-manifest.json +12 -7
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/spec-gate.cjs +11 -68
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/common/AGENTS.md +4 -10
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
|
@@ -20,7 +20,7 @@ const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
|
|
|
20
20
|
const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
|
|
21
21
|
const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
|
|
22
22
|
const FEATURE = 'demo';
|
|
23
|
-
const TASK_REL = '
|
|
23
|
+
const TASK_REL = 'task-01-x.md';
|
|
24
24
|
const VALID_BASE = '0123456789abcdef0123456789abcdef01234567';
|
|
25
25
|
const VALID_HEAD = '89abcdef0123456789abcdef0123456789abcdef';
|
|
26
26
|
const FIXTURE_ARTIFACT = Buffer.from('cafekit claude fixture artifact\n');
|
|
@@ -29,11 +29,6 @@ const FIXTURE_ARTIFACT_DIGEST = crypto.createHash('sha256').update(FIXTURE_ARTIF
|
|
|
29
29
|
function runHook(payload, cwd) {
|
|
30
30
|
const projectRoot = cwd || payload.cwd;
|
|
31
31
|
const specsRoot = path.join(projectRoot, 'specs');
|
|
32
|
-
if (fs.existsSync(specsRoot)) {
|
|
33
|
-
for (const featureName of fs.readdirSync(specsRoot)) {
|
|
34
|
-
installFeatureReceipt(projectRoot, featureName, payload.session_id || 'test');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
32
|
// PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
|
|
38
33
|
// monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
|
|
39
34
|
const env = { ...process.env, PROJECT_ROOT: projectRoot };
|
|
@@ -69,7 +64,7 @@ function runtimeContext(dir) {
|
|
|
69
64
|
return PROVENANCE.deriveRuntimeContext({
|
|
70
65
|
projectRoot: dir,
|
|
71
66
|
specsRoot: path.join(dir, 'specs'),
|
|
72
|
-
specFile: path.join(dir, 'specs', FEATURE, '
|
|
67
|
+
specFile: path.join(dir, 'specs', FEATURE, 'plan.md'),
|
|
73
68
|
featureName: FEATURE,
|
|
74
69
|
runtimeSession: 'test',
|
|
75
70
|
});
|
|
@@ -118,38 +113,6 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
|
|
|
118
113
|
return dir;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
|
-
function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
|
|
122
|
-
const featureDir = path.join(dir, 'specs', featureName);
|
|
123
|
-
const specFile = path.join(featureDir, 'spec.json');
|
|
124
|
-
if (!fs.existsSync(specFile)) return;
|
|
125
|
-
let spec;
|
|
126
|
-
try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { return; }
|
|
127
|
-
const lifecyclePhase = spec.current_phase || spec.phase;
|
|
128
|
-
const explicitCloseout = ['completed', 'complete'].includes(spec.status)
|
|
129
|
-
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
130
|
-
if (!explicitCloseout) return;
|
|
131
|
-
const tasks = Object.values(spec.task_registry || {});
|
|
132
|
-
if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) return;
|
|
133
|
-
const context = PROVENANCE.deriveRuntimeContext({
|
|
134
|
-
projectRoot: dir,
|
|
135
|
-
specsRoot: path.join(dir, 'specs'),
|
|
136
|
-
specFile,
|
|
137
|
-
featureName,
|
|
138
|
-
runtimeSession: session,
|
|
139
|
-
});
|
|
140
|
-
fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
|
|
141
|
-
`Feature: ${featureName}`,
|
|
142
|
-
'Expected: final integration verification passes',
|
|
143
|
-
'Observed: final integration verification passed',
|
|
144
|
-
'Verification: PASS',
|
|
145
|
-
'Command: node --test',
|
|
146
|
-
'Exit: 0',
|
|
147
|
-
`Base: ${context.base}`,
|
|
148
|
-
`Head: ${context.head}`,
|
|
149
|
-
'',
|
|
150
|
-
].join('\n'));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
116
|
function clearCache() {
|
|
154
117
|
try { fs.unlinkSync(CACHE); } catch { /* absent */ }
|
|
155
118
|
}
|
|
@@ -164,52 +127,32 @@ function readCache() {
|
|
|
164
127
|
}
|
|
165
128
|
|
|
166
129
|
/**
|
|
167
|
-
* Build a minimal
|
|
130
|
+
* Build a minimal process-first packet under dir: plan.md plus one flat task
|
|
131
|
+
* whose Verification Plan runs `npm test` and whose inline Receipt varies.
|
|
168
132
|
* @param {object} opts
|
|
169
133
|
* @param {string} [opts.taskStatus='done']
|
|
170
|
-
* @param {
|
|
171
|
-
* @param {string} [opts.mdStatus='done']
|
|
172
|
-
* @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
|
|
134
|
+
* @param {'valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
|
|
173
135
|
* @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
|
|
174
|
-
* @param {object|null} [opts.workflowPolicy] — if set, persist completion obligations
|
|
175
|
-
* @param {string} [opts.phase='closeout'] — persisted lifecycle boundary
|
|
176
136
|
*/
|
|
177
137
|
function makeFixture(opts = {}) {
|
|
178
138
|
const {
|
|
179
139
|
taskStatus = 'done',
|
|
180
|
-
completed_at = '2026-07-01T00:00:00Z',
|
|
181
|
-
mdStatus = 'done',
|
|
182
140
|
evidence = 'valid',
|
|
183
141
|
runtime = null,
|
|
184
|
-
workflowPolicy = null,
|
|
185
|
-
phase = 'closeout',
|
|
186
142
|
} = opts;
|
|
187
143
|
const dir = tmpDir();
|
|
188
144
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
189
|
-
|
|
190
|
-
fs.
|
|
191
|
-
|
|
192
|
-
const entry = { status: taskStatus };
|
|
193
|
-
if (completed_at !== null) entry.completed_at = completed_at;
|
|
194
|
-
|
|
195
|
-
const spec = {
|
|
196
|
-
status: 'in_progress',
|
|
197
|
-
feature_name: FEATURE,
|
|
198
|
-
current_phase: phase,
|
|
199
|
-
task_registry: { [TASK_REL]: entry },
|
|
200
|
-
};
|
|
201
|
-
if (workflowPolicy) spec.workflow_policy = workflowPolicy;
|
|
202
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
|
|
145
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
203
147
|
|
|
204
|
-
let
|
|
148
|
+
let receiptBlock = '';
|
|
205
149
|
if (evidence === 'valid') {
|
|
206
|
-
|
|
207
|
-
'##
|
|
150
|
+
receiptBlock = [
|
|
151
|
+
'## Receipt',
|
|
208
152
|
'',
|
|
209
153
|
'Verification: PASS',
|
|
210
154
|
'Command: npm test',
|
|
211
155
|
'Exit: 0',
|
|
212
|
-
'Result: PASS',
|
|
213
156
|
`Base: ${VALID_BASE}`,
|
|
214
157
|
`Head: ${VALID_HEAD}`,
|
|
215
158
|
`Artifact: output/bundle.js (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
|
|
@@ -219,34 +162,38 @@ function makeFixture(opts = {}) {
|
|
|
219
162
|
'```',
|
|
220
163
|
'',
|
|
221
164
|
].join('\n');
|
|
222
|
-
} else if (evidence === 'legacy-valid') {
|
|
223
|
-
evidenceBlock = `## Evidence\n\nVerification: PASS\nCommand: npm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\nnpm test — passed, exit code 0\n`;
|
|
224
165
|
} else if (evidence === 'failed') {
|
|
225
|
-
|
|
166
|
+
receiptBlock = '## Receipt\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
|
|
226
167
|
} else if (evidence === 'fence-only') {
|
|
227
|
-
|
|
168
|
+
receiptBlock = '## Receipt\n\n```\nnpm test\n```\n';
|
|
228
169
|
} else if (evidence === 'placeholder') {
|
|
229
|
-
|
|
230
|
-
'##
|
|
170
|
+
receiptBlock = [
|
|
171
|
+
'## Receipt',
|
|
231
172
|
'',
|
|
232
173
|
'Command: `{{TYPECHECK / TEST COMMAND}}`',
|
|
233
174
|
'Expected: {{What proves success}}',
|
|
234
175
|
'',
|
|
235
176
|
].join('\n');
|
|
236
177
|
} else if (evidence === 'missing-evidence') {
|
|
237
|
-
|
|
238
|
-
} else {
|
|
239
|
-
evidenceBlock = '';
|
|
178
|
+
receiptBlock = '## Risk Assessment\n\nNone.\n';
|
|
240
179
|
}
|
|
241
180
|
|
|
242
181
|
fs.writeFileSync(
|
|
243
|
-
path.join(
|
|
182
|
+
path.join(featureDir, TASK_REL),
|
|
244
183
|
[
|
|
245
|
-
|
|
184
|
+
'# Task 01: example',
|
|
246
185
|
'',
|
|
247
|
-
|
|
186
|
+
`Status: ${taskStatus}`,
|
|
248
187
|
'',
|
|
249
|
-
|
|
188
|
+
'## Dependencies',
|
|
189
|
+
'',
|
|
190
|
+
'- none',
|
|
191
|
+
'',
|
|
192
|
+
'## Verification Plan',
|
|
193
|
+
'',
|
|
194
|
+
'- Command: npm test',
|
|
195
|
+
'',
|
|
196
|
+
receiptBlock,
|
|
250
197
|
].join('\n'),
|
|
251
198
|
);
|
|
252
199
|
|
|
@@ -261,9 +208,7 @@ function makeFixture(opts = {}) {
|
|
|
261
208
|
if (evidence === 'valid') {
|
|
262
209
|
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
263
210
|
fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
|
|
264
|
-
|
|
265
|
-
if (evidence === 'valid' || evidence === 'legacy-valid') {
|
|
266
|
-
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
211
|
+
const taskFile = path.join(featureDir, TASK_REL);
|
|
267
212
|
fs.writeFileSync(taskFile, bindFixtureReceipt(dir, fs.readFileSync(taskFile, 'utf8')));
|
|
268
213
|
}
|
|
269
214
|
|
|
@@ -295,7 +240,7 @@ function installClaudeGate(root, policyMode = 'valid') {
|
|
|
295
240
|
path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
|
|
296
241
|
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
297
242
|
);
|
|
298
|
-
for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs'
|
|
243
|
+
for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs']) {
|
|
299
244
|
fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
|
|
300
245
|
}
|
|
301
246
|
if (policyMode === 'valid') {
|
|
@@ -401,31 +346,21 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
|
|
|
401
346
|
const dir = tmpDir();
|
|
402
347
|
const gate = installClaudeGate(dir);
|
|
403
348
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
349
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
350
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
404
351
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
405
352
|
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
406
353
|
fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
|
|
407
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
408
|
-
status: 'in_progress',
|
|
409
|
-
current_phase: 'closeout',
|
|
410
|
-
feature_name: FEATURE,
|
|
411
|
-
task_registry: {
|
|
412
|
-
[TASK_REL]: {
|
|
413
|
-
status: 'done',
|
|
414
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
415
|
-
artifacts: ['output/bundle.js'],
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
}));
|
|
419
354
|
fs.writeFileSync(path.join(featureDir, TASK_REL), [
|
|
420
|
-
'# Task', '', '
|
|
355
|
+
'# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
|
|
421
356
|
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
422
357
|
'Artifact: output/bundle.js', suffix,
|
|
358
|
+
'```', 'node --test', 'ok 1', '```',
|
|
423
359
|
].join('\n'));
|
|
424
360
|
fs.writeFileSync(
|
|
425
361
|
path.join(featureDir, TASK_REL),
|
|
426
362
|
bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
|
|
427
363
|
);
|
|
428
|
-
installFeatureReceipt(dir);
|
|
429
364
|
try {
|
|
430
365
|
const result = spawnSync(process.execPath, [gate], {
|
|
431
366
|
cwd: dir,
|
|
@@ -455,6 +390,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
455
390
|
for (const { artifactPath, symlink } of cases) {
|
|
456
391
|
const dir = tmpDir();
|
|
457
392
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
393
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
394
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
458
395
|
const artifactBytes = Buffer.from(`artifact-${artifactPath}\n`);
|
|
459
396
|
const digest = crypto.createHash('sha256').update(artifactBytes).digest('hex');
|
|
460
397
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
@@ -466,20 +403,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
466
403
|
} else {
|
|
467
404
|
fs.writeFileSync(path.join(dir, 'outside.js'), artifactBytes);
|
|
468
405
|
}
|
|
469
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
470
|
-
status: 'in_progress',
|
|
471
|
-
current_phase: 'closeout',
|
|
472
|
-
feature_name: FEATURE,
|
|
473
|
-
task_registry: {
|
|
474
|
-
[TASK_REL]: {
|
|
475
|
-
status: 'done',
|
|
476
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
477
|
-
artifacts: [artifactPath],
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
|
-
}));
|
|
481
406
|
fs.writeFileSync(path.join(featureDir, TASK_REL), bindFixtureReceipt(dir, [
|
|
482
|
-
'# Task', '', '
|
|
407
|
+
'# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
|
|
483
408
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
484
409
|
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
485
410
|
`Artifact: ${artifactPath} (sha256:${digest})`,
|
|
@@ -625,27 +550,6 @@ test('empty or malformed Claude hook payload fails closed', () => {
|
|
|
625
550
|
}
|
|
626
551
|
});
|
|
627
552
|
|
|
628
|
-
test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
|
|
629
|
-
const dir = makeFixture({
|
|
630
|
-
workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
631
|
-
});
|
|
632
|
-
try {
|
|
633
|
-
const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
|
|
634
|
-
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
635
|
-
spec.proofs = {
|
|
636
|
-
needsInspection: 'inspection completed',
|
|
637
|
-
needsIndependentAudit: 'PASS',
|
|
638
|
-
needsResearchGrounding: 'research completed',
|
|
639
|
-
};
|
|
640
|
-
fs.writeFileSync(specPath, JSON.stringify(spec));
|
|
641
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
642
|
-
assert.equal(body?.decision, 'block');
|
|
643
|
-
assert.match(body.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
|
|
644
|
-
} finally {
|
|
645
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
|
|
649
553
|
test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
|
|
650
554
|
const dir = makeFixture({ evidence: 'placeholder' });
|
|
651
555
|
try {
|
|
@@ -686,70 +590,20 @@ test('9. a code fence alone is not verification proof', () => {
|
|
|
686
590
|
}
|
|
687
591
|
});
|
|
688
592
|
|
|
689
|
-
test('10. completed_at must be a valid ISO timestamp', () => {
|
|
690
|
-
const dir = makeFixture({ completed_at: 'yesterday' });
|
|
691
|
-
try {
|
|
692
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
693
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
694
|
-
assert.ok(body);
|
|
695
|
-
assert.match(body.reason, /\bcompleted_at\b/);
|
|
696
|
-
} finally {
|
|
697
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
|
|
702
|
-
const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
|
|
703
|
-
try {
|
|
704
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
|
|
705
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
706
|
-
assert.strictEqual(readCache()[FEATURE][TASK_REL], 'pending');
|
|
707
|
-
|
|
708
|
-
const specFile = path.join(dir, 'specs', FEATURE, 'spec.json');
|
|
709
|
-
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
710
|
-
spec.task_registry[TASK_REL] = {
|
|
711
|
-
status: 'done',
|
|
712
|
-
completed_at: '2026-07-01T00:00:00Z',
|
|
713
|
-
};
|
|
714
|
-
spec.current_phase = 'closeout';
|
|
715
|
-
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
716
|
-
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
717
|
-
fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
|
|
718
|
-
|
|
719
|
-
assert.ok(parseBlock(runHook({}, dir).stdout), 're-completed failing task must be gated');
|
|
720
|
-
} finally {
|
|
721
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
test('12. legacy successful receipt remains read-compatible', () => {
|
|
726
|
-
const dir = makeFixture({ evidence: 'legacy-valid' });
|
|
727
|
-
try {
|
|
728
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
729
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
730
|
-
} finally {
|
|
731
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
|
|
735
593
|
test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
736
594
|
const dir = tmpDir();
|
|
737
595
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
596
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
597
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
738
598
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
739
599
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
740
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
741
|
-
status: 'in_progress',
|
|
742
|
-
feature_name: FEATURE,
|
|
743
|
-
current_phase: 'closeout',
|
|
744
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
745
|
-
}));
|
|
746
600
|
// Only Base, missing Head
|
|
747
601
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
|
|
748
602
|
'# Task',
|
|
749
603
|
'',
|
|
750
|
-
'
|
|
604
|
+
'Status: done',
|
|
751
605
|
'',
|
|
752
|
-
'##
|
|
606
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
753
607
|
'',
|
|
754
608
|
'Verification: PASS',
|
|
755
609
|
'Command: npm test',
|
|
@@ -773,21 +627,17 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
|
773
627
|
test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
774
628
|
const dir = tmpDir();
|
|
775
629
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
630
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
631
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
776
632
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
777
633
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
778
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
779
|
-
status: 'in_progress',
|
|
780
|
-
feature_name: FEATURE,
|
|
781
|
-
current_phase: 'closeout',
|
|
782
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
783
|
-
}));
|
|
784
634
|
// Only Head, missing Base
|
|
785
635
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), [
|
|
786
636
|
'# Task',
|
|
787
637
|
'',
|
|
788
|
-
'
|
|
638
|
+
'Status: done',
|
|
789
639
|
'',
|
|
790
|
-
'##
|
|
640
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
791
641
|
'',
|
|
792
642
|
'Verification: PASS',
|
|
793
643
|
'Command: npm test',
|
|
@@ -811,20 +661,16 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
|
811
661
|
test('15. provenance with both Base and Head passes', () => {
|
|
812
662
|
const dir = tmpDir();
|
|
813
663
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
664
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
665
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
814
666
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
815
667
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
816
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
817
|
-
status: 'in_progress',
|
|
818
|
-
feature_name: FEATURE,
|
|
819
|
-
current_phase: 'closeout',
|
|
820
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
821
|
-
}));
|
|
822
668
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
|
|
823
669
|
'# Task',
|
|
824
670
|
'',
|
|
825
|
-
'
|
|
671
|
+
'Status: done',
|
|
826
672
|
'',
|
|
827
|
-
'##
|
|
673
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
828
674
|
'',
|
|
829
675
|
'Verification: PASS',
|
|
830
676
|
'Command: npm test',
|
|
@@ -844,57 +690,6 @@ test('15. provenance with both Base and Head passes', () => {
|
|
|
844
690
|
}
|
|
845
691
|
});
|
|
846
692
|
|
|
847
|
-
test('16. registry task path with ../ escape or sibling-prefix is rejected (check a)', () => {
|
|
848
|
-
const dir = tmpDir();
|
|
849
|
-
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
850
|
-
const evilFeature = path.join(dir, 'specs', `${FEATURE}-evil`);
|
|
851
|
-
const maliciousRel = `../${FEATURE}-evil/tasks/task.md`;
|
|
852
|
-
const maliciousAbs = path.join(evilFeature, 'tasks/task.md');
|
|
853
|
-
// Prepare evil file with valid receipt outside feature (should be ignored due to path traversal)
|
|
854
|
-
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
855
|
-
fs.mkdirSync(path.join(evilFeature, 'tasks'), { recursive: true });
|
|
856
|
-
fs.writeFileSync(maliciousAbs, [
|
|
857
|
-
'# Task',
|
|
858
|
-
'',
|
|
859
|
-
'**Status:** done',
|
|
860
|
-
'',
|
|
861
|
-
'## Evidence',
|
|
862
|
-
'',
|
|
863
|
-
'Verification: PASS',
|
|
864
|
-
'Command: npm test',
|
|
865
|
-
'Exit: 0',
|
|
866
|
-
`Base: ${VALID_BASE}`,
|
|
867
|
-
`Head: ${VALID_HEAD}`,
|
|
868
|
-
'```',
|
|
869
|
-
'pass',
|
|
870
|
-
'```',
|
|
871
|
-
].join('\n'));
|
|
872
|
-
// Also create sibling-prefix style path: tasks/../../demo-evil/tasks/task.md
|
|
873
|
-
const siblingPrefixRel = `tasks/../../${FEATURE}-evil/tasks/task.md`;
|
|
874
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
875
|
-
status: 'in_progress',
|
|
876
|
-
feature_name: FEATURE,
|
|
877
|
-
current_phase: 'closeout',
|
|
878
|
-
task_registry: {
|
|
879
|
-
[maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
880
|
-
[siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
881
|
-
[path.resolve(featureDir, maliciousRel)]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
882
|
-
},
|
|
883
|
-
}));
|
|
884
|
-
try {
|
|
885
|
-
seedCache({ [FEATURE]: { [maliciousRel]: 'pending', [siblingPrefixRel]: 'pending', [path.resolve(featureDir, maliciousRel)]: 'pending' } });
|
|
886
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
887
|
-
assert.ok(body, 'traversal path should block');
|
|
888
|
-
assert.strictEqual(body.decision, 'block');
|
|
889
|
-
// Must fail check a (file + Status)
|
|
890
|
-
assert.match(body.reason, /\ba\b/);
|
|
891
|
-
// Ensure malicious path is named in reason
|
|
892
|
-
assert.ok(body.reason.includes(maliciousRel) || body.reason.includes(siblingPrefixRel));
|
|
893
|
-
} finally {
|
|
894
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
|
|
898
693
|
test('17. empty or bare provenance fails — Base:/Head: require non-empty same-line values', () => {
|
|
899
694
|
const cases = [
|
|
900
695
|
{ evidence: ['Verification: PASS','Command: npm test','Exit: 0','Base:','Head: def456'].join('\n'), desc: 'empty Base:' },
|
|
@@ -908,14 +703,10 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
|
|
|
908
703
|
for (const { evidence, desc } of cases) {
|
|
909
704
|
const dir = tmpDir();
|
|
910
705
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
706
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
707
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
911
708
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
912
|
-
fs.writeFileSync(path.join(
|
|
913
|
-
status: 'in_progress',
|
|
914
|
-
feature_name: FEATURE,
|
|
915
|
-
current_phase: 'closeout',
|
|
916
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
917
|
-
}));
|
|
918
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
709
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
919
710
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
920
711
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
921
712
|
assert.ok(body, `empty/bare provenance should block for ${desc}`);
|
|
@@ -934,15 +725,17 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
|
|
|
934
725
|
for (const { evidence, shouldPass } of validCases) {
|
|
935
726
|
const dir = tmpDir();
|
|
936
727
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
728
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
729
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
937
730
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
fs.writeFileSync(
|
|
731
|
+
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
732
|
+
const taskText = (receipt) => ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + receipt,'```','pass','```'].join('\n');
|
|
733
|
+
// Base and Head bind to the live tree only for a committed task file that has since
|
|
734
|
+
// changed, so every case commits the receipt and then edits it: a pass case rebinds
|
|
735
|
+
// to the runtime-derived pair, a fail case keeps its arbitrary values.
|
|
736
|
+
fs.writeFileSync(taskFile, taskText(evidence));
|
|
737
|
+
commitAll(dir, 'receipt');
|
|
738
|
+
fs.writeFileSync(taskFile, shouldPass ? taskText(bindFixtureReceipt(dir, evidence)) : `${taskText(evidence)}\n`);
|
|
946
739
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
947
740
|
const result = runHook({}, dir);
|
|
948
741
|
if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
|
|
@@ -979,8 +772,8 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
|
|
|
979
772
|
// mutate: strip Verification: PASS and Command — keep file as done
|
|
980
773
|
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
981
774
|
fs.writeFileSync(taskFile, [
|
|
982
|
-
'# Task', '', '
|
|
983
|
-
'##
|
|
775
|
+
'# Task', '', 'Status: done', '',
|
|
776
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '',
|
|
984
777
|
'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
985
778
|
'```', 'pass', '```',
|
|
986
779
|
].join('\n'));
|
|
@@ -1004,8 +797,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1004
797
|
// mutate: remove Head, leave only Base
|
|
1005
798
|
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
1006
799
|
fs.writeFileSync(taskFile, [
|
|
1007
|
-
'# Task', '', '
|
|
1008
|
-
'##
|
|
800
|
+
'# Task', '', 'Status: done', '',
|
|
801
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
|
|
1009
802
|
'```', 'pass', '```',
|
|
1010
803
|
].join('\n'));
|
|
1011
804
|
let body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -1013,8 +806,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1013
806
|
assert.match(body.reason, /\bprovenance\b/);
|
|
1014
807
|
// mutate back to include both but with empty Head value (stale)
|
|
1015
808
|
fs.writeFileSync(taskFile, [
|
|
1016
|
-
'# Task', '', '
|
|
1017
|
-
'##
|
|
809
|
+
'# Task', '', 'Status: done', '',
|
|
810
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
|
|
1018
811
|
'```', 'pass', '```',
|
|
1019
812
|
].join('\n'));
|
|
1020
813
|
body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -1025,22 +818,6 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1025
818
|
}
|
|
1026
819
|
});
|
|
1027
820
|
|
|
1028
|
-
test('22. cache-hit: deleted task file blocks even though spec still says done', () => {
|
|
1029
|
-
const dir = makeFixture({ evidence: 'valid' });
|
|
1030
|
-
try {
|
|
1031
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1032
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1033
|
-
assert.strictEqual(readCache()[FEATURE][TASK_REL], 'done');
|
|
1034
|
-
fs.unlinkSync(path.join(dir, 'specs', FEATURE, TASK_REL));
|
|
1035
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
1036
|
-
assert.ok(body, 'deleted receipt file should block on cache-hit');
|
|
1037
|
-
assert.strictEqual(body.decision, 'block');
|
|
1038
|
-
assert.match(body.reason, /\ba\b/);
|
|
1039
|
-
} finally {
|
|
1040
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
821
|
test('23. malformed cache JSON does not bypass validation', () => {
|
|
1045
822
|
// valid receipt with malformed cache → must still PASS (fail-open on cache parse, not on receipt)
|
|
1046
823
|
const dirValid = makeFixture({ evidence: 'valid' });
|
|
@@ -1093,9 +870,10 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
|
|
|
1093
870
|
for (const { evidence, check, desc } of cases) {
|
|
1094
871
|
const dir = tmpDir();
|
|
1095
872
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
873
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
874
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1096
875
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1097
|
-
fs.writeFileSync(path.join(
|
|
1098
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
876
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1099
877
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1100
878
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1101
879
|
assert.ok(body, `placeholder case ${desc} should block`);
|
|
@@ -1107,8 +885,8 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
|
|
|
1107
885
|
const dir2 = tmpDir();
|
|
1108
886
|
const featureDir2 = path.join(dir2, 'specs', FEATURE);
|
|
1109
887
|
fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
|
|
1110
|
-
fs.writeFileSync(path.join(featureDir2, '
|
|
1111
|
-
fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','
|
|
888
|
+
fs.writeFileSync(path.join(featureDir2, 'plan.md'), '# Demo plan\n');
|
|
889
|
+
fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm run todo:test', '', '## Receipt','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`,'```','pass','```'].join('\n')));
|
|
1112
890
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1113
891
|
assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
|
|
1114
892
|
fs.rmSync(dir2, { recursive: true, force: true });
|
|
@@ -1123,9 +901,10 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
|
|
|
1123
901
|
for (const { evidence, desc } of cases) {
|
|
1124
902
|
const dir = tmpDir();
|
|
1125
903
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
904
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
905
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1126
906
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1127
|
-
fs.writeFileSync(path.join(
|
|
1128
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
907
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: pnpm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1129
908
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1130
909
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1131
910
|
assert.ok(body, `explicit failure ${desc} should block`);
|
|
@@ -1137,8 +916,9 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
|
|
|
1137
916
|
test('27. task markdown symlink outside feature must be rejected (check a)', () => {
|
|
1138
917
|
const dir = tmpDir();
|
|
1139
918
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
919
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
920
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1140
921
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1141
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
|
|
1142
922
|
const outside = path.join(dir, 'outside.md');
|
|
1143
923
|
fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
|
|
1144
924
|
const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
@@ -1148,23 +928,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
|
|
|
1148
928
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1149
929
|
assert.ok(body, 'symlink task outside should block');
|
|
1150
930
|
assert.strictEqual(body.decision, 'block');
|
|
1151
|
-
assert.match(body.reason,
|
|
1152
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
test('28. malformed spec.json must block with invalid_specs', () => {
|
|
1156
|
-
const dir = tmpDir();
|
|
1157
|
-
fs.mkdirSync(path.join(dir, 'specs', 'good'), { recursive: true });
|
|
1158
|
-
fs.mkdirSync(path.join(dir, 'specs', 'bad'), { recursive: true });
|
|
1159
|
-
fs.writeFileSync(path.join(dir, 'specs', 'good', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'good', task_registry: {} }));
|
|
1160
|
-
fs.writeFileSync(path.join(dir, 'specs', 'bad', 'spec.json'), '{ malformed');
|
|
1161
|
-
seedCache({});
|
|
1162
|
-
const { stdout } = runHook({}, dir);
|
|
1163
|
-
const body = parseBlock(stdout);
|
|
1164
|
-
assert.ok(body, 'malformed spec should block');
|
|
1165
|
-
assert.strictEqual(body.decision, 'block');
|
|
1166
|
-
assert.match(body.reason, /invalid spec/i);
|
|
1167
|
-
assert.match(body.reason, /bad/);
|
|
931
|
+
assert.match(body.reason, /invalid workflow packet/);
|
|
1168
932
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1169
933
|
});
|
|
1170
934
|
|
|
@@ -1174,7 +938,6 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
|
|
|
1174
938
|
fs.mkdirSync(specsDir, { recursive: true });
|
|
1175
939
|
const outside = path.join(dir, 'outside-feat');
|
|
1176
940
|
fs.mkdirSync(outside, { recursive: true });
|
|
1177
|
-
fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1178
941
|
const link = path.join(specsDir, 'linked');
|
|
1179
942
|
fs.symlinkSync(outside, link);
|
|
1180
943
|
// non-explicit should block with invalid_specs (fail-closed, not phantom no-active)
|
|
@@ -1182,7 +945,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
|
|
|
1182
945
|
const nonExplicit = parseBlock(runHook({}, dir).stdout);
|
|
1183
946
|
assert.ok(nonExplicit, 'non-explicit symlink outside should block with invalid_specs');
|
|
1184
947
|
assert.strictEqual(nonExplicit.decision, 'block');
|
|
1185
|
-
assert.match(nonExplicit.reason, /invalid
|
|
948
|
+
assert.match(nonExplicit.reason, /invalid workflow packet/i);
|
|
1186
949
|
assert.match(nonExplicit.reason, /linked/);
|
|
1187
950
|
// explicit should block explicit_malformed
|
|
1188
951
|
const body = parseBlock(runHook({ featureName: 'linked' }, dir).stdout);
|
|
@@ -1224,15 +987,11 @@ test('30. phantom vectors via gate evidence must block with verification_state (
|
|
|
1224
987
|
for (const v of vectors) {
|
|
1225
988
|
const dir = tmpDir();
|
|
1226
989
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
990
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
991
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1227
992
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1228
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
1229
|
-
status: 'in_progress',
|
|
1230
|
-
feature_name: FEATURE,
|
|
1231
|
-
current_phase: 'closeout',
|
|
1232
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1233
|
-
}));
|
|
1234
993
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|
|
1235
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','
|
|
994
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1236
995
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1237
996
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1238
997
|
assert.ok(body, `vector should block: ${v}`);
|
|
@@ -1271,14 +1030,9 @@ test('30. phantom vectors via gate evidence must block with verification_state (
|
|
|
1271
1030
|
const dir = tmpDir();
|
|
1272
1031
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1273
1032
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1274
|
-
fs.writeFileSync(path.join(featureDir, '
|
|
1275
|
-
status: 'in_progress',
|
|
1276
|
-
feature_name: FEATURE,
|
|
1277
|
-
current_phase: 'closeout',
|
|
1278
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1279
|
-
}));
|
|
1033
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1280
1034
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|
|
1281
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','
|
|
1035
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1282
1036
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1283
1037
|
const { stdout } = runHook({}, dir);
|
|
1284
1038
|
assert.strictEqual(stdout, '', `positive control should not block: ${v}`);
|
|
@@ -2026,3 +1780,89 @@ test('53. an indented or sub-heading Command never becomes the canonical one', (
|
|
|
2026
1780
|
}
|
|
2027
1781
|
}
|
|
2028
1782
|
});
|
|
1783
|
+
|
|
1784
|
+
function plantLegacyPacket(dir, name = 'old') {
|
|
1785
|
+
const legacyDir = path.join(dir, 'specs', name);
|
|
1786
|
+
fs.mkdirSync(path.join(legacyDir, 'tasks'), { recursive: true });
|
|
1787
|
+
fs.writeFileSync(path.join(legacyDir, 'spec.json'), JSON.stringify({
|
|
1788
|
+
feature_name: name,
|
|
1789
|
+
status: 'in_progress',
|
|
1790
|
+
task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
|
|
1791
|
+
}));
|
|
1792
|
+
fs.writeFileSync(path.join(legacyDir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function writeActiveFeature(dir, featureName) {
|
|
1796
|
+
fs.mkdirSync(path.join(dir, 'specs', '_shared'), { recursive: true });
|
|
1797
|
+
fs.writeFileSync(path.join(dir, 'specs', '_shared', 'active-feature.json'), JSON.stringify({ featureName }));
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
test('a leftover legacy packet alone never blocks Stop', () => {
|
|
1801
|
+
const dir = tmpDir();
|
|
1802
|
+
try {
|
|
1803
|
+
plantLegacyPacket(dir);
|
|
1804
|
+
clearCache();
|
|
1805
|
+
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1806
|
+
} finally {
|
|
1807
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
test('a recorded active feature naming a legacy packet does not block Stop', () => {
|
|
1812
|
+
const dir = makeFixture({ evidence: 'valid' });
|
|
1813
|
+
try {
|
|
1814
|
+
plantLegacyPacket(dir);
|
|
1815
|
+
writeActiveFeature(dir, 'old');
|
|
1816
|
+
clearCache();
|
|
1817
|
+
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1818
|
+
} finally {
|
|
1819
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
test('a recorded active feature naming a missing packet still blocks as explicit_not_found', () => {
|
|
1824
|
+
const dir = makeFixture({ evidence: 'valid' });
|
|
1825
|
+
try {
|
|
1826
|
+
writeActiveFeature(dir, 'nope');
|
|
1827
|
+
clearCache();
|
|
1828
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1829
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1830
|
+
assert.match(body.reason, /explicit_not_found/);
|
|
1831
|
+
} finally {
|
|
1832
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
test('a process-first packet with a dependency cycle and a done task blocks Stop', () => {
|
|
1837
|
+
const dir = tmpDir();
|
|
1838
|
+
try {
|
|
1839
|
+
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1840
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
1841
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
1842
|
+
const task = (status, dependency) => [
|
|
1843
|
+
'# Task', '', `Status: ${status}`, '', '## Dependencies', '', `- ${dependency}`, '',
|
|
1844
|
+
'## Verification Plan', '', '- Command: npm test', '',
|
|
1845
|
+
].join('\n');
|
|
1846
|
+
fs.writeFileSync(path.join(featureDir, 'task-01-a.md'), task('done', 'task-02-b.md'));
|
|
1847
|
+
fs.writeFileSync(path.join(featureDir, 'task-02-b.md'), task('pending', 'task-01-a.md'));
|
|
1848
|
+
clearCache();
|
|
1849
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1850
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1851
|
+
assert.match(body.reason, /invalid workflow packet/);
|
|
1852
|
+
} finally {
|
|
1853
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
test('a packet holding both plan.md and spec.json is read as process-first and its unreceipted done task blocks', () => {
|
|
1858
|
+
const dir = makeFixture({ evidence: 'missing-evidence' });
|
|
1859
|
+
try {
|
|
1860
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'spec.json'), JSON.stringify({ feature_name: FEATURE }));
|
|
1861
|
+
clearCache();
|
|
1862
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1863
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1864
|
+
assert.match(body.reason, new RegExp(TASK_REL));
|
|
1865
|
+
} finally {
|
|
1866
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1867
|
+
}
|
|
1868
|
+
});
|