@haposoft/cafekit 0.16.7 → 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 (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -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');
@@ -611,40 +666,12 @@ test('Codex inspect hook scans native shell commands and structured paths', () =
611
666
  });
612
667
  });
613
668
 
614
- test('Codex task scaffold hook blocks apply_patch Add File with stderr guidance', () => {
615
- inHookFixture((root, hooks) => {
616
- const result = runHook(path.join(hooks, 'task-scaffold-guard.cjs'), root, {
617
- cwd: root,
618
- session_id: 'session-a',
619
- hook_event_name: 'PreToolUse',
620
- tool_name: 'apply_patch',
621
- tool_input: {
622
- command: '*** Begin Patch\n*** Add File: specs/auth/tasks/task-R0-01-auth.md\n+x\n*** End Patch\n'
623
- }
624
- });
625
-
626
- assert.equal(result.status, 2);
627
- assert.match(result.stderr, /TASK SCAFFOLD REQUIRED/);
628
- assert.match(result.stderr, /node \.codex\/scripts\/spec-scaffold\.cjs auth/);
629
- });
630
- });
631
-
632
669
  test('Codex completion gate cannot be bypassed from a nested cwd', () => {
633
670
  inHookFixture((root, hooks) => {
634
671
  const nested = path.join(root, 'packages', 'app');
635
672
  const feature = path.join(root, 'specs', 'auth');
636
- const taskPath = 'tasks/task-R0-01-auth.md';
637
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
638
673
  fs.mkdirSync(nested, { recursive: true });
639
- const spec = {
640
- status: 'in_progress',
641
- feature_name: 'auth',
642
- task_registry: {
643
- [taskPath]: { status: 'pending', completed_at: null }
644
- }
645
- };
646
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
647
- fs.writeFileSync(path.join(feature, taskPath), 'Status: pending\n');
674
+ writeProcessTask(feature, 'pending', null, 'node --test');
648
675
  const gate = path.join(hooks, 'spec-gate.cjs');
649
676
  const payload = {
650
677
  cwd: nested,
@@ -658,32 +685,19 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
658
685
  assert.equal(seeded.status, 0);
659
686
  assert.equal(seeded.stdout, '');
660
687
 
661
- spec.task_registry[taskPath] = {
662
- status: 'done',
663
- completed_at: '2026-07-29T10:00:00.000Z'
664
- };
665
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
666
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
667
-
668
- const blockedBeforeCloseout = runHook(gate, nested, payload);
669
- assert.equal(blockedBeforeCloseout.status, 0);
670
- assert.equal(JSON.parse(blockedBeforeCloseout.stdout).decision, 'block');
671
- assert.match(JSON.parse(blockedBeforeCloseout.stdout).reason, /verification receipt/);
672
-
673
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
674
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
675
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
676
- ].join('\n'), 'auth'));
677
- const validBeforeCloseout = runHook(gate, nested, payload);
678
- assert.equal(validBeforeCloseout.status, 0);
679
- assert.equal(validBeforeCloseout.stdout, '');
680
- assert.equal(fs.existsSync(path.join(feature, 'feature-receipt.md')), false);
681
-
682
- spec.current_phase = 'closeout';
683
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
684
- const closeout = runHook(gate, nested, payload);
685
- assert.equal(closeout.status, 0);
686
- 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, '');
687
701
  });
688
702
  });
689
703
 
@@ -783,20 +797,15 @@ test('Codex process-v3 Receipt command must match the exact Verification Plan co
783
797
  });
784
798
 
785
799
  test('Codex recorded active feature turns the identity block into receipt validation', () => {
786
- // Issue #79 end to end on this runtime. Two legacy packets both claiming closeout make
787
- // the gate answer a missing-receipt violation with an identity complaint whose stated
788
- // 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.
789
803
  inHookFixture((root, hooks) => {
790
804
  for (const name of ['alpha', 'beta']) {
791
805
  const feature = path.join(root, 'specs', name);
792
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
793
- fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify({
794
- feature_name: name,
795
- status: 'done',
796
- current_phase: 'closeout',
797
- task_registry: { 'tasks/task-R0-01-x.md': { status: 'done', completed_at: '2026-01-01T00:00:00.000Z' } },
798
- }, null, 2)}\n`);
799
- 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');
800
809
  }
801
810
  const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
802
811
 
@@ -898,16 +907,7 @@ test('empty or forged Codex hook authority fails closed', () => {
898
907
  assert.match(block.reason, /hook payload (is empty|must be a JSON object)/i);
899
908
  }
900
909
 
901
- const feature = path.join(root, 'specs', 'auth');
902
- const taskPath = 'tasks/task.md';
903
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
904
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
905
- status: 'in_progress',
906
- current_phase: 'closeout',
907
- feature_name: 'auth',
908
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
909
- }));
910
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
910
+ writeProcessTask(path.join(root, 'specs', 'auth'), 'done');
911
911
  fs.writeFileSync(path.join(root, '.codex', 'runtime.json'), JSON.stringify({
912
912
  spec: { completion_gate: false },
913
913
  }));
@@ -929,40 +929,6 @@ test('empty or forged Codex hook authority fails closed', () => {
929
929
  });
930
930
  });
931
931
 
932
- test('explicit Strict Codex completion ignores worker-writable proof strings and remains blocked', () => {
933
- inHookFixture((root, hooks) => {
934
- const feature = path.join(root, 'specs', 'auth');
935
- const taskPath = 'tasks/task.md';
936
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
937
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
938
- status: 'in_progress',
939
- current_phase: 'closeout',
940
- feature_name: 'auth',
941
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
942
- proofs: {
943
- needsInspection: 'inspection completed',
944
- needsIndependentAudit: 'PASS',
945
- needsResearchGrounding: 'research completed',
946
- },
947
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
948
- }));
949
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
950
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
951
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
952
- ].join('\n'), 'auth'));
953
- const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
954
- cwd: root,
955
- session_id: 'session-a',
956
- hook_event_name: 'Stop',
957
- stop_hook_active: false,
958
- });
959
- assert.equal(result.status, 0, result.stderr);
960
- const block = JSON.parse(result.stdout);
961
- assert.equal(block.decision, 'block');
962
- assert.match(block.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
963
- });
964
- });
965
-
966
932
  test('Codex state hook persists direct native event fields at project root', () => {
967
933
  inHookFixture((root, hooks) => {
968
934
  const nested = path.join(root, 'packages', 'app');
@@ -1036,24 +1002,6 @@ test('Codex state hook persists direct native event fields at project root', ()
1036
1002
  });
1037
1003
  });
1038
1004
 
1039
- test('semantic review authority hook is registered only for SubagentStop on Claude and Codex', () => {
1040
- const configurations = [
1041
- JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'), 'utf8')).hooks,
1042
- JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks.json'), 'utf8')).hooks,
1043
- ];
1044
- for (const hooks of configurations) {
1045
- const registrations = [];
1046
- for (const [event, groups] of Object.entries(hooks)) {
1047
- for (const group of groups) {
1048
- for (const hook of group.hooks || []) {
1049
- if (String(hook.command || '').includes('semantic-review-authority.cjs')) registrations.push(event);
1050
- }
1051
- }
1052
- }
1053
- assert.deepEqual(registrations, ['SubagentStop']);
1054
- }
1055
- });
1056
-
1057
1005
  test('Codex canonical receipt provenance requires both Base and Head', () => {
1058
1006
  const receipt = require(path.join(CODEX_HOOKS, 'lib', 'spec-receipt.cjs'));
1059
1007
  const onlyBase = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
@@ -1089,61 +1037,55 @@ test('Codex canonical receipt provenance requires both Base and Head', () => {
1089
1037
  // Integration via spec-gate hook: only Base blocks, both passes
1090
1038
  inHookFixture((root, hooks) => {
1091
1039
  const feature = path.join(root, 'specs', 'auth');
1092
- const taskPath = 'tasks/task-R0-01-auth.md';
1093
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1094
1040
  const gate = path.join(hooks, 'spec-gate.cjs');
1095
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 });
1096
1044
 
1097
1045
  // only Base -> block
1098
- 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' } } }));
1099
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
1100
- fs.mkdirSync(payload.cwd, { recursive: true });
1046
+ writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc', ...output]);
1101
1047
  const blocked = runHook(gate, payload.cwd, payload);
1102
1048
  assert.equal(JSON.parse(blocked.stdout).decision, 'block');
1103
1049
  assert.match(JSON.parse(blocked.stdout).reason, /\bprovenance\b/);
1104
1050
 
1105
1051
  // both -> no block
1106
- 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'));
1107
1053
  const passed = runHook(gate, payload.cwd, payload);
1108
1054
  assert.equal(passed.stdout, '');
1109
1055
 
1110
1056
  // empty Base: should block provenance — reset gate cache so re-checked as newly-done
1111
1057
  try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
1112
- 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]);
1113
1059
  const blockedEmpty = runHook(gate, payload.cwd, payload);
1114
1060
  assert.equal(JSON.parse(blockedEmpty.stdout).decision, 'block');
1115
1061
  assert.match(JSON.parse(blockedEmpty.stdout).reason, /\bprovenance\b/);
1116
1062
 
1117
1063
  // bare base_sha without colon should block — reset cache again
1118
1064
  try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
1119
- 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]);
1120
1066
  const blockedBare = runHook(gate, payload.cwd, payload);
1121
1067
  assert.equal(JSON.parse(blockedBare.stdout).decision, 'block');
1122
1068
  assert.match(JSON.parse(blockedBare.stdout).reason, /\bprovenance\b/);
1123
1069
  });
1124
1070
  });
1125
1071
 
1126
- 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', () => {
1127
1073
  inHookFixture((root, hooks) => {
1128
1074
  const feature = path.join(root, 'specs', 'auth');
1129
- const taskPath = 'tasks/task-R0-01-auth.md';
1130
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1075
+ const taskPath = 'task-01-auth.md';
1131
1076
  const gate = path.join(hooks, 'spec-gate.cjs');
1132
1077
  const appCwd = path.join(root, 'packages', 'app');
1133
1078
  fs.mkdirSync(appCwd, { recursive: true });
1134
1079
  const payload = { cwd: appCwd, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
1135
1080
  const cacheFile = path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json');
1136
- const validBodyTemplate = [
1137
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
1138
- 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1139
- '```', 'pass', '```', '',
1140
- ].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'));
1141
1085
 
1142
1086
  // first-run (no cache) with valid receipt → no block, cache seeded
1143
1087
  try { fs.rmSync(cacheFile, { force: true }); } catch {}
1144
- 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' } } }));
1145
- const validBody = bindReceipt(root, validBodyTemplate);
1146
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1088
+ writeValid();
1147
1089
  let res = runHook(gate, appCwd, payload);
1148
1090
  assert.equal(res.stdout, '', 'first-run valid should not block');
1149
1091
  assert.ok(fs.existsSync(cacheFile), 'cache file must be created on first valid run');
@@ -1155,7 +1097,7 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
1155
1097
  assert.equal(res.stdout, '', 'cache-hit unchanged valid must not block');
1156
1098
 
1157
1099
  // mutation: remove Command/Verification → must block even though cache says done
1158
- 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]);
1159
1101
  res = runHook(gate, appCwd, payload);
1160
1102
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'mutated receipt missing Verification/Command should block on cache-hit');
1161
1103
  assert.match(JSON.parse(res.stdout).reason, /\bverification_state\b|\bcommand\b/);
@@ -1164,33 +1106,21 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
1164
1106
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'second hit after mutation still blocks');
1165
1107
 
1166
1108
  // restore valid, then mutate provenance (remove Head) → block
1167
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1109
+ writeValid();
1168
1110
  assert.equal(runHook(gate, appCwd, payload).stdout, '', 'restored valid should pass again');
1169
- 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]);
1170
1112
  res = runHook(gate, appCwd, payload);
1171
1113
  assert.equal(JSON.parse(res.stdout).decision, 'block');
1172
1114
  assert.match(JSON.parse(res.stdout).reason, /\bprovenance\b/);
1173
1115
 
1174
- // deletion → block with check a
1175
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1176
- assert.equal(runHook(gate, appCwd, payload).stdout, '');
1177
- fs.unlinkSync(path.join(feature, taskPath));
1178
- res = runHook(gate, appCwd, payload);
1179
- assert.equal(JSON.parse(res.stdout).decision, 'block');
1180
- assert.match(JSON.parse(res.stdout).reason, /\ba\b/);
1181
- // restore for next sub-test
1182
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1183
- try { fs.rmSync(cacheFile, { force: true }); } catch {}
1184
- assert.equal(runHook(gate, appCwd, payload).stdout, '');
1185
-
1186
1116
  // malformed cache with valid receipt → must still pass (cache parse fail-open)
1117
+ writeValid();
1187
1118
  fs.writeFileSync(cacheFile, '{ malformed');
1188
- fs.writeFileSync(path.join(feature, taskPath), validBody);
1189
1119
  res = runHook(gate, appCwd, payload);
1190
1120
  assert.equal(res.stdout, '', 'malformed cache with valid receipt must not block');
1191
1121
  // malformed cache with invalid receipt → must still block
1192
1122
  fs.writeFileSync(cacheFile, '{ malformed again');
1193
- fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
1123
+ writeProcessTask(feature, 'done');
1194
1124
  res = runHook(gate, appCwd, payload);
1195
1125
  assert.equal(JSON.parse(res.stdout).decision, 'block', 'malformed cache with invalid receipt must block');
1196
1126
  });
@@ -1200,18 +1130,11 @@ test('Codex cache identity separates same-feature receipts from different roots'
1200
1130
  const contexts = [];
1201
1131
  for (let index = 0; index < 2; index += 1) {
1202
1132
  const context = inHookFixture((root, hooks) => {
1203
- const feature = path.join(root, 'specs', 'auth');
1204
- const taskPath = 'tasks/task.md';
1205
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1206
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
1207
- status: 'in_progress',
1208
- feature_name: 'auth',
1209
- task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
1210
- }));
1211
- fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
1212
- 'Status: done', '', '## Evidence', '', 'Verification: PASS',
1213
- 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1214
- ].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');
1215
1138
  const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
1216
1139
  cwd: root,
1217
1140
  session_id: 'session-a',
@@ -1248,33 +1171,32 @@ test('Codex P0 regression: placeholder, explicit failure, artifact, symlink and
1248
1171
  assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: TBD\n').includes('artifact_hash'));
1249
1172
  assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: \n').includes('artifact_hash'));
1250
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`), []);
1251
- // Probe D/E: symlink spec/task
1174
+ // Probe D/E: symlink packet and task
1252
1175
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-reg-'));
1253
1176
  try {
1254
1177
  const specsDir = path2.join(tmp, 'specs');
1255
- fs2.mkdirSync(specsDir, { recursive: true });
1256
- fs2.mkdirSync(path2.join(specsDir, 'valid'), { recursive: true });
1257
- fs2.writeFileSync(path2.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1178
+ writeProcessTask(path2.join(specsDir, 'valid'), 'pending');
1258
1179
  const outside = path2.join(tmp, 'outside');
1259
- fs2.mkdirSync(outside, { recursive: true });
1260
- fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1261
- const link = path2.join(specsDir, 'linked');
1262
- fs2.symlinkSync(outside, link);
1263
- assert.equal(specUtils.resolveActiveSpec(tmp, {}, 'linked', null).error, 'explicit_malformed');
1264
- assert.equal(specUtils.resolveActiveSpec(tmp, {}, null, path2.join(specsDir, 'linked', 'spec.json')).error, 'explicit_malformed');
1265
- // 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
1266
1186
  const featDir = path2.join(specsDir, 'valid');
1267
- fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
1268
1187
  const outsideTask = path2.join(tmp, 'outside-task.md');
1269
- fs2.writeFileSync(outsideTask, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1270
- const taskLink = path2.join(featDir, 'tasks', 'task.md');
1271
- fs2.symlinkSync(outsideTask, taskLink);
1272
- const fails = specReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
1273
- assert.ok(fails.includes('a'));
1274
- // malformed spec
1275
- fs2.mkdirSync(path2.join(specsDir, 'bad'), { recursive: true });
1276
- fs2.writeFileSync(path2.join(specsDir, 'bad', 'spec.json'), '{ bad');
1277
- 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);
1278
1200
  assert.equal(mal.error, 'invalid_specs');
1279
1201
  } finally {
1280
1202
  fs2.rmSync(tmp, { recursive: true, force: true });
@@ -1325,17 +1247,10 @@ test('Codex adapter parity - phantom vectors via shared validator (r3)', () => {
1325
1247
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-tap-heading-'));
1326
1248
  try {
1327
1249
  const featureDir = path.join(tmp, 'feature');
1328
- const taskPath = 'tasks/task.md';
1329
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1330
- const taskPrefix = ['# Task', '', '**Status:** done', '', '## Evidence', '', base.trim()];
1331
- fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# fail 1', ''].join('\n'));
1332
- const tapFailure = receipt.checkReceipt(featureDir, taskPath, { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
1333
- assert.ok(tapFailure.includes('c'), 'unindented TAP # fail 1 must reach the shared validator');
1334
-
1335
- fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# Notes', 'FAILED tests/test_demo.py', ''].join('\n'));
1336
- const markdownBody = receipt.evidenceBody(fs.readFileSync(path.join(featureDir, taskPath), 'utf8'));
1337
- assert.doesNotMatch(markdownBody, /FAILED tests\/test_demo\.py/);
1338
- 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}`);
1339
1254
  } finally {
1340
1255
  fs.rmSync(tmp, { recursive: true, force: true });
1341
1256
  }
@@ -1365,38 +1280,20 @@ test('Codex adapter enforces declared task artifact SHA-256 with missing, invali
1365
1280
  const digest = crypto.createHash('sha256').update(artifact).digest('hex');
1366
1281
  const cases = [
1367
1282
  ['', true],
1368
- ['sha256: abc123\n', true],
1369
- [`sha256: ${'0'.repeat(64)}\n`, true],
1370
- [`sha256: ${digest}\n`, false],
1283
+ ['sha256: abc123', true],
1284
+ [`sha256: ${'0'.repeat(64)}`, true],
1285
+ [`sha256: ${digest}`, false],
1371
1286
  ];
1372
1287
  for (const [hashLine, shouldBlock] of cases) {
1373
1288
  inHookFixture((root, hooks) => {
1374
1289
  const feature = path.join(root, 'specs', 'artifact-demo');
1375
- const taskPath = 'tasks/task.md';
1376
- fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
1377
1290
  fs.mkdirSync(path.join(root, 'output'), { recursive: true });
1378
1291
  fs.writeFileSync(path.join(root, 'output', 'bundle.js'), artifact);
1379
- fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
1380
- status: 'in_progress',
1381
- current_phase: 'closeout',
1382
- feature_name: 'artifact-demo',
1383
- task_registry: {
1384
- [taskPath]: {
1385
- status: 'done',
1386
- completed_at: '2026-08-11T00:00:00.000Z',
1387
- artifacts: ['output/bundle.js'],
1388
- },
1389
- },
1390
- }));
1391
- fs.writeFileSync(path.join(feature, taskPath), [
1392
- '# Task', '', '**Status:** done', '', '## Evidence', '',
1292
+ writeProcessTask(feature, 'done', null, 'node --test');
1293
+ writeProcessTask(feature, 'done', bindReceipt(root, [
1393
1294
  'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
1394
- 'Artifact: output/bundle.js', hashLine,
1395
- ].join('\n'));
1396
- fs.writeFileSync(
1397
- path.join(feature, taskPath),
1398
- bindReceipt(root, fs.readFileSync(path.join(feature, taskPath), 'utf8'), 'artifact-demo'),
1399
- );
1295
+ 'Artifact: output/bundle.js', hashLine, '```', 'node --test', 'ok 1', '```',
1296
+ ].join('\n'), 'artifact-demo').split('\n'), 'node --test');
1400
1297
  const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
1401
1298
  cwd: root,
1402
1299
  session_id: 'session-a',