@haposoft/cafekit 0.15.3 → 0.16.0-rc.2

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.
Files changed (90) hide show
  1. package/README.md +4 -2
  2. package/bin/__tests__/codex-hooks.test.js +140 -0
  3. package/bin/__tests__/codex-native.test.js +2 -2
  4. package/bin/__tests__/develop-contract.test.js +1120 -0
  5. package/bin/__tests__/installer-safety.test.js +510 -2
  6. package/bin/__tests__/package-inventory.test.js +258 -0
  7. package/bin/__tests__/secret-scanner.test.js +157 -0
  8. package/bin/__tests__/validator-grounding.test.js +713 -0
  9. package/bin/install.js +5 -1
  10. package/bin/lib/codex-install.js +21 -6
  11. package/bin/lib/context.js +17 -6
  12. package/bin/lib/copy-utils.js +31 -0
  13. package/bin/lib/instruction-blocks.js +85 -0
  14. package/bin/lib/managed-writer.js +4 -1
  15. package/bin/lib/opencode-install.js +75 -30
  16. package/bin/phases/claude-runtime.js +103 -4
  17. package/bin/phases/codex-runtime.js +9 -5
  18. package/bin/phases/post-install.js +29 -22
  19. package/bin/phases/root-config.js +89 -10
  20. package/package.json +14 -4
  21. package/src/claude/CLAUDE.md +6 -122
  22. package/src/claude/agents/code-auditor.md +28 -22
  23. package/src/claude/agents/debugger.md +3 -3
  24. package/src/claude/agents/deployer.md +1 -1
  25. package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
  26. package/src/claude/agents/project-manager.md +1 -1
  27. package/src/claude/agents/spec-maker.md +2 -2
  28. package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
  29. package/src/claude/hooks/agent.cjs +7 -14
  30. package/src/claude/hooks/rules.cjs +33 -53
  31. package/src/claude/hooks/spec-gate.cjs +66 -21
  32. package/src/claude/hooks/spec-state.cjs +5 -0
  33. package/src/claude/migration-manifest.json +9 -2
  34. package/src/claude/runtime.json +0 -3
  35. package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
  36. package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
  37. package/src/claude/scripts/spec-ground.cjs +189 -83
  38. package/src/claude/scripts/validate-spec-output.cjs +294 -30
  39. package/src/claude/scripts/workflow-policy.cjs +609 -0
  40. package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
  41. package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
  42. package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
  43. package/src/claude/skills/backend-development/SKILL.md +4 -4
  44. package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
  45. package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
  46. package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
  47. package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
  48. package/src/claude/skills/backend-development/references/backend-security.md +55 -3
  49. package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
  50. package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
  51. package/src/claude/skills/brainstorm/SKILL.md +2 -3
  52. package/src/claude/skills/code-review/SKILL.md +12 -8
  53. package/src/claude/skills/code-review/references/verification-gate.md +4 -4
  54. package/src/claude/skills/debug/SKILL.md +2 -0
  55. package/src/claude/skills/develop/SKILL.md +100 -52
  56. package/src/claude/skills/develop/references/parallel-waves.md +105 -26
  57. package/src/claude/skills/develop/references/quality-gate.md +78 -38
  58. package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
  59. package/src/claude/skills/git/SKILL.md +44 -50
  60. package/src/claude/skills/hotfix/SKILL.md +10 -8
  61. package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
  62. package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
  63. package/src/claude/skills/inspect/SKILL.md +12 -30
  64. package/src/claude/skills/mobile-development/SKILL.md +12 -12
  65. package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
  66. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
  67. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
  68. package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
  69. package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
  70. package/src/claude/skills/question/SKILL.md +13 -13
  71. package/src/claude/skills/research/SKILL.md +2 -2
  72. package/src/claude/skills/specs/SKILL.md +32 -15
  73. package/src/claude/skills/specs/references/review.md +3 -3
  74. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  75. package/src/claude/skills/specs/templates/spec-state.json +7 -3
  76. package/src/claude/skills/sync/SKILL.md +26 -1
  77. package/src/claude/skills/sync/references/sync-protocols.md +3 -3
  78. package/src/claude/skills/test/SKILL.md +12 -4
  79. package/src/claude/skills/test/references/failure-triage.md +17 -17
  80. package/src/claude/skills/test/references/test-memory.md +2 -2
  81. package/src/codex/AGENTS.md +16 -24
  82. package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
  83. package/src/codex/hooks/rules.cjs +27 -26
  84. package/src/codex/hooks/spec-gate.cjs +19 -8
  85. package/src/codex/hooks/spec-state.cjs +10 -1
  86. package/src/common/AGENTS.md +43 -0
  87. package/src/opencode/AGENTS.md +21 -121
  88. package/src/opencode/plugins/rules.ts +26 -34
  89. package/src/opencode/plugins/spec-gate.ts +283 -0
  90. package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
@@ -0,0 +1,1120 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const crypto = require('node:crypto');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+ const { test } = require('node:test');
10
+
11
+ const PACKAGE_ROOT = path.join(__dirname, '../..');
12
+ const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
13
+ const POLICY = require(POLICY_PATH);
14
+ const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
15
+ const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
16
+ const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
17
+ const SYNC_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/SKILL.md');
18
+ const CODE_REVIEW_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/code-review/SKILL.md');
19
+ const PARALLEL_WAVES = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/parallel-waves.md');
20
+ const PROVENANCE = path.join(PACKAGE_ROOT, '../../docs/provenance.md');
21
+
22
+ function read(filePath) {
23
+ return fs.readFileSync(filePath, 'utf8');
24
+ }
25
+
26
+ function manifestAgents() {
27
+ const manifest = JSON.parse(read(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json')));
28
+ return manifest.agents.required.map((fileName) => path.basename(fileName, path.extname(fileName)));
29
+ }
30
+
31
+ test('CLI rejects flash and parallel before any mutation', () => {
32
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-policy-cli-'));
33
+ const statePath = path.join(root, 'state.json');
34
+ fs.writeFileSync(statePath, '{"status":"in_progress"}\n');
35
+ const before = fs.readFileSync(statePath, 'utf8');
36
+ try {
37
+ const result = spawnSync(process.execPath, [POLICY_PATH, '--flash', '--parallel', '--json'], {
38
+ cwd: root,
39
+ encoding: 'utf8',
40
+ });
41
+ assert.equal(result.status, 2, `${result.stdout}\n${result.stderr}`);
42
+ const payload = JSON.parse(result.stdout);
43
+ assert.equal(payload.ok, false);
44
+ assert.equal(payload.failFast, true);
45
+ assert.match(payload.message, /incompatible/);
46
+ assert.equal(fs.readFileSync(statePath, 'utf8'), before);
47
+ } finally {
48
+ fs.rmSync(root, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ test('develop pre-state guard names executable policy contract', () => {
53
+ const develop = read(DEVELOP);
54
+ assert.match(develop, /node \.claude\/scripts\/workflow-policy\.cjs --flash --parallel --json/);
55
+ assert.match(develop, /No spec state, task receipt, worktree, subagent, or commit was created/);
56
+ assert.match(develop, /flash\+parallel fail-fast/i);
57
+ });
58
+
59
+ test('delegation plan uses only shipped agents and exact Deep sequence', () => {
60
+ const shipped = new Set(manifestAgents());
61
+ assert.deepEqual(POLICY.delegationPlan({ tier: 'Light', mode: 'full-spec', taskCount: 2 }).delegated, []);
62
+ assert.deepEqual(POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' }).delegated, ['code-auditor']);
63
+ const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 }).delegated;
64
+ assert.deepEqual(deep, [
65
+ 'inspector', 'implementer', 'test-runner', 'code-auditor',
66
+ 'inspector', 'implementer', 'test-runner', 'code-auditor',
67
+ ]);
68
+ for (const agent of deep) assert.ok(shipped.has(agent), `Deep agent is not shipped: ${agent}`);
69
+ assert.doesNotMatch(read(GATE), /spec-review|quality-review/);
70
+ assert.doesNotMatch(read(DEVELOP), /spec-review|quality-review/);
71
+ });
72
+
73
+ test('lane classifier selects Direct for explicit reversible low-risk work', () => {
74
+ const result = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
75
+ assert.equal(result.lane, 'Direct');
76
+ assert.equal(result.automaticLane, 'Direct');
77
+ assert.deepEqual(result.risks, []);
78
+ assert.deepEqual(POLICY.delegationPlan({ lane: 'Direct' }).delegated, []);
79
+ assert.equal(POLICY.lanePolicy(result).requiresSpec, false);
80
+ });
81
+
82
+ test('lane classifier forces Critical for named high-risk signals', () => {
83
+ const result = POLICY.classifyLane({
84
+ reversible: true,
85
+ riskSignals: { auth: true, migration: true, publicContract: true },
86
+ });
87
+ assert.equal(result.lane, 'Critical');
88
+ assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
89
+ assert.deepEqual(POLICY.delegationPlan({ lane: 'Critical' }).delegated, [
90
+ 'inspector', 'implementer', 'test-runner', 'code-auditor',
91
+ ]);
92
+ });
93
+
94
+ test('lane override keeps automatic result and surfaces downgrade warning', () => {
95
+ const result = POLICY.classifyLane({
96
+ reversible: true,
97
+ riskSignals: { privacy: true },
98
+ override: 'Direct',
99
+ userAuthorized: true,
100
+ });
101
+ assert.equal(result.lane, 'Direct');
102
+ assert.equal(result.automaticLane, 'Critical');
103
+ assert.equal(result.overridden, true);
104
+ assert.match(result.warnings.join(' '), /downgrad|review and evidence coverage/i);
105
+ assert.ok(POLICY.lanePolicy(result).warnings.length >= 2);
106
+ });
107
+
108
+ test('unsafe downgrade without user authorization is blocked', () => {
109
+ assert.throws(() => POLICY.classifyLane({
110
+ reversible: true,
111
+ riskSignals: { privacy: true },
112
+ override: 'Direct',
113
+ }), /requires explicit user authorization/);
114
+ // also via CLI
115
+ const cli = spawnSync(process.execPath, [
116
+ POLICY_PATH,
117
+ '--classify-lane',
118
+ '--task-json',
119
+ JSON.stringify({ riskSignals: { auth: true }, override: 'Direct' }),
120
+ '--json',
121
+ ], { encoding: 'utf8' });
122
+ assert.equal(cli.status, 2);
123
+ assert.match(cli.stderr, /requires explicit user authorization/);
124
+ });
125
+
126
+ test('forged approval via legacy approved field is rejected', () => {
127
+ assert.throws(() => POLICY.approvalState({ generated: true, approved: true }), /Legacy approval state/);
128
+ const cli = spawnSync(process.execPath, [
129
+ POLICY_PATH,
130
+ '--approval-state',
131
+ '--task-json',
132
+ JSON.stringify({ generated: true, approved: true }),
133
+ '--json',
134
+ ], { encoding: 'utf8' });
135
+ assert.equal(cli.status, 2);
136
+ assert.match(cli.stderr, /Legacy approval state/);
137
+ const legacyCheck = POLICY.validateApprovalSchema({ approvals: { requirements: { generated: true, approved: true } } });
138
+ assert.equal(legacyCheck.valid, false);
139
+ assert.equal(legacyCheck.legacy, true);
140
+ assert.match(legacyCheck.error, /Legacy/);
141
+ });
142
+
143
+ test('approval state never infers user approval from generated or agent validation', () => {
144
+ const state = POLICY.approvalState({ generated: true, agent_validated: true });
145
+ assert.equal(state.generated, true);
146
+ assert.equal(state.agent_validated, true);
147
+ assert.equal(state.user_approved, false);
148
+ assert.equal(state.ready, false);
149
+ assert.equal(state.schema_version, '2.0');
150
+ assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
151
+ const cli = spawnSync(process.execPath, [
152
+ POLICY_PATH,
153
+ '--approval-state',
154
+ '--task-json',
155
+ JSON.stringify({ generated: true, agent_validated: true }),
156
+ '--json',
157
+ ], { encoding: 'utf8' });
158
+ assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
159
+ assert.equal(JSON.parse(cli.stdout).state.user_approved, false);
160
+ });
161
+
162
+ test('CLI exposes JSON lane classification and develop has no universal spec hard gate', () => {
163
+ const cli = spawnSync(process.execPath, [
164
+ POLICY_PATH,
165
+ '--classify-lane',
166
+ '--task-json',
167
+ JSON.stringify({ reversible: true, lowRisk: true, isolated: true }),
168
+ '--json',
169
+ ], { encoding: 'utf8' });
170
+ assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
171
+ const payload = JSON.parse(cli.stdout);
172
+ assert.equal(payload.classification.lane, 'Direct');
173
+ assert.equal(payload.policy.requiresSpec, false);
174
+ const develop = read(DEVELOP);
175
+ assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
176
+ assert.match(develop, /Direct.*bypass spec\/state/i);
177
+ assert.match(develop, /Standard.*approved.*spec/i);
178
+ assert.match(develop, /Critical.*strict evidence/i);
179
+ assert.match(develop, /C --> D\s*$/m);
180
+ assert.doesNotMatch(develop, /D2\[Step 3/);
181
+ assert.match(read(path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md')), /requirements\.md.*design\.md.*current layout/i);
182
+ });
183
+
184
+ test('review verdict consumer handles PASS, FAIL, and BLOCKED', () => {
185
+ assert.deepEqual(POLICY.consumeReviewVerdict('PASS'), { action: 'proceed', terminal: false });
186
+ assert.deepEqual(POLICY.consumeReviewVerdict('FAIL'), { action: 'fix-and-rerun', terminal: false });
187
+ assert.deepEqual(POLICY.consumeReviewVerdict('BLOCKED'), {
188
+ action: 'stop',
189
+ terminal: true,
190
+ blocker: 'review returned BLOCKED',
191
+ });
192
+ assert.throws(() => POLICY.consumeReviewVerdict('NO_TESTS'), /Unsupported review verdict/);
193
+ const cli = spawnSync(process.execPath, [POLICY_PATH, '--consume-verdict', '--verdict', 'BLOCKED', '--json'], { encoding: 'utf8' });
194
+ assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
195
+ assert.deepEqual(JSON.parse(cli.stdout).action, 'stop');
196
+ assert.match(read(GATE), /PASS \| FAIL \| BLOCKED/);
197
+ assert.match(read(CODE_REVIEW_SKILL), /PASS \| FAIL \| BLOCKED/);
198
+ assert.match(read(TEST_SKILL), /PASS.*FAIL.*BLOCKED/);
199
+ });
200
+
201
+ test('flash PASS promotes proof without completion, finalize alone completes', () => {
202
+ const initial = {
203
+ status: 'in_progress',
204
+ receipt: 'FLASH_UNVERIFIED',
205
+ blocker: 'awaiting /hapo:test <feature>',
206
+ dependencyBlocked: true,
207
+ unblocks: false,
208
+ };
209
+ for (const verdict of ['FAIL', 'BLOCKED', 'NO_TESTS']) {
210
+ const result = POLICY.promoteFlashTask(initial, verdict);
211
+ assert.equal(result.status, 'in_progress');
212
+ assert.equal(result.receipt, 'FLASH_UNVERIFIED');
213
+ assert.equal(result.dependencyBlocked, true);
214
+ assert.equal(result.unblocks, false);
215
+ assert.match(result.blocker, /verification|test proof/);
216
+ }
217
+ const promoted = POLICY.promoteFlashTask(initial, 'PASS', 'Verification: PASS\nCommand: pnpm test\nResult: PASS');
218
+ assert.equal(promoted.status, 'in_progress');
219
+ assert.equal(promoted.receipt.startsWith('Verification: PASS'), true);
220
+ assert.equal(promoted.blocker, null);
221
+ assert.equal(promoted.dependencyBlocked, true);
222
+ assert.equal(promoted.unblocks, false);
223
+ assert.equal(promoted.readyForSync, true);
224
+ assert.equal(POLICY.finalizeFlashTask(promoted, 'sync'), promoted);
225
+ const finalized = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
226
+ assert.equal(finalized.status, 'done');
227
+ assert.equal(finalized.dependencyBlocked, false);
228
+ assert.equal(finalized.unblocks, true);
229
+ assert.equal(finalized.readyForSync, false);
230
+ assert.equal(POLICY.isStaleFlashDone({ status: 'done', receipt: 'FLASH_UNVERIFIED' }), true);
231
+ assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/);
232
+ assert.match(read(SYNC_SKILL), /sync-finalize/);
233
+ });
234
+
235
+ test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
236
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
237
+ const claude = path.join(root, '.claude');
238
+ fs.mkdirSync(path.join(claude, 'hooks'), { recursive: true });
239
+ fs.mkdirSync(path.join(claude, 'scripts'), { recursive: true });
240
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/spec-gate.cjs'), path.join(claude, 'hooks/spec-gate.cjs'));
241
+ fs.copyFileSync(POLICY_PATH, path.join(claude, 'scripts/workflow-policy.cjs'));
242
+ fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
243
+ fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
244
+ status: 'in_progress',
245
+ task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
246
+ }));
247
+ try {
248
+ const result = spawnSync(process.execPath, [path.join(claude, 'hooks/spec-gate.cjs')], {
249
+ cwd: root,
250
+ env: { ...process.env, PROJECT_ROOT: root },
251
+ input: JSON.stringify({ cwd: root }),
252
+ encoding: 'utf8',
253
+ });
254
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
255
+ const payload = JSON.parse(result.stdout);
256
+ assert.equal(payload.decision, 'block');
257
+ assert.match(payload.reason, /FLASH_UNVERIFIED/);
258
+ } finally {
259
+ fs.rmSync(root, { recursive: true, force: true });
260
+ }
261
+ });
262
+
263
+ test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
264
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
265
+ const claude = path.join(root, '.claude');
266
+ fs.mkdirSync(path.join(claude, 'hooks'), { recursive: true });
267
+ fs.mkdirSync(path.join(claude, 'scripts'), { recursive: true });
268
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/spec-gate.cjs'), path.join(claude, 'hooks/spec-gate.cjs'));
269
+ fs.copyFileSync(POLICY_PATH, path.join(claude, 'scripts/workflow-policy.cjs'));
270
+ const specDir = path.join(root, 'specs', 'demo', 'tasks');
271
+ fs.mkdirSync(specDir, { recursive: true });
272
+ // Done task without canonical receipt (missing Command, provenance)
273
+ fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
274
+ status: 'in_progress',
275
+ task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
276
+ }));
277
+ fs.writeFileSync(path.join(specDir, 'task.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
278
+ try {
279
+ const result = spawnSync(process.execPath, [path.join(claude, 'hooks/spec-gate.cjs')], {
280
+ cwd: root,
281
+ env: { ...process.env, PROJECT_ROOT: root },
282
+ input: JSON.stringify({ cwd: root }),
283
+ encoding: 'utf8',
284
+ });
285
+ assert.equal(result.status, 0);
286
+ const payload = JSON.parse(result.stdout);
287
+ assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
288
+ assert.match(payload.reason, /tasks\/task\.md/);
289
+ // Also test that with canonical receipt it passes
290
+ fs.writeFileSync(path.join(specDir, 'task.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nBase: abc123\nHead: def456\n```\nnpm test\nPASS\n```\n');
291
+ // Clear cache to simulate fresh first-run with valid receipt
292
+ try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
293
+ try { fs.unlinkSync(path.join(__dirname, '..', '.logs', 'spec-gate-last.json')); } catch {}
294
+ // Need to clear the cache that the hook uses (under claude hooks .logs) - we already cleared above
295
+ // Re-run with valid receipt - should not block
296
+ const result2 = spawnSync(process.execPath, [path.join(claude, 'hooks/spec-gate.cjs')], {
297
+ cwd: root,
298
+ env: { ...process.env, PROJECT_ROOT: root },
299
+ input: JSON.stringify({ cwd: root }),
300
+ encoding: 'utf8',
301
+ });
302
+ assert.equal(result2.stdout, '', 'valid canonical receipt on first run must not block');
303
+ } finally {
304
+ fs.rmSync(root, { recursive: true, force: true });
305
+ try { fs.unlinkSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/.logs/spec-gate-last.json')); } catch {}
306
+ }
307
+ });
308
+
309
+ test('canonical receipt requires command, exit, provenance and unambiguous PASS', () => {
310
+ assert.deepEqual(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead: def\n'), []);
311
+ const missingCommand = POLICY.validateCanonicalReceipt('Verification: PASS\nExit: 0\nBase: a\nHead: b\n');
312
+ assert.ok(missingCommand.includes('command'));
313
+ const missingProvenance = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\n');
314
+ assert.ok(missingProvenance.includes('provenance'));
315
+ const missingVerification = POLICY.validateCanonicalReceipt('Command: pnpm test\nExit: 0\nBase: a\nHead: b\n');
316
+ assert.ok(missingVerification.includes('verification_state'));
317
+ const artifactWithoutHash = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: dist/bundle.js\n');
318
+ assert.ok(artifactWithoutHash.includes('artifact_hash'));
319
+ // CLI validation
320
+ const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n' }), '--json'], { encoding: 'utf8' });
321
+ assert.equal(cli.status, 0);
322
+ assert.equal(JSON.parse(cli.stdout).ok, true);
323
+ });
324
+
325
+ test('canonical receipt provenance requires both Base and Head (or both base_sha and head_sha)', () => {
326
+ // Only Base fails - single endpoint must not satisfy provenance
327
+ const onlyBase = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\n');
328
+ assert.ok(onlyBase.includes('provenance'), 'only Base should fail provenance');
329
+ // Only Head fails
330
+ const onlyHead = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nHead: def456\n');
331
+ assert.ok(onlyHead.includes('provenance'), 'only Head should fail provenance');
332
+ // Both Base and Head passes
333
+ const bothLabels = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\nHead: def456\n');
334
+ assert.deepEqual(bothLabels, [], 'both Base and Head should pass');
335
+ // Only base_sha fails
336
+ const onlyBaseSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc123\n');
337
+ assert.ok(onlyBaseSha.includes('provenance'), 'only base_sha should fail provenance');
338
+ // Only head_sha fails
339
+ const onlyHeadSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nhead_sha: def456\n');
340
+ assert.ok(onlyHeadSha.includes('provenance'), 'only head_sha should fail provenance');
341
+ // Both base_sha and head_sha passes
342
+ const bothSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc123\nhead_sha: def456\n');
343
+ assert.deepEqual(bothSha, [], 'both base_sha and head_sha should pass');
344
+ // Alternation-precedence false acceptance check: ensure single Head or base_sha alone does not pass
345
+ const singleHeadLower = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nHead: single\n');
346
+ assert.ok(singleHeadLower.includes('provenance'));
347
+ // Empty / bare provenance must fail — same-line non-empty value required
348
+ const emptyBase = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: def456\n');
349
+ assert.ok(emptyBase.includes('provenance'), 'empty Base: should fail');
350
+ const spacesBase = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: \nHead: def456\n');
351
+ assert.ok(spacesBase.includes('provenance'), 'Base: with only spaces should fail');
352
+ const emptyHead = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead:\n');
353
+ assert.ok(emptyHead.includes('provenance'), 'empty Head: should fail');
354
+ const spacesHead = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead: \n');
355
+ assert.ok(spacesHead.includes('provenance'), 'Head: with only spaces should fail');
356
+ const bareBaseSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha\nhead_sha: def\n');
357
+ assert.ok(bareBaseSha.includes('provenance'), 'bare base_sha without colon/value should fail');
358
+ const emptyBaseSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha:\nhead_sha: def\n');
359
+ assert.ok(emptyBaseSha.includes('provenance'), 'empty base_sha: should fail');
360
+ const spacesBaseSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: \nhead_sha: def\n');
361
+ assert.ok(spacesBaseSha.includes('provenance'), 'base_sha: with only spaces should fail');
362
+ const bareHeadSha = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc\nhead_sha\n');
363
+ assert.ok(bareHeadSha.includes('provenance'), 'bare head_sha without colon/value should fail');
364
+ const bareBoth = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha head_sha\n');
365
+ assert.ok(bareBoth.includes('provenance'), 'bare base_sha head_sha without colon/value should fail');
366
+ const baseEmptyHeadValid = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: def\n');
367
+ assert.ok(baseEmptyHeadValid.includes('provenance'));
368
+ const validWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\nHead: def456\n');
369
+ assert.deepEqual(validWithValues, [], 'valid Base and Head with values should pass');
370
+ const validShaWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc\nhead_sha: def\n');
371
+ assert.deepEqual(validShaWithValues, [], 'valid base_sha and head_sha with values should pass');
372
+ // CLI: only Base via --validate-receipt should fail
373
+ const cliOnlyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\n' }), '--json'], { encoding: 'utf8' });
374
+ assert.equal(cliOnlyBase.status, 2);
375
+ assert.equal(JSON.parse(cliOnlyBase.stdout).ok, false);
376
+ assert.ok(JSON.parse(cliOnlyBase.stdout).failures.includes('provenance'));
377
+ const cliBoth = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n' }), '--json'], { encoding: 'utf8' });
378
+ assert.equal(cliBoth.status, 0);
379
+ assert.equal(JSON.parse(cliBoth.stdout).ok, true);
380
+ // CLI empty Base should fail
381
+ const cliEmptyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: b\n' }), '--json'], { encoding: 'utf8' });
382
+ assert.equal(cliEmptyBase.status, 2);
383
+ assert.ok(JSON.parse(cliEmptyBase.stdout).failures.includes('provenance'));
384
+ });
385
+
386
+ test('lane traces - Direct, Standard, Critical classification, override, state mutation and completion', () => {
387
+ // Direct: isolated reversible low-risk
388
+ const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
389
+ assert.equal(direct.lane, 'Direct');
390
+ assert.equal(direct.automaticLane, 'Direct');
391
+ assert.deepEqual(POLICY.lanePolicy(direct).delegated, []);
392
+ assert.equal(POLICY.lanePolicy(direct).requiresSpec, false);
393
+ assert.equal(POLICY.lanePolicy(direct).shipPoint, 'task');
394
+ // Standard: default
395
+ const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
396
+ assert.equal(standard.lane, 'Standard');
397
+ assert.deepEqual(POLICY.lanePolicy(standard).delegated, ['code-auditor']);
398
+ assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
399
+ // Critical: auth signal
400
+ const critical = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
401
+ assert.equal(critical.lane, 'Critical');
402
+ assert.ok(critical.risks.includes('auth'));
403
+ assert.deepEqual(POLICY.lanePolicy(critical).delegated, ['inspector', 'implementer', 'test-runner', 'code-auditor']);
404
+ // Override: Direct requesting Critical is allowed (upgrade) without extra auth
405
+ const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
406
+ assert.equal(upgrade.lane, 'Critical');
407
+ assert.equal(upgrade.automaticLane, 'Direct');
408
+ // Downgrade without auth is blocked
409
+ assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /requires explicit user authorization/);
410
+ // Downgrade with auth succeeds and surfaces warning
411
+ const downgrade = POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true });
412
+ assert.equal(downgrade.lane, 'Standard');
413
+ assert.match(downgrade.warnings.join(' '), /Downgrade authorized by user/);
414
+ // State mutation: approvalState
415
+ const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
416
+ assert.equal(pendingApproval.ready, false);
417
+ const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
418
+ assert.equal(agentValidated.ready, false, 'agent_validated alone must not imply ready');
419
+ const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
420
+ assert.equal(approved.ready, true);
421
+ // Completion: flash work remains in_progress and does not unblock
422
+ const flashTask = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', blocker: 'awaiting /hapo:test <feature>', dependencyBlocked: true, unblocks: false };
423
+ const failRemains = POLICY.promoteFlashTask(flashTask, 'FAIL');
424
+ assert.equal(failRemains.status, 'in_progress');
425
+ assert.equal(failRemains.unblocks, false);
426
+ const blockedRemains = POLICY.promoteFlashTask(flashTask, 'BLOCKED');
427
+ assert.equal(blockedRemains.unblocks, false);
428
+ const noTestsRemains = POLICY.promoteFlashTask(flashTask, 'NO_TESTS');
429
+ assert.equal(noTestsRemains.unblocks, false);
430
+ const promoted = POLICY.promoteFlashTask(flashTask, 'PASS', 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
431
+ assert.equal(promoted.readyForSync, true);
432
+ assert.equal(promoted.unblocks, false, 'promoted flash must not unblock until sync-finalize');
433
+ const finalized = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
434
+ assert.equal(finalized.status, 'done');
435
+ assert.equal(finalized.unblocks, true);
436
+ });
437
+
438
+ test('flash selective promotion - only specific task is promoted, not blanket', () => {
439
+ const registry = {
440
+ 'tasks/task-a.md': { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', dependencyBlocked: true, unblocks: false },
441
+ 'tasks/task-b.md': { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', dependencyBlocked: true, unblocks: false },
442
+ };
443
+ const promotedA = POLICY.promoteFlashTask(registry['tasks/task-a.md'], 'PASS', 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
444
+ const notPromotedB = registry['tasks/task-b.md']; // unchanged
445
+ assert.equal(promotedA.readyForSync, true);
446
+ assert.equal(notPromotedB.receipt, 'FLASH_UNVERIFIED');
447
+ assert.equal(notPromotedB.readyForSync, undefined);
448
+ // FAIL on B does not affect A
449
+ const failB = POLICY.promoteFlashTask(notPromotedB, 'FAIL');
450
+ assert.equal(failB.receipt, 'FLASH_UNVERIFIED');
451
+ assert.equal(promotedA.receipt.startsWith('Verification: PASS'), true);
452
+ });
453
+
454
+ test('parallel waves require immutable provenance receipts and safe recovery', () => {
455
+ const waves = read(PARALLEL_WAVES);
456
+ for (const field of ['base_sha', 'head_sha', 'branch', 'worktree_path', 'commit_range']) {
457
+ assert.match(waves, new RegExp(`\\b${field}\\b`));
458
+ }
459
+ assert.match(waves, /git diff(?: --stat)? [^\\n]*(?:BASE_SHA|base_sha)[^\\n]*(?:HEAD_SHA|head_sha)/i);
460
+ assert.match(waves, /new commit|new commits|Every fix is a new commit/i);
461
+ assert.match(waves, /destination tree must be clean/i);
462
+ assert.match(waves, /compatible with the selected base/i);
463
+ assert.match(waves, /consent.*commit|commit.*consent/i);
464
+ assert.match(waves, /retention.*retained|retained.*worktree/i);
465
+ assert.match(waves, /cleanup_authorization|explicit discard/i);
466
+ assert.match(waves, /directory overlap|lockfiles|manifests|export barrels|migrations|registries|generated artifacts|shared state writers/i);
467
+ assert.match(waves, /affected integration|final.*integration/i);
468
+ for (const category of ['baseline', 'environment', 'spec', 'code']) {
469
+ assert.match(waves, new RegExp(`\\b${category}\\b`));
470
+ }
471
+ assert.match(waves, /Do not blind-retry/i);
472
+ });
473
+
474
+ test('provenance ledger defines reuse contract and source anchors', () => {
475
+ assert.equal(fs.existsSync(PROVENANCE), true);
476
+ const provenance = read(PROVENANCE);
477
+ assert.match(provenance, /\bidea\b/);
478
+ assert.match(provenance, /\bclean-room\b/);
479
+ assert.match(provenance, /copied-text.*never valid|never valid.*copied-text/i);
480
+ assert.match(provenance, /never copy source text verbatim/i);
481
+ for (const column of ['Pattern', 'Source anchor', 'Reuse type', 'CafeKit destination', 'Evidence/status']) {
482
+ assert.match(provenance, new RegExp(`\\b${column.replace('/', '\\/')}\\b`, 'i'));
483
+ }
484
+ assert.match(provenance, /AgentKit|cafekit-ref/);
485
+ assert.match(provenance, /before implementation/i);
486
+ // H1 remediation: distinguish survey vs verified implementation, fail unsupported claims
487
+ assert.match(provenance, /external survey|not committed/i);
488
+ assert.match(provenance, /survey only|not used|No borrowed text recorded/i);
489
+ assert.match(provenance, /Source anchor.*plans\//);
490
+ assert.match(provenance, /Evidence\/status/i);
491
+ assert.match(provenance, /grep -r.*cafekit-ref/i);
492
+ assert.doesNotMatch(provenance, /AgentKit T1.*implemented as direct source/i);
493
+ // shipped runtime must not contain cafekit-ref/AgentKit verbatim (only ledger/plans may)
494
+ const shippedHits = spawnSync('grep', ['-rn', 'cafekit-ref', 'packages/spec/src', '--include=*.md', '--include=*.cjs', '--include=*.ts'], { encoding: 'utf8' });
495
+ const agentKitHits = spawnSync('grep', ['-rn', 'AgentKit', 'packages/spec/src', '--include=*.md', '--include=*.cjs', '--include=*.ts'], { encoding: 'utf8' });
496
+ assert.equal(shippedHits.stdout.trim(), '', 'shipped runtime must not contain cafekit-ref verbatim');
497
+ assert.equal(agentKitHits.stdout.trim(), '', 'shipped runtime must not contain AgentKit verbatim');
498
+ });
499
+
500
+ const BENCHMARK_SCRIPT = path.join(PACKAGE_ROOT, 'scripts/benchmark-workflow.mjs');
501
+ const BENCHMARK_CORPUS_SCHEMA = path.join(PACKAGE_ROOT, 'benchmarks/corpus.schema.json');
502
+ const BENCHMARK_CONFIG_EXAMPLE = path.join(PACKAGE_ROOT, 'benchmarks/benchmark-config.example.json');
503
+ const BENCHMARK_RUBRIC = path.join(PACKAGE_ROOT, 'benchmarks/rubric.md');
504
+ const BENCHMARK_DOCS = path.join(PACKAGE_ROOT, '../../docs/benchmark-workflow.md');
505
+
506
+ function canonicalBenchmark(value) {
507
+ if (Array.isArray(value)) return `[${value.map(canonicalBenchmark).join(',')}]`;
508
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalBenchmark(value[key])}`).join(',')}}`;
509
+ return JSON.stringify(value);
510
+ }
511
+
512
+ function benchmarkHash(value) {
513
+ const crypto = require('node:crypto');
514
+ return `sha256:${crypto.createHash('sha256').update(canonicalBenchmark(value)).digest('hex')}`;
515
+ }
516
+
517
+ function benchmarkRawHash(value) {
518
+ return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
519
+ }
520
+
521
+ function writeBenchmarkConfig(file, config) {
522
+ const frozen = { ...config };
523
+ delete frozen.config_sha256;
524
+ frozen.config_sha256 = benchmarkHash(frozen);
525
+ fs.writeFileSync(file, JSON.stringify(frozen));
526
+ }
527
+
528
+ function makeBenchmarkFixture(root) {
529
+ const corpus = {
530
+ schema_version: 'b1.v1', status: 'frozen', corpus_id: 'fixture-only-corpus',
531
+ tasks: [
532
+ { task_id: 'fixture-direct', lane: 'Direct', prompt: 'fixture-only reversible task', repo_sample: 'fixture-repo', acceptance: { criteria: ['fixture criterion'] }, risk: { level: 'low' } },
533
+ { task_id: 'fixture-critical', lane: 'Critical', prompt: 'fixture-only negative control', repo_sample: 'fixture-repo', acceptance: { criteria: ['fixture contract'] }, risk: { level: 'high', reasons: ['fixture negative control'] } },
534
+ ],
535
+ };
536
+ const corpusSha = benchmarkHash(corpus);
537
+ const common = {
538
+ schema_version: 'b1.v1', status: 'frozen', experiment_id: 'fixture-only-experiment',
539
+ model: { name: 'fixture-model', version: 'fixture-version' }, reasoning_effort: 'fixture',
540
+ repo: { identifier: 'fixture-repo', commit: 'abc1234567890', clean_initial_tree_sha: benchmarkHash('fixture-clean-tree') },
541
+ permissions_fingerprint: benchmarkHash('fixture-permissions'), tool_availability_fingerprint: benchmarkHash('fixture-tools'),
542
+ corpus_sha256: corpusSha, repeat_policy: { repeats_per_task: 2, context_isolated: true },
543
+ input_usd_per_1k: 1, output_usd_per_1k: 2,
544
+ };
545
+ const configs = ['baseline', 'treatment'].map((arm) => {
546
+ const config = { ...common, arm };
547
+ config.config_sha256 = benchmarkHash(config);
548
+ return config;
549
+ });
550
+ const artifactRef = 'fixture-artifact.bin';
551
+ const artifactBytes = Buffer.from('fixture-only benchmark artifact\n');
552
+ fs.writeFileSync(path.join(root, artifactRef), artifactBytes);
553
+ const artifactSha = benchmarkRawHash(artifactBytes);
554
+ const receipts = [];
555
+ for (const config of configs) for (const task of corpus.tasks) for (const repeat of [1, 2]) {
556
+ const treatment = config.arm === 'treatment';
557
+ const critical = task.lane === 'Critical';
558
+ const wall = critical ? 300 : (treatment ? 50 : 100) * repeat;
559
+ const input = (treatment ? 100 : 200) * repeat;
560
+ const output = (treatment ? 50 : 100) * repeat;
561
+ receipts.push({
562
+ task_id: task.task_id, lane: task.lane, arm: config.arm, repeat,
563
+ model: config.model, reasoning_effort: config.reasoning_effort,
564
+ repo_commit: config.repo.commit, clean_initial_tree_sha: config.repo.clean_initial_tree_sha,
565
+ permissions_fingerprint: config.permissions_fingerprint, tool_availability_fingerprint: config.tool_availability_fingerprint,
566
+ corpus_sha256: config.corpus_sha256, config_sha256: config.config_sha256,
567
+ wall_ms: wall, input_tokens: input, output_tokens: output, context_loaded_tokens: 500,
568
+ tool_calls: 2, subagent_calls: critical ? 1 : 0, correctness: true, regression: false,
569
+ unsupported_completion_claim: false, user_corrections: 0, useful_reviewer_findings: 1,
570
+ false_positive_reviewer_findings: 0,
571
+ evidence: { artifact_ref: artifactRef, artifact_sha256: artifactSha, command: 'fixture-only command' },
572
+ });
573
+ }
574
+ const paths = { corpus: path.join(root, 'corpus.json'), receipts: path.join(root, 'receipts.json') };
575
+ fs.writeFileSync(paths.corpus, JSON.stringify(corpus));
576
+ fs.writeFileSync(paths.receipts, JSON.stringify({ receipts }));
577
+ paths.configs = configs.map((config, index) => {
578
+ const file = path.join(root, `${config.arm}-${index}.json`);
579
+ fs.writeFileSync(file, JSON.stringify(config));
580
+ return file;
581
+ });
582
+ return paths;
583
+ }
584
+
585
+ test('B1 benchmark artifacts expose bounded CLI contract', () => {
586
+ for (const file of [BENCHMARK_SCRIPT, BENCHMARK_CORPUS_SCHEMA, BENCHMARK_CONFIG_EXAMPLE, BENCHMARK_RUBRIC, BENCHMARK_DOCS]) assert.equal(fs.existsSync(file), true, file);
587
+ assert.match(read(BENCHMARK_DOCS), /live baseline.*treatment.*pending/i);
588
+ assert.match(read(BENCHMARK_DOCS), /`npm test`.*workflow correctness/i);
589
+ assert.match(read(BENCHMARK_CONFIG_EXAMPLE), /TEMPLATE ONLY|example.template/i);
590
+ assert.match(read(BENCHMARK_RUBRIC), /Direct/);
591
+ assert.match(read(BENCHMARK_RUBRIC), /Standard/);
592
+ assert.match(read(BENCHMARK_RUBRIC), /Critical/);
593
+ });
594
+
595
+ test('B1 validator rejects missing freeze fields and accepts fixture-only corpus', () => {
596
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-validation-'));
597
+ const fixture = makeBenchmarkFixture(root);
598
+ try {
599
+ assert.equal(JSON.parse(read(fixture.corpus)).status, 'frozen');
600
+ const valid = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', fixture.receipts], { encoding: 'utf8' });
601
+ assert.equal(valid.status, 0, `${valid.stdout}\n${valid.stderr}`);
602
+ assert.equal(JSON.parse(valid.stdout).status, 'valid');
603
+ const invalidConfig = JSON.parse(read(fixture.configs[0]));
604
+ delete invalidConfig.clean_initial_tree_sha;
605
+ delete invalidConfig.config_sha256;
606
+ const invalidPath = path.join(root, 'missing-freeze.json');
607
+ fs.writeFileSync(invalidPath, JSON.stringify(invalidConfig));
608
+ const invalid = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', invalidPath], { encoding: 'utf8' });
609
+ assert.equal(invalid.status, 2);
610
+ assert.match(invalid.stderr, /missing or placeholder freeze field/);
611
+ } finally {
612
+ fs.rmSync(root, { recursive: true, force: true });
613
+ }
614
+ });
615
+
616
+ test('B1 validator fails closed for templates, prompts, parity, and artifacts', () => {
617
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-integrity-'));
618
+ const fixture = makeBenchmarkFixture(root);
619
+ try {
620
+ const templateCorpus = JSON.parse(read(fixture.corpus));
621
+ templateCorpus.status = 'example_template';
622
+ const templatePath = path.join(root, 'template-corpus.json');
623
+ fs.writeFileSync(templatePath, JSON.stringify(templateCorpus));
624
+ const template = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', templatePath, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', fixture.receipts], { encoding: 'utf8' });
625
+ assert.equal(template.status, 2);
626
+ assert.match(template.stderr, /example_template.*receipt validation.*live summary/);
627
+
628
+ const placeholderCorpus = JSON.parse(read(fixture.corpus));
629
+ placeholderCorpus.tasks[0].prompt = '{{replace_me}}';
630
+ const placeholderPath = path.join(root, 'placeholder-corpus.json');
631
+ fs.writeFileSync(placeholderPath, JSON.stringify(placeholderCorpus));
632
+ const placeholder = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', placeholderPath, '--config', fixture.configs[0]], { encoding: 'utf8' });
633
+ assert.equal(placeholder.status, 2);
634
+ assert.match(placeholder.stderr, /tasks\[0\]\.prompt.*missing or placeholder/);
635
+
636
+ const emptyPromptWithHashCorpus = JSON.parse(read(fixture.corpus));
637
+ emptyPromptWithHashCorpus.tasks[0].prompt = '';
638
+ emptyPromptWithHashCorpus.tasks[0].prompt_sha256 = `sha256:${'1'.repeat(64)}`;
639
+ const emptyPromptWithHashPath = path.join(root, 'empty-prompt-with-hash-corpus.json');
640
+ fs.writeFileSync(emptyPromptWithHashPath, JSON.stringify(emptyPromptWithHashCorpus));
641
+ const emptyPromptWithHash = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', emptyPromptWithHashPath, '--config', fixture.configs[0]], { encoding: 'utf8' });
642
+ assert.equal(emptyPromptWithHash.status, 2);
643
+ assert.match(emptyPromptWithHash.stderr, /tasks\[0\]: exactly one of prompt or prompt_sha256 required/);
644
+
645
+ const treatment = JSON.parse(read(fixture.configs[1]));
646
+ treatment.experiment_id = 'different-experiment';
647
+ const parityPath = path.join(root, 'parity-treatment.json');
648
+ writeBenchmarkConfig(parityPath, treatment);
649
+ const parity = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', parityPath], { encoding: 'utf8' });
650
+ assert.equal(parity.status, 2);
651
+ assert.match(parity.stderr, /differ only by arm/);
652
+
653
+ const receipts = JSON.parse(read(fixture.receipts));
654
+ const missingArtifactHash = JSON.parse(read(fixture.receipts));
655
+ delete missingArtifactHash.receipts[0].evidence.artifact_sha256;
656
+ const missingArtifactHashPath = path.join(root, 'missing-artifact-hash.json');
657
+ fs.writeFileSync(missingArtifactHashPath, JSON.stringify(missingArtifactHash));
658
+ const missingHash = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', missingArtifactHashPath], { encoding: 'utf8' });
659
+ assert.equal(missingHash.status, 2);
660
+ assert.match(missingHash.stderr, /evidence\.artifact_sha256.*missing or placeholder/);
661
+
662
+ receipts.receipts[0].evidence.artifact_sha256 = benchmarkRawHash(Buffer.from('wrong artifact'));
663
+ const wrongHashPath = path.join(root, 'wrong-artifact-hash.json');
664
+ fs.writeFileSync(wrongHashPath, JSON.stringify(receipts));
665
+ const wrongHash = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', wrongHashPath], { encoding: 'utf8' });
666
+ assert.equal(wrongHash.status, 2);
667
+ assert.match(wrongHash.stderr, /artifact_sha256.*raw-byte hash mismatch/);
668
+
669
+ receipts.receipts[0].evidence.artifact_ref = 'https://example.invalid/artifact.json';
670
+ const uriPath = path.join(root, 'uri-artifact.json');
671
+ fs.writeFileSync(uriPath, JSON.stringify(receipts));
672
+ const uri = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', uriPath], { encoding: 'utf8' });
673
+ assert.equal(uri.status, 2);
674
+ assert.match(uri.stderr, /artifact_ref.*relative local file.*URI/);
675
+
676
+ const outsideArtifact = path.join(path.dirname(root), `${path.basename(root)}-outside.bin`);
677
+ fs.writeFileSync(outsideArtifact, Buffer.from('outside receipt bundle'));
678
+ const traversalReceipts = JSON.parse(read(fixture.receipts));
679
+ traversalReceipts.receipts[0].evidence.artifact_ref = path.relative(root, outsideArtifact);
680
+ traversalReceipts.receipts[0].evidence.artifact_sha256 = benchmarkRawHash(fs.readFileSync(outsideArtifact));
681
+ const traversalPath = path.join(root, 'traversal-artifact.json');
682
+ fs.writeFileSync(traversalPath, JSON.stringify(traversalReceipts));
683
+ const traversal = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', traversalPath], { encoding: 'utf8' });
684
+ assert.equal(traversal.status, 2);
685
+ assert.match(traversal.stderr, /artifact_ref.*path escapes receipt bundle directory/);
686
+ fs.rmSync(outsideArtifact, { force: true });
687
+ } finally {
688
+ fs.rmSync(root, { recursive: true, force: true });
689
+ }
690
+ });
691
+
692
+ test('B1 summary groups fixture-only receipts by arm/lane with deterministic quantiles', () => {
693
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-summary-'));
694
+ const fixture = makeBenchmarkFixture(root);
695
+ try {
696
+ const result = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', fixture.receipts], { encoding: 'utf8' });
697
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
698
+ const summary = JSON.parse(result.stdout);
699
+ assert.equal(summary.status, 'ready');
700
+ assert.equal(summary.groups.baseline.Direct.metrics.wall_ms.median, 150);
701
+ assert.equal(summary.groups.baseline.Direct.metrics.wall_ms.p25, 125);
702
+ assert.equal(summary.groups.baseline.Direct.metrics.wall_ms.p75, 175);
703
+ assert.equal(summary.groups.treatment.Direct.quality_rates.correctness, 1);
704
+ assert.equal(summary.rollout_recommendation.gates.Critical.pass, true);
705
+
706
+ const countAggregation = JSON.parse(read(fixture.receipts));
707
+ const countedReceipt = countAggregation.receipts.find((item) => item.arm === 'baseline' && item.lane === 'Direct' && item.repeat === 1);
708
+ countedReceipt.user_corrections = 2;
709
+ countedReceipt.useful_reviewer_findings = 3;
710
+ countedReceipt.false_positive_reviewer_findings = 4;
711
+ const countAggregationPath = path.join(root, 'count-aggregation.json');
712
+ fs.writeFileSync(countAggregationPath, JSON.stringify(countAggregation));
713
+ const countAggregationResult = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', countAggregationPath], { encoding: 'utf8' });
714
+ assert.equal(countAggregationResult.status, 0, `${countAggregationResult.stdout}\n${countAggregationResult.stderr}`);
715
+ const countAggregationSummary = JSON.parse(countAggregationResult.stdout);
716
+ assert.equal(countAggregationSummary.groups.baseline.Direct.quality_rates.user_correction_rate, 1);
717
+ assert.equal(countAggregationSummary.groups.baseline.Direct.quality_rates.useful_reviewer_finding_rate, 2);
718
+ assert.equal(countAggregationSummary.groups.baseline.Direct.quality_rates.false_positive_reviewer_finding_rate, 2);
719
+
720
+ const degraded = JSON.parse(read(fixture.receipts));
721
+ for (const receipt of degraded.receipts.filter((item) => item.arm === 'treatment')) {
722
+ receipt.useful_reviewer_findings = 0;
723
+ receipt.false_positive_reviewer_findings = 1;
724
+ }
725
+ const degradedPath = path.join(root, 'degraded-review-quality.json');
726
+ fs.writeFileSync(degradedPath, JSON.stringify(degraded));
727
+ const degradedResult = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', degradedPath], { encoding: 'utf8' });
728
+ assert.equal(degradedResult.status, 0, `${degradedResult.stdout}\n${degradedResult.stderr}`);
729
+ const degradedSummary = JSON.parse(degradedResult.stdout);
730
+ assert.equal(degradedSummary.status, 'not-ready');
731
+ assert.equal(degradedSummary.rollout_recommendation.gates.Critical.quality_pass, false);
732
+ assert.equal(degradedSummary.rollout_recommendation.gates.Critical.pass, false);
733
+
734
+ const lowRiskQualityFailure = JSON.parse(read(fixture.receipts));
735
+ for (const receipt of lowRiskQualityFailure.receipts.filter((item) => item.lane === 'Direct')) receipt.correctness = false;
736
+ const lowRiskQualityFailurePath = path.join(root, 'low-risk-quality-failure.json');
737
+ fs.writeFileSync(lowRiskQualityFailurePath, JSON.stringify(lowRiskQualityFailure));
738
+ const lowRiskQualityFailureResult = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', lowRiskQualityFailurePath], { encoding: 'utf8' });
739
+ assert.equal(lowRiskQualityFailureResult.status, 0, `${lowRiskQualityFailureResult.stdout}\n${lowRiskQualityFailureResult.stderr}`);
740
+ const lowRiskQualityFailureSummary = JSON.parse(lowRiskQualityFailureResult.stdout);
741
+ assert.equal(lowRiskQualityFailureSummary.rollout_recommendation.gates.Direct.quality_pass, false);
742
+ assert.equal(lowRiskQualityFailureSummary.rollout_recommendation.gates.Direct.pass, false);
743
+ assert.equal(lowRiskQualityFailureSummary.rollout_recommendation.gates.Critical.pass, true);
744
+ } finally {
745
+ fs.rmSync(root, { recursive: true, force: true });
746
+ }
747
+ });
748
+
749
+ test('B1 rollout rejects incomplete fixture-only lane/repeat matrices', () => {
750
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-completeness-'));
751
+ const fixture = makeBenchmarkFixture(root);
752
+ try {
753
+ const payload = JSON.parse(read(fixture.receipts));
754
+ const completeReceipts = payload.receipts;
755
+ const partialPath = path.join(root, 'partial.json');
756
+ fs.writeFileSync(partialPath, JSON.stringify({ receipts: completeReceipts.filter((receipt) => !(receipt.arm === 'treatment' && receipt.lane === 'Direct' && receipt.repeat === 1)) }));
757
+ const partial = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', partialPath], { encoding: 'utf8' });
758
+ assert.equal(partial.status, 0, `${partial.stdout}\n${partial.stderr}`);
759
+ const partialSummary = JSON.parse(partial.stdout);
760
+ assert.equal(partialSummary.status, 'not-ready');
761
+ assert.equal(partialSummary.rollout_recommendation.gates.Direct.treatment_matrix.complete, false);
762
+ const incompleteValidation = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', partialPath], { encoding: 'utf8' });
763
+ assert.equal(incompleteValidation.status, 2);
764
+ assert.match(incompleteValidation.stderr, /incomplete receipt matrix.*treatment\/Direct\/fixture-direct\/1/);
765
+
766
+ const missingLanePath = path.join(root, 'missing-lane.json');
767
+ fs.writeFileSync(missingLanePath, JSON.stringify({ receipts: completeReceipts.filter((receipt) => !(receipt.arm === 'treatment' && receipt.lane === 'Critical')) }));
768
+ const missingLane = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', missingLanePath], { encoding: 'utf8' });
769
+ assert.equal(missingLane.status, 0, `${missingLane.stdout}\n${missingLane.stderr}`);
770
+ const missingLaneSummary = JSON.parse(missingLane.stdout);
771
+ assert.equal(missingLaneSummary.status, 'not-ready');
772
+ assert.equal(missingLaneSummary.rollout_recommendation.gates.Critical.treatment_matrix.complete, false);
773
+
774
+ const outOfRange = JSON.parse(JSON.stringify(completeReceipts));
775
+ outOfRange[0].repeat = 3;
776
+ const outOfRangePath = path.join(root, 'out-of-range.json');
777
+ fs.writeFileSync(outOfRangePath, JSON.stringify({ receipts: outOfRange }));
778
+ const invalidRepeat = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', outOfRangePath], { encoding: 'utf8' });
779
+ assert.equal(invalidRepeat.status, 2);
780
+ assert.match(invalidRepeat.stderr, /exceeds repeat_policy/);
781
+
782
+ const duplicateArm = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[0]], { encoding: 'utf8' });
783
+ assert.equal(duplicateArm.status, 2);
784
+ assert.match(duplicateArm.stderr, /duplicate arm/);
785
+
786
+ const invalidCorpus = JSON.parse(read(fixture.corpus));
787
+ delete invalidCorpus.corpus_id;
788
+ const invalidCorpusPath = path.join(root, 'missing-corpus-id.json');
789
+ fs.writeFileSync(invalidCorpusPath, JSON.stringify(invalidCorpus));
790
+ const missingCorpusId = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', invalidCorpusPath, '--config', fixture.configs[0]], { encoding: 'utf8' });
791
+ assert.equal(missingCorpusId.status, 2);
792
+ assert.match(missingCorpusId.stderr, /corpus_id.*missing or placeholder/);
793
+
794
+ const invalidCost = JSON.parse(read(fixture.configs[0]));
795
+ invalidCost.input_usd_per_1k = 'not-a-rate';
796
+ const invalidCostPath = path.join(root, 'invalid-cost.json');
797
+ fs.writeFileSync(invalidCostPath, JSON.stringify(invalidCost));
798
+ const invalidCostResult = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', invalidCostPath], { encoding: 'utf8' });
799
+ assert.equal(invalidCostResult.status, 2);
800
+ assert.match(invalidCostResult.stderr, /config\.input_usd_per_1k.*non-negative number/);
801
+ } finally {
802
+ fs.rmSync(root, { recursive: true, force: true });
803
+ }
804
+ });
805
+
806
+ test('B1 summary marks no receipts exploratory instead of claiming a run', () => {
807
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-empty-'));
808
+ const fixture = makeBenchmarkFixture(root);
809
+ try {
810
+ const result = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0]], { encoding: 'utf8' });
811
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
812
+ const summary = JSON.parse(result.stdout);
813
+ assert.equal(summary.status, 'exploratory/no-live-runs');
814
+ assert.equal(summary.live_runs, false);
815
+ } finally {
816
+ fs.rmSync(root, { recursive: true, force: true });
817
+ }
818
+ });
819
+
820
+ test('B1 run requires explicit runner contract and rejects placeholders/shell strings', () => {
821
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-run-runner-'));
822
+ const fixture = makeBenchmarkFixture(root);
823
+ try {
824
+ // Missing runner
825
+ const missing = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--out', path.join(root, 'out-missing')], { encoding: 'utf8' });
826
+ assert.equal(missing.status, 2);
827
+ assert.match(missing.stderr, /runner contract is required/);
828
+ assert.match(missing.stderr, /explicit command array/);
829
+
830
+ // Placeholder runner (command contains placeholder)
831
+ const placeholderRunner = path.join(root, 'placeholder-runner.json');
832
+ fs.writeFileSync(placeholderRunner, JSON.stringify({ schema_version: 'b1.v1', command: ['node', '{{replace_me}}'] }));
833
+ const placeholder = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', placeholderRunner, '--out', path.join(root, 'out-placeholder')], { encoding: 'utf8' });
834
+ assert.equal(placeholder.status, 2);
835
+ assert.match(placeholder.stderr, /missing or placeholder freeze field/);
836
+
837
+ // Shell string forbidden (command as string)
838
+ const shellRunner = path.join(root, 'shell-runner.json');
839
+ fs.writeFileSync(shellRunner, JSON.stringify({ schema_version: 'b1.v1', command: 'node runner.mjs' }));
840
+ const shell = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', shellRunner, '--out', path.join(root, 'out-shell')], { encoding: 'utf8' });
841
+ assert.equal(shell.status, 2);
842
+ assert.match(shell.stderr, /explicit argv|shell string is forbidden/);
843
+
844
+ // Missing out/receipts
845
+ const minimalRunner = path.join(root, 'minimal-runner.json');
846
+ fs.writeFileSync(minimalRunner, JSON.stringify({ schema_version: 'b1.v1', command: ['node', '-e', 'process.exit(0)'] }));
847
+ const noOut = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', minimalRunner], { encoding: 'utf8' });
848
+ assert.equal(noOut.status, 2);
849
+ assert.match(noOut.stderr, /output is required/);
850
+ } finally {
851
+ fs.rmSync(root, { recursive: true, force: true });
852
+ }
853
+ });
854
+
855
+ test('B1 run executes via explicit argv, captures wall_ms/artifact, and remains honest no-live-runs without execution', () => {
856
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-run-execute-'));
857
+ const fixture = makeBenchmarkFixture(root);
858
+ try {
859
+ // Honest no-live-runs: summarize without receipts must stay exploratory
860
+ const noLive = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'summarize', '--corpus', fixture.corpus, '--config', fixture.configs[0]], { encoding: 'utf8' });
861
+ assert.equal(noLive.status, 0);
862
+ const noLiveSummary = JSON.parse(noLive.stdout);
863
+ assert.equal(noLiveSummary.status, 'exploratory/no-live-runs');
864
+ assert.equal(noLiveSummary.live_runs, false);
865
+ assert.equal(noLiveSummary.rollout_recommendation.status, 'exploratory');
866
+ // Also validate without receipts is valid_no_receipts, not fabricated success
867
+ const validNoReceipts = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0]], { encoding: 'utf8' });
868
+ assert.equal(validNoReceipts.status, 0);
869
+ assert.equal(JSON.parse(validNoReceipts.stdout).status, 'valid_no_receipts');
870
+
871
+ // Create minimal deterministic runner that writes artifact and emits metrics JSON
872
+ const runnerScript = path.join(root, 'runner.mjs');
873
+ fs.writeFileSync(runnerScript, `
874
+ import fs from 'node:fs';
875
+ import path from 'node:path';
876
+ let input='';
877
+ process.stdin.on('data', c=>input+=c);
878
+ process.stdin.on('end', ()=>{
879
+ const payload=JSON.parse(input);
880
+ fs.mkdirSync(path.dirname(payload.artifact_path), {recursive:true});
881
+ fs.writeFileSync(payload.artifact_path, 'artifact:'+payload.task_id+'/'+payload.repeat+'\\n');
882
+ const out={input_tokens:100, output_tokens:50, context_loaded_tokens:500, tool_calls:2, subagent_calls:0, correctness:true, regression:false, unsupported_completion_claim:false, user_corrections:0, useful_reviewer_findings:1, false_positive_reviewer_findings:0};
883
+ process.stdout.write(JSON.stringify(out));
884
+ });
885
+ `);
886
+ const runnerJson = path.join(root, 'runner.json');
887
+ fs.writeFileSync(runnerJson, JSON.stringify({ schema_version: 'b1.v1', command: ['node', runnerScript] }));
888
+ const outDir = path.join(root, 'out-live');
889
+ const run = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', runnerJson, '--out', outDir], { encoding: 'utf8' });
890
+ assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`);
891
+ const runResult = JSON.parse(run.stdout);
892
+ assert.equal(runResult.status, 'executed');
893
+ assert.equal(runResult.live_runs, true);
894
+ assert.ok(fs.existsSync(path.join(outDir, 'receipts.json')), 'receipts.json must be written');
895
+ // Receipts must be consumable by existing validate/summarize path
896
+ const receipts = JSON.parse(fs.readFileSync(path.join(outDir, 'receipts.json'), 'utf8'));
897
+ assert.ok(Array.isArray(receipts.receipts) || Array.isArray(receipts));
898
+ const validate = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--receipts', path.join(outDir, 'receipts.json')], { encoding: 'utf8' });
899
+ assert.equal(validate.status, 0, `${validate.stdout}\n${validate.stderr}`);
900
+ assert.equal(JSON.parse(validate.stdout).status, 'valid');
901
+ // Ensure receipts capture wall_ms, command, artifact hash and are frozen
902
+ const firstReceipt = (receipts.receipts || receipts)[0];
903
+ assert.ok(typeof firstReceipt.wall_ms === 'number' && firstReceipt.wall_ms >= 0, 'wall_ms captured');
904
+ assert.ok(typeof firstReceipt.evidence.command === 'string' && firstReceipt.evidence.command.includes('node'), 'command captured');
905
+ assert.match(firstReceipt.evidence.artifact_sha256, /^sha256:[0-9a-f]{64}$/);
906
+ assert.equal(firstReceipt.corpus_sha256, JSON.parse(fs.readFileSync(fixture.corpus, 'utf8')).corpus_sha256 || runResult.corpus_sha256 || firstReceipt.corpus_sha256);
907
+ // Artifact file must exist and hash must match raw bytes
908
+ const artifactPath = path.join(outDir, firstReceipt.evidence.artifact_ref);
909
+ assert.ok(fs.existsSync(artifactPath), 'artifact file must exist inside bundle');
910
+ const bytes = fs.readFileSync(artifactPath);
911
+ const expectedHash = `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
912
+ assert.equal(firstReceipt.evidence.artifact_sha256, expectedHash);
913
+ // Ensure no secrets leakage: receipts must not contain env secrets (check that known secret pattern not in file)
914
+ const receiptsText = fs.readFileSync(path.join(outDir, 'receipts.json'), 'utf8');
915
+ assert.doesNotMatch(receiptsText, /OPENAI_API_KEY|AWS_SECRET|GITHUB_TOKEN/);
916
+ // Ensure artifact_ref is relative and does not escape
917
+ assert.doesNotMatch(firstReceipt.evidence.artifact_ref, /^\//);
918
+ assert.doesNotMatch(firstReceipt.evidence.artifact_ref, /^\.\./);
919
+ assert.doesNotMatch(firstReceipt.evidence.artifact_ref, /^https?:/);
920
+ } finally {
921
+ fs.rmSync(root, { recursive: true, force: true });
922
+ }
923
+ });
924
+
925
+ test('B1 run rejects mismatched corpus hash, artifact escape, and partial matrices fail-closed', () => {
926
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-run-safety-'));
927
+ const fixture = makeBenchmarkFixture(root);
928
+ try {
929
+ const runnerScript = path.join(root, 'safe-runner.mjs');
930
+ fs.writeFileSync(runnerScript, `
931
+ import fs from 'node:fs';
932
+ import path from 'node:path';
933
+ let input='';
934
+ process.stdin.on('data', c=>input+=c);
935
+ process.stdin.on('end', ()=>{
936
+ const payload=JSON.parse(input);
937
+ fs.mkdirSync(path.dirname(payload.artifact_path), {recursive:true});
938
+ fs.writeFileSync(payload.artifact_path, 'ok');
939
+ const out={input_tokens:100, output_tokens:50, context_loaded_tokens:500, tool_calls:2, subagent_calls:0, correctness:true, regression:false, unsupported_completion_claim:false, user_corrections:0, useful_reviewer_findings:1, false_positive_reviewer_findings:0};
940
+ process.stdout.write(JSON.stringify(out));
941
+ });
942
+ `);
943
+ const runnerJson = path.join(root, 'runner.json');
944
+ fs.writeFileSync(runnerJson, JSON.stringify({ schema_version: 'b1.v1', command: ['node', runnerScript] }));
945
+
946
+ // Mismatched corpus_sha256 in config must be rejected before execution
947
+ const badCorpus = JSON.parse(fs.readFileSync(fixture.corpus, 'utf8'));
948
+ badCorpus.corpus_id = 'tampered-id';
949
+ const badCorpusPath = path.join(root, 'bad-corpus.json');
950
+ fs.writeFileSync(badCorpusPath, JSON.stringify(badCorpus));
951
+ const mismatch = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', badCorpusPath, '--config', fixture.configs[0], '--runner', runnerJson, '--out', path.join(root, 'out-mismatch')], { encoding: 'utf8' });
952
+ assert.equal(mismatch.status, 2);
953
+ assert.match(mismatch.stderr, /corpus_sha256.*does not match corpus|freeze hash mismatch/);
954
+
955
+ // Artifact escape via validator: craft receipt with traversal artifact_ref and expect reject
956
+ const traversalReceipts = JSON.parse(fs.readFileSync(fixture.receipts, 'utf8'));
957
+ const outsideArtifact = path.join(path.dirname(root), 'outside.bin');
958
+ fs.writeFileSync(outsideArtifact, Buffer.from('outside'));
959
+ traversalReceipts.receipts[0].evidence.artifact_ref = path.relative(root, outsideArtifact);
960
+ traversalReceipts.receipts[0].evidence.artifact_sha256 = `sha256:${crypto.createHash('sha256').update(fs.readFileSync(outsideArtifact)).digest('hex')}`;
961
+ const traversalPath = path.join(root, 'traversal.json');
962
+ fs.writeFileSync(traversalPath, JSON.stringify(traversalReceipts));
963
+ const traversal = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--config', fixture.configs[1], '--receipts', traversalPath], { encoding: 'utf8' });
964
+ assert.equal(traversal.status, 2);
965
+ assert.match(traversal.stderr, /path escapes receipt bundle directory/);
966
+ fs.rmSync(outsideArtifact, { force: true });
967
+
968
+ // Partial matrix: run with runner that fails on one task/repeat must not be reported as valid success
969
+ // Simulate by creating a runner that exits non-zero for one specific task
970
+ const flakyRunner = path.join(root, 'flaky-runner.mjs');
971
+ fs.writeFileSync(flakyRunner, `
972
+ import fs from 'node:fs';
973
+ import path from 'node:path';
974
+ let input='';
975
+ process.stdin.on('data', c=>input+=c);
976
+ process.stdin.on('end', ()=>{
977
+ const payload=JSON.parse(input);
978
+ if (payload.task_id==='fixture-direct' && payload.repeat===1 && payload.arm==='baseline') process.exit(7);
979
+ fs.mkdirSync(path.dirname(payload.artifact_path), {recursive:true});
980
+ fs.writeFileSync(payload.artifact_path, 'ok');
981
+ const out={input_tokens:100, output_tokens:50, context_loaded_tokens:500, tool_calls:2, subagent_calls:0, correctness:true, regression:false, unsupported_completion_claim:false, user_corrections:0, useful_reviewer_findings:1, false_positive_reviewer_findings:0};
982
+ process.stdout.write(JSON.stringify(out));
983
+ });
984
+ `);
985
+ const flakyJson = path.join(root, 'flaky.json');
986
+ fs.writeFileSync(flakyJson, JSON.stringify({ schema_version: 'b1.v1', command: ['node', flakyRunner] }));
987
+ const flakyOut = path.join(root, 'out-flaky');
988
+ const flakyRun = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', flakyJson, '--out', flakyOut], { encoding: 'utf8' });
989
+ assert.equal(flakyRun.status, 2);
990
+ assert.match(flakyRun.stderr, /runner exited non-zero|no receipt fabricated/);
991
+ // No valid live success should be claimed from partial run
992
+ if (fs.existsSync(path.join(flakyOut, 'receipts.json'))) {
993
+ const maybeReceipts = JSON.parse(fs.readFileSync(path.join(flakyOut, 'receipts.json'), 'utf8'));
994
+ if (maybeReceipts.receipts) {
995
+ const incompleteValidate = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'validate', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--receipts', path.join(flakyOut, 'receipts.json')], { encoding: 'utf8' });
996
+ assert.equal(incompleteValidate.status, 2);
997
+ assert.match(incompleteValidate.stderr, /incomplete receipt matrix|runner exited/);
998
+ }
999
+ }
1000
+ } finally {
1001
+ fs.rmSync(root, { recursive: true, force: true });
1002
+ }
1003
+ });
1004
+
1005
+ test('B1 runner secret safety: command argv with secret-like assignment/flag is fail-closed and stderr is redacted', () => {
1006
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-b1-secret-'));
1007
+ const fixture = makeBenchmarkFixture(root);
1008
+ try {
1009
+ // Helper runner that would succeed if not blocked
1010
+ const okRunnerScript = path.join(root, 'ok.mjs');
1011
+ fs.writeFileSync(okRunnerScript, `
1012
+ import fs from 'node:fs';
1013
+ import path from 'node:path';
1014
+ let input='';
1015
+ process.stdin.on('data', c=>input+=c);
1016
+ process.stdin.on('end', ()=>{
1017
+ const p=JSON.parse(input);
1018
+ fs.mkdirSync(path.dirname(p.artifact_path), {recursive:true});
1019
+ fs.writeFileSync(p.artifact_path,'ok');
1020
+ process.stdout.write(JSON.stringify({input_tokens:1,output_tokens:1,context_loaded_tokens:1,tool_calls:0,subagent_calls:0,correctness:true,regression:false,unsupported_completion_claim:false,user_corrections:0,useful_reviewer_findings:0,false_positive_reviewer_findings:0}));
1021
+ });
1022
+ `);
1023
+ // Secret-like argv must be rejected fail-closed, value not shown, keep shell:false
1024
+ const cases = [
1025
+ { desc: 'env assignment', command: ['node', okRunnerScript, 'OPENAI_API_KEY=sk-1234567890abcdefghij123456'] },
1026
+ { desc: 'flag equals', command: ['node', okRunnerScript, '--api-key=sk-1234567890abcdefghij123456'] },
1027
+ { desc: 'flag spaced', command: ['node', okRunnerScript, '--api-key', 'sk-1234567890abcdefghij123456'] },
1028
+ { desc: 'password', command: ['node', okRunnerScript, '--password=supersecret1234'] },
1029
+ { desc: 'jwt', command: ['node', okRunnerScript, '--jwt-secret=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9abcdefghij'] },
1030
+ { desc: 'github token', command: ['node', okRunnerScript, 'GITHUB_TOKEN=ghp_1234567890abcdefghijklmnopqrstuv'] },
1031
+ ];
1032
+ for (const c of cases) {
1033
+ const runnerJson = path.join(root, `runner-${c.desc.replace(/\s+/g,'-')}.json`);
1034
+ fs.writeFileSync(runnerJson, JSON.stringify({ schema_version: 'b1.v1', command: c.command }));
1035
+ const out = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', runnerJson, '--out', path.join(root, `out-${c.desc}`)], { encoding: 'utf8' });
1036
+ assert.equal(out.status, 2, `case ${c.desc} should be blocked`);
1037
+ assert.match(out.stderr, /secret-like assignment\/flag is forbidden/);
1038
+ assert.match(out.stderr, /Value not shown/);
1039
+ // Must not leak secret value in error or evidence
1040
+ assert.doesNotMatch(out.stderr, /sk-1234567890/);
1041
+ assert.doesNotMatch(out.stderr, /supersecret/);
1042
+ assert.doesNotMatch(out.stderr, /ghp_/);
1043
+ assert.doesNotMatch(out.stderr, /eyJhbGci/);
1044
+ // Also ensure stderr snippet does not contain raw secret if leaked via stderr
1045
+ // (the runner never executed, so no stderr from runner, just validation error)
1046
+ }
1047
+
1048
+ // Safe names must NOT be flagged (no false positive)
1049
+ const safeCases = [
1050
+ ['node', okRunnerScript, '--tokenizer', 'bert-base'],
1051
+ ['node', okRunnerScript, '--api-key-file', '/tmp/keyfile'],
1052
+ ['node', okRunnerScript, '--token-path', '/tmp/token'],
1053
+ ['node', okRunnerScript, '--password-hint', 'my hint'],
1054
+ ['node', okRunnerScript, '--password-label', 'label'],
1055
+ ];
1056
+ for (const [idx, command] of safeCases.entries()) {
1057
+ const runnerJson = path.join(root, `safe-${idx}.json`);
1058
+ fs.writeFileSync(runnerJson, JSON.stringify({ schema_version: 'b1.v1', command }));
1059
+ const out = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', runnerJson, '--out', path.join(root, `out-safe-${idx}`)], { encoding: 'utf8' });
1060
+ // Should not be blocked for secret-like; it should either succeed or fail for other reasons but not secret-like
1061
+ assert.doesNotMatch(out.stderr, /secret-like assignment\/flag is forbidden/, `safe case ${command.join(' ')} must not be flagged`);
1062
+ assert.equal(out.status, 0, `safe case ${command.join(' ')} should execute (got ${out.stderr})`);
1063
+ // Clean out for next
1064
+ fs.rmSync(path.join(root, `out-safe-${idx}`), { recursive: true, force: true });
1065
+ }
1066
+
1067
+ // Stderr redaction: runner that exits non-zero and leaks secret in stderr must have [REDACTED] and not raw secret
1068
+ const leakyRunner = path.join(root, 'leaky.mjs');
1069
+ fs.writeFileSync(leakyRunner, `
1070
+ import fs from 'node:fs';
1071
+ let input='';
1072
+ process.stdin.on('data', c=>input+=c);
1073
+ process.stdin.on('end', ()=>{
1074
+ console.error('failed to connect with OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuv and token ghp_1234567890abcdefghijklmnopqrstuv');
1075
+ console.error('also password supersecret1234 and jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9abcdefghij');
1076
+ process.exit(2);
1077
+ });
1078
+ `);
1079
+ const leakyJson = path.join(root, 'leaky.json');
1080
+ fs.writeFileSync(leakyJson, JSON.stringify({ schema_version: 'b1.v1', command: ['node', leakyRunner] }));
1081
+ const leakyOut = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', leakyJson, '--out', path.join(root, 'out-leaky')], { encoding: 'utf8' });
1082
+ assert.equal(leakyOut.status, 2);
1083
+ assert.match(leakyOut.stderr, /runner exited non-zero/);
1084
+ assert.match(leakyOut.stderr, /\[REDACTED\]/);
1085
+ assert.doesNotMatch(leakyOut.stderr, /sk-1234567890/);
1086
+ assert.doesNotMatch(leakyOut.stderr, /ghp_1234567890/);
1087
+ assert.doesNotMatch(leakyOut.stderr, /supersecret/);
1088
+ assert.doesNotMatch(leakyOut.stderr, /eyJhbGci/);
1089
+ // Ensure no receipt was fabricated
1090
+ assert.equal(fs.existsSync(path.join(root, 'out-leaky', 'receipts.json')), false, 'no receipt should be fabricated on stderr secret leak');
1091
+
1092
+ // Also test that evidence.command never contains secret when runner would have been allowed (it is blocked, so no evidence)
1093
+ // For a runner that does not contain secret, evidence should contain the safe command
1094
+ const safeRunnerForEvidence = path.join(root, 'evidence.mjs');
1095
+ fs.writeFileSync(safeRunnerForEvidence, `
1096
+ import fs from 'node:fs';
1097
+ import path from 'node:path';
1098
+ let input='';
1099
+ process.stdin.on('data', c=>input+=c);
1100
+ process.stdin.on('end', ()=>{
1101
+ const p=JSON.parse(input);
1102
+ fs.mkdirSync(path.dirname(p.artifact_path),{recursive:true});
1103
+ fs.writeFileSync(p.artifact_path,'evidence');
1104
+ process.stdout.write(JSON.stringify({input_tokens:1,output_tokens:1,context_loaded_tokens:1,tool_calls:0,subagent_calls:0,correctness:true,regression:false,unsupported_completion_claim:false,user_corrections:0,useful_reviewer_findings:0,false_positive_reviewer_findings:0}));
1105
+ });
1106
+ `);
1107
+ const evidenceRunnerJson = path.join(root, 'evidence.json');
1108
+ fs.writeFileSync(evidenceRunnerJson, JSON.stringify({ schema_version: 'b1.v1', command: ['node', safeRunnerForEvidence, '--verbose'] }));
1109
+ const evidenceOut = path.join(root, 'out-evidence');
1110
+ const evidenceRun = spawnSync(process.execPath, [BENCHMARK_SCRIPT, 'run', '--corpus', fixture.corpus, '--config', fixture.configs[0], '--runner', evidenceRunnerJson, '--out', evidenceOut], { encoding: 'utf8' });
1111
+ assert.equal(evidenceRun.status, 0);
1112
+ const evidenceReceipts = JSON.parse(fs.readFileSync(path.join(evidenceOut, 'receipts.json'), 'utf8'));
1113
+ const ev = (evidenceReceipts.receipts || evidenceReceipts)[0].evidence;
1114
+ assert.match(ev.command, /node/);
1115
+ assert.doesNotMatch(ev.command, /sk-|ghp_|eyJ/);
1116
+ assert.doesNotMatch(JSON.stringify(evidenceReceipts), /OPENAI_API_KEY/);
1117
+ } finally {
1118
+ fs.rmSync(root, { recursive: true, force: true });
1119
+ }
1120
+ });