@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.
Files changed (98) hide show
  1. package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
  2. package/bin/__tests__/codex-hooks.test.js +531 -29
  3. package/bin/__tests__/codex-native.test.js +228 -7
  4. package/bin/__tests__/develop-contract.test.js +653 -185
  5. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
  6. package/bin/__tests__/installer-safety.test.js +39 -0
  7. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  8. package/bin/__tests__/package-inventory.test.js +705 -13
  9. package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
  10. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
  11. package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
  12. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
  13. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
  14. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  15. package/bin/__tests__/validator-grounding.test.js +2013 -221
  16. package/bin/lib/managed-writer.js +12 -3
  17. package/bin/phases/codex-runtime.js +17 -1
  18. package/package.json +5 -2
  19. package/src/claude/CLAUDE.md +6 -0
  20. package/src/claude/agents/code-auditor.md +20 -0
  21. package/src/claude/agents/spec-maker.md +143 -99
  22. package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
  23. package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
  24. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  25. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  26. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  27. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  28. package/src/claude/hooks/completion-authority.cjs +13 -7
  29. package/src/claude/hooks/docs-sync.cjs +5 -2
  30. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  31. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  32. package/src/claude/hooks/privacy-block.cjs +99 -28
  33. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  34. package/src/claude/hooks/spec-gate.cjs +47 -117
  35. package/src/claude/hooks/spec-state.cjs +8 -2
  36. package/src/claude/migration-manifest.json +9 -1
  37. package/src/claude/rules/state-sync.md +52 -21
  38. package/src/claude/rules/workflow.md +17 -9
  39. package/src/claude/scripts/change-firewall.cjs +1748 -0
  40. package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
  41. package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
  42. package/src/claude/scripts/spec-final-state.cjs +290 -0
  43. package/src/claude/scripts/spec-ground.cjs +571 -173
  44. package/src/claude/scripts/spec-readiness.cjs +425 -0
  45. package/src/claude/scripts/spec-receipt.cjs +162 -0
  46. package/src/claude/scripts/spec-resolver.cjs +67 -16
  47. package/src/claude/scripts/spec-scaffold.cjs +1594 -139
  48. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  49. package/src/claude/scripts/validate-spec-output.cjs +3110 -218
  50. package/src/claude/scripts/workflow-policy.cjs +501 -183
  51. package/src/claude/settings/settings.json +4 -0
  52. package/src/claude/skills/code-review/SKILL.md +12 -3
  53. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  54. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  55. package/src/claude/skills/develop/SKILL.md +47 -60
  56. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  57. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  58. package/src/claude/skills/specs/SKILL.md +183 -177
  59. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  60. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  61. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  62. package/src/claude/skills/specs/references/review.md +214 -262
  63. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  64. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  65. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  66. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  67. package/src/claude/skills/specs/rules/design-review.md +42 -97
  68. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  69. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  70. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  71. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  72. package/src/claude/skills/specs/templates/design.md +49 -263
  73. package/src/claude/skills/specs/templates/requirements.md +26 -36
  74. package/src/claude/skills/specs/templates/research.md +9 -98
  75. package/src/claude/skills/specs/templates/spec-state.json +35 -85
  76. package/src/claude/skills/specs/templates/task.md +30 -102
  77. package/src/claude/skills/sync/SKILL.md +33 -10
  78. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  79. package/src/claude/skills/test/SKILL.md +17 -7
  80. package/src/codex/AGENTS.md +6 -0
  81. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  82. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  83. package/src/codex/hooks/completion-authority.cjs +13 -7
  84. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  85. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  86. package/src/codex/hooks/lib/privacy-state.cjs +61 -38
  87. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  88. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  89. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  90. package/src/codex/hooks/privacy-approval.cjs +6 -2
  91. package/src/codex/hooks/privacy-block.cjs +94 -15
  92. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  93. package/src/codex/hooks/spec-gate.cjs +49 -34
  94. package/src/codex/hooks/spec-state.cjs +10 -3
  95. package/src/codex/hooks/state.cjs +2 -0
  96. package/src/codex/hooks.json +6 -1
  97. package/src/codex/rules/state-sync.md +47 -27
  98. package/src/common/AGENTS.md +1 -1
@@ -14,6 +14,14 @@ const REQUIRED_PAYLOAD = [
14
14
  'src/claude/scripts/scan-staged-secrets.cjs',
15
15
  'src/claude/scripts/workflow-policy.cjs',
16
16
  'src/claude/scripts/provenance.cjs',
17
+ 'src/claude/scripts/spec-receipt.cjs',
18
+ 'src/claude/scripts/spec-ground.cjs',
19
+ 'src/claude/scripts/spec-final-state.cjs',
20
+ 'src/claude/scripts/spec-readiness.cjs',
21
+ 'src/claude/scripts/spec-semantic-model.cjs',
22
+ 'src/claude/scripts/validate-spec-output.cjs',
23
+ 'src/claude/scripts/spec-authoring-validation.cjs',
24
+ 'src/claude/scripts/spec-authoring-digest.cjs',
17
25
  ];
18
26
  const FORBIDDEN_PAYLOAD = [
19
27
  /(^|\/)\.logs(\/|$)/,
@@ -31,12 +39,18 @@ const RUNTIMES = {
31
39
  };
32
40
 
33
41
  function npmPack(args, cwd) {
34
- const result = spawnSync('npm', ['pack', '--ignore-scripts', ...args], {
35
- cwd,
36
- encoding: 'utf8',
37
- });
38
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
39
- return JSON.parse(result.stdout)[0];
42
+ const cache = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-npm-pack-cache-'));
43
+ try {
44
+ const result = spawnSync('npm', ['pack', '--ignore-scripts', ...args], {
45
+ cwd,
46
+ encoding: 'utf8',
47
+ env: { ...process.env, npm_config_cache: cache },
48
+ });
49
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
50
+ return JSON.parse(result.stdout)[0];
51
+ } finally {
52
+ fs.rmSync(cache, { recursive: true, force: true });
53
+ }
40
54
  }
41
55
 
42
56
  function packedInventory(tarball) {
@@ -62,12 +76,47 @@ function assertCleanInventory(inventory) {
62
76
  }
63
77
  }
64
78
 
65
- function installPacked(tarball, root) {
79
+ function packageDirectory(name, from) {
80
+ let directory = path.dirname(require.resolve(name, { paths: [from] }));
81
+ while (directory !== path.dirname(directory)) {
82
+ const manifest = path.join(directory, 'package.json');
83
+ if (fs.existsSync(manifest)) {
84
+ const metadata = JSON.parse(fs.readFileSync(manifest, 'utf8'));
85
+ if (metadata.name === name) return { directory, metadata };
86
+ }
87
+ directory = path.dirname(directory);
88
+ }
89
+ throw new Error(`cannot resolve local runtime dependency ${name}`);
90
+ }
91
+
92
+ function packedRuntimeClosure(destination) {
93
+ fs.mkdirSync(destination, { recursive: true });
94
+ const rootManifest = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
95
+ const queue = Object.keys(rootManifest.dependencies || {});
96
+ const visited = new Set();
97
+ const tarballs = [];
98
+ while (queue.length > 0) {
99
+ const name = queue.shift();
100
+ if (visited.has(name)) continue;
101
+ visited.add(name);
102
+ const resolved = packageDirectory(name, PACKAGE_ROOT);
103
+ const packed = npmPack(['--ignore-scripts', '--pack-destination', destination, '--json'], resolved.directory);
104
+ tarballs.push(path.join(destination, packed.filename));
105
+ queue.push(...Object.keys(resolved.metadata.dependencies || {}));
106
+ }
107
+ return tarballs.sort();
108
+ }
109
+
110
+ function installPacked(tarball, root, runtimeClosure) {
66
111
  fs.mkdirSync(root, { recursive: true });
67
112
  const result = spawnSync('npm', [
68
113
  'install', '--offline', '--ignore-scripts', '--no-audit', '--no-fund',
69
- '--package-lock=false', '--prefix', root, tarball,
70
- ], { cwd: root, encoding: 'utf8' });
114
+ '--package-lock=false', '--prefix', root, tarball, ...runtimeClosure,
115
+ ], {
116
+ cwd: root,
117
+ encoding: 'utf8',
118
+ env: { ...process.env, npm_config_cache: path.join(root, '.cafekit-npm-cache') },
119
+ });
71
120
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
72
121
  const installer = path.join(root, 'node_modules', '@haposoft', 'cafekit', 'bin', 'install.js');
73
122
  assert.ok(fs.existsSync(installer), 'npm install must resolve packed package bin');
@@ -129,6 +178,9 @@ function assertInstalledProvenance(root, platform, fixture) {
129
178
  const helper = path.join(scripts, 'provenance.cjs');
130
179
  const policy = path.join(scripts, 'workflow-policy.cjs');
131
180
  const resolver = path.join(scripts, 'spec-resolver.cjs');
181
+ const receipt = path.join(scripts, 'spec-receipt.cjs');
182
+ const finalState = path.join(scripts, 'spec-final-state.cjs');
183
+ const readiness = path.join(scripts, 'spec-readiness.cjs');
132
184
  const gate = path.join(
133
185
  root,
134
186
  platform === 'claude' ? '.claude/hooks/spec-gate.cjs' : '.codex/hooks/spec-gate.cjs'
@@ -137,14 +189,37 @@ function assertInstalledProvenance(root, platform, fixture) {
137
189
  const authority = path.join(authorityDir, 'completion-authority.cjs');
138
190
  const authorityCheck = path.join(authorityDir, 'completion-authority-check.cjs');
139
191
  const authorityState = path.join(authorityDir, 'completion-authority-state.cjs');
140
- for (const file of [helper, policy, resolver]) {
192
+ const semanticAuthority = path.join(authorityDir, 'semantic-review-authority.cjs');
193
+ for (const file of [helper, policy, resolver, receipt, finalState, readiness]) {
141
194
  assert.equal(fs.existsSync(file), true, `${platform} installed file missing: ${file}`);
142
195
  }
143
196
  assert.equal(fs.existsSync(gate), true, `${platform} installed gate missing: ${gate}`);
144
197
 
145
- for (const file of [authority, authorityCheck, authorityState]) {
198
+ for (const file of [authority, authorityCheck, authorityState, semanticAuthority]) {
146
199
  assert.equal(fs.existsSync(file), true, `${platform} installed completion authority missing: ${file}`);
147
200
  }
201
+ const finalStateBytes = fs.readFileSync(finalState);
202
+ fs.rmSync(finalState);
203
+ const missingFinalState = spawnSync(process.execPath, [authority, '--stop'], {
204
+ cwd: root,
205
+ env: { ...process.env, PROJECT_ROOT: root },
206
+ input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
207
+ encoding: 'utf8',
208
+ });
209
+ assert.equal(JSON.parse(missingFinalState.stdout).decision, 'block', `${platform} missing final-state closure must fail closed`);
210
+ const escapedFinalState = path.join(root, 'escaped-spec-final-state.cjs');
211
+ fs.writeFileSync(escapedFinalState, finalStateBytes);
212
+ fs.symlinkSync(escapedFinalState, finalState);
213
+ const symlinkFinalState = spawnSync(process.execPath, [authority, '--stop'], {
214
+ cwd: root,
215
+ env: { ...process.env, PROJECT_ROOT: root },
216
+ input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
217
+ encoding: 'utf8',
218
+ });
219
+ assert.equal(JSON.parse(symlinkFinalState.stdout).decision, 'block', `${platform} symlinked final-state closure must fail closed`);
220
+ fs.rmSync(finalState);
221
+ fs.writeFileSync(finalState, finalStateBytes);
222
+ fs.rmSync(escapedFinalState);
148
223
  const authorityCheckBytes = fs.readFileSync(authorityCheck);
149
224
  fs.writeFileSync(authorityCheck, 'module.exports = {};\n');
150
225
  const malformedAuthority = spawnSync(process.execPath, [authority, '--stop'], {
@@ -304,13 +379,26 @@ function assertInstalledScripts(root, platform) {
304
379
  const scripts = path.join(root, RUNTIMES[platform].root, 'scripts');
305
380
  const policy = path.join(scripts, 'workflow-policy.cjs');
306
381
  const scanner = path.join(scripts, 'scan-staged-secrets.cjs');
382
+ const grounder = path.join(scripts, 'spec-ground.cjs');
383
+ const finalState = path.join(scripts, 'spec-final-state.cjs');
384
+ const readiness = path.join(scripts, 'spec-readiness.cjs');
385
+ const validator = path.join(scripts, 'validate-spec-output.cjs');
307
386
  assert.ok(fs.existsSync(policy), `installed policy missing: ${policy}`);
308
387
  assert.ok(fs.existsSync(scanner), `installed scanner missing: ${scanner}`);
388
+ assert.ok(fs.existsSync(grounder), `installed validator dependency missing: ${grounder}`);
389
+ assert.ok(fs.existsSync(finalState), `installed final-state dependency missing: ${finalState}`);
390
+ assert.ok(fs.existsSync(readiness), `installed readiness finalizer missing: ${readiness}`);
391
+ assert.ok(fs.existsSync(validator), `installed validator missing: ${validator}`);
309
392
 
310
393
  const policyRun = spawnSync(process.execPath, [policy, '--json'], { cwd: root, encoding: 'utf8' });
311
394
  assert.equal(policyRun.status, 0, `${policyRun.stdout}\n${policyRun.stderr}`);
312
395
  assert.equal(JSON.parse(policyRun.stdout).contract, 'execution-policy');
313
396
 
397
+ const validatorRun = spawnSync(process.execPath, [validator], { cwd: root, encoding: 'utf8' });
398
+ assert.equal(validatorRun.status, 2, `${validatorRun.stdout}\n${validatorRun.stderr}`);
399
+ assert.match(validatorRun.stderr, /Usage: .*validate-spec-output\.cjs/);
400
+ assert.doesNotMatch(validatorRun.stderr, /MODULE_NOT_FOUND|Cannot find module/);
401
+
314
402
  const safe = path.join(root, 'safe.txt');
315
403
  fs.writeFileSync(safe, 'mode=safe\n');
316
404
  spawnSync('git', ['init', '-q'], { cwd: root });
@@ -320,6 +408,421 @@ function assertInstalledScripts(root, platform) {
320
408
  assert.match(scannerRun.stdout, /No staged secrets found/);
321
409
  }
322
410
 
411
+ function installedSemanticPaths(root, platform) {
412
+ const runtimeRoot = path.join(root, RUNTIMES[platform].root);
413
+ const skillRoot = platform === 'codex'
414
+ ? path.join(root, '.agents', 'skills')
415
+ : path.join(runtimeRoot, 'skills');
416
+ const paths = {
417
+ policy: path.join(runtimeRoot, 'scripts', 'workflow-policy.cjs'),
418
+ scaffold: path.join(runtimeRoot, 'scripts', 'spec-scaffold.cjs'),
419
+ validator: path.join(runtimeRoot, 'scripts', 'validate-spec-output.cjs'),
420
+ authoringValidator: path.join(runtimeRoot, 'scripts', 'spec-authoring-validation.cjs'),
421
+ authoringDigest: path.join(runtimeRoot, 'scripts', 'spec-authoring-digest.cjs'),
422
+ grounder: path.join(runtimeRoot, 'scripts', 'spec-ground.cjs'),
423
+ finalState: path.join(runtimeRoot, 'scripts', 'spec-final-state.cjs'),
424
+ readiness: path.join(runtimeRoot, 'scripts', 'spec-readiness.cjs'),
425
+ semanticModel: path.join(runtimeRoot, 'scripts', 'spec-semantic-model.cjs'),
426
+ resolver: path.join(runtimeRoot, 'scripts', 'spec-resolver.cjs'),
427
+ provenance: path.join(runtimeRoot, 'scripts', 'provenance.cjs'),
428
+ completion: path.join(runtimeRoot, 'hooks', 'completion-authority-check.cjs'),
429
+ semanticAuthority: path.join(runtimeRoot, 'hooks', 'semantic-review-authority.cjs'),
430
+ stateTemplate: path.join(skillRoot, 'specs', 'templates', 'spec-state.json'),
431
+ };
432
+ for (const [name, target] of Object.entries(paths)) {
433
+ assert.equal(fs.existsSync(target), true, `${platform} installed ${name} missing: ${target}`);
434
+ assert.equal(path.relative(root, target).startsWith('..'), false, `${platform} ${name} escaped install root`);
435
+ }
436
+ return paths;
437
+ }
438
+
439
+ function runInstalled(script, args, root, expected = 0) {
440
+ const result = spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
441
+ assert.equal(result.status, expected, `${script}\n${result.stdout}\n${result.stderr}`);
442
+ return result;
443
+ }
444
+
445
+ function writeJson(target, value) {
446
+ fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
447
+ }
448
+
449
+ function materializeSpecState(paths, root, feature, { strict = false } = {}) {
450
+ const featureDir = path.join(root, 'specs', feature);
451
+ runInstalled(paths.scaffold, [feature], root);
452
+ const state = JSON.parse(fs.readFileSync(path.join(featureDir, 'spec.json'), 'utf8'));
453
+ state.scope_lock.in_scope = [`${feature} behavior`];
454
+ state.scope_lock.out_of_scope = ['unrelated behavior'];
455
+ if (strict) {
456
+ state.workflow_policy.assurance_level = 'Strict';
457
+ state.workflow_policy.classified_minimum.assurance_level = 'Strict';
458
+ state.workflow_policy.risks = ['auth'];
459
+ }
460
+ writeJson(path.join(featureDir, 'spec.json'), state);
461
+ return { featureDir, specFile: path.join(featureDir, 'spec.json'), state };
462
+ }
463
+
464
+ function requirementsProjection({ proof = false } = {}) {
465
+ return `# Requirements
466
+
467
+ ## Requirements
468
+
469
+ ### Requirement 1: Installed behavior
470
+
471
+ - **R1.1**: The CommonJS entrypoint \`src/entry.js\` shall return an object containing only status \`enabled\` when its sole \`valid\` argument is the boolean \`true\`; it shall return an object containing only status \`rejected\` for \`false\`, a missing argument, or any non-boolean value.${proof ? '\n- **R1.2**: The installed verifier shall prove the observed entry behavior through its grounded evidence boundary.' : ''}
472
+ `;
473
+ }
474
+
475
+ const TYPED_ANCHOR_COLUMNS = ['ID', 'Type', 'Target', 'Role', 'Access', 'Action'];
476
+
477
+ function typedAnchorTable(rows) {
478
+ assert.ok(Array.isArray(rows) && rows.length > 0, 'typed anchor projection requires rows');
479
+ const header = `| ${TYPED_ANCHOR_COLUMNS.join(' | ')} |`;
480
+ const separator = `|${TYPED_ANCHOR_COLUMNS.map(() => '---').join('|')}|`;
481
+ const body = rows.map((row) => {
482
+ assert.deepEqual(Object.keys(row), ['id', 'type', 'target', 'role', 'access', 'action']);
483
+ assert.ok(Object.values(row).every((value) => typeof value === 'string' && value.trim() !== ''));
484
+ return `| ${row.id} | ${row.type} | \`${row.target}\` | ${row.role} | ${row.access} | ${row.action} |`;
485
+ });
486
+ return [header, separator, ...body].join('\n');
487
+ }
488
+
489
+ function designProjection({
490
+ expected = 'exit 0; boolean true returns the exact enabled object; false, undefined, null, `"true"`, 1, and an object return the exact rejected object',
491
+ taskful = false,
492
+ } = {}) {
493
+ const anchors = typedAnchorTable([{
494
+ id: 'A-D-01', type: 'file', target: taskful ? 'src/design-boundary.js' : 'src/entry.js',
495
+ role: taskful ? 'existing design boundary for the entrypoint' : 'existing runtime entrypoint and contract owner',
496
+ access: 'read', action: 'read',
497
+ }]);
498
+ const reachabilityAnchors = taskful ? 'A-R1-01-01, A-R1-02-02' : 'A-D-01';
499
+ return `# Design
500
+
501
+ ## Boundary
502
+
503
+ - **Owns:** The exact return contract of the CommonJS function exported by \`src/entry.js\`.
504
+ - **Reads:** One \`valid\` argument; only the boolean \`true\` is valid.
505
+ - **Writes/exposes:** Exactly one status field whose value is \`enabled\` or \`rejected\`.
506
+ - **Outside boundary:** Unrelated runtime behavior.
507
+
508
+ ## Typed Anchors
509
+
510
+ ${anchors}
511
+
512
+ ## Decisions and Invariants
513
+
514
+ ### D1 — Installed result decision
515
+
516
+ - **Decision:** \`valid === true\` returns an object containing only status \`enabled\`; every other value returns an object containing only status \`rejected\`.
517
+ - **Rejects ambiguity:** Truthy non-booleans such as \`"true"\` never count as valid.
518
+ - **Negative path:** \`false\`, \`undefined\`, \`null\`, strings, numbers, and objects return rejected.
519
+ - **Anchors:** A-D-01
520
+
521
+ ### I1 — Rejection invariant
522
+
523
+ Any input other than the boolean \`true\` never returns enabled.
524
+
525
+ ### C1 — Result contract
526
+
527
+ - **Owner:** A-D-01
528
+ - **Consumers:** \`test/entry.test.js\` and callers of the CommonJS export.
529
+ - **Shape/behavior:** The result is an object with exactly one \`status\` field set to \`enabled\` or \`rejected\`.
530
+ - **Compatibility:** The strict boolean discriminator, exact object shape, and two status values remain stable.
531
+
532
+ ## Verification Definitions
533
+
534
+ - **V1**: Criteria R1.1; Owner ${taskful ? 'R1-01' : 'A-D-01'}; ${taskful ? 'Proof criteria R1.2; Proof owner R1-02; Evidence anchor A-R1-02-02; ' : ''}Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected ${expected}; Negative/failure \`false\`, missing input, \`null\`, and truthy non-booleans all return the exact rejected object; Reachability/grounding entrypoint \`src/entry.js\` via ${reachabilityAnchors}.
535
+ `;
536
+ }
537
+
538
+ function completeSemanticReview(paths, root, fixture, counterexamples) {
539
+ // C16/D13: only the installed spec-authoring-validation.cjs coordinator may
540
+ // flip authoring.* to validated and write the matching receipt (I21/R3.9) —
541
+ // run it over the fixture's current bytes instead of hand-writing the enum.
542
+ // Lifecycle order (I15/R3.7): authoring -> coordinator -> semantic review ->
543
+ // readiness, never the reverse.
544
+ runInstalled(paths.scaffold, [fixture.state.feature_name, '--sync-semantic-model'], root);
545
+ runInstalled(paths.authoringValidator, [fixture.featureDir], root);
546
+ const state = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
547
+ fixture.reviewResult = {
548
+ verdict: 'PASS',
549
+ findings: [],
550
+ unresolved_decisions: [],
551
+ graph_coverage: [
552
+ 'criterion_local', 'cross_criterion', 'runtime_path',
553
+ 'assumption_provenance', 'compatibility_migration',
554
+ ].map((surface) => ({
555
+ surface, covered: true,
556
+ notes: 'The review covers this semantic surface against the canonical model.',
557
+ })),
558
+ reviewed_criteria: counterexamples.map(({ criterion }) => criterion),
559
+ counterexamples,
560
+ reviewer_evidence: null,
561
+ };
562
+ if (state.workflow_policy.assurance_level === 'Strict') {
563
+ const digest = runInstalled(paths.validator, [fixture.featureDir, '--semantic-digest'], root).stdout.trim();
564
+ assert.match(digest, /^sha256:[a-f0-9]{64}$/);
565
+ return digest;
566
+ }
567
+ const reviewFile = path.join(fixture.featureDir, '.review-result.json');
568
+ writeJson(reviewFile, fixture.reviewResult);
569
+ runInstalled(paths.readiness, [fixture.featureDir, '--review-result', reviewFile], root);
570
+ fs.unlinkSync(reviewFile);
571
+ runInstalled(paths.validator, [fixture.featureDir], root);
572
+ runInstalled(paths.grounder, [fixture.featureDir, '--root', root], root);
573
+ return JSON.parse(fs.readFileSync(fixture.specFile, 'utf8')).validation.semantic_review.semantic_digest;
574
+ }
575
+
576
+ function createTasklessFixture(paths, root, feature, options = {}) {
577
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
578
+ fs.mkdirSync(path.join(root, 'test'), { recursive: true });
579
+ fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = (valid) => ({ status: valid === true ? "enabled" : "rejected" });\n');
580
+ fs.writeFileSync(path.join(root, 'src', 'design-boundary.js'), 'module.exports = { entry: "src/entry.js" };\n');
581
+ fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), `const test = require('node:test');
582
+ const assert = require('node:assert/strict');
583
+ const entry = require('../src/entry.js');
584
+
585
+ test('entry accepts only the boolean true discriminator', () => {
586
+ assert.deepEqual(entry(true), { status: 'enabled' });
587
+ for (const invalid of [false, undefined, null, 'true', 1, {}]) {
588
+ assert.deepEqual(entry(invalid), { status: 'rejected' });
589
+ }
590
+ });
591
+ `);
592
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node --test' } }));
593
+ const fixture = materializeSpecState(paths, root, feature, options);
594
+ fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection());
595
+ fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection());
596
+ fixture.digest = completeSemanticReview(paths, root, fixture, [{
597
+ criterion: 'R1.1', case_kind: 'failure',
598
+ scenario: 'The installed entry receives input without the required discriminator.',
599
+ expected: 'The installed entry returns rejected and never returns enabled.',
600
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
601
+ }]);
602
+ return fixture;
603
+ }
604
+
605
+ function taskProjection({ id, title, ownedPath, criterion, verificationRef, role, artifact = false }) {
606
+ const anchors = typedAnchorTable([
607
+ { id: `A-${id}-01`, type: 'file', target: ownedPath, role: 'owner', access: 'write', action: 'modify' },
608
+ ...(artifact ? [{
609
+ id: `A-${id}-02`, type: 'artifact', target: `artifacts/${id}.json`,
610
+ role: 'verifier', access: 'write', action: 'create',
611
+ }] : []),
612
+ {
613
+ id: `A-${id}-${artifact ? '03' : '02'}`, type: 'command',
614
+ target: 'node --test test/entry.test.js', role: 'verifier', access: 'read', action: 'read',
615
+ },
616
+ ]);
617
+ return `# Task ${id}: ${title}
618
+ **Status:** pending
619
+
620
+ ## Outcome
621
+
622
+ Deliver observable ${title.toLowerCase()} behavior through the installed entrypoint.
623
+
624
+ ## Scope
625
+
626
+ - **In scope:** Exact behavior owned at ${ownedPath}.
627
+ - **Out of scope:** Unrelated runtime behavior.
628
+
629
+ ## Anchors and Ownership
630
+
631
+ ${anchors}
632
+
633
+ ## Changes
634
+
635
+ - [ ] Implement the exact owned behavior. _Requirements: ${criterion.slice(1)}_
636
+
637
+ ## Acceptance
638
+
639
+ - **${criterion}:** The installed command returns the criterion-specific observable state.
640
+
641
+ ## Dependencies
642
+
643
+ - none
644
+
645
+ ## Verification Plan
646
+
647
+ - **Verification ref:** ${verificationRef}
648
+ - **Task role:** ${role}
649
+ - **Command:** \`node --test test/entry.test.js\`
650
+ - **Expected:** Exit code 0 and the criterion-specific state is observed.
651
+ - **Negative path:** Invalid input returns the named rejected state.
652
+ - **Reachability:** \`src/entry.js\` is reached by the installed test command.
653
+ `;
654
+ }
655
+
656
+ function createTaskFixture(paths, root, feature) {
657
+ fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
658
+ const fixture = materializeSpecState(paths, root, feature);
659
+ fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection({ proof: true }));
660
+ fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection({
661
+ expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json`',
662
+ taskful: true,
663
+ }));
664
+ const taskOne = 'tasks/task-R1-01-implement.md';
665
+ const taskTwo = 'tasks/task-R1-02-verify.md';
666
+ fs.mkdirSync(path.join(fixture.featureDir, 'tasks'), { recursive: true });
667
+ fs.writeFileSync(path.join(fixture.featureDir, taskOne), taskProjection({
668
+ id: 'R1-01', title: 'Implement behavior', ownedPath: 'src/entry.js', criterion: 'R1.1',
669
+ verificationRef: 'V1', role: 'subject implements R1.1',
670
+ }));
671
+ fs.writeFileSync(path.join(fixture.featureDir, taskTwo), taskProjection({
672
+ id: 'R1-02', title: 'Verify behavior', ownedPath: 'test/entry.test.js', criterion: 'R1.2',
673
+ verificationRef: 'V1', role: 'verifier verifies V1 through separately owned R1.2 proof', artifact: true,
674
+ }));
675
+ const state = fixture.state;
676
+ state.authoring.tasks = 'draft';
677
+ state.task_files = [taskOne, taskTwo];
678
+ state.task_registry = Object.fromEntries([
679
+ [taskOne, { id: 'R1-01', title: 'Implement behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
680
+ [taskTwo, { id: 'R1-02', title: 'Verify behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
681
+ ]);
682
+ state.coordination.boundaries = [{
683
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
684
+ write_sets: { 'R1-01': ['src/entry.js'], 'R1-02': ['test/entry.test.js', 'artifacts/R1-02.json'] },
685
+ }, {
686
+ id: 'B-PROOF', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
687
+ verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
688
+ }];
689
+ writeJson(fixture.specFile, state);
690
+ fixture.digest = completeSemanticReview(paths, root, fixture, [{
691
+ criterion: 'R1.1', case_kind: 'failure',
692
+ scenario: 'Invalid input reaches the installed implementation boundary.',
693
+ expected: 'The implementation returns rejected and never reports enabled.',
694
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
695
+ }, {
696
+ criterion: 'R1.2', case_kind: 'failure',
697
+ scenario: 'The verification command completes without producing its proof artifact.',
698
+ expected: 'The verification criterion remains unsatisfied until the artifact exists.',
699
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
700
+ }]);
701
+ return fixture;
702
+ }
703
+
704
+ function initializeGit(root) {
705
+ fs.writeFileSync(path.join(root, '.gitignore'), `${'node_' + 'modules'}/\n`);
706
+ for (const args of [
707
+ ['init', '-q'], ['config', 'user.name', 'CafeKit Packed E2E'],
708
+ ['config', 'user.email', 'packed-e2e@example.invalid'],
709
+ ['add', '--', '.gitignore', 'src/entry.js', 'src/design-boundary.js', 'test/entry.test.js', 'package.json'],
710
+ ['commit', '--no-gpg-sign', '-qm', 'packed semantic fixture'],
711
+ ]) {
712
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
713
+ assert.equal(result.status, 0, `${args.join(' ')}\n${result.stdout}\n${result.stderr}`);
714
+ }
715
+ }
716
+
717
+ function assertInstalledCompletion(paths, root, platform, fixture) {
718
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-completion-home-'));
719
+ const code = `
720
+ const fs = require('fs');
721
+ const path = require('path');
722
+ const policy = require(process.argv[1]);
723
+ const provenance = require(process.argv[2]);
724
+ const checker = require(process.argv[3]);
725
+ const resolver = require(process.argv[4]);
726
+ const root = process.argv[5];
727
+ const feature = process.argv[6];
728
+ const specFile = path.join(root, 'specs', feature, 'spec.json');
729
+ const state = JSON.parse(fs.readFileSync(specFile, 'utf8'));
730
+ state.status = 'done';
731
+ fs.writeFileSync(specFile, JSON.stringify(state, null, 2) + '\\n');
732
+ const context = provenance.deriveRuntimeContext({ projectRoot: root, specsRoot: path.join(root, 'specs'), specFile, featureName: feature, runtimeSession: 'packed-session' });
733
+ fs.writeFileSync(path.join(root, 'specs', feature, 'feature-receipt.md'), ['Verification: PASS','Command: node --test','Exit: 0','Result: PASS','Expected: installed behavior passes','Observed: installed behavior passed','Base: ' + context.base,'Head: ' + context.head,'Feature: ' + feature].join('\\n') + '\\n');
734
+ const result = checker.evaluateCloseout({ resolver, policy, projectRoot: root, runtime: {}, payload: { session_id: 'packed-session', featureName: feature } });
735
+ process.stdout.write(JSON.stringify({ ok: result.ok, active: result.active, reason: result.reason || null, feature: result.candidate && result.candidate.featureName }));`;
736
+ const result = spawnSync(process.execPath, ['-e', code, paths.policy, paths.provenance, paths.completion, paths.resolver, root, fixture.state.feature_name], {
737
+ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: home },
738
+ });
739
+ fs.rmSync(home, { recursive: true, force: true });
740
+ assert.equal(result.status, 0, `${platform}\n${result.stdout}\n${result.stderr}`);
741
+ assert.deepEqual(JSON.parse(result.stdout), {
742
+ ok: true, active: true, reason: null, feature: fixture.state.feature_name,
743
+ });
744
+ }
745
+
746
+ function assertInstalledResolution(paths, root, platform, explicitFeature) {
747
+ const code = `
748
+ const checker = require(process.argv[1]); const resolver = require(process.argv[2]);
749
+ const root = process.argv[3]; const platform = process.argv[4]; const feature = process.argv[5];
750
+ const base = { projectRoot: root, runtime: {}, payload: {} };
751
+ if (platform === 'claude') base.resolver = resolver;
752
+ const explicit = checker.resolveCandidate({ ...base, payload: { featureName: feature } });
753
+ const ambiguous = checker.resolveCandidate(base);
754
+ process.stdout.write(JSON.stringify({ explicit: explicit && explicit.featureName, error: ambiguous && ambiguous.error, candidates: ambiguous && ambiguous.candidates }));`;
755
+ const result = spawnSync(process.execPath, ['-e', code, paths.completion, paths.resolver, root, platform, explicitFeature], { cwd: root, encoding: 'utf8' });
756
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
757
+ const resolution = JSON.parse(result.stdout);
758
+ assert.equal(resolution.explicit, explicitFeature);
759
+ assert.equal(resolution.error, 'multiple_persisted');
760
+ assert.ok(resolution.candidates.length >= 2);
761
+ }
762
+
763
+ function assertStrictSimulatedHandlerGuardrail(paths, root, fixture) {
764
+ // Simulated SubagentStop handler chain — NOT a live Codex host E2E.
765
+ // This exercises the installed hook's handler logic via direct spawn,
766
+ // not a real Codex binary dispatching SubagentStop. For live host,
767
+ // see the opt-in CAFEKIT_CODEX_HOST_E2E test below.
768
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-strict-home-'));
769
+ const code = `
770
+ const authority = require(process.argv[1]); const readiness = require(process.argv[2]); const root = process.argv[3]; const feature = process.argv[4]; const digest = process.argv[5]; const review = JSON.parse(process.argv[6]);
771
+ const spec_file = 'specs/' + feature + '/spec.json';
772
+ const marker = authority.MARKER + JSON.stringify({ feature_name: feature, spec_file, semantic_digest: digest, verdict: 'PASS' });
773
+ const before = authority.verifyAttestation(root, spec_file, feature, digest);
774
+ const {spawnSync} = require('child_process');
775
+ const authorityPath = process.argv[1];
776
+ function handlerInvoke(payload) {
777
+ return spawnSync(process.execPath, [authorityPath], {cwd: root, input: JSON.stringify(payload), encoding:'utf8', env: {...process.env, HOME: process.env.HOME, USERPROFILE: process.env.HOME, PROJECT_ROOT: root}});
778
+ }
779
+ const forgedPayload = {cwd: root, hook_event_name: 'SubagentStop', session_id: 'self-session', agent_id: 'self', agent_type: 'spec-maker', last_assistant_message: marker};
780
+ const forgedRes = handlerInvoke(forgedPayload);
781
+ const afterForged = authority.verifyAttestation(root, spec_file, feature, digest);
782
+ const observedPayload = {cwd: root, hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code-auditor', last_assistant_message: marker};
783
+ const observedRes = handlerInvoke(observedPayload);
784
+ const afterObserved = authority.verifyAttestation(root, spec_file, feature, digest);
785
+ const finalized = readiness.finalizeReadiness({ specDir: require('path').join(root, 'specs', feature), projectRoot: root, reviewResult: review });
786
+ process.stdout.write(JSON.stringify({ before: before.ok, forged: afterForged.ok, afterForged: afterForged.ok, observed: afterObserved.ok, afterObserved: afterObserved.ok, ready: finalized.spec.ready_for_implementation, forgedStderr: forgedRes.stderr.trim(), observedStderr: observedRes.stderr.trim() }));`;
787
+ const result = spawnSync(process.execPath, ['-e', code, paths.semanticAuthority, paths.readiness, root, fixture.state.feature_name, fixture.digest, JSON.stringify(fixture.reviewResult)], {
788
+ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: home, PROJECT_ROOT: root },
789
+ });
790
+ fs.rmSync(home, { recursive: true, force: true });
791
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
792
+ const out = JSON.parse(result.stdout);
793
+ // Simulated handler: forged self-attestation via spec-maker must NOT create observation, code-auditor must.
794
+ assert.equal(out.before, false, 'strict attestation must be missing before simulated handler observation');
795
+ assert.equal(out.afterForged, false, 'forged self-attestation must NOT create simulated handler observation');
796
+ assert.equal(out.afterObserved, true, 'code-auditor simulated handler must create observation');
797
+ assert.equal(out.ready, true, 'SPEC_READY after simulated handler observation');
798
+ assert.ok(out.forgedStderr.includes('rejected') || out.forgedStderr === '', 'forged handler invoke should be rejected or silent but not create observation');
799
+ assert.equal(out.observedStderr, '', 'code-auditor simulated handler observation should be silent');
800
+ }
801
+
802
+ function assertStaleDigestMutations(paths, root, taskless, taskBearing) {
803
+ const requirementFile = path.join(taskless.featureDir, 'requirements.md');
804
+ const requirementBytes = fs.readFileSync(requirementFile);
805
+ fs.appendFileSync(requirementFile, '\nThe installed Markdown meaning changed.\n');
806
+ assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
807
+ fs.writeFileSync(requirementFile, requirementBytes);
808
+
809
+ const tasklessState = JSON.parse(fs.readFileSync(taskless.specFile, 'utf8'));
810
+ const originalPolicy = JSON.parse(JSON.stringify(tasklessState.workflow_policy));
811
+ tasklessState.workflow_policy.planning_depth = 'Full';
812
+ writeJson(taskless.specFile, tasklessState);
813
+ assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
814
+ tasklessState.workflow_policy = originalPolicy;
815
+ writeJson(taskless.specFile, tasklessState);
816
+
817
+ const taskState = JSON.parse(fs.readFileSync(taskBearing.specFile, 'utf8'));
818
+ const originalId = taskState.coordination.boundaries[0].id;
819
+ taskState.coordination.boundaries[0].id = 'B-OWN-MUTATED';
820
+ writeJson(taskBearing.specFile, taskState);
821
+ assert.match(runInstalled(paths.validator, [taskBearing.featureDir], root, 1).stderr, /semantic_digest: stale/);
822
+ taskState.coordination.boundaries[0].id = originalId;
823
+ writeJson(taskBearing.specFile, taskState);
824
+ }
825
+
323
826
  function assertTransforms(root, platform) {
324
827
  const skill = path.join(root, RUNTIMES[platform].root === '.codex' ? '.agents/skills' : `${RUNTIMES[platform].root}/skills`, 'develop', 'SKILL.md');
325
828
  const content = fs.readFileSync(skill, 'utf8');
@@ -348,6 +851,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
348
851
  try {
349
852
  const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
350
853
  const tarball = path.join(destination, packed.filename);
854
+ const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
351
855
  assertCleanInventory(packedInventory(tarball));
352
856
 
353
857
  const cases = [
@@ -360,7 +864,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
360
864
  for (const matrixCase of cases) {
361
865
  for (const lang of [null, 'vi']) {
362
866
  const project = path.join(root, `${matrixCase.name}-${lang ? 'lang-vi' : 'no-lang'}`);
363
- const installer = installPacked(tarball, project);
867
+ const installer = installPacked(tarball, project, runtimeClosure);
364
868
  runInstaller(installer, project, matrixCase.platforms, lang);
365
869
  for (const platform of matrixCase.platforms) {
366
870
  assertHookLanguage(project, platform, lang, `${project}-${matrixCase.name}-${lang || 'none'}`);
@@ -388,6 +892,43 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
388
892
  }
389
893
  });
390
894
 
895
+ test('packed Claude and Codex installs execute semantic kernel behavior without package source', () => {
896
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-semantic-'));
897
+ const destination = path.join(root, 'pack');
898
+ fs.mkdirSync(destination, { recursive: true });
899
+ try {
900
+ const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
901
+ const tarball = path.join(destination, packed.filename);
902
+ const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
903
+ assertCleanInventory(packedInventory(tarball));
904
+
905
+ for (const platform of ['claude', 'codex']) {
906
+ const project = path.join(root, platform);
907
+ const installer = installPacked(tarball, project, runtimeClosure);
908
+ runInstaller(installer, project, [platform], null);
909
+ const packedSource = path.resolve(path.dirname(installer), '..', 'src');
910
+ fs.rmSync(packedSource, { recursive: true, force: true });
911
+ assert.equal(fs.existsSync(packedSource), false, `${platform} package source must be unavailable`);
912
+
913
+ const paths = installedSemanticPaths(project, platform);
914
+ const taskless = createTasklessFixture(paths, project, 'compact-installed');
915
+ initializeGit(project);
916
+ assertInstalledCompletion(paths, project, platform, taskless);
917
+
918
+ const strict = createTasklessFixture(paths, project, 'strict-installed', { strict: true });
919
+ const taskBearing = createTaskFixture(paths, project, 'tasks-installed');
920
+ assertInstalledResolution(paths, project, platform, 'strict-installed');
921
+ assertStrictSimulatedHandlerGuardrail(paths, project, strict);
922
+ assertStaleDigestMutations(paths, project, taskless, taskBearing);
923
+ runInstalled(paths.validator, [taskless.featureDir], project);
924
+ runInstalled(paths.validator, [taskBearing.featureDir], project);
925
+ runInstalled(paths.grounder, [taskBearing.featureDir, '--root', project], project);
926
+ }
927
+ } finally {
928
+ fs.rmSync(root, { recursive: true, force: true });
929
+ }
930
+ });
931
+
391
932
  test('packed Claude and Codex installs self-contain runtime provenance and fail closed without it', () => {
392
933
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-provenance-package-'));
393
934
  const destination = path.join(root, 'pack');
@@ -395,11 +936,12 @@ test('packed Claude and Codex installs self-contain runtime provenance and fail
395
936
  try {
396
937
  const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
397
938
  const tarball = path.join(destination, packed.filename);
939
+ const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
398
940
  assertCleanInventory(packedInventory(tarball));
399
941
 
400
942
  for (const platform of ['claude', 'codex']) {
401
943
  const project = path.join(root, platform);
402
- const installer = installPacked(tarball, project);
944
+ const installer = installPacked(tarball, project, runtimeClosure);
403
945
  runInstaller(installer, project, [platform], null);
404
946
 
405
947
  const packedSource = path.resolve(path.dirname(installer), '..', 'src');
@@ -413,3 +955,153 @@ test('packed Claude and Codex installs self-contain runtime provenance and fail
413
955
  fs.rmSync(root, { recursive: true, force: true });
414
956
  }
415
957
  });
958
+
959
+ test('packed Codex live host E2E via codex binary (opt-in)', async (t) => {
960
+ if (process.env.CAFEKIT_CODEX_HOST_E2E !== '1') {
961
+ t.skip('opt-in only: set CAFEKIT_CODEX_HOST_E2E=1 to run live Codex host');
962
+ return;
963
+ }
964
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-live-codex-'));
965
+ const destination = path.join(root, 'pack');
966
+ fs.mkdirSync(destination, { recursive: true });
967
+ const cafekitHome = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-live-home-'));
968
+ const originalHome = process.env.HOME;
969
+ const originalUserProfile = process.env.USERPROFILE;
970
+ const originalCodexHome = process.env.CODEX_HOME;
971
+ // Preserve original home for Codex auth (without inspecting contents)
972
+ const authHome = originalCodexHome || (originalHome ? path.join(originalHome, '.codex') : null);
973
+ let project;
974
+ try {
975
+ const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
976
+ const tarball = path.join(destination, packed.filename);
977
+ const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
978
+ project = path.join(root, 'codex-live');
979
+ const installer = installPacked(tarball, project, runtimeClosure);
980
+ runInstaller(installer, project, ['codex'], null);
981
+ const paths = installedSemanticPaths(project, 'codex');
982
+ const fixture = createTasklessFixture(paths, project, 'live-strict', { strict: true });
983
+ initializeGit(project);
984
+ // Verify via child process to keep CafeKit state isolated without mutating global env
985
+ const verifyViaChild = (digest) => {
986
+ const script = 'const a=require(process.argv[1]); console.log(JSON.stringify(a.verifyAttestation(process.argv[2],process.argv[3],process.argv[4],process.argv[5])))';
987
+ const res = spawnSync(process.execPath, ['-e', script, paths.semanticAuthority, project, path.join(project, 'specs', 'live-strict', 'spec.json'), 'live-strict', digest], {
988
+ encoding: 'utf8',
989
+ env: { ...process.env, HOME: cafekitHome, USERPROFILE: cafekitHome },
990
+ });
991
+ assert.equal(res.status, 0, `verify child failed: ${res.stderr}`);
992
+ return JSON.parse(res.stdout);
993
+ };
994
+ const before = verifyViaChild(fixture.digest);
995
+ assert.equal(before.ok, false, 'before host observation, Strict attestation must be missing (fail-closed)');
996
+
997
+ const model = process.env.CAFEKIT_CODEX_MODEL || 'gpt-5.6-luna';
998
+ const reasoning = process.env.CAFEKIT_CODEX_REASONING || 'max';
999
+ const codexBin = process.env.CAFEKIT_CODEX_BIN || 'codex';
1000
+ const canonicalProject = fs.realpathSync(project);
1001
+ const codeAuditorConfig = path.join(canonicalProject, '.codex', 'agents', 'code_auditor.toml');
1002
+ const semanticAuthorityHook = fs.realpathSync(paths.semanticAuthority);
1003
+ const shellQuote = (value) => `'${String(value).replaceAll("'", `'\\''`)}'`;
1004
+ const posixHookCommand = `${shellQuote(process.execPath)} ${shellQuote(semanticAuthorityHook)}`;
1005
+ const encodedHookPath = Buffer.from(semanticAuthorityHook, 'utf8').toString('base64url');
1006
+ const windowsHookCommand = 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encodedHookPath;
1007
+ const subagentStopHooks = `[{ matcher = "*", hooks = [{ type = "command", command = ${JSON.stringify(posixHookCommand)}, commandWindows = ${JSON.stringify(windowsHookCommand)} }] }]`;
1008
+ assert.equal(fs.existsSync(codeAuditorConfig), true, 'packed install must contain code_auditor config');
1009
+ const versionCheck = spawnSync(codexBin, ['--version'], { encoding: 'utf8' });
1010
+ if (versionCheck.status !== 0) {
1011
+ assert.fail(`Codex binary not available at ${codexBin}: ${versionCheck.stderr.trim()} (exit ${versionCheck.status})`);
1012
+ }
1013
+ const controllerPrompt = [
1014
+ 'You are a CafeKit controller. Do NOT emit CAFEKIT_SEMANTIC_REVIEW_ATTESTATION yourself.',
1015
+ `Spawn exactly one subagent with agent_type="code_auditor" to perform Strict semantic review for feature live-strict.`,
1016
+ 'Wait for that exact subagent to finish by calling wait_agent before you answer.',
1017
+ 'This is a specs-only authoring review: assess requirements, design, semantic model, and counterexample coverage only. Do not require implementation code, execution receipts, or a Develop run.',
1018
+ 'The request has assurance_level: Strict. Keep the review bounded to this fixture and do not invoke a skill.',
1019
+ 'This is intentionally a pre-finalization candidate: semantic_model is null and semantic_review is not-run until a PASS marker is observed and spec-readiness atomically promotes them. Do not run or require the full validator before that host event because it must fail closed at this stage.',
1020
+ `The subagent must recompute the digest via: node .codex/scripts/validate-spec-output.cjs specs/live-strict --semantic-digest`,
1021
+ `The exact review result that will be passed to the finalizer is ${JSON.stringify(fixture.reviewResult)}. Verify every criterion, counterexample, decision ref, verification ref, and artifact statement before PASS.`,
1022
+ `If the subagent verifies PASS and the digest matches ${fixture.digest}, it must emit exactly:`,
1023
+ `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION {"feature_name":"live-strict","spec_file":"specs/live-strict/spec.json","semantic_digest":"${fixture.digest}","verdict":"PASS"}`,
1024
+ 'No other marker. After the subagent, do not add any marker.',
1025
+ ].join(' ');
1026
+
1027
+ const execArgs = [
1028
+ 'exec',
1029
+ '-m', model,
1030
+ '-c', `model_reasoning_effort=${reasoning}`,
1031
+ // Project agents are intentionally skipped until a user trusts the repo.
1032
+ // This disposable E2E must not mutate global trust, so register the
1033
+ // packed profile explicitly for this process. Static packed tests cover
1034
+ // the normal trusted-project auto-discovery path and file schema.
1035
+ '-c', 'agents.code_auditor.description="CafeKit Strict semantic reviewer"',
1036
+ '-c', `agents.code_auditor.config_file=${JSON.stringify(codeAuditorConfig)}`,
1037
+ // Isolate this E2E from the user's config/hooks while retaining auth.
1038
+ // The process-scoped hook executes the packed entrypoint directly.
1039
+ '--ignore-user-config',
1040
+ // Only thread-spawned Codex subagents emit the supported SubagentStop
1041
+ // event. The legacy internal path emits spawn PostToolUse but no
1042
+ // hook-observable completion payload and must remain fail-closed.
1043
+ '--enable', 'multi_agent_v2',
1044
+ '-c', 'features.hooks=true',
1045
+ '-c', `hooks.SubagentStop=${subagentStopHooks}`,
1046
+ '-s', 'read-only',
1047
+ '--dangerously-bypass-hook-trust',
1048
+ '--ephemeral',
1049
+ '-C', canonicalProject,
1050
+ controllerPrompt,
1051
+ ];
1052
+ const codexResult = spawnSync(codexBin, execArgs, {
1053
+ cwd: project,
1054
+ encoding: 'utf8',
1055
+ timeout: 600000,
1056
+ maxBuffer: 16 * 1024 * 1024,
1057
+ env: { ...process.env, HOME: cafekitHome, USERPROFILE: cafekitHome, ...(authHome ? { CODEX_HOME: authHome } : {}) },
1058
+ });
1059
+ if (codexResult.status !== 0) {
1060
+ const cliOutput = `${codexResult.stderr.trim()}\n${codexResult.stdout.trim()}`.trim();
1061
+ const processState = [
1062
+ `status=${codexResult.status}`,
1063
+ `signal=${codexResult.signal || 'none'}`,
1064
+ codexResult.error ? `spawn_error=${codexResult.error.code || codexResult.error.message}` : null,
1065
+ ].filter(Boolean).join(', ');
1066
+ const isEnvAuth = /authentication|unauthorized|credential|not logged in|login required|CODEX_HOME|not.*found|ENOENT/i.test(cliOutput);
1067
+ if (isEnvAuth) {
1068
+ assert.fail(`Live Codex host failed due to CLI/environment (${processState}): ${cliOutput.slice(0, 1600)}`);
1069
+ }
1070
+ assert.fail(`Live Codex host exec failed (${processState}): ${cliOutput.slice(0, 1600)}`);
1071
+ }
1072
+ // Verify via public authority API only (no HOME inspection) via child process
1073
+ const after = verifyViaChild(fixture.digest);
1074
+ const liveTrace = `${codexResult.stderr || ''}\n${codexResult.stdout || ''}`.trim();
1075
+ assert.equal(
1076
+ after.ok,
1077
+ true,
1078
+ `host observation must exist after Codex reviewer completion event (got ${after.reason})\n` +
1079
+ `Codex trace tail:\n${liveTrace.slice(-6000)}`,
1080
+ );
1081
+ assert.equal(after.record.reviewer_agent_type, 'code_auditor', 'observation must be from code_auditor');
1082
+ const reviewFile = path.join(project, 'specs', 'live-strict', '.live-review.json');
1083
+ fs.writeFileSync(reviewFile, JSON.stringify(fixture.reviewResult));
1084
+ const readinessResult = spawnSync(process.execPath, [paths.readiness, path.join(project, 'specs', 'live-strict'), '--review-result', reviewFile], {
1085
+ cwd: project,
1086
+ encoding: 'utf8',
1087
+ env: { ...process.env, HOME: cafekitHome },
1088
+ });
1089
+ fs.unlinkSync(reviewFile);
1090
+ assert.equal(readinessResult.status, 0, `readiness should succeed after host observation: ${readinessResult.stderr.trim()}`);
1091
+ assert.match(readinessResult.stdout, /SPEC_READY/);
1092
+ const specAfter = JSON.parse(fs.readFileSync(path.join(project, 'specs', 'live-strict', 'spec.json'), 'utf8'));
1093
+ assert.equal(specAfter.ready_for_implementation, true, 'SPEC_READY after live host observation');
1094
+ } finally {
1095
+ // Restore exact environment on all paths
1096
+ if (originalHome !== undefined) process.env.HOME = originalHome; else delete process.env.HOME;
1097
+ if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile; else delete process.env.USERPROFILE;
1098
+ if (originalCodexHome !== undefined) process.env.CODEX_HOME = originalCodexHome; else delete process.env.CODEX_HOME;
1099
+ // Clear require cache for authority to avoid polluting other tests
1100
+ try {
1101
+ const tmpPaths = path.join(root || os.tmpdir(), 'codex-live', '.codex', 'hooks', 'semantic-review-authority.cjs');
1102
+ delete require.cache[require.resolve(tmpPaths)];
1103
+ } catch {}
1104
+ fs.rmSync(root, { recursive: true, force: true });
1105
+ fs.rmSync(cafekitHome, { recursive: true, force: true });
1106
+ }
1107
+ });