@haposoft/cafekit 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/bin/__tests__/codex-hooks.test.js +169 -236
  3. package/bin/__tests__/codex-native.test.js +54 -114
  4. package/bin/__tests__/develop-contract.test.js +101 -1361
  5. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  6. package/bin/__tests__/package-inventory.test.js +1 -395
  7. package/bin/__tests__/spec-narrowing.test.js +30 -134
  8. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  9. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  10. package/bin/__tests__/validator-grounding.test.js +17 -2929
  11. package/bin/lib/managed-writer.js +2 -3
  12. package/bin/phases/codex-runtime.js +6 -3
  13. package/bin/phases/copy-payload.js +13 -28
  14. package/bin/phases/omp-runtime.js +6 -3
  15. package/package.json +2 -3
  16. package/src/claude/agents/code-auditor.md +9 -6
  17. package/src/claude/agents/debugger.md +3 -9
  18. package/src/claude/agents/docs-keeper.md +4 -7
  19. package/src/claude/agents/git-ops.md +2 -4
  20. package/src/claude/agents/implementer.md +6 -7
  21. package/src/claude/agents/inspector.md +2 -0
  22. package/src/claude/agents/project-manager.md +2 -2
  23. package/src/claude/agents/spec-maker.md +4 -4
  24. package/src/claude/agents/test-runner.md +8 -11
  25. package/src/claude/agents/ui-ux-designer.md +0 -1
  26. package/src/claude/hooks/__tests__/spec-gate.test.js +170 -330
  27. package/src/claude/hooks/__tests__/spec-state-touched.test.js +26 -0
  28. package/src/claude/hooks/spec-gate.cjs +8 -63
  29. package/src/claude/hooks/spec-state.cjs +47 -38
  30. package/src/claude/migration-manifest.json +12 -7
  31. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  32. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  33. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  34. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  35. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  36. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  37. package/src/claude/rules/ai-dev-rules.md +1 -2
  38. package/src/claude/rules/workflow.md +1 -1
  39. package/src/claude/scripts/spec-receipt.cjs +0 -114
  40. package/src/claude/scripts/spec-resolver.cjs +41 -457
  41. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  42. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  43. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  44. package/src/claude/skills/code-review/SKILL.md +25 -108
  45. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  46. package/src/claude/skills/develop/SKILL.md +22 -70
  47. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  48. package/src/claude/skills/research/SKILL.md +24 -55
  49. package/src/claude/skills/sync/SKILL.md +12 -34
  50. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  51. package/src/claude/skills/test/SKILL.md +39 -123
  52. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  53. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  54. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  55. package/src/claude/skills/web-testing/SKILL.md +2 -20
  56. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  57. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  58. package/src/codex/hooks/spec-gate.cjs +11 -68
  59. package/src/codex/hooks/spec-state.cjs +37 -41
  60. package/src/common/AGENTS.md +4 -10
  61. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  62. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  63. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  64. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  65. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  66. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  67. package/src/claude/hooks/lib/context.cjs +0 -527
  68. package/src/claude/hooks/lib/detect.cjs +0 -474
  69. package/src/claude/scripts/change-firewall.cjs +0 -1748
  70. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  71. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  72. package/src/claude/scripts/spec-final-state.cjs +0 -290
  73. package/src/claude/scripts/spec-ground.cjs +0 -650
  74. package/src/claude/scripts/spec-readiness.cjs +0 -425
  75. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  76. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  77. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  78. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  79. package/src/claude/skills/specs/templates/design.md +0 -82
  80. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  81. package/src/claude/skills/specs/templates/requirements.md +0 -42
  82. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  83. package/src/claude/skills/specs/templates/task.md +0 -47
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-0.18.0-blue.svg)](https://github.com/haposoft/cafekit)
5
+ [![Version](https://img.shields.io/badge/version-0.18.1-blue.svg)](https://github.com/haposoft/cafekit)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
  [![Claude%20Code](https://img.shields.io/badge/Claude%20Code-Native-orange.svg)](https://claude.ai/code)
8
8
  [![Codex%20CLI](https://img.shields.io/badge/Codex%20CLI-Native-111111.svg)](https://developers.openai.com/codex)
@@ -19,10 +19,6 @@ const { stateDir: codexStateDir } = require(
19
19
  );
20
20
 
21
21
  function runHook(file, cwd, payload) {
22
- if (path.basename(file) === 'spec-gate.cjs') {
23
- const rootResult = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
24
- if (rootResult.status === 0) installFeatureReceipts(rootResult.stdout.trim(), payload.session_id || 'session-a');
25
- }
26
22
  return spawnSync(process.execPath, [file], {
27
23
  cwd,
28
24
  input: JSON.stringify(payload),
@@ -34,7 +30,7 @@ function runtimeContext(root, feature = 'auth', session = 'session-a') {
34
30
  return PROVENANCE.deriveRuntimeContext({
35
31
  projectRoot: root,
36
32
  specsRoot: path.join(root, 'specs'),
37
- specFile: path.join(root, 'specs', feature, 'spec.json'),
33
+ specFile: path.join(root, 'specs', feature, 'plan.md'),
38
34
  featureName: feature,
39
35
  runtimeSession: session,
40
36
  });
@@ -55,34 +51,18 @@ function workflowRuntimeContext(root, feature = 'auth', session = 'session-a') {
55
51
  });
56
52
  }
57
53
 
58
- function installFeatureReceipts(root, session = 'session-a') {
59
- const specsRoot = path.join(root, 'specs');
60
- if (!fs.existsSync(specsRoot)) return;
61
- for (const feature of fs.readdirSync(specsRoot)) {
62
- const featureDir = path.join(specsRoot, feature);
63
- const specFile = path.join(featureDir, 'spec.json');
64
- if (!fs.existsSync(specFile)) continue;
65
- let spec;
66
- try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { continue; }
67
- const lifecyclePhase = spec.current_phase || spec.phase;
68
- const explicitCloseout = ['completed', 'complete'].includes(spec.status)
69
- || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
70
- if (!explicitCloseout) continue;
71
- const tasks = Object.values(spec.task_registry || {});
72
- if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) continue;
73
- const context = runtimeContext(root, feature, session);
74
- fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
75
- `Feature: ${feature}`,
76
- 'Expected: final integration verification passes',
77
- 'Observed: final integration verification passed',
78
- 'Verification: PASS',
79
- 'Command: node --test',
80
- 'Exit: 0',
81
- `Base: ${context.base}`,
82
- `Head: ${context.head}`,
83
- '',
84
- ].join('\n'));
85
- }
54
+ /** A process-first packet with one flat task whose Verification Plan runs `command`. */
55
+ function writeProcessTask(featureDir, status, receiptLines = null, command = 'pnpm test', taskName = 'task-01-auth.md') {
56
+ fs.mkdirSync(featureDir, { recursive: true });
57
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
58
+ const lines = [
59
+ '# Task 01', '', `Status: ${status}`, '',
60
+ '## Dependencies', '', '- none', '',
61
+ '## Verification Plan', '', `- Command: ${command}`, '',
62
+ ];
63
+ if (receiptLines) lines.push('## Receipt', '', ...receiptLines, '');
64
+ fs.writeFileSync(path.join(featureDir, taskName), lines.join('\n'));
65
+ return path.join(featureDir, taskName);
86
66
  }
87
67
 
88
68
  function inHookFixture(run, options = {}) {
@@ -121,12 +101,6 @@ function inHookFixture(run, options = {}) {
121
101
  path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'),
122
102
  path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'),
123
103
  );
124
- for (const file of ['validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
125
- fs.copyFileSync(
126
- path.join(PACKAGE_ROOT, 'src/claude/scripts', file),
127
- path.join(root, '.codex', 'scripts', file),
128
- );
129
- }
130
104
  fs.writeFileSync(path.join(root, '.codex', 'scripts', 'spec-scaffold.cjs'), '');
131
105
  for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
132
106
  const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
@@ -245,6 +219,87 @@ function inInstalledProcessSpecStateFixture(run) {
245
219
  });
246
220
  }
247
221
 
222
+ function writeCodexLegacyPacket(root, name = 'old') {
223
+ const dir = path.join(root, 'specs', name);
224
+ fs.mkdirSync(path.join(dir, 'tasks'), { recursive: true });
225
+ fs.writeFileSync(path.join(dir, 'spec.json'), JSON.stringify({
226
+ feature_name: name,
227
+ status: 'in_progress',
228
+ task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
229
+ }));
230
+ fs.writeFileSync(path.join(dir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
231
+ }
232
+
233
+ const CODEX_LEGACY_NOTICE = /`specs\/old` is a legacy Specs packet without plan\.md; CafeKit no longer reads it/g;
234
+
235
+ test('Codex spec-state notices a leftover legacy packet once per session, also when it is the only packet', () => {
236
+ inHookFixture((root, hooks) => {
237
+ writeCodexLegacyPacket(root);
238
+ const prompt = (session) => runHook(path.join(hooks, 'spec-state.cjs'), root, {
239
+ cwd: root, session_id: session, hook_event_name: 'UserPromptSubmit', prompt: 'Continue',
240
+ });
241
+ const first = prompt('session-a');
242
+ assert.equal(first.status, 0, first.stderr);
243
+ assert.equal((first.stdout.match(CODEX_LEGACY_NOTICE) || []).length, 1);
244
+ assert.equal((prompt('session-a').stdout.match(CODEX_LEGACY_NOTICE) || []).length, 0, 'the same session is not told twice');
245
+ assert.equal((prompt('session-b').stdout.match(CODEX_LEGACY_NOTICE) || []).length, 1, 'a new session is told once');
246
+ });
247
+ });
248
+
249
+ test('Codex Stop ignores a leftover legacy packet and still blocks a malformed process-first packet', () => {
250
+ inHookFixture((root, hooks) => {
251
+ const gate = path.join(hooks, 'spec-gate.cjs');
252
+ const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
253
+ writeCodexLegacyPacket(root);
254
+ const legacyOnly = runHook(gate, root, payload);
255
+ assert.equal(legacyOnly.status, 0, legacyOnly.stderr);
256
+ assert.equal(legacyOnly.stdout, '', 'a legacy packet alone never blocks');
257
+
258
+ const shared = path.join(root, 'specs', '_shared');
259
+ fs.mkdirSync(shared, { recursive: true });
260
+ fs.writeFileSync(path.join(shared, 'active-feature.json'), '{"featureName": "old"}\n');
261
+ assert.equal(runHook(gate, root, payload).stdout, '', 'a recorded legacy packet never blocks');
262
+ fs.rmSync(shared, { recursive: true, force: true });
263
+
264
+ const feature = path.join(root, 'specs', 'cycle');
265
+ writeProcessTask(feature, 'done', null, 'pnpm test', 'task-01-a.md');
266
+ writeProcessTask(feature, 'pending', null, 'pnpm test', 'task-02-b.md');
267
+ for (const [name, dependency] of [['task-01-a.md', 'task-02-b.md'], ['task-02-b.md', 'task-01-a.md']]) {
268
+ const file = path.join(feature, name);
269
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('- none', `- ${dependency}`));
270
+ }
271
+ const cycle = runHook(gate, root, payload);
272
+ assert.equal(cycle.status, 0, cycle.stderr);
273
+ const block = JSON.parse(cycle.stdout);
274
+ assert.equal(block.decision, 'block');
275
+ assert.match(block.reason, /invalid workflow packet/);
276
+ });
277
+ });
278
+
279
+ test('Codex Stop still blocks a recorded missing packet and reads a plan.md + spec.json packet as process-first', () => {
280
+ inHookFixture((root, hooks) => {
281
+ const gate = path.join(hooks, 'spec-gate.cjs');
282
+ const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
283
+ const feature = path.join(root, 'specs', 'auth');
284
+ writeProcessTask(feature, 'done');
285
+ fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ feature_name: 'auth' }));
286
+ const hybrid = runHook(gate, root, payload);
287
+ assert.equal(hybrid.status, 0, hybrid.stderr);
288
+ const hybridBlock = JSON.parse(hybrid.stdout);
289
+ assert.equal(hybridBlock.decision, 'block');
290
+ assert.match(hybridBlock.reason, /task-01-auth\.md/);
291
+
292
+ const shared = path.join(root, 'specs', '_shared');
293
+ fs.mkdirSync(shared, { recursive: true });
294
+ fs.writeFileSync(path.join(shared, 'active-feature.json'), '{"featureName": "nope"}\n');
295
+ const missing = runHook(gate, root, payload);
296
+ assert.equal(missing.status, 0, missing.stderr);
297
+ const missingBlock = JSON.parse(missing.stdout);
298
+ assert.equal(missingBlock.decision, 'block');
299
+ assert.match(missingBlock.reason, /explicit_not_found/);
300
+ });
301
+ });
302
+
248
303
  test('Codex installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
249
304
  inInstalledProcessSpecStateFixture(({ findCacheFile, hooks, runState, writeTask }) => {
250
305
  writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
@@ -615,18 +670,8 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
615
670
  inHookFixture((root, hooks) => {
616
671
  const nested = path.join(root, 'packages', 'app');
617
672
  const feature = path.join(root, 'specs', 'auth');
618
- const taskPath = 'tasks/task-R0-01-auth.md';
619
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
620
673
  fs.mkdirSync(nested, { recursive: true });
621
- const spec = {
622
- status: 'in_progress',
623
- feature_name: 'auth',
624
- task_registry: {
625
- [taskPath]: { status: 'pending', completed_at: null }
626
- }
627
- };
628
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
629
- fs.writeFileSync(path.join(feature, taskPath), 'Status: pending\n');
674
+ writeProcessTask(feature, 'pending', null, 'node --test');
630
675
  const gate = path.join(hooks, 'spec-gate.cjs');
631
676
  const payload = {
632
677
  cwd: nested,
@@ -640,32 +685,19 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
640
685
  assert.equal(seeded.status, 0);
641
686
  assert.equal(seeded.stdout, '');
642
687
 
643
- spec.task_registry[taskPath] = {
644
- status: 'done',
645
- completed_at: '2026-07-29T10:00:00.000Z'
646
- };
647
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
648
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
649
-
650
- const blockedBeforeCloseout = runHook(gate, nested, payload);
651
- assert.equal(blockedBeforeCloseout.status, 0);
652
- assert.equal(JSON.parse(blockedBeforeCloseout.stdout).decision, 'block');
653
- assert.match(JSON.parse(blockedBeforeCloseout.stdout).reason, /verification receipt/);
654
-
655
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
656
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
657
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
658
- ].join('\n'), 'auth'));
659
- const validBeforeCloseout = runHook(gate, nested, payload);
660
- assert.equal(validBeforeCloseout.status, 0);
661
- assert.equal(validBeforeCloseout.stdout, '');
662
- assert.equal(fs.existsSync(path.join(feature, 'feature-receipt.md')), false);
663
-
664
- spec.current_phase = 'closeout';
665
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
666
- const closeout = runHook(gate, nested, payload);
667
- assert.equal(closeout.status, 0);
668
- assert.equal(closeout.stdout, '');
688
+ writeProcessTask(feature, 'done', null, 'node --test');
689
+ const blocked = runHook(gate, nested, payload);
690
+ assert.equal(blocked.status, 0);
691
+ assert.equal(JSON.parse(blocked.stdout).decision, 'block');
692
+ assert.match(JSON.parse(blocked.stdout).reason, /verification receipt/);
693
+
694
+ writeProcessTask(feature, 'done', bindReceipt(root, [
695
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
696
+ '```', 'node --test', 'ok 1', '```',
697
+ ].join('\n')).split('\n'), 'node --test');
698
+ const valid = runHook(gate, nested, payload);
699
+ assert.equal(valid.status, 0);
700
+ assert.equal(valid.stdout, '');
669
701
  });
670
702
  });
671
703
 
@@ -765,20 +797,15 @@ test('Codex process-v3 Receipt command must match the exact Verification Plan co
765
797
  });
766
798
 
767
799
  test('Codex recorded active feature turns the identity block into receipt validation', () => {
768
- // Issue #79 end to end on this runtime. Two legacy packets both claiming closeout make
769
- // the gate answer a missing-receipt violation with an identity complaint whose stated
770
- // remedy nothing wrote. Naming the feature resolves it and the violation surfaces.
800
+ // Issue #79 end to end on this runtime. Two unfinished packets make the gate answer a
801
+ // missing-receipt violation with an identity complaint; naming the feature resolves it
802
+ // and the violation surfaces.
771
803
  inHookFixture((root, hooks) => {
772
804
  for (const name of ['alpha', 'beta']) {
773
805
  const feature = path.join(root, 'specs', name);
774
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
775
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify({
776
- feature_name: name,
777
- status: 'done',
778
- current_phase: 'closeout',
779
- task_registry: { 'tasks/task-R0-01-x.md': { status: 'done', completed_at: '2026-01-01T00:00:00.000Z' } },
780
- }, null, 2)}\n`);
781
- fs.writeFileSync(path.join(feature, 'tasks', 'task-R0-01-x.md'), '# Task\n\nStatus: done\n');
806
+ writeProcessTask(feature, 'done', null, 'node --test', 'task-01-x.md');
807
+ fs.writeFileSync(path.join(feature, 'task-02-y.md'),
808
+ '# Task 02\n\nStatus: pending\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: node --test\n');
782
809
  }
783
810
  const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
784
811
 
@@ -880,16 +907,7 @@ test('empty or forged Codex hook authority fails closed', () => {
880
907
  assert.match(block.reason, /hook payload (is empty|must be a JSON object)/i);
881
908
  }
882
909
 
883
- const feature = path.join(root, 'specs', 'auth');
884
- const taskPath = 'tasks/task.md';
885
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
886
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
887
- status: 'in_progress',
888
- current_phase: 'closeout',
889
- feature_name: 'auth',
890
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
891
- }));
892
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
910
+ writeProcessTask(path.join(root, 'specs', 'auth'), 'done');
893
911
  fs.writeFileSync(path.join(root, '.codex', 'runtime.json'), JSON.stringify({
894
912
  spec: { completion_gate: false },
895
913
  }));
@@ -911,40 +929,6 @@ test('empty or forged Codex hook authority fails closed', () => {
911
929
  });
912
930
  });
913
931
 
914
- test('explicit Strict Codex completion ignores worker-writable proof strings and remains blocked', () => {
915
- inHookFixture((root, hooks) => {
916
- const feature = path.join(root, 'specs', 'auth');
917
- const taskPath = 'tasks/task.md';
918
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
919
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
920
- status: 'in_progress',
921
- current_phase: 'closeout',
922
- feature_name: 'auth',
923
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
924
- proofs: {
925
- needsInspection: 'inspection completed',
926
- needsIndependentAudit: 'PASS',
927
- needsResearchGrounding: 'research completed',
928
- },
929
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
930
- }));
931
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
932
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
933
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
934
- ].join('\n'), 'auth'));
935
- const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
936
- cwd: root,
937
- session_id: 'session-a',
938
- hook_event_name: 'Stop',
939
- stop_hook_active: false,
940
- });
941
- assert.equal(result.status, 0, result.stderr);
942
- const block = JSON.parse(result.stdout);
943
- assert.equal(block.decision, 'block');
944
- assert.match(block.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
945
- });
946
- });
947
-
948
932
  test('Codex state hook persists direct native event fields at project root', () => {
949
933
  inHookFixture((root, hooks) => {
950
934
  const nested = path.join(root, 'packages', 'app');
@@ -1053,61 +1037,55 @@ test('Codex canonical receipt provenance requires both Base and Head', () => {
1053
1037
  // Integration via spec-gate hook: only Base blocks, both passes
1054
1038
  inHookFixture((root, hooks) => {
1055
1039
  const feature = path.join(root, 'specs', 'auth');
1056
- const taskPath = 'tasks/task-R0-01-auth.md';
1057
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1058
1040
  const gate = path.join(hooks, 'spec-gate.cjs');
1059
1041
  const payload = { cwd: path.join(root, 'packages', 'app'), session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
1042
+ const output = ['```', 'pnpm test', 'ok 1', '```'];
1043
+ fs.mkdirSync(payload.cwd, { recursive: true });
1060
1044
 
1061
1045
  // only Base -> block
1062
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'closeout', feature_name: 'auth', task_registry: { [taskPath]: { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } } }));
1063
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
1064
- fs.mkdirSync(payload.cwd, { recursive: true });
1046
+ writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc', ...output]);
1065
1047
  const blocked = runHook(gate, payload.cwd, payload);
1066
1048
  assert.equal(JSON.parse(blocked.stdout).decision, 'block');
1067
1049
  assert.match(JSON.parse(blocked.stdout).reason, /\bprovenance\b/);
1068
1050
 
1069
1051
  // both -> no block
1070
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, `Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\n`));
1052
+ writeProcessTask(feature, 'done', bindReceipt(root, ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, ...output].join('\n')).split('\n'));
1071
1053
  const passed = runHook(gate, payload.cwd, payload);
1072
1054
  assert.equal(passed.stdout, '');
1073
1055
 
1074
1056
  // empty Base: should block provenance — reset gate cache so re-checked as newly-done
1075
1057
  try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
1076
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: def\n');
1058
+ writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base:', 'Head: def', ...output]);
1077
1059
  const blockedEmpty = runHook(gate, payload.cwd, payload);
1078
1060
  assert.equal(JSON.parse(blockedEmpty.stdout).decision, 'block');
1079
1061
  assert.match(JSON.parse(blockedEmpty.stdout).reason, /\bprovenance\b/);
1080
1062
 
1081
1063
  // bare base_sha without colon should block — reset cache again
1082
1064
  try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
1083
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha head_sha\n');
1065
+ writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'base_sha head_sha', ...output]);
1084
1066
  const blockedBare = runHook(gate, payload.cwd, payload);
1085
1067
  assert.equal(JSON.parse(blockedBare.stdout).decision, 'block');
1086
1068
  assert.match(JSON.parse(blockedBare.stdout).reason, /\bprovenance\b/);
1087
1069
  });
1088
1070
  });
1089
1071
 
1090
- test('Codex cache hardening: every done re-validated — mutation, deletion, malformed cache, unchanged valid', () => {
1072
+ test('Codex cache hardening: every done re-validated — mutation, malformed cache, unchanged valid', () => {
1091
1073
  inHookFixture((root, hooks) => {
1092
1074
  const feature = path.join(root, 'specs', 'auth');
1093
- const taskPath = 'tasks/task-R0-01-auth.md';
1094
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1075
+ const taskPath = 'task-01-auth.md';
1095
1076
  const gate = path.join(hooks, 'spec-gate.cjs');
1096
1077
  const appCwd = path.join(root, 'packages', 'app');
1097
1078
  fs.mkdirSync(appCwd, { recursive: true });
1098
1079
  const payload = { cwd: appCwd, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
1099
1080
  const cacheFile = path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json');
1100
- const validBodyTemplate = [
1101
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
1102
- 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1103
- '```', 'pass', '```', '',
1104
- ].join('\n');
1081
+ const output = ['```', 'pass', '```'];
1082
+ const writeValid = () => writeProcessTask(feature, 'done', (writeProcessTask(feature, 'done'), bindReceipt(root, [
1083
+ 'Verification: PASS', 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, ...output,
1084
+ ].join('\n'))).split('\n'));
1105
1085
 
1106
1086
  // first-run (no cache) with valid receipt → no block, cache seeded
1107
1087
  try { fs.rmSync(cacheFile, { force: true }); } catch {}
1108
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'closeout', feature_name: 'auth', task_registry: { [taskPath]: { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } } }));
1109
- const validBody = bindReceipt(root, validBodyTemplate);
1110
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1088
+ writeValid();
1111
1089
  let res = runHook(gate, appCwd, payload);
1112
1090
  assert.equal(res.stdout, '', 'first-run valid should not block');
1113
1091
  assert.ok(fs.existsSync(cacheFile), 'cache file must be created on first valid run');
@@ -1119,7 +1097,7 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
1119
1097
  assert.equal(res.stdout, '', 'cache-hit unchanged valid must not block');
1120
1098
 
1121
1099
  // mutation: remove Command/Verification → must block even though cache says done
1122
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nExit: 0\nBase: abc123\nHead: def456\n```\npass\n```\n');
1100
+ writeProcessTask(feature, 'done', ['Exit: 0', 'Base: abc123', 'Head: def456', ...output]);
1123
1101
  res = runHook(gate, appCwd, payload);
1124
1102
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'mutated receipt missing Verification/Command should block on cache-hit');
1125
1103
  assert.match(JSON.parse(res.stdout).reason, /\bverification_state\b|\bcommand\b/);
@@ -1128,33 +1106,21 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
1128
1106
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'second hit after mutation still blocks');
1129
1107
 
1130
1108
  // restore valid, then mutate provenance (remove Head) → block
1131
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1109
+ writeValid();
1132
1110
  assert.equal(runHook(gate, appCwd, payload).stdout, '', 'restored valid should pass again');
1133
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\n```\npass\n```\n');
1111
+ writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc123', ...output]);
1134
1112
  res = runHook(gate, appCwd, payload);
1135
1113
  assert.equal(JSON.parse(res.stdout).decision, 'block');
1136
1114
  assert.match(JSON.parse(res.stdout).reason, /\bprovenance\b/);
1137
1115
 
1138
- // deletion → block with check a
1139
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1140
- assert.equal(runHook(gate, appCwd, payload).stdout, '');
1141
- fs.unlinkSync(path.join(feature, taskPath));
1142
- res = runHook(gate, appCwd, payload);
1143
- assert.equal(JSON.parse(res.stdout).decision, 'block');
1144
- assert.match(JSON.parse(res.stdout).reason, /\ba\b/);
1145
- // restore for next sub-test
1146
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1147
- try { fs.rmSync(cacheFile, { force: true }); } catch {}
1148
- assert.equal(runHook(gate, appCwd, payload).stdout, '');
1149
-
1150
1116
  // malformed cache with valid receipt → must still pass (cache parse fail-open)
1117
+ writeValid();
1151
1118
  fs.writeFileSync(cacheFile, '{ malformed');
1152
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1153
1119
  res = runHook(gate, appCwd, payload);
1154
1120
  assert.equal(res.stdout, '', 'malformed cache with valid receipt must not block');
1155
1121
  // malformed cache with invalid receipt → must still block
1156
1122
  fs.writeFileSync(cacheFile, '{ malformed again');
1157
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
1123
+ writeProcessTask(feature, 'done');
1158
1124
  res = runHook(gate, appCwd, payload);
1159
1125
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'malformed cache with invalid receipt must block');
1160
1126
  });
@@ -1164,18 +1130,11 @@ test('Codex cache identity separates same-feature receipts from different roots'
1164
1130
  const contexts = [];
1165
1131
  for (let index = 0; index < 2; index += 1) {
1166
1132
  const context = inHookFixture((root, hooks) => {
1167
- const feature = path.join(root, 'specs', 'auth');
1168
- const taskPath = 'tasks/task.md';
1169
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1170
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
1171
- status: 'in_progress',
1172
- feature_name: 'auth',
1173
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
1174
- }));
1175
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
1176
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
1177
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1178
- ].join('\n')));
1133
+ writeProcessTask(path.join(root, 'specs', 'auth'), 'done', null, 'node --test');
1134
+ writeProcessTask(path.join(root, 'specs', 'auth'), 'done', bindReceipt(root, [
1135
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1136
+ '```', 'node --test', 'ok 1', '```',
1137
+ ].join('\n')).split('\n'), 'node --test');
1179
1138
  const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
1180
1139
  cwd: root,
1181
1140
  session_id: 'session-a',
@@ -1212,33 +1171,32 @@ test('Codex P0 regression: placeholder, explicit failure, artifact, symlink and
1212
1171
  assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: TBD\n').includes('artifact_hash'));
1213
1172
  assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: \n').includes('artifact_hash'));
1214
1173
  assert.deepEqual(specReceipt.validateCanonicalReceipt(`Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\nArtifact: bundle\nsha256: ${'a'.repeat(64)}\n`), []);
1215
- // Probe D/E: symlink spec/task
1174
+ // Probe D/E: symlink packet and task
1216
1175
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-reg-'));
1217
1176
  try {
1218
1177
  const specsDir = path2.join(tmp, 'specs');
1219
- fs2.mkdirSync(specsDir, { recursive: true });
1220
- fs2.mkdirSync(path2.join(specsDir, 'valid'), { recursive: true });
1221
- fs2.writeFileSync(path2.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1178
+ writeProcessTask(path2.join(specsDir, 'valid'), 'pending');
1222
1179
  const outside = path2.join(tmp, 'outside');
1223
- fs2.mkdirSync(outside, { recursive: true });
1224
- fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1225
- const link = path2.join(specsDir, 'linked');
1226
- fs2.symlinkSync(outside, link);
1227
- assert.equal(specUtils.resolveActiveSpec(tmp, {}, 'linked', null).error, 'explicit_malformed');
1228
- assert.equal(specUtils.resolveActiveSpec(tmp, {}, null, path2.join(specsDir, 'linked', 'spec.json')).error, 'explicit_malformed');
1229
- // task symlink
1180
+ writeProcessTask(outside, 'pending');
1181
+ fs2.symlinkSync(outside, path2.join(specsDir, 'linked'));
1182
+ assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, 'linked', null).error, 'explicit_malformed');
1183
+ assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, null, path2.join(specsDir, 'linked', 'plan.md')).error, 'explicit_malformed');
1184
+ fs2.rmSync(path2.join(specsDir, 'linked'));
1185
+ // task symlink to a file outside the packet
1230
1186
  const featDir = path2.join(specsDir, 'valid');
1231
- fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
1232
1187
  const outsideTask = path2.join(tmp, 'outside-task.md');
1233
- fs2.writeFileSync(outsideTask, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1234
- const taskLink = path2.join(featDir, 'tasks', 'task.md');
1235
- fs2.symlinkSync(outsideTask, taskLink);
1236
- const fails = specReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
1237
- assert.ok(fails.includes('a'));
1238
- // malformed spec
1239
- fs2.mkdirSync(path2.join(specsDir, 'bad'), { recursive: true });
1240
- fs2.writeFileSync(path2.join(specsDir, 'bad', 'spec.json'), '{ bad');
1241
- const mal = specUtils.resolveActiveSpec(tmp, {}, null, null);
1188
+ fs2.copyFileSync(path2.join(featDir, 'task-01-auth.md'), outsideTask);
1189
+ fs2.symlinkSync(outsideTask, path2.join(featDir, 'task-02-link.md'));
1190
+ const proof = specReceipt.checkWorkflowReceiptDetails(featDir, 'task-02-link.md', null);
1191
+ assert.ok(proof.failures.includes('unsafe_path'), `task symlink must be unsafe, got ${proof.failures}`);
1192
+ assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, null, null).error, 'invalid_specs');
1193
+ fs2.rmSync(path2.join(featDir, 'task-02-link.md'));
1194
+ // malformed packet: dependency on a missing task
1195
+ const bad = path2.join(specsDir, 'bad');
1196
+ writeProcessTask(bad, 'pending');
1197
+ fs2.writeFileSync(path2.join(bad, 'task-01-auth.md'),
1198
+ fs2.readFileSync(path2.join(bad, 'task-01-auth.md'), 'utf8').replace('- none', '- task-09-missing.md'));
1199
+ const mal = specUtils.resolveWorkflowCandidate(tmp, {}, null, null);
1242
1200
  assert.equal(mal.error, 'invalid_specs');
1243
1201
  } finally {
1244
1202
  fs2.rmSync(tmp, { recursive: true, force: true });
@@ -1289,17 +1247,10 @@ test('Codex adapter parity - phantom vectors via shared validator (r3)', () => {
1289
1247
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-tap-heading-'));
1290
1248
  try {
1291
1249
  const featureDir = path.join(tmp, 'feature');
1292
- const taskPath = 'tasks/task.md';
1293
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1294
- const taskPrefix = ['# Task', '', '**Status:** done', '', '## Evidence', '', base.trim()];
1295
- fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# fail 1', ''].join('\n'));
1296
- const tapFailure = receipt.checkReceipt(featureDir, taskPath, { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
1297
- assert.ok(tapFailure.includes('c'), 'unindented TAP # fail 1 must reach the shared validator');
1298
-
1299
- fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# Notes', 'FAILED tests/test_demo.py', ''].join('\n'));
1300
- const markdownBody = receipt.evidenceBody(fs.readFileSync(path.join(featureDir, taskPath), 'utf8'));
1301
- assert.doesNotMatch(markdownBody, /FAILED tests\/test_demo\.py/);
1302
- assert.deepEqual(receipt.validateCanonicalReceipt(markdownBody), [], 'a real Markdown heading must still end Evidence');
1250
+ const receiptFields = base.trim().split('\n');
1251
+ writeProcessTask(featureDir, 'done', [...receiptFields, '```', 'pnpm test', '# fail 1', '```']);
1252
+ const tapFailure = receipt.checkWorkflowReceiptDetails(featureDir, 'task-01-auth.md', null);
1253
+ assert.ok(tapFailure.failures.includes('verification_state'), `unindented TAP # fail 1 must reach the shared validator, got ${tapFailure.failures}`);
1303
1254
  } finally {
1304
1255
  fs.rmSync(tmp, { recursive: true, force: true });
1305
1256
  }
@@ -1329,38 +1280,20 @@ test('Codex adapter enforces declared task artifact SHA-256 with missing, invali
1329
1280
  const digest = crypto.createHash('sha256').update(artifact).digest('hex');
1330
1281
  const cases = [
1331
1282
  ['', true],
1332
- ['sha256: abc123\n', true],
1333
- [`sha256: ${'0'.repeat(64)}\n`, true],
1334
- [`sha256: ${digest}\n`, false],
1283
+ ['sha256: abc123', true],
1284
+ [`sha256: ${'0'.repeat(64)}`, true],
1285
+ [`sha256: ${digest}`, false],
1335
1286
  ];
1336
1287
  for (const [hashLine, shouldBlock] of cases) {
1337
1288
  inHookFixture((root, hooks) => {
1338
1289
  const feature = path.join(root, 'specs', 'artifact-demo');
1339
- const taskPath = 'tasks/task.md';
1340
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1341
1290
  fs.mkdirSync(path.join(root, 'output'), { recursive: true });
1342
1291
  fs.writeFileSync(path.join(root, 'output', 'bundle.js'), artifact);
1343
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
1344
- status: 'in_progress',
1345
- current_phase: 'closeout',
1346
- feature_name: 'artifact-demo',
1347
- task_registry: {
1348
- [taskPath]: {
1349
- status: 'done',
1350
- completed_at: '2026-08-11T00:00:00.000Z',
1351
- artifacts: ['output/bundle.js'],
1352
- },
1353
- },
1354
- }));
1355
- fs.writeFileSync(path.join(feature, taskPath), [
1356
- '# Task', '', '**Status:** done', '', '## Evidence', '',
1292
+ writeProcessTask(feature, 'done', null, 'node --test');
1293
+ writeProcessTask(feature, 'done', bindReceipt(root, [
1357
1294
  'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1358
- 'Artifact: output/bundle.js', hashLine,
1359
- ].join('\n'));
1360
- fs.writeFileSync(
1361
- path.join(feature, taskPath),
1362
- bindReceipt(root, fs.readFileSync(path.join(feature, taskPath), 'utf8'), 'artifact-demo'),
1363
- );
1295
+ 'Artifact: output/bundle.js', hashLine, '```', 'node --test', 'ok 1', '```',
1296
+ ].join('\n'), 'artifact-demo').split('\n'), 'node --test');
1364
1297
  const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
1365
1298
  cwd: root,
1366
1299
  session_id: 'session-a',