@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4

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