@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5
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/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +705 -13
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +99 -28
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +214 -262
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +35 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +61 -38
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -26,6 +26,12 @@ const FIXTURE_ARTIFACT_DIGEST = crypto.createHash('sha256').update(FIXTURE_ARTIF
|
|
|
26
26
|
|
|
27
27
|
function runHook(payload, cwd) {
|
|
28
28
|
const projectRoot = cwd || payload.cwd;
|
|
29
|
+
const specsRoot = path.join(projectRoot, 'specs');
|
|
30
|
+
if (fs.existsSync(specsRoot)) {
|
|
31
|
+
for (const featureName of fs.readdirSync(specsRoot)) {
|
|
32
|
+
installFeatureReceipt(projectRoot, featureName, payload.session_id || 'test');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
// PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
|
|
30
36
|
// monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
|
|
31
37
|
const env = { ...process.env, PROJECT_ROOT: projectRoot };
|
|
@@ -72,6 +78,38 @@ function bindFixtureReceipt(dir, value) {
|
|
|
72
78
|
return value.replaceAll(VALID_BASE, context.base).replaceAll(VALID_HEAD, context.head);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
|
|
82
|
+
const featureDir = path.join(dir, 'specs', featureName);
|
|
83
|
+
const specFile = path.join(featureDir, 'spec.json');
|
|
84
|
+
if (!fs.existsSync(specFile)) return;
|
|
85
|
+
let spec;
|
|
86
|
+
try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { return; }
|
|
87
|
+
const lifecyclePhase = spec.current_phase || spec.phase;
|
|
88
|
+
const explicitCloseout = ['completed', 'complete'].includes(spec.status)
|
|
89
|
+
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
90
|
+
if (!explicitCloseout) return;
|
|
91
|
+
const tasks = Object.values(spec.task_registry || {});
|
|
92
|
+
if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) return;
|
|
93
|
+
const context = PROVENANCE.deriveRuntimeContext({
|
|
94
|
+
projectRoot: dir,
|
|
95
|
+
specsRoot: path.join(dir, 'specs'),
|
|
96
|
+
specFile,
|
|
97
|
+
featureName,
|
|
98
|
+
runtimeSession: session,
|
|
99
|
+
});
|
|
100
|
+
fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
|
|
101
|
+
`Feature: ${featureName}`,
|
|
102
|
+
'Expected: final integration verification passes',
|
|
103
|
+
'Observed: final integration verification passed',
|
|
104
|
+
'Verification: PASS',
|
|
105
|
+
'Command: node --test',
|
|
106
|
+
'Exit: 0',
|
|
107
|
+
`Base: ${context.base}`,
|
|
108
|
+
`Head: ${context.head}`,
|
|
109
|
+
'',
|
|
110
|
+
].join('\n'));
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
function clearCache() {
|
|
76
114
|
try { fs.unlinkSync(CACHE); } catch { /* absent */ }
|
|
77
115
|
}
|
|
@@ -94,6 +132,7 @@ function readCache() {
|
|
|
94
132
|
* @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
|
|
95
133
|
* @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
|
|
96
134
|
* @param {object|null} [opts.workflowPolicy] — if set, persist completion obligations
|
|
135
|
+
* @param {string} [opts.phase='closeout'] — persisted lifecycle boundary
|
|
97
136
|
*/
|
|
98
137
|
function makeFixture(opts = {}) {
|
|
99
138
|
const {
|
|
@@ -103,6 +142,7 @@ function makeFixture(opts = {}) {
|
|
|
103
142
|
evidence = 'valid',
|
|
104
143
|
runtime = null,
|
|
105
144
|
workflowPolicy = null,
|
|
145
|
+
phase = 'closeout',
|
|
106
146
|
} = opts;
|
|
107
147
|
const dir = tmpDir();
|
|
108
148
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
@@ -115,7 +155,7 @@ function makeFixture(opts = {}) {
|
|
|
115
155
|
const spec = {
|
|
116
156
|
status: 'in_progress',
|
|
117
157
|
feature_name: FEATURE,
|
|
118
|
-
current_phase:
|
|
158
|
+
current_phase: phase,
|
|
119
159
|
task_registry: { [TASK_REL]: entry },
|
|
120
160
|
};
|
|
121
161
|
if (workflowPolicy) spec.workflow_policy = workflowPolicy;
|
|
@@ -208,7 +248,12 @@ function installClaudeGate(root, policyMode = 'valid') {
|
|
|
208
248
|
fs.mkdirSync(hooks, { recursive: true });
|
|
209
249
|
fs.mkdirSync(scripts, { recursive: true });
|
|
210
250
|
fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
|
|
211
|
-
|
|
251
|
+
for (const name of [
|
|
252
|
+
'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
|
|
253
|
+
]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
|
|
254
|
+
for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs', 'validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
|
|
255
|
+
fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
|
|
256
|
+
}
|
|
212
257
|
if (policyMode === 'valid') {
|
|
213
258
|
fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'), path.join(scripts, 'workflow-policy.cjs'));
|
|
214
259
|
fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'), path.join(scripts, 'provenance.cjs'));
|
|
@@ -317,6 +362,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
|
|
|
317
362
|
fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
|
|
318
363
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
319
364
|
status: 'in_progress',
|
|
365
|
+
current_phase: 'closeout',
|
|
320
366
|
feature_name: FEATURE,
|
|
321
367
|
task_registry: {
|
|
322
368
|
[TASK_REL]: {
|
|
@@ -335,6 +381,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
|
|
|
335
381
|
path.join(featureDir, TASK_REL),
|
|
336
382
|
bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
|
|
337
383
|
);
|
|
384
|
+
installFeatureReceipt(dir);
|
|
338
385
|
try {
|
|
339
386
|
const result = spawnSync(process.execPath, [gate], {
|
|
340
387
|
cwd: dir,
|
|
@@ -346,7 +393,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
|
|
|
346
393
|
if (expected) {
|
|
347
394
|
const block = parseBlock(result.stdout);
|
|
348
395
|
assert.equal(block?.decision, 'block', `${label} must block`);
|
|
349
|
-
assert.match(block.reason, /\
|
|
396
|
+
assert.match(block.reason, /\bartifact_hash\b/);
|
|
350
397
|
} else {
|
|
351
398
|
assert.equal(result.stdout, '', `${label} must pass`);
|
|
352
399
|
}
|
|
@@ -377,6 +424,7 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
377
424
|
}
|
|
378
425
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
379
426
|
status: 'in_progress',
|
|
427
|
+
current_phase: 'closeout',
|
|
380
428
|
feature_name: FEATURE,
|
|
381
429
|
task_registry: {
|
|
382
430
|
[TASK_REL]: {
|
|
@@ -395,7 +443,7 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
395
443
|
try {
|
|
396
444
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
397
445
|
assert.equal(body?.decision, 'block', `${artifactPath} must block`);
|
|
398
|
-
assert.match(body.reason, /\
|
|
446
|
+
assert.match(body.reason, /\bartifact_hash\b/);
|
|
399
447
|
} finally {
|
|
400
448
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
401
449
|
}
|
|
@@ -442,6 +490,32 @@ test('4. stop_hook_active: true → exit 0 silent even with violating task', ()
|
|
|
442
490
|
}
|
|
443
491
|
});
|
|
444
492
|
|
|
493
|
+
test('4b. done task before explicit closeout without receipt blocks', () => {
|
|
494
|
+
const dir = makeFixture({ evidence: 'missing-evidence', phase: 'implementation' });
|
|
495
|
+
try {
|
|
496
|
+
const { code, stdout } = runHook({}, dir);
|
|
497
|
+
assert.strictEqual(code, 0);
|
|
498
|
+
const body = parseBlock(stdout);
|
|
499
|
+
assert.strictEqual(body?.decision, 'block');
|
|
500
|
+
assert.match(body.reason, new RegExp(TASK_REL));
|
|
501
|
+
assert.strictEqual(fs.existsSync(path.join(dir, 'specs', FEATURE, 'feature-receipt.md')), false);
|
|
502
|
+
} finally {
|
|
503
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('4c. done task before explicit closeout with valid task receipt is silent', () => {
|
|
508
|
+
const dir = makeFixture({ evidence: 'valid', phase: 'implementation' });
|
|
509
|
+
try {
|
|
510
|
+
const { code, stdout } = runHook({}, dir);
|
|
511
|
+
assert.strictEqual(code, 0);
|
|
512
|
+
assert.strictEqual(stdout, '');
|
|
513
|
+
assert.strictEqual(fs.existsSync(path.join(dir, 'specs', FEATURE, 'feature-receipt.md')), false);
|
|
514
|
+
} finally {
|
|
515
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
445
519
|
test('5. first run (no cache) with done-without-receipt → block (cache is not truth)', () => {
|
|
446
520
|
const dir = makeFixture({ evidence: 'missing-evidence' });
|
|
447
521
|
try {
|
|
@@ -507,9 +581,9 @@ test('empty or malformed Claude hook payload fails closed', () => {
|
|
|
507
581
|
}
|
|
508
582
|
});
|
|
509
583
|
|
|
510
|
-
test('
|
|
584
|
+
test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
|
|
511
585
|
const dir = makeFixture({
|
|
512
|
-
workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } }),
|
|
586
|
+
workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
513
587
|
});
|
|
514
588
|
try {
|
|
515
589
|
const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
|
|
@@ -528,7 +602,7 @@ test('Critical completion ignores worker-writable proof strings and remains bloc
|
|
|
528
602
|
}
|
|
529
603
|
});
|
|
530
604
|
|
|
531
|
-
test('7. Evidence with {{...}} placeholder → blocked (
|
|
605
|
+
test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
|
|
532
606
|
const dir = makeFixture({ evidence: 'placeholder' });
|
|
533
607
|
try {
|
|
534
608
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
@@ -538,8 +612,8 @@ test('7. Evidence with {{...}} placeholder → blocked (check c)', () => {
|
|
|
538
612
|
assert.ok(body, 'expected block JSON on stdout');
|
|
539
613
|
assert.strictEqual(body.decision, 'block');
|
|
540
614
|
assert.ok(
|
|
541
|
-
body.reason.includes(TASK_REL) && /\
|
|
542
|
-
`reason must cite task path and
|
|
615
|
+
body.reason.includes(TASK_REL) && /\bplaceholder\b/.test(body.reason),
|
|
616
|
+
`reason must cite task path and placeholder; got: ${body.reason}`,
|
|
543
617
|
);
|
|
544
618
|
} finally {
|
|
545
619
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -552,7 +626,7 @@ test('8. explicit failure cannot pass even when PASS text is present', () => {
|
|
|
552
626
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
553
627
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
554
628
|
assert.ok(body);
|
|
555
|
-
assert.match(body.reason, /\
|
|
629
|
+
assert.match(body.reason, /\bverification_state\b/);
|
|
556
630
|
} finally {
|
|
557
631
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
558
632
|
}
|
|
@@ -574,14 +648,14 @@ test('10. completed_at must be a valid ISO timestamp', () => {
|
|
|
574
648
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
575
649
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
576
650
|
assert.ok(body);
|
|
577
|
-
assert.match(body.reason, /\
|
|
651
|
+
assert.match(body.reason, /\bcompleted_at\b/);
|
|
578
652
|
} finally {
|
|
579
653
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
580
654
|
}
|
|
581
655
|
});
|
|
582
656
|
|
|
583
|
-
test('11. done → pending cache transition is persisted and
|
|
584
|
-
const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending' });
|
|
657
|
+
test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
|
|
658
|
+
const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
|
|
585
659
|
try {
|
|
586
660
|
seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
|
|
587
661
|
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
@@ -593,6 +667,7 @@ test('11. done → pending cache transition is persisted and can be gated again'
|
|
|
593
667
|
status: 'done',
|
|
594
668
|
completed_at: '2026-07-01T00:00:00Z',
|
|
595
669
|
};
|
|
670
|
+
spec.current_phase = 'closeout';
|
|
596
671
|
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
597
672
|
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
598
673
|
fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
|
|
@@ -621,7 +696,7 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
|
621
696
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
622
697
|
status: 'in_progress',
|
|
623
698
|
feature_name: FEATURE,
|
|
624
|
-
current_phase: '
|
|
699
|
+
current_phase: 'closeout',
|
|
625
700
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
626
701
|
}));
|
|
627
702
|
// Only Base, missing Head
|
|
@@ -645,7 +720,7 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
|
645
720
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
646
721
|
assert.ok(body, 'only Base should block');
|
|
647
722
|
assert.strictEqual(body.decision, 'block');
|
|
648
|
-
assert.match(body.reason, /\
|
|
723
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
649
724
|
} finally {
|
|
650
725
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
651
726
|
}
|
|
@@ -659,7 +734,7 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
|
659
734
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
660
735
|
status: 'in_progress',
|
|
661
736
|
feature_name: FEATURE,
|
|
662
|
-
current_phase: '
|
|
737
|
+
current_phase: 'closeout',
|
|
663
738
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
664
739
|
}));
|
|
665
740
|
// Only Head, missing Base
|
|
@@ -683,7 +758,7 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
|
683
758
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
684
759
|
assert.ok(body, 'only Head should block');
|
|
685
760
|
assert.strictEqual(body.decision, 'block');
|
|
686
|
-
assert.match(body.reason, /\
|
|
761
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
687
762
|
} finally {
|
|
688
763
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
689
764
|
}
|
|
@@ -697,7 +772,7 @@ test('15. provenance with both Base and Head passes', () => {
|
|
|
697
772
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
698
773
|
status: 'in_progress',
|
|
699
774
|
feature_name: FEATURE,
|
|
700
|
-
current_phase: '
|
|
775
|
+
current_phase: 'closeout',
|
|
701
776
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
702
777
|
}));
|
|
703
778
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
|
|
@@ -755,7 +830,7 @@ test('16. registry task path with ../ escape or sibling-prefix is rejected (chec
|
|
|
755
830
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
756
831
|
status: 'in_progress',
|
|
757
832
|
feature_name: FEATURE,
|
|
758
|
-
current_phase: '
|
|
833
|
+
current_phase: 'closeout',
|
|
759
834
|
task_registry: {
|
|
760
835
|
[maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
761
836
|
[siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
@@ -793,7 +868,7 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
|
|
|
793
868
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
794
869
|
status: 'in_progress',
|
|
795
870
|
feature_name: FEATURE,
|
|
796
|
-
current_phase: '
|
|
871
|
+
current_phase: 'closeout',
|
|
797
872
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
798
873
|
}));
|
|
799
874
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
@@ -801,7 +876,7 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
|
|
|
801
876
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
802
877
|
assert.ok(body, `empty/bare provenance should block for ${desc}`);
|
|
803
878
|
assert.strictEqual(body.decision, 'block', `empty/bare should block: ${desc}`);
|
|
804
|
-
assert.match(body.reason, /\
|
|
879
|
+
assert.match(body.reason, /\bprovenance\b/, `should fail provenance for ${desc}`);
|
|
805
880
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
806
881
|
}
|
|
807
882
|
});
|
|
@@ -819,7 +894,7 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
|
|
|
819
894
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
820
895
|
status: 'in_progress',
|
|
821
896
|
feature_name: FEATURE,
|
|
822
|
-
current_phase: '
|
|
897
|
+
current_phase: 'closeout',
|
|
823
898
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
824
899
|
}));
|
|
825
900
|
const boundEvidence = shouldPass ? bindFixtureReceipt(dir, evidence) : evidence;
|
|
@@ -827,7 +902,7 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
|
|
|
827
902
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
828
903
|
const result = runHook({}, dir);
|
|
829
904
|
if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
|
|
830
|
-
else assert.match(parseBlock(result.stdout).reason, /\
|
|
905
|
+
else assert.match(parseBlock(result.stdout).reason, /\bprovenance\b/, 'arbitrary valid-length SHA values must block');
|
|
831
906
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
832
907
|
}
|
|
833
908
|
});
|
|
@@ -868,7 +943,7 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
|
|
|
868
943
|
const blocked = parseBlock(runHook({}, dir).stdout);
|
|
869
944
|
assert.ok(blocked, 'mutated receipt missing Verification/Command should block on cache-hit');
|
|
870
945
|
assert.strictEqual(blocked.decision, 'block');
|
|
871
|
-
assert.match(blocked.reason, /\
|
|
946
|
+
assert.match(blocked.reason, /\bverification_state\b|\bcommand\b/);
|
|
872
947
|
// cache must not have been promoted to still-valid done; next run must still block
|
|
873
948
|
const secondBlocked = parseBlock(runHook({}, dir).stdout);
|
|
874
949
|
assert.ok(secondBlocked, 'second hit after mutation must still block');
|
|
@@ -891,7 +966,7 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
891
966
|
].join('\n'));
|
|
892
967
|
let body = parseBlock(runHook({}, dir).stdout);
|
|
893
968
|
assert.ok(body, 'missing Head provenance should block on cache-hit');
|
|
894
|
-
assert.match(body.reason, /\
|
|
969
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
895
970
|
// mutate back to include both but with empty Head value (stale)
|
|
896
971
|
fs.writeFileSync(taskFile, [
|
|
897
972
|
'# Task', '', '**Status:** done', '',
|
|
@@ -900,7 +975,7 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
900
975
|
].join('\n'));
|
|
901
976
|
body = parseBlock(runHook({}, dir).stdout);
|
|
902
977
|
assert.ok(body, 'empty Head should block as stale provenance');
|
|
903
|
-
assert.match(body.reason, /\
|
|
978
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
904
979
|
} finally {
|
|
905
980
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
906
981
|
}
|
|
@@ -964,18 +1039,18 @@ test('24. first-run (no cache file) with valid receipt passes and seeds cache',
|
|
|
964
1039
|
|
|
965
1040
|
test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty must block', () => {
|
|
966
1041
|
const cases = [
|
|
967
|
-
{ evidence: ['Verification: PASS','Command: TODO','Exit: 0','Base: a','Head: b'].join('\n'), check:
|
|
968
|
-
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: TBD','Head: b'].join('\n'), check:
|
|
969
|
-
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: N/A'].join('\n'), check:
|
|
970
|
-
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','base_sha: pending','head_sha: unknown'].join('\n'), check:
|
|
971
|
-
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: '].join('\n'), check:
|
|
972
|
-
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: TBD'].join('\n'), check:
|
|
1042
|
+
{ evidence: ['Verification: PASS','Command: TODO','Exit: 0','Base: a','Head: b'].join('\n'), check: /\bcommand\b/, desc: 'Command TODO' },
|
|
1043
|
+
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: TBD','Head: b'].join('\n'), check: /\bprovenance\b/, desc: 'Base TBD' },
|
|
1044
|
+
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: N/A'].join('\n'), check: /\bprovenance\b/, desc: 'Head N/A' },
|
|
1045
|
+
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','base_sha: pending','head_sha: unknown'].join('\n'), check: /\bprovenance\b/, desc: 'base_sha pending' },
|
|
1046
|
+
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: '].join('\n'), check: /\bartifact_hash\b/, desc: 'artifact empty sha' },
|
|
1047
|
+
{ evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: TBD'].join('\n'), check: /\bartifact_hash\b/, desc: 'artifact TBD sha' },
|
|
973
1048
|
];
|
|
974
1049
|
for (const { evidence, check, desc } of cases) {
|
|
975
1050
|
const dir = tmpDir();
|
|
976
1051
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
977
1052
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
978
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: '
|
|
1053
|
+
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' } } }));
|
|
979
1054
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
980
1055
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
981
1056
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -988,7 +1063,7 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
|
|
|
988
1063
|
const dir2 = tmpDir();
|
|
989
1064
|
const featureDir2 = path.join(dir2, 'specs', FEATURE);
|
|
990
1065
|
fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
|
|
991
|
-
fs.writeFileSync(path.join(featureDir2, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: '
|
|
1066
|
+
fs.writeFileSync(path.join(featureDir2, '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' } } }));
|
|
992
1067
|
fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`,'```','pass','```'].join('\n')));
|
|
993
1068
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
994
1069
|
assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
|
|
@@ -1005,7 +1080,7 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
|
|
|
1005
1080
|
const dir = tmpDir();
|
|
1006
1081
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1007
1082
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1008
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: '
|
|
1083
|
+
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' } } }));
|
|
1009
1084
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
1010
1085
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1011
1086
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -1019,7 +1094,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
|
|
|
1019
1094
|
const dir = tmpDir();
|
|
1020
1095
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1021
1096
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1022
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: '
|
|
1097
|
+
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' } } }));
|
|
1023
1098
|
const outside = path.join(dir, 'outside.md');
|
|
1024
1099
|
fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
|
|
1025
1100
|
const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
@@ -1073,7 +1148,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
|
|
|
1073
1148
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1074
1149
|
});
|
|
1075
1150
|
|
|
1076
|
-
test('30. phantom vectors via gate evidence must block with
|
|
1151
|
+
test('30. phantom vectors via gate evidence must block with verification_state (r3)', () => {
|
|
1077
1152
|
const vectors = [
|
|
1078
1153
|
'ℹ tests 0',
|
|
1079
1154
|
'Tests: 0 total',
|
|
@@ -1109,7 +1184,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
|
|
|
1109
1184
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
1110
1185
|
status: 'in_progress',
|
|
1111
1186
|
feature_name: FEATURE,
|
|
1112
|
-
current_phase: '
|
|
1187
|
+
current_phase: 'closeout',
|
|
1113
1188
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1114
1189
|
}));
|
|
1115
1190
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|
|
@@ -1118,7 +1193,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
|
|
|
1118
1193
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1119
1194
|
assert.ok(body, `vector should block: ${v}`);
|
|
1120
1195
|
assert.strictEqual(body.decision, 'block', `vector ${v} should block`);
|
|
1121
|
-
assert.match(body.reason, /\
|
|
1196
|
+
assert.match(body.reason, /\bverification_state\b/, `vector ${v} should fail verification_state`);
|
|
1122
1197
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1123
1198
|
}
|
|
1124
1199
|
// positive controls must not block
|
|
@@ -1155,7 +1230,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
|
|
|
1155
1230
|
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
1156
1231
|
status: 'in_progress',
|
|
1157
1232
|
feature_name: FEATURE,
|
|
1158
|
-
current_phase: '
|
|
1233
|
+
current_phase: 'closeout',
|
|
1159
1234
|
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1160
1235
|
}));
|
|
1161
1236
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|