@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
@@ -14,16 +14,9 @@ const POLICY = require(POLICY_PATH);
14
14
  const PROVENANCE_HELPER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs'));
15
15
  const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
16
16
  const RECEIPTS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-receipt.cjs'));
17
- const FINAL_STATE = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-final-state.cjs'));
18
- const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
19
- const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
20
- const SEMANTIC_AUTHORITY = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs'));
21
- const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
22
- const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
23
17
  const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
24
18
  const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
25
19
  const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
26
- const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
27
20
  const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
28
21
  const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
29
22
  const TEST_SOURCE_ROOT = path.dirname(TEST_SKILL);
@@ -65,7 +58,6 @@ after(() => fs.rmSync(RUNTIME_ROOT, { recursive: true, force: true }));
65
58
  const EXPECTED_BASE = RUNTIME_CONTEXT.base;
66
59
  const EXPECTED_HEAD = RUNTIME_CONTEXT.head;
67
60
  const EXPECTED_PROVENANCE = { base: EXPECTED_BASE, head: EXPECTED_HEAD };
68
- const RECEIPT_BINDING = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
69
61
 
70
62
  function initFixtureGit(root) {
71
63
  for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['add', '-A'], ['commit', '-qm', 'fixture']]) {
@@ -80,9 +72,6 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
80
72
  const hooksRoot = path.join(PACKAGE_ROOT, 'src', 'claude', 'hooks');
81
73
  for (const hook of [
82
74
  'spec-gate.cjs',
83
- 'completion-authority-check.cjs',
84
- 'completion-authority-state.cjs',
85
- 'semantic-review-authority.cjs',
86
75
  ]) {
87
76
  const target = path.join(destinationRoot, 'hooks', hook);
88
77
  fs.mkdirSync(path.dirname(target), { recursive: true });
@@ -163,182 +152,10 @@ function assertVocabularyIsLegacyOnly(filePath, regions) {
163
152
  );
164
153
  }
165
154
 
166
- function runNode(root, script, args) {
167
- return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
168
- }
169
-
170
- function commandOutput(result) {
171
- return `${result.stdout}\n${result.stderr}`;
172
- }
173
-
174
- function createCanonicalAuthoringFixture() {
175
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
176
- fs.mkdirSync(path.join(root, 'src'), { recursive: true });
177
- fs.mkdirSync(path.join(root, 'test'), { recursive: true });
178
- fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
179
- fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
180
- fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
181
- const scaffold = runNode(root, SCAFFOLD, ['feature']);
182
- assert.equal(scaffold.status, 0, commandOutput(scaffold));
183
- const specDir = path.join(root, 'specs', 'feature');
184
- const specPath = path.join(specDir, 'spec.json');
185
- fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
186
-
187
- ## Requirements
188
- ### Requirement 1: Observable service behavior
189
- - **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
190
- - **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
191
- `);
192
- fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
193
-
194
- ## Architecture
195
- The entrypoint exposes the bounded service behavior and a separate proof artifact.
196
-
197
- ## Typed Anchors
198
- | ID | Type | Target | Role | Access | Action |
199
- |---|---|---|---|---|---|
200
- | A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
201
- | A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
202
-
203
- ## Canonical Contracts & Invariants
204
- ### D1 — Dispatch decision
205
- The entrypoint dispatches exactly once.
206
- ### I1 — Invalid-state invariant
207
- Invalid input never returns enabled state.
208
- ### C1 — Result contract
209
- The result contains one enabled boolean.
210
-
211
- ## Verification Definitions
212
- - **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command \`node --test test/service.test.js\`; Expected exit 0 with enabled state and separate proof evidence; Negative/failure invalid input returns rejected state without enabled output; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01, A-D-02.
213
- `);
214
- return { root, specDir, specPath };
215
- }
216
-
217
- function createOrdinaryAuthoringFixture() {
218
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
219
- fs.mkdirSync(path.join(root, 'src'), { recursive: true });
220
- fs.mkdirSync(path.join(root, 'test'), { recursive: true });
221
- fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
222
- fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
223
- const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
224
- assert.equal(scaffold.status, 0, commandOutput(scaffold));
225
- const specDir = path.join(root, 'specs', 'ordinary');
226
- const specPath = path.join(specDir, 'spec.json');
227
- fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
228
-
229
- ## Requirements
230
- ### Requirement 1: Delete behavior
231
- - **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
232
- `);
233
- fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
234
-
235
- ## Typed Anchors
236
- | ID | Type | Target | Role | Access | Action |
237
- |---|---|---|---|---|---|
238
- | A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
239
-
240
- ## Canonical Contracts & Invariants
241
- ### D1 — Delete decision
242
- The selected record is deleted rather than archived.
243
- ### I1 — State invariant
244
- Deleted state is reported only after deletion succeeds.
245
- ### C1 — Result contract
246
- The result contains the literal deleted state.
247
-
248
- ## Verification Definitions
249
- - **V1**: Criteria R1.1; Owner A-D-01; Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected exit 0 and the service reports deleted state; Negative/failure a missing record returns not-found without deleted state; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01.
250
- `);
251
- const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
252
- assert.equal(semantic.status, 0, commandOutput(semantic));
253
- const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
254
- assert.equal(authoring.status, 0, commandOutput(authoring));
255
- initFixtureGit(root);
256
- return { root, specDir, specPath };
257
- }
258
-
259
- function ordinaryReview() {
260
- return canonicalPassReview(['R1.1'], [{
261
- criterion: 'R1.1', case_kind: 'failure',
262
- scenario: 'Deletion targets a record that does not exist in the current store.',
263
- expected: 'The service returns not-found and never reports the record as deleted.',
264
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
265
- }]);
266
- }
267
-
268
- function canonicalPassReview(reviewedCriteria, counterexamples) {
269
- return {
270
- verdict: 'PASS',
271
- findings: [],
272
- unresolved_decisions: [],
273
- graph_coverage: [
274
- 'criterion_local', 'cross_criterion', 'runtime_path',
275
- 'assumption_provenance', 'compatibility_migration',
276
- ].map((surface) => ({
277
- surface, covered: true,
278
- notes: 'The review covers this semantic surface against the canonical model.',
279
- })),
280
- reviewed_criteria: reviewedCriteria,
281
- counterexamples,
282
- reviewer_evidence: null,
283
- };
284
- }
285
-
286
- function promoteCanonicalModel(fixture) {
287
- const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
288
- assert.equal(result.status, 0, commandOutput(result));
289
- }
290
-
291
- function makeCanonicalReady(fixture) {
292
- const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
293
- assert.equal(authoring.status, 0, commandOutput(authoring));
294
- const result = READINESS.finalizeReadiness({
295
- specDir: fixture.specDir,
296
- projectRoot: fixture.root,
297
- reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
298
- criterion: 'R1.1', case_kind: 'failure',
299
- scenario: 'The service receives invalid input without its required discriminator.',
300
- expected: 'The service returns rejected state and never emits enabled true.',
301
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
302
- }, {
303
- criterion: 'R1.2', case_kind: 'adversarial',
304
- scenario: 'The service reports enabled state without producing separate proof evidence.',
305
- expected: 'Verification fails because the independent proof artifact is absent.',
306
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
307
- }]),
308
- });
309
- return result.spec;
310
- }
311
-
312
- function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
313
- const context = PROVENANCE_HELPER.deriveRuntimeContext({
314
- projectRoot: root,
315
- specsRoot: path.join(root, 'specs'),
316
- specFile: path.join(root, 'specs', feature, 'spec.json'),
317
- featureName: feature,
318
- runtimeSession: session,
319
- });
320
- return value.replaceAll('0123456789abcdef0123456789abcdef01234567', context.base)
321
- .replaceAll('89abcdef0123456789abcdef0123456789abcdef', context.head);
322
- }
323
-
324
155
  function canonicalReceipt(command = 'pnpm test', extra = '') {
325
156
  return `Verification: PASS\nCommand: ${command}\nExit: 0\nBase: ${EXPECTED_BASE}\nHead: ${EXPECTED_HEAD}\n${extra}`;
326
157
  }
327
158
 
328
- function canonicalFlashTask(overrides = {}) {
329
- return {
330
- status: 'in_progress',
331
- receipt: 'FLASH_UNVERIFIED',
332
- blocker: 'awaiting /cf:test <feature>',
333
- dependencyBlocked: true,
334
- unblocks: false,
335
- feature_name: 'demo',
336
- runtime_context: RUNTIME_CONTEXT,
337
- expected_provenance: { ...EXPECTED_PROVENANCE },
338
- ...overrides,
339
- };
340
- }
341
-
342
159
  function read(filePath) {
343
160
  return fs.readFileSync(filePath, 'utf8');
344
161
  }
@@ -381,388 +198,22 @@ test('develop flags retain their process-v3 contracts', () => {
381
198
  assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
382
199
  });
383
200
 
384
- test('delegation plan consumes legacy tiers as obligations without an agent chain', () => {
385
- const light = POLICY.delegationPlan({ tier: 'Light', mode: 'full-spec', taskCount: 2 });
386
- const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
387
- const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
388
- assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
389
- assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
390
- assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
391
- for (const plan of [light, standard, deep]) {
392
- assert.equal(Object.hasOwn(plan, 'delegated'), false);
393
- assert.doesNotMatch(JSON.stringify(plan), /inspector|implementer|test-runner|code-auditor/);
394
- }
201
+ test('review and sync skills keep the verdict vocabulary and the sync-finalize path', () => {
395
202
  assert.doesNotMatch(read(GATE), /spec-review|quality-review/);
396
203
  assert.doesNotMatch(read(DEVELOP), /spec-review|quality-review/);
204
+ assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
205
+ assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
206
+ assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
207
+ assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
208
+ assert.match(read(SYNC_SKILL), /sync-finalize/);
397
209
  });
398
210
 
399
- test('lane classifier selects Direct for explicit reversible low-risk work', () => {
400
- const result = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
401
- assert.equal(result.lane, 'Direct');
402
- assert.equal(result.automaticLane, 'Direct');
403
- assert.deepEqual(result.risks, []);
404
- const policy = POLICY.lanePolicy(result);
405
- assert.deepEqual(policy.proof_obligations, ['needsExecutionProof']);
406
- assert.equal(policy.artifact_profile, 'targeted');
407
- assert.equal(policy.requiresSpec, false);
408
- assert.equal(policy.requiresState, false);
409
- assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
410
- });
411
-
412
- test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
413
- const result = POLICY.classifyLane({
414
- reversible: true,
415
- riskSignals: { auth: true, migration: true, publicContract: true },
416
- });
417
- assert.equal(result.lane, 'Standard');
418
- assert.equal(result.assuranceLevel, 'Elevated');
419
- assert.equal(result.automaticAssuranceLevel, 'Elevated');
420
- assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
421
- assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
422
- 'needsInspection', 'needsExecutionProof',
423
- ]);
424
-
425
- const strict = POLICY.classifyLane({
426
- riskSignals: { auth: true },
427
- assurance_level: 'Strict',
428
- });
429
- assert.equal(strict.lane, 'Critical');
430
- assert.equal(strict.automaticLane, 'Standard');
431
- assert.equal(strict.assuranceLevel, 'Strict');
432
- assert.equal(strict.automaticAssuranceLevel, 'Elevated');
433
- assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
434
- });
435
-
436
- test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
437
- const initial = POLICY.persistWorkflowPolicySnapshot(
438
- { feature_name: 'bounded' },
439
- { riskSignals: {} },
440
- );
441
- const snapshot = initial.workflow_policy;
442
- assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
443
- assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
444
- assert.equal(snapshot.planning_depth, 'Compact');
445
- assert.equal(snapshot.assurance_level, 'Routine');
446
- assert.deepEqual(snapshot.classified_minimum, {
447
- planning_depth: 'Compact',
448
- assurance_level: 'Routine',
449
- });
450
- for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
451
- assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
452
- }
453
- const view = POLICY.readWorkflowPolicySnapshot(initial);
454
- assert.equal(view.lane, 'Standard');
455
- assert.equal(view.automatic_lane, 'Standard');
456
- assert.equal(view.artifact_profile, 'bounded');
457
- assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
458
- assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
459
- assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
460
-
461
- const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
462
- riskSignals: { auth: true },
463
- override: 'Direct',
464
- });
465
- assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
466
-
467
- const malformed = { ...snapshot, lane: 'Critical' };
468
- const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
469
- assert.equal(validation.valid, false);
470
- assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
471
- assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
472
-
473
- const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
474
- const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
475
- assert.equal(shapeValidation.valid, false);
476
- assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
477
-
478
- const malformedRisks = { ...snapshot, risks: { auth: true } };
479
- const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
480
- assert.equal(risksValidation.valid, false);
481
- assert.match(risksValidation.errors.join('; '), /risks must be an array/);
482
- });
483
-
484
- test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
485
- const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
486
- const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
487
- const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
488
- const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
489
-
490
- for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
491
- assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
492
- }
493
- assert.equal(escalatedElevated.lane, 'Standard');
494
- assert.equal(escalatedElevated.automatic_lane, 'Standard');
495
- assert.equal(escalatedElevated.planning_depth, 'None');
496
- assert.equal(escalatedElevated.assurance_level, 'Elevated');
497
-
498
- const forgedDirectAuth = { ...direct, risks: ['auth'] };
499
- const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
500
- const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
501
- for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
502
- const result = POLICY.validateWorkflowPolicySnapshot(forged);
503
- assert.equal(result.valid, false);
504
- assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
505
- }
506
- assert.equal(
507
- POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
508
- true,
509
- 'changing one normalized risk to another does not invent a higher assurance minimum',
510
- );
511
- for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
512
- const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
513
- assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
514
- assert.equal(escalated.assurance_level, 'Elevated');
515
- assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
516
- }
517
- assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
518
- assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
519
- assert.throws(() => POLICY.lanePolicy({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
520
-
521
- for (const explicitValue of [null, undefined]) {
522
- const state = { workflow_policy: explicitValue, reversible: true, lowRisk: true, isolated: true };
523
- assert.throws(() => POLICY.workflowPolicySnapshot(state), /Invalid workflow policy snapshot/);
524
- assert.throws(() => POLICY.escalateWorkflowPolicy(state, {}), /Invalid workflow policy snapshot/);
525
- assert.throws(() => POLICY.lanePolicy(state), /Invalid workflow policy snapshot/);
526
- }
527
-
528
- const forgedLanePolicyCli = spawnSync(process.execPath, [
529
- POLICY_PATH,
530
- '--lane-policy',
531
- '--task-json',
532
- JSON.stringify({ workflow_policy: forgedDirectAuth }),
533
- '--json',
534
- ], { encoding: 'utf8' });
535
- assert.equal(forgedLanePolicyCli.status, 2, `${forgedLanePolicyCli.stdout}\n${forgedLanePolicyCli.stderr}`);
536
- assert.match(forgedLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
537
-
538
- const nullLanePolicyCli = spawnSync(process.execPath, [
539
- POLICY_PATH,
540
- '--lane-policy',
541
- '--task-json',
542
- JSON.stringify({ workflow_policy: null, reversible: true, lowRisk: true, isolated: true }),
543
- '--json',
544
- ], { encoding: 'utf8' });
545
- assert.equal(nullLanePolicyCli.status, 2, `${nullLanePolicyCli.stdout}\n${nullLanePolicyCli.stderr}`);
546
- assert.match(nullLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
547
-
548
- const validatePolicyCli = spawnSync(process.execPath, [
549
- POLICY_PATH,
550
- '--validate-policy',
551
- '--task-json',
552
- JSON.stringify({ workflow_policy: forgedDirectAuth }),
553
- '--json',
554
- ], { encoding: 'utf8' });
555
- assert.equal(validatePolicyCli.status, 2, `${validatePolicyCli.stdout}\n${validatePolicyCli.stderr}`);
556
- assert.equal(JSON.parse(validatePolicyCli.stdout).valid, false);
557
-
558
- const validateNullPolicyCli = spawnSync(process.execPath, [
559
- POLICY_PATH,
560
- '--validate-policy',
561
- '--task-json',
562
- JSON.stringify({ workflow_policy: null }),
563
- '--json',
564
- ], { encoding: 'utf8' });
565
- assert.equal(validateNullPolicyCli.status, 2, `${validateNullPolicyCli.stdout}\n${validateNullPolicyCli.stderr}`);
566
- assert.equal(JSON.parse(validateNullPolicyCli.stdout).valid, false);
567
- });
568
-
569
- test('P1 workflow policy escalation is monotonic across all newly classified risks', () => {
570
- const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
571
- assert.equal(direct.lane, 'Direct');
572
- assert.deepEqual(POLICY.escalateWorkflowPolicy(direct, {}), direct, 'no new risks must not escalate');
573
-
574
- const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
575
- assert.equal(standard.lane, 'Standard');
576
- assert.equal(standard.artifact_profile, 'targeted');
577
- assert.equal(standard.planning_depth, 'None');
578
- assert.equal(standard.assurance_level, 'Elevated');
579
- assert.equal(standard.risks.includes('new-standard-risk'), true);
580
- assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
581
-
582
- const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
583
- assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
584
- assert.equal(ambiguityElevated.assurance_level, 'Elevated');
585
-
586
- const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
587
- assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
588
- assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
589
-
590
- const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
591
- assert.equal(critical.lane, 'Critical');
592
- assert.equal(critical.artifact_profile, 'targeted');
593
- assert.equal(critical.planning_depth, 'None');
594
- assert.equal(critical.assurance_level, 'Strict');
595
- assert.equal(critical.automatic_assurance_level, 'Elevated');
596
- assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
597
- assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
598
-
599
- const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
600
- assert.equal(noDowngrade.lane, 'Critical');
601
- assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
602
- assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
603
-
604
- const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
605
- assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
606
- });
607
-
608
- test('P1 legacy execution tier is read-compatible without becoming emitted policy authority', () => {
609
- const legacy = { design_context: { execution_tier: 'Deep' } };
610
- const snapshot = POLICY.workflowPolicySnapshot(legacy);
611
- assert.equal(snapshot.lane, 'Critical');
612
- assert.equal(snapshot.artifact_profile, 'strict');
613
- const policy = POLICY.lanePolicy(legacy);
614
- assert.equal(policy.lane, 'Critical');
615
- assert.equal(Object.hasOwn(policy, 'execution_tier'), false);
616
- assert.equal(Object.hasOwn(policy, 'executionTier'), false);
617
- const legacyPlan = POLICY.delegationPlan({ tier: 'Deep' });
618
- assert.equal(Object.hasOwn(legacyPlan, 'execution_tier'), false);
619
- assert.equal(Object.hasOwn(legacyPlan, 'executionTier'), false);
620
- assert.deepEqual(legacyPlan.proof_obligations, snapshot.proof_obligations);
621
- assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
622
- });
623
-
624
- test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
625
- const cases = [
626
- { lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
627
- { lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
628
- { lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
629
- ];
630
- for (const fixture of cases) {
631
- const v1 = {
632
- version: '1',
633
- lane: fixture.lane,
634
- automatic_lane: fixture.lane,
635
- risks: fixture.risks,
636
- artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
637
- proof_obligations: fixture.obligations,
638
- actor_needs: POLICY.actorNeedsFor(fixture.obligations),
639
- override_receipt: null,
640
- };
641
- assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
642
- const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
643
- const before = JSON.stringify(source);
644
- const adapted = POLICY.readWorkflowPolicySnapshot(source);
645
- assert.equal(adapted.version, '2');
646
- assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
647
- assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
648
- assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
649
- assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
650
- const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
651
- assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
652
- assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
653
- const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
654
- assert.equal(migratedView.lane, adapted.lane, fixture.lane);
655
- assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
656
- assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
657
- migrated.assurance_level,
658
- migrated.risks,
659
- ), fixture.lane);
660
- }
661
- });
662
-
663
- test('post-classification downgrade is unsupported', () => {
664
- assert.throws(
665
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
666
- (error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
667
- );
668
- });
669
-
670
- test('caller-requested downgrade is blocked before persistence', () => {
671
- assert.throws(
672
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
673
- (error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
674
- );
675
- // also via CLI
676
- const cli = spawnSync(process.execPath, [
677
- POLICY_PATH,
678
- '--classify-lane',
679
- '--task-json',
680
- JSON.stringify({ riskSignals: { auth: true }, override: 'Direct' }),
681
- '--json',
682
- ], { encoding: 'utf8' });
683
- assert.equal(cli.status, 2);
684
- assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
685
- });
686
-
687
- test('forged approval via legacy approved field is rejected', () => {
688
- assert.throws(() => POLICY.approvalState({ generated: true, approved: true }), /Legacy approval state/);
689
- const cli = spawnSync(process.execPath, [
690
- POLICY_PATH,
691
- '--approval-state',
692
- '--task-json',
693
- JSON.stringify({ generated: true, approved: true }),
694
- '--json',
695
- ], { encoding: 'utf8' });
696
- assert.equal(cli.status, 2);
697
- assert.match(cli.stderr, /Legacy approval state/);
698
- const legacyCheck = POLICY.validateApprovalSchema({ approvals: { requirements: { generated: true, approved: true } } });
699
- assert.equal(legacyCheck.valid, false);
700
- assert.equal(legacyCheck.legacy, true);
701
- assert.match(legacyCheck.error, /Legacy/);
702
- });
703
-
704
- test('technical approval readiness ignores legacy user_approved', () => {
705
- const state = POLICY.approvalState({ generated: true, agent_validated: true });
706
- assert.equal(state.generated, true);
707
- assert.equal(state.agent_validated, true);
708
- assert.equal(state.ready, true);
709
- assert.equal(state.schema_version, '2.0');
710
- assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
711
- assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
712
- const cli = spawnSync(process.execPath, [
713
- POLICY_PATH,
714
- '--approval-state',
715
- '--task-json',
716
- JSON.stringify({ generated: true, agent_validated: true }),
717
- '--json',
718
- ], { encoding: 'utf8' });
719
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
720
- assert.equal(JSON.parse(cli.stdout).state.ready, true);
721
- });
722
-
723
- test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
724
- const absent = POLICY.validateApprovalSchema({});
725
- assert.equal(absent.valid, true);
726
- assert.equal(absent.absent, true);
727
- assert.equal(POLICY.approvalState({}).present, false);
728
-
729
- const invalidSpecs = [
730
- [],
731
- { approvals: null },
732
- { approvals: [] },
733
- { approvals: {} },
734
- { approvals: { requirements: null } },
735
- { approvals: { requirements: { generated: true, agent_validated: true, user_approved: null } } },
736
- { approvals: { requirements: { generated: true, agent_validated: true, user_approved: true, extra: false } } },
737
- ];
738
- for (const spec of invalidSpecs) {
739
- assert.equal(POLICY.validateApprovalSchema(spec).valid, false, `invalid approval should fail: ${JSON.stringify(spec)}`);
740
- assert.throws(() => POLICY.approvalState(spec), /approval|approvals|schema/i);
741
- }
742
- assert.throws(() => POLICY.approvalState({ schema_version: null }), /schema_version/);
743
- assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
744
- });
745
-
746
- test('CLI exposes a derived lane without making it primary Develop authority', () => {
747
- const cli = spawnSync(process.execPath, [
748
- POLICY_PATH,
749
- '--classify-lane',
750
- '--task-json',
751
- JSON.stringify({ reversible: true, lowRisk: true, isolated: true }),
752
- '--json',
753
- ], { encoding: 'utf8' });
754
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
755
- const payload = JSON.parse(cli.stdout);
756
- assert.equal(payload.classification.lane, 'Direct');
757
- assert.equal(payload.policy.requiresSpec, false);
211
+ test('Develop names the process-first packet as its only authority', () => {
758
212
  const develop = read(DEVELOP);
759
213
  const regions = markdownLegacyRegions(develop);
760
214
  assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
761
215
  assert.doesNotMatch(regions.primary, /planning_depth|assurance_level|execution_tier|\blane\b/i);
762
- assert.match(regions.legacy, /planning_depth/);
763
- assert.match(regions.legacy, /assurance_level/);
764
- assert.match(regions.legacy, /execution_tier/);
765
- assert.match(regions.legacy, /derived lane/i);
216
+ assert.doesNotMatch(develop, /## Legacy workflow compatibility|Also supports existing legacy Specs packets/);
766
217
  assert.match(regions.primary, /specs\/<feature>\/plan\.md[\s\S]*task-NN-\*\.md/i);
767
218
  assert.match(regions.primary, /one unblocked task at a time/i);
768
219
  assert.match(regions.primary, /Verification Plan/);
@@ -771,84 +222,69 @@ test('CLI exposes a derived lane without making it primary Develop authority', (
771
222
  assert.match(develop, /--notes.*opt-in/i);
772
223
  assert.doesNotMatch(develop, /--no-notes/);
773
224
  assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
774
- const fullRoutine = POLICY.readWorkflowPolicySnapshot({
775
- workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
776
- });
777
- const compactStrict = POLICY.readWorkflowPolicySnapshot({
778
- workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
779
- });
780
- assert.equal(fullRoutine.planning_depth, 'Full');
781
- assert.equal(fullRoutine.assurance_level, 'Routine');
782
- assert.equal(compactStrict.planning_depth, 'Compact');
783
- assert.equal(compactStrict.assurance_level, 'Strict');
784
- assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
785
- assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
786
225
  });
787
226
 
788
227
  test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
789
228
  const specs = read(SPECS);
790
229
  const regions = markdownLegacyRegions(specs);
791
230
  const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
792
- assert.equal(legacyHeadings.length, 1);
231
+ assert.equal(legacyHeadings.length, 0);
793
232
  assert.match(regions.primary, /specs\/<feature>\/[\s\S]*plan\.md[\s\S]*task-01-<slug>\.md[\s\S]*task-02-<slug>\.md/i);
794
233
  assert.match(regions.primary, /Task files are flat beside `plan\.md`/i);
795
234
  assert.match(regions.primary, /one task at a time/i);
796
235
  assert.match(regions.primary, /inline `## Receipt`/i);
797
- assert.match(regions.primary, /C1 — Scope[\s\S]*C2 — Findings[\s\S]*C3 — Done/i);
798
- assert.match(regions.legacy, /spec\.json/i);
799
- assert.match(regions.legacy, /never requires the legacy kernel/i);
236
+ assert.match(regions.primary, /GATE-SCOPE[\s\S]*GATE-REVIEW[\s\S]*GATE-DONE/i);
237
+ assert.doesNotMatch(specs, /installed legacy adapters?/i);
800
238
  assertVocabularyIsLegacyOnly(SPECS, regions);
801
239
  });
802
240
 
803
- test('core execution agents default to process-first state and isolate legacy packets', () => {
241
+ test('core execution agents default to process-first state and carry no legacy adapter', () => {
804
242
  for (const filePath of PROCESS_FIRST_AGENTS) {
805
243
  const content = read(filePath);
806
244
  const name = path.basename(filePath);
807
245
  assert.match(content, /plan\.md/i, `${name} must read the process-first plan`);
808
246
  assert.match(content, /task-NN-\*\.md|task-NN-<slug>\.md/i, `${name} must understand flat tasks`);
809
- assert.match(content, /legacy/i, `${name} must isolate legacy compatibility`);
247
+ assert.doesNotMatch(content, /legacy|spec\.json|scope_lock/i, `${name} must not teach a legacy adapter`);
810
248
  }
811
249
 
812
250
  const implementer = read(PROCESS_FIRST_AGENTS[0]);
813
251
  assert.match(implementer, /Under every dispatch mode[\s\S]{0,160}do NOT edit[\s\S]{0,80}`plan\.md`[\s\S]{0,80}`Status:`[\s\S]{0,80}`## Receipt`/i);
814
252
  assert.match(implementer, /controller/i);
815
- assert.match(implementer, /process-first work[\s\S]{0,100}Ownership[\s\S]{0,100}`Related Files` only for a valid legacy adapter/i);
253
+ assert.match(implementer, /process-first work[\s\S]{0,100}Ownership\s+boundary\./i);
816
254
 
817
255
  const auditor = read(PROCESS_FIRST_AGENTS[2]);
818
- assert.match(auditor, /accepted C1\/C2 decisions/i);
819
- assert.match(auditor, /Only for a valid legacy adapter[\s\S]*`Related Files`[\s\S]*`## Evidence`/i);
820
- assert.match(auditor, /Never require those legacy artifacts from a[\s\S]*process-first packet/i);
821
- assert.match(auditor, /attestation belongs only to the valid legacy/i);
256
+ assert.match(auditor, /accepted GATE-SCOPE\/GATE-REVIEW decisions/i);
257
+ assert.doesNotMatch(auditor, /CAFEKIT_SEMANTIC_REVIEW_ATTESTATION/);
822
258
 
823
259
  const docsKeeper = read(PROCESS_FIRST_AGENTS[3]);
824
260
  assert.match(docsKeeper, /never[\s\S]{0,80}invent or write Status, Receipt, approval, or execution proof/i);
825
261
 
826
262
  const deployer = read(PROCESS_FIRST_AGENTS[4]);
827
263
  assert.match(deployer, /current final inline Receipt/i);
828
- assert.match(deployer, /C3\/release authorization/i);
264
+ assert.match(deployer, /GATE-DONE\/release authorization/i);
829
265
  assert.match(deployer, /does not write process-first Status\/Receipt/i);
830
266
  });
831
267
 
832
268
  test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Legacy sections', () => {
833
269
  const surfaces = [
834
- ['develop', DEVELOP],
835
- ['parallel waves', PARALLEL_WAVES],
836
- ['sync', SYNC_SKILL],
837
- ['sync protocols', SYNC_PROTOCOLS],
270
+ ['develop', DEVELOP, 0],
271
+ ['parallel waves', PARALLEL_WAVES, 0],
272
+ ['sync', SYNC_SKILL, 0],
273
+ ['sync protocols', SYNC_PROTOCOLS, 0],
838
274
  ];
839
275
  const regionsByName = new Map();
840
- let legacyCorpus = '';
841
276
 
842
- for (const [name, filePath] of surfaces) {
277
+ for (const [name, filePath, expectedLegacy] of surfaces) {
843
278
  const regions = markdownLegacyRegions(read(filePath));
844
279
  const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
845
- assert.equal(legacyHeadings.length, 1, `${name} must have exactly one Legacy heading`);
280
+ assert.equal(legacyHeadings.length, expectedLegacy, `${name} must have exactly ${expectedLegacy} Legacy heading(s)`);
846
281
  assert.match(regions.primary, /inline (?:`## )?Receipts?/i, `${name} must teach inline Receipts`);
847
- assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
848
- assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
282
+ if (expectedLegacy === 1) {
283
+ assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
284
+ assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
285
+ }
849
286
  assertVocabularyIsLegacyOnly(filePath, regions);
850
287
  regionsByName.set(name, regions);
851
- legacyCorpus += `\n${regions.legacy}`;
852
288
  }
853
289
 
854
290
  const develop = regionsByName.get('develop').primary;
@@ -874,7 +310,7 @@ test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Leg
874
310
  assert.match(protocols, /`plan\.md` and flat `task-\*\.md` files/i);
875
311
  assert.match(protocols, /acceptance IDs/i);
876
312
  assert.match(protocols, /current inline Receipt/i);
877
- assert.match(legacyCorpus, /sync-finalize/i);
313
+ assert.match(regionsByName.get('sync').primary, /sync-finalize/i);
878
314
  });
879
315
 
880
316
  test('Develop process-first source contract preserves selection, recovery, final-Head, parallel, and Flash boundaries', () => {
@@ -1117,7 +553,7 @@ test('Claude installed Test preserves plan-native proof and references', () => {
1117
553
  const installedReview = fs.readFileSync(path.join(root, '.claude/skills/code-review/SKILL.md'), 'utf8');
1118
554
  assert.equal(installedReview, fs.readFileSync(CODE_REVIEW_SKILL, 'utf8'));
1119
555
  assert.match(installedReview, /controller-validated `test-proof-v1`/);
1120
- assert.match(installedReview, /Do not load or follow legacy separate-receipt paragraphs/i);
556
+ assert.match(installedReview, /For proof consumption, this skill's `## Execution-proof boundary` is\s+authoritative/i);
1121
557
  assert.equal(fs.existsSync(path.join(root, '.agents/skills/test')), false);
1122
558
  } finally {
1123
559
  fs.rmSync(root, { recursive: true, force: true });
@@ -1131,432 +567,24 @@ test('specs-usage-guide documents Test proof handoff without timing claims', ()
1131
567
  assert.match(section, /Test không ghi `Status:` hay inline[\s\S]*Develop controller là writer duy nhất/i);
1132
568
  assert.match(section, /tracked\/untracked\/ignored drift/i);
1133
569
  assert.match(section, /HTTPS\/localhost origin[\s\S]*fresh consent/i);
1134
- assert.match(section, /separate-receipt adapter/i);
570
+ assert.match(section, /chỉ nằm trong `## Receipt` inline/i);
1135
571
  assert.match(section, /không phải benchmark thời gian/i);
1136
572
  assert.match(section, /không chứng minh live adherence/i);
1137
573
  assert.doesNotMatch(section, /\b\d+(?:\.\d+)?\s*(?:ms|milliseconds?|seconds?|minutes?)\b/i);
1138
574
  assert.doesNotMatch(section, /\[(?:LIVE_)?VERIFIED\]/i);
1139
575
  });
1140
576
 
1141
- test('canonical verdict adapter handles completion and unfinished decisions', () => {
1142
- assert.deepEqual(POLICY.REVIEW_VERDICTS, ['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
1143
- assert.doesNotThrow(() => POLICY.assertVerdict('PASS_WITH_WARNINGS'));
1144
- assert.throws(() => POLICY.assertVerdict('PARTIAL'), /Unsupported canonical verdict/);
1145
- assert.equal(POLICY.consumeReviewVerdict('PASS').completion, 'unfinished');
1146
- assert.equal(POLICY.consumeReviewVerdict('PASS').unfinished, true);
1147
- assert.equal(POLICY.consumeReviewVerdict('PASS').createsCompletion, false);
1148
- assert.equal(POLICY.consumeReviewVerdict('PASS_WITH_WARNINGS').unfinished, true);
1149
- assert.equal(POLICY.consumeReviewVerdict('FAIL').retry, true);
1150
- assert.equal(POLICY.consumeReviewVerdict('BLOCKED').retry, false);
1151
- assert.equal(POLICY.consumeReviewVerdict('BLOCKED').unfinished, true);
1152
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').completion, 'unfinished');
1153
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').completion, 'unfinished');
1154
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').verdict, 'BLOCKED');
1155
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').verdict, 'BLOCKED');
1156
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').retry, false);
1157
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').retry, false);
1158
- assert.throws(() => POLICY.consumeReviewVerdict('UNKNOWN'), /Unsupported canonical verdict/);
1159
- const cli = spawnSync(process.execPath, [POLICY_PATH, '--consume-verdict', '--verdict', 'BLOCKED', '--json'], { encoding: 'utf8' });
1160
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
1161
- assert.deepEqual(JSON.parse(cli.stdout).action, 'stop');
1162
- assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1163
- assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1164
- assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1165
- });
1166
-
1167
- test('completion decision requires canonical execution receipt and every workflow obligation', () => {
1168
- const receipt = canonicalReceipt();
1169
- const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1170
- const audit = {
1171
- schema_version: '1',
1172
- reviewer_session_id: 'review-session-1',
1173
- implementation_session_id: 'implementation-session-1',
1174
- expected_provenance: { ...EXPECTED_PROVENANCE },
1175
- evidence: 'independent audit report for the bound change',
1176
- verdict: 'PASS',
1177
- };
1178
- const missing = POLICY.completionDecision('PASS', {
1179
- workflow_policy: critical,
1180
- receipt_binding: RECEIPT_BINDING,
1181
- execution_receipt: receipt,
1182
- proofs: {
1183
- needsInspection: { evidence: 'inspection receipt' },
1184
- needsResearchGrounding: { evidence: 'research receipt' },
1185
- },
1186
- });
1187
- assert.equal(missing.completion, 'unfinished');
1188
- assert.ok(missing.missingProof.includes('needsIndependentAudit'));
1189
- assert.equal(missing.unfinished, true);
1190
-
1191
- const complete = POLICY.completionDecision('PASS', {
1192
- workflow_policy: critical,
1193
- receipt_binding: RECEIPT_BINDING,
1194
- execution_receipt: receipt,
1195
- proofs: {
1196
- needsInspection: { evidence: 'inspection receipt' },
1197
- needsResearchGrounding: { evidence: 'research receipt' },
1198
- needsIndependentAudit: audit,
1199
- },
1200
- });
1201
- assert.equal(complete.completion, 'complete');
1202
- assert.equal(complete.unfinished, false);
1203
- assert.equal(complete.warnings, undefined);
1204
- const warnings = POLICY.completionDecision('PASS_WITH_WARNINGS', {
1205
- workflow_policy: critical,
1206
- receipt_binding: RECEIPT_BINDING,
1207
- execution_receipt: receipt,
1208
- proofs: {
1209
- needsInspection: { evidence: 'inspection receipt' },
1210
- needsResearchGrounding: { evidence: 'research receipt' },
1211
- needsIndependentAudit: audit,
1212
- },
1213
- });
1214
- assert.equal(warnings.completion, 'unfinished');
1215
- assert.equal(warnings.unfinished, true);
1216
- assert.match(warnings.blocker, /literal PASS/);
1217
- assert.equal(POLICY.completionDecision('PASS', {
1218
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1219
- receipt_binding: RECEIPT_BINDING,
1220
- execution_receipt: receipt,
1221
- }).completion, 'unfinished');
1222
- assert.equal(POLICY.completionDecision('PASS', {
1223
- workflow_policy: {},
1224
- receipt_binding: RECEIPT_BINDING,
1225
- execution_receipt: receipt,
1226
- }).completion, 'unfinished');
1227
- assert.equal(POLICY.completionDecision('PASS', {
1228
- workflow_policy: critical,
1229
- execution_receipt: receipt,
1230
- }).completion, 'unfinished', 'arbitrary Base/Head without runtime binding must not complete');
1231
- assert.equal(POLICY.completionDecision('PASS', {
1232
- workflow_policy: critical,
1233
- receipt_binding: RECEIPT_BINDING,
1234
- execution_receipt: receipt,
1235
- proofs: {
1236
- needsInspection: { evidence: 'inspection receipt' },
1237
- needsResearchGrounding: { evidence: 'research receipt' },
1238
- needsIndependentAudit: { independent: true, evidence: 'marker only', verdict: 'PASS' },
1239
- },
1240
- }).completion, 'unfinished', 'independent marker must not satisfy audit');
1241
- assert.equal(POLICY.validateIndependentAuditEvidence({ independent: true }, EXPECTED_PROVENANCE).valid, false);
1242
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, verdict: 'PASS_WITH_WARNINGS' }, EXPECTED_PROVENANCE).valid, false);
1243
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, reviewer_session_id: audit.implementation_session_id }, EXPECTED_PROVENANCE).valid, false);
1244
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, expected_provenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }, EXPECTED_PROVENANCE).valid, false);
1245
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, extra: 'forged' }, EXPECTED_PROVENANCE).valid, false);
1246
- assert.equal(POLICY.validateIndependentAuditEvidence([audit], EXPECTED_PROVENANCE).valid, false);
1247
- assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, RUNTIME_CONTEXT).valid, true);
1248
- assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, { ...RUNTIME_CONTEXT }).valid, false, 'forged runtime session context must not satisfy audit binding');
1249
- assert.equal(POLICY.completionDecision('PASS', {}).completion, 'unfinished');
1250
- const artifactContext = {
1251
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
1252
- receipt_binding: RECEIPT_BINDING,
1253
- task_context: { artifacts: ['output/bundle.js'], expected_provenance: { ...EXPECTED_PROVENANCE } },
1254
- execution_receipt: receipt,
1255
- proofs: { needsInspection: { evidence: 'inspection receipt' } },
1256
- receipt_options: { requireArtifactHash: false, artifactPaths: [] },
1257
- };
1258
- const artifactBlocked = POLICY.completionDecision('PASS', artifactContext);
1259
- assert.equal(artifactBlocked.completion, 'unfinished', 'caller receipt options must not weaken task artifact requirements');
1260
- assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
1261
- });
1262
-
1263
- test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
1264
- const fixture = createCanonicalAuthoringFixture();
1265
- try {
1266
- let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1267
- assert.equal(spec.semantic_model, null);
1268
- spec.ready_for_implementation = true;
1269
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1270
- let validation = VALIDATOR.validateSpec(fixture.specDir);
1271
- assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
1272
-
1273
- spec.ready_for_implementation = false;
1274
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1275
- promoteCanonicalModel(fixture);
1276
- spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1277
- assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
1278
- spec.ready_for_implementation = true;
1279
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1280
- validation = VALIDATOR.validateSpec(fixture.specDir);
1281
- assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
1282
- assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
1283
-
1284
- spec.ready_for_implementation = false;
1285
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1286
- spec = makeCanonicalReady(fixture);
1287
- validation = VALIDATOR.validateSpec(fixture.specDir);
1288
- assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
1289
- assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
1290
-
1291
- const specs = read(SPECS);
1292
- const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
1293
- assert.ok(executableBlocks.every((block) => !/cf:?develop/i.test(block)), 'Specs must never execute Develop');
1294
- assert.match(specs, /An implementation workflow owns execution/i);
1295
- assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
1296
- assert.match(specs, /The user decides\s+at C3 whether the feature is done/i);
1297
- } finally {
1298
- fs.rmSync(fixture.root, { recursive: true, force: true });
1299
- }
1300
- });
1301
-
1302
- test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
1303
- const fixture = createOrdinaryAuthoringFixture();
1304
- try {
1305
- const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1306
- authored.decisions = [
1307
- { id: 'Q1', classification: 'repository_fact', statement: 'The runtime entrypoint is src/entry.js.', status: 'grounded', evidence: 'Repository anchor A-D-01 resolves to src/entry.js.' },
1308
- { id: 'Q2', classification: 'reversible_assumption', statement: 'Use the existing direct module export for this bounded change.', status: 'recorded', evidence: 'Reversal is limited to src/entry.js and does not alter product data.' },
1309
- ];
1310
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
1311
- const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
1312
- assert.equal(result.spec.ready_for_implementation, true);
1313
- assert.equal(result.spec.semantic_model.criteria.length, 1);
1314
- assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
1315
- assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
1316
-
1317
- const beforeDigest = result.semantic_digest;
1318
- const design = path.join(fixture.specDir, 'design.md');
1319
- const originalDesign = fs.readFileSync(design, 'utf8');
1320
- fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
1321
- const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
1322
- assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
1323
- const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
1324
- assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
1325
- assert.notEqual(decisionDigest.digest, beforeDigest);
1326
- fs.writeFileSync(design, originalDesign);
1327
-
1328
- const requirements = path.join(fixture.specDir, 'requirements.md');
1329
- fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
1330
- const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
1331
- assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
1332
- const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
1333
- assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
1334
- assert.notEqual(changed.digest, beforeDigest);
1335
- assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
1336
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1337
- });
1338
-
1339
- test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
1340
- const fixture = createOrdinaryAuthoringFixture();
1341
- try {
1342
- const original = fs.readFileSync(fixture.specPath);
1343
- for (const mutate of [
1344
- (review) => ({ ...review, reviewed_criteria: [] }),
1345
- (review) => ({ ...review, counterexamples: [] }),
1346
- ]) {
1347
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
1348
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1349
- }
1350
- fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
1351
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
1352
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1353
- fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
1354
-
1355
- process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
1356
- try {
1357
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
1358
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1359
- } finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
1360
-
1361
- const blocked = JSON.parse(original);
1362
- blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
1363
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
1364
- const blockedBytes = fs.readFileSync(fixture.specPath);
1365
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
1366
- assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
1367
-
1368
- delete blocked.decisions;
1369
- blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
1370
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
1371
- const strictBytes = fs.readFileSync(fixture.specPath);
1372
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
1373
- assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
1374
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1375
- });
1376
-
1377
- test('Strict atomic readiness succeeds only after current allowlisted host observation', () => {
1378
- const fixture = createOrdinaryAuthoringFixture();
1379
- try {
1380
- const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1381
- spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
1382
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1383
- const before = fs.readFileSync(fixture.specPath);
1384
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict readiness/);
1385
- assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
1386
-
1387
- const digest = VALIDATOR.computeSemanticDigest21(fixture.specDir, spec);
1388
- assert.equal(digest.errors.length, 0, digest.errors.join('\n'));
1389
- const authorityHome = path.join(fixture.root, 'authority-home');
1390
- const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'ordinary', spec_file: 'specs/ordinary/spec.json', semantic_digest: digest.digest, verdict: 'PASS' })}`;
1391
- const observed = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
1392
- cwd: fixture.root,
1393
- env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
1394
- input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code_auditor', last_assistant_message: claim, cwd: fixture.root }),
1395
- encoding: 'utf8',
1396
- });
1397
- assert.equal(observed.status, 0, observed.stderr);
1398
- const reviewFile = path.join(fixture.root, 'review.json');
1399
- fs.writeFileSync(reviewFile, `${JSON.stringify(ordinaryReview())}\n`);
1400
- const finalized = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'), fixture.specDir, '--review-result', reviewFile], {
1401
- cwd: fixture.root,
1402
- env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
1403
- encoding: 'utf8',
1404
- });
1405
- assert.equal(finalized.status, 0, `${finalized.stdout}\n${finalized.stderr}`);
1406
- assert.equal(JSON.parse(fs.readFileSync(fixture.specPath, 'utf8')).ready_for_implementation, true);
1407
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1408
- });
1409
-
1410
- test('done is a final-state request bound to current semantic model and digest', () => {
1411
- const fixture = createCanonicalAuthoringFixture();
1412
- try {
1413
- promoteCanonicalModel(fixture);
1414
- const spec = makeCanonicalReady(fixture);
1415
- spec.status = 'done';
1416
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1417
- const candidate = {
1418
- featureDir: fixture.specDir,
1419
- featureName: 'feature',
1420
- specFile: fixture.specPath,
1421
- spec,
1422
- };
1423
- const dependencies = () => ({
1424
- validator: VALIDATOR,
1425
- grounder: GROUNDER,
1426
- semanticAuthority: SEMANTIC_AUTHORITY,
1427
- });
1428
- const current = FINAL_STATE.validateCanonicalFinalState({
1429
- policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
1430
- });
1431
- assert.equal(current.ok, true, current.reason);
1432
- assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
1433
-
1434
- const failedReceipt = structuredClone(spec);
1435
- failedReceipt.validation.semantic_review.verdict = 'FAIL';
1436
- const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
1437
- policy: POLICY,
1438
- projectRoot: fixture.root,
1439
- candidate: { ...candidate, spec: failedReceipt },
1440
- dependencies,
1441
- });
1442
- assert.equal(receiptMutation.ok, false);
1443
- assert.match(receiptMutation.reason, /PASS CONTINUE/);
1444
-
1445
- const mismatchedHistory = structuredClone(spec);
1446
- const latest = mismatchedHistory.validation.semantic_review_history.entries[
1447
- mismatchedHistory.validation.semantic_review_history.entries.length - 1
1448
- ];
1449
- latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
1450
- const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
1451
- policy: POLICY,
1452
- projectRoot: fixture.root,
1453
- candidate: { ...candidate, spec: mismatchedHistory },
1454
- dependencies,
1455
- });
1456
- assert.equal(digestMismatch.ok, false);
1457
- assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
1458
-
1459
- const requirementsPath = path.join(fixture.specDir, 'requirements.md');
1460
- const requirements = fs.readFileSync(requirementsPath, 'utf8');
1461
- fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
1462
- const staleDigest = FINAL_STATE.validateCanonicalFinalState({
1463
- policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
1464
- });
1465
- assert.equal(staleDigest.ok, false);
1466
- assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
1467
-
1468
- fs.writeFileSync(requirementsPath, requirements);
1469
- const staleModel = structuredClone(spec);
1470
- staleModel.semantic_model.criteria[0].owner = 'A-D-02';
1471
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
1472
- const invalidModel = FINAL_STATE.validateCanonicalFinalState({
1473
- policy: POLICY,
1474
- projectRoot: fixture.root,
1475
- candidate: { ...candidate, spec: staleModel },
1476
- dependencies,
1477
- });
1478
- assert.equal(invalidModel.ok, false);
1479
- assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
1480
- } finally {
1481
- fs.rmSync(fixture.root, { recursive: true, force: true });
1482
- }
1483
- });
1484
-
1485
- test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
1486
- const initial = canonicalFlashTask();
1487
- for (const verdict of ['FAIL', 'BLOCKED', 'PARTIAL', 'NO_TESTS', 'PASS_WITH_WARNINGS']) {
1488
- const result = POLICY.promoteFlashTask(initial, verdict);
1489
- assert.equal(result.status, 'in_progress');
1490
- assert.equal(result.receipt, 'FLASH_UNVERIFIED');
1491
- assert.equal(result.dependencyBlocked, true);
1492
- assert.equal(result.unblocks, false);
1493
- assert.match(result.blocker, /verification|test proof/);
1494
- }
1495
- const promoted = POLICY.promoteFlashTask(initial, 'PASS', canonicalReceipt());
1496
- assert.equal(promoted.status, 'in_progress');
1497
- assert.equal(promoted.receipt.startsWith('Verification: PASS'), true);
1498
- assert.equal(promoted.blocker, null);
1499
- assert.equal(promoted.dependencyBlocked, true);
1500
- assert.equal(promoted.unblocks, false);
1501
- assert.equal(promoted.readyForSync, true);
1502
- assert.equal(POLICY.finalizeFlashTask(promoted, 'sync'), promoted);
1503
- const forgedFinalize = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
1504
- assert.equal(forgedFinalize.status, 'in_progress');
1505
- assert.equal(forgedFinalize.dependencyBlocked, true);
1506
- assert.equal(forgedFinalize.unblocks, false);
1507
- const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', promoted.receipt);
1508
- assert.equal(finalized.status, 'done');
1509
- assert.equal(finalized.dependencyBlocked, false);
1510
- assert.equal(finalized.unblocks, true);
1511
- assert.equal(finalized.readyForSync, false);
1512
- assert.equal(POLICY.isStaleFlashDone({ status: 'done', receipt: 'FLASH_UNVERIFIED' }), true);
1513
- assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
1514
- assert.match(read(SYNC_SKILL), /sync-finalize/);
1515
- });
1516
-
1517
- test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
1518
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
1519
- const specGate = installClaudeRuntimeClosure(root);
1520
- assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
1521
- assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
1522
- fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
1523
- fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
1524
- status: 'in_progress',
1525
- current_phase: 'closeout',
1526
- feature_name: 'demo',
1527
- task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
1528
- }));
1529
- initFixtureGit(root);
1530
- try {
1531
- const result = spawnSync(process.execPath, [specGate], {
1532
- cwd: root,
1533
- env: { ...process.env, PROJECT_ROOT: root },
1534
- input: JSON.stringify({ cwd: root, session_id: 'test' }),
1535
- encoding: 'utf8',
1536
- });
1537
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
1538
- const payload = JSON.parse(result.stdout);
1539
- assert.equal(payload.decision, 'block');
1540
- assert.match(payload.reason, /FLASH_UNVERIFIED/);
1541
- } finally {
1542
- fs.rmSync(root, { recursive: true, force: true });
1543
- }
1544
- });
1545
-
1546
577
  test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
1547
578
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
1548
579
  const claude = path.join(root, '.claude');
1549
580
  const specGate = installClaudeRuntimeClosure(root);
1550
- const specDir = path.join(root, 'specs', 'demo', 'tasks');
1551
- fs.mkdirSync(specDir, { recursive: true });
1552
- // Done task without canonical receipt (missing Command, provenance)
1553
- fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
1554
- status: 'done',
1555
- current_phase: 'closeout',
1556
- feature_name: 'demo',
1557
- task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
1558
- }));
1559
- fs.writeFileSync(path.join(specDir, 'task-R1-01-one.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\n```\nnpm test\n```\n');
581
+ const featureDir = path.join(root, 'specs', 'demo');
582
+ fs.mkdirSync(featureDir, { recursive: true });
583
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo\nSpecs-Contract: process-first-ready-v1\n');
584
+ const taskFile = path.join(featureDir, 'task-01-one.md');
585
+ const head = '# Task 01: One\n\nStatus: done\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: pnpm test\n\n';
586
+ // Done task without a canonical receipt (no Command, no provenance)
587
+ fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\n\`\`\`\nnpm test\n\`\`\`\n`);
1560
588
  initFixtureGit(root);
1561
589
  try {
1562
590
  assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
@@ -1569,22 +597,18 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
1569
597
  assert.equal(result.status, 0);
1570
598
  const payload = JSON.parse(result.stdout);
1571
599
  assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
1572
- assert.match(payload.reason, /tasks\/task-R1-01-one\.md/);
600
+ assert.match(payload.reason, /task-01-one\.md/);
1573
601
  assert.match(payload.reason, /receipt|command|provenance/i);
1574
- // Also test that with canonical receipt it passes
1575
- fs.writeFileSync(path.join(specDir, 'task-R1-01-one.md'), '# Task R1-01: One\n\n**Status:** done\n\n## Outcome\n\nVerified result.\n\n## Scope and Typed Anchors\n\n- **In scope:** focused result\n- **Out of scope:** none\n\n## Changes\n\n- [x] Verify result.\n\n## Acceptance\n\n- Result is verified.\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- **Command:** `pnpm test`\n- **Expected:** focused tests pass\n- **Negative path:** not relevant for fixture\n- **Reachability:** fixture test entrypoint\n');
1576
- fs.mkdirSync(path.join(root, 'specs', 'demo', 'receipts'), { recursive: true });
1577
- fs.writeFileSync(
1578
- path.join(root, 'specs', 'demo', 'receipts', 'task-R1-01-one.md'),
1579
- bindFixtureReceipt(root, '# Task Receipt\n\nTask: task-R1-01-one.md\nTask path: tasks/task-R1-01-one.md\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nExpected: focused tests pass\nObserved: focused tests passed\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n```\nnpm test\nPASS\n```\n'),
1580
- );
1581
- fs.writeFileSync(
1582
- path.join(root, 'specs', 'demo', 'feature-receipt.md'),
1583
- bindFixtureReceipt(root, '# Feature Receipt\n\nFeature: demo\nVerification: PASS\nCommand: pnpm test\nExit: 0\nResult: PASS\nExpected: integrated feature passes\nObserved: integrated feature passed\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n'),
1584
- );
602
+ const context = PROVENANCE_HELPER.deriveRuntimeContext({
603
+ projectRoot: root,
604
+ specsRoot: path.join(root, 'specs'),
605
+ specFile: path.join(featureDir, 'plan.md'),
606
+ featureName: 'demo',
607
+ runtimeSession: 'test',
608
+ });
609
+ fs.writeFileSync(taskFile, `${head}## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${context.base}\nHead: ${context.head}\n\`\`\`\npnpm test\nPASS\n\`\`\`\n`);
1585
610
  // Clear cache to simulate fresh first-run with valid receipt
1586
611
  try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
1587
- // Re-run with valid receipt - should not block
1588
612
  const result2 = spawnSync(process.execPath, [specGate], {
1589
613
  cwd: root,
1590
614
  env: { ...process.env, PROJECT_ROOT: root },
@@ -1607,10 +631,6 @@ test('canonical receipt requires command, exit, provenance and unambiguous PASS'
1607
631
  assert.ok(missingVerification.includes('verification_state'));
1608
632
  const artifactWithoutHash = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: dist/bundle.js\n');
1609
633
  assert.ok(artifactWithoutHash.includes('artifact_hash'));
1610
- // CLI validation
1611
- const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
1612
- assert.equal(cli.status, 0);
1613
- assert.equal(JSON.parse(cli.stdout).ok, true);
1614
634
  });
1615
635
 
1616
636
  test('canonical receipt provenance requires both Base and Head (or both base_sha and head_sha)', () => {
@@ -1660,27 +680,10 @@ test('canonical receipt provenance requires both Base and Head (or both base_sha
1660
680
  assert.deepEqual(validWithValues, [], 'valid Base and Head with values should pass');
1661
681
  const validShaWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: 0123456789abcdef0123456789abcdef01234567\nhead_sha: 89abcdef0123456789abcdef0123456789abcdef\n');
1662
682
  assert.deepEqual(validShaWithValues, [], 'valid base_sha and head_sha with values should pass');
1663
- // CLI: only Base via --validate-receipt should fail
1664
- const cliOnlyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\n' }), '--json'], { encoding: 'utf8' });
1665
- assert.equal(cliOnlyBase.status, 2);
1666
- assert.equal(JSON.parse(cliOnlyBase.stdout).ok, false);
1667
- assert.ok(JSON.parse(cliOnlyBase.stdout).failures.includes('provenance'));
1668
- const cliBoth = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\n' }), '--json'], { encoding: 'utf8' });
1669
- assert.equal(cliBoth.status, 0);
1670
- assert.equal(JSON.parse(cliBoth.stdout).ok, true);
1671
- // CLI empty Base should fail
1672
- const cliEmptyBase = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: b\n' }), '--json'], { encoding: 'utf8' });
1673
- assert.equal(cliEmptyBase.status, 2);
1674
- assert.ok(JSON.parse(cliEmptyBase.stdout).failures.includes('provenance'));
1675
683
  });
1676
684
 
1677
685
  test('canonical receipt binds expected provenance when the runtime supplies it', () => {
1678
686
  const body = canonicalReceipt();
1679
- const binding = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
1680
- assert.deepEqual(binding.expectedProvenance, EXPECTED_PROVENANCE);
1681
- assert.equal(binding.requireProvenanceBinding, true);
1682
- assert.equal(binding.runtimeContext.context_id, RUNTIME_CONTEXT.context_id);
1683
- assert.throws(() => POLICY.createReceiptBinding(EXPECTED_PROVENANCE), /runtime-derived provenance context/);
1684
687
  assert.ok(POLICY.validateCanonicalReceipt(body, { requireProvenanceBinding: true }).includes('provenance'));
1685
688
  assert.deepEqual(POLICY.validateCanonicalReceipt(body, { expectedProvenance: EXPECTED_PROVENANCE }), []);
1686
689
  assert.ok(POLICY.validateCanonicalReceipt(body, { expectedProvenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }).includes('provenance'));
@@ -1774,37 +777,24 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
1774
777
  assert.equal(withIgnoredCache.head, initial.head, 'nested ignored node_modules/cache files must not enter Head');
1775
778
  assert.equal(withChangedIgnoredCache.head, initial.head, 'changing nested ignored cache data must not change Head');
1776
779
 
1777
- const forgedContext = { ...initial };
1778
- const forgedDecision = POLICY.completionDecision('PASS', {
1779
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1780
- runtime_context: forgedContext,
1781
- execution_receipt: exactReceipt,
1782
- });
1783
- assert.equal(forgedDecision.completion, 'unfinished');
1784
- assert.deepEqual(forgedDecision.missingProof, ['runtime_provenance']);
1785
-
1786
- const flash = {
1787
- status: 'in_progress',
1788
- receipt: 'FLASH_UNVERIFIED',
1789
- blocker: 'awaiting test proof',
1790
- dependencyBlocked: true,
1791
- unblocks: false,
1792
- runtime_context: initial,
1793
- };
1794
- const promoted = POLICY.promoteFlashTask(flash, 'PASS', exactReceipt);
1795
- assert.equal(promoted.readyForSync, true);
1796
-
780
+ // A source change after the receipt was written leaves its Base/Head stale.
1797
781
  fs.writeFileSync(sourceFile, 'mutated source\n');
1798
- const stale = POLICY.completionDecision('PASS', {
1799
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1800
- receipt_binding: POLICY.createReceiptBinding(initial),
1801
- execution_receipt: exactReceipt,
1802
- });
1803
- assert.equal(stale.completion, 'unfinished');
1804
- assert.ok(stale.missingProof.some((item) => item.includes('execution_receipt:provenance')));
1805
- const staleFinalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', exactReceipt, initial);
1806
- assert.equal(staleFinalized.status, 'in_progress');
1807
- assert.equal(staleFinalized.readyForSync, false);
782
+ // The pre-change context is recomputed against the current tree, so the old pair no longer binds.
783
+ assert.ok(
784
+ POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
785
+ runtimeContext: initial,
786
+ requireProvenanceBinding: true,
787
+ })).includes('provenance'),
788
+ 'a receipt bound to the pre-change tree must go stale',
789
+ );
790
+ // A copied context is not runtime-derived and must never supply the expected pair.
791
+ assert.ok(
792
+ POLICY.validateCanonicalReceipt(exactReceipt, POLICY.receiptValidatorOptions({}, {
793
+ runtimeContext: { ...initial },
794
+ requireProvenanceBinding: true,
795
+ })).includes('provenance'),
796
+ 'a copied runtime context must not bind',
797
+ );
1808
798
 
1809
799
  fs.writeFileSync(path.join(root, 'src', 'untracked.js'), 'untracked source\n');
1810
800
  const withUntracked = PROVENANCE_HELPER.deriveRuntimeContext(input);
@@ -1898,76 +888,6 @@ test('runtime provenance chooses a deterministic root when all history is Specs-
1898
888
  }
1899
889
  });
1900
890
 
1901
- test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
1902
- // Direct: isolated reversible low-risk
1903
- const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
1904
- assert.equal(direct.lane, 'Direct');
1905
- assert.equal(direct.automaticLane, 'Direct');
1906
- assert.deepEqual(POLICY.lanePolicy(direct).proof_obligations, ['needsExecutionProof']);
1907
- assert.equal(POLICY.lanePolicy(direct).requiresSpec, false);
1908
- assert.equal(POLICY.lanePolicy(direct).shipPoint, 'task');
1909
- // Standard: default
1910
- const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
1911
- assert.equal(standard.lane, 'Standard');
1912
- assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
1913
- assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
1914
- // Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
1915
- const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
1916
- assert.equal(elevated.lane, 'Standard');
1917
- assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
1918
- const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
1919
- assert.equal(critical.lane, 'Critical');
1920
- assert.ok(critical.risks.includes('auth'));
1921
- assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
1922
- // Override: Direct requesting Critical is allowed (upgrade) without extra auth
1923
- const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
1924
- assert.equal(upgrade.lane, 'Critical');
1925
- assert.equal(upgrade.automaticLane, 'Direct');
1926
- // Downgrade without auth is blocked
1927
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
1928
- // Caller-owned booleans cannot weaken the classified minimum.
1929
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
1930
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
1931
- // State mutation: approvalState
1932
- const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
1933
- assert.equal(pendingApproval.ready, false);
1934
- const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
1935
- assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
1936
- const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
1937
- assert.equal(approved.ready, true);
1938
- // Completion: flash work remains in_progress and does not unblock
1939
- const flashTask = canonicalFlashTask();
1940
- const failRemains = POLICY.promoteFlashTask(flashTask, 'FAIL');
1941
- assert.equal(failRemains.status, 'in_progress');
1942
- assert.equal(failRemains.unblocks, false);
1943
- const blockedRemains = POLICY.promoteFlashTask(flashTask, 'BLOCKED');
1944
- assert.equal(blockedRemains.unblocks, false);
1945
- const noTestsRemains = POLICY.promoteFlashTask(flashTask, 'NO_TESTS');
1946
- assert.equal(noTestsRemains.unblocks, false);
1947
- const promoted = POLICY.promoteFlashTask(flashTask, 'PASS', canonicalReceipt());
1948
- assert.equal(promoted.readyForSync, true);
1949
- assert.equal(promoted.unblocks, false, 'promoted flash must not unblock until sync-finalize');
1950
- const finalized = POLICY.syncFinalizeFlashTask(flashTask, 'PASS', promoted.receipt);
1951
- assert.equal(finalized.status, 'done');
1952
- assert.equal(finalized.unblocks, true);
1953
- });
1954
-
1955
- test('flash selective promotion - only specific task is promoted, not blanket', () => {
1956
- const registry = {
1957
- 'tasks/task-a.md': canonicalFlashTask(),
1958
- 'tasks/task-b.md': canonicalFlashTask(),
1959
- };
1960
- const promotedA = POLICY.promoteFlashTask(registry['tasks/task-a.md'], 'PASS', canonicalReceipt());
1961
- const notPromotedB = registry['tasks/task-b.md']; // unchanged
1962
- assert.equal(promotedA.readyForSync, true);
1963
- assert.equal(notPromotedB.receipt, 'FLASH_UNVERIFIED');
1964
- assert.equal(notPromotedB.readyForSync, undefined);
1965
- // FAIL on B does not affect A
1966
- const failB = POLICY.promoteFlashTask(notPromotedB, 'FAIL');
1967
- assert.equal(failB.receipt, 'FLASH_UNVERIFIED');
1968
- assert.equal(promotedA.receipt.startsWith('Verification: PASS'), true);
1969
- });
1970
-
1971
891
  test('parallel waves require immutable provenance receipts and safe recovery', () => {
1972
892
  const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
1973
893
  assert.match(waves, /only for explicit parallel execution/i);
@@ -2675,36 +1595,6 @@ test('P0 receipt fail-closed: Exit 1/-1/abc/conflict/empty command rejected', ()
2675
1595
  assert.deepEqual(fails, [], `case ${desc} should pass, got ${fails}`);
2676
1596
  }
2677
1597
  }
2678
- // CLI parity: Exit 1 must be rejected via CLI
2679
- const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: `${base}Exit: 1\n` }), '--json'], { encoding: 'utf8' });
2680
- assert.equal(cli.status, 2);
2681
- assert.equal(JSON.parse(cli.stdout).ok, false);
2682
- });
2683
-
2684
- test('P0 flash marker-only must not promote or finalize', () => {
2685
- const flash = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
2686
- const markerOnly = 'Verification: PASS';
2687
- const promoted = POLICY.promoteFlashTask(flash, 'PASS', markerOnly);
2688
- assert.equal(promoted.status, 'in_progress');
2689
- assert.equal(promoted.receipt, 'FLASH_UNVERIFIED');
2690
- assert.equal(promoted.readyForSync, false);
2691
- assert.equal(promoted.dependencyBlocked, true);
2692
- assert.equal(promoted.unblocks, false);
2693
- assert.match(promoted.blocker, /canonical receipt/);
2694
- // Even with PASS marker but missing command/exit/provenance, finalize must not complete
2695
- const fakePromoted = { status: 'in_progress', receipt: markerOnly, readyForSync: true, dependencyBlocked: true, unblocks: false };
2696
- const notFinalized = POLICY.finalizeFlashTask(fakePromoted, 'sync-finalize');
2697
- assert.equal(notFinalized.status, 'in_progress', 'marker-only receipt must not finalize');
2698
- // Valid receipt promotes correctly
2699
- const valid = canonicalReceipt();
2700
- const good = POLICY.promoteFlashTask(flash, 'PASS', valid);
2701
- assert.equal(good.readyForSync, true);
2702
- assert.equal(good.receipt, valid.trim());
2703
- const forgedFinalize = POLICY.finalizeFlashTask(good, 'sync-finalize');
2704
- assert.equal(forgedFinalize.status, 'in_progress');
2705
- assert.equal(forgedFinalize.unblocks, false);
2706
- const finalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', valid);
2707
- assert.equal(finalized.status, 'done');
2708
1598
  });
2709
1599
 
2710
1600
  test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact receipts compatible', () => {
@@ -2728,123 +1618,12 @@ test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact
2728
1618
  }
2729
1619
  });
2730
1620
 
2731
- test('P0 sync-finalize derives promotion from current FLASH_UNVERIFIED plus explicit PASS proof', () => {
2732
- const receipt = canonicalReceipt('node --test');
2733
- const forged = {
2734
- status: 'in_progress',
2735
- receipt,
2736
- readyForSync: true,
2737
- dependencyBlocked: true,
2738
- unblocks: false,
2739
- flashTransition: 'promoted',
2740
- promotionReceipt: receipt,
2741
- };
2742
- const direct = spawnSync(process.execPath, [
2743
- POLICY_PATH,
2744
- '--sync-finalize',
2745
- '--task-json',
2746
- JSON.stringify(forged),
2747
- '--json',
2748
- ], { encoding: 'utf8' });
2749
- assert.equal(direct.status, 2, `${direct.stdout}\n${direct.stderr}`);
2750
- const directPayload = JSON.parse(direct.stdout);
2751
- assert.equal(directPayload.ok, false);
2752
- assert.equal(directPayload.task.status, 'in_progress');
2753
- assert.equal(directPayload.task.receipt, 'FLASH_UNVERIFIED');
2754
- assert.equal(directPayload.task.readyForSync, false);
2755
- assert.equal(directPayload.task.dependencyBlocked, true);
2756
- assert.equal(directPayload.task.unblocks, false);
2757
- assert.match(directPayload.message, /current task must be FLASH_UNVERIFIED.*canonical/i);
2758
-
2759
- const minimal = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED' };
2760
- const minimalResult = POLICY.syncFinalizeFlashTask(minimal, 'PASS', receipt);
2761
- assert.equal(minimalResult.status, 'in_progress');
2762
- assert.match(minimalResult.blocker, /exact canonical stored state/);
2763
- const forgedOpen = { ...canonicalFlashTask(), readyForSync: true };
2764
- const forgedOpenResult = POLICY.syncFinalizeFlashTask(forgedOpen, 'PASS', receipt);
2765
- assert.equal(forgedOpenResult.status, 'in_progress');
2766
- assert.equal(forgedOpenResult.unblocks, false);
2767
-
2768
- const current = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
2769
- const supported = spawnSync(process.execPath, [
2770
- POLICY_PATH,
2771
- '--sync-finalize',
2772
- '--task-json',
2773
- JSON.stringify(current),
2774
- '--verdict',
2775
- 'PASS',
2776
- '--proof',
2777
- receipt,
2778
- '--project-root',
2779
- RUNTIME_ROOT,
2780
- '--specs-root',
2781
- path.join(RUNTIME_ROOT, 'specs'),
2782
- '--spec-file',
2783
- RUNTIME_SPEC,
2784
- '--feature-name',
2785
- 'demo',
2786
- '--runtime-session',
2787
- 'implementation-session-1',
2788
- '--json',
2789
- ], { encoding: 'utf8' });
2790
- assert.equal(supported.status, 0, `${supported.stdout}\n${supported.stderr}`);
2791
- assert.equal(JSON.parse(supported.stdout).task.status, 'done');
2792
- const warningFinalize = POLICY.syncFinalizeFlashTask(current, 'PASS_WITH_WARNINGS', receipt);
2793
- assert.equal(warningFinalize.status, 'in_progress');
2794
- assert.equal(warningFinalize.unblocks, false);
2795
-
2796
- const initial = canonicalFlashTask();
2797
- const promoted = POLICY.promoteFlashTask(initial, 'PASS', receipt);
2798
- const persisted = JSON.parse(JSON.stringify(promoted));
2799
- assert.equal(persisted.flashTransition, 'promoted');
2800
- assert.equal(persisted.promotionReceipt, receipt.trim());
2801
- const forgedPersistedFinalize = POLICY.finalizeFlashTask(persisted, 'sync-finalize');
2802
- assert.equal(forgedPersistedFinalize.status, 'in_progress');
2803
- assert.equal(forgedPersistedFinalize.unblocks, false);
2804
- const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', receipt);
2805
- assert.equal(finalized.status, 'done');
2806
- assert.equal(finalized.flashTransition, 'finalized');
2807
-
2808
- const artifactInitial = { ...initial, artifacts: ['output/bundle.js'] };
2809
- const artifactPromotion = POLICY.promoteFlashTask(artifactInitial, 'PASS', receipt);
2810
- assert.equal(artifactPromotion.readyForSync, false, 'flash promotion must apply task artifact requirements');
2811
-
2812
- const badProof = spawnSync(process.execPath, [
2813
- POLICY_PATH,
2814
- '--sync-finalize',
2815
- '--task-json',
2816
- JSON.stringify(current),
2817
- '--verdict',
2818
- 'PASS',
2819
- '--proof',
2820
- 'Verification: PASS',
2821
- '--json',
2822
- ], { encoding: 'utf8' });
2823
- assert.equal(badProof.status, 2);
2824
- assert.equal(JSON.parse(badProof.stdout).task.status, 'in_progress');
2825
-
2826
- const badArtifact = spawnSync(process.execPath, [
2827
- POLICY_PATH,
2828
- '--sync-finalize',
2829
- '--task-json',
2830
- JSON.stringify({ ...current, artifacts: ['output/bundle.js'] }),
2831
- '--verdict',
2832
- 'PASS',
2833
- '--proof',
2834
- receipt,
2835
- '--json',
2836
- ], { encoding: 'utf8' });
2837
- assert.equal(badArtifact.status, 2);
2838
- assert.equal(JSON.parse(badArtifact.stdout).task.status, 'in_progress');
2839
-
2840
- const markerOnly = {
2841
- ...forged,
2842
- flashTransition: 'promoted',
2843
- promotionReceipt: 'Verification: PASS',
2844
- receipt: 'Verification: PASS',
2845
- };
2846
- assert.equal(POLICY.finalizeFlashTask(markerOnly, 'sync-finalize').status, 'in_progress');
2847
- });
1621
+ function writeWorkflowPacketAt(featureDir, taskStatus = 'pending', dependency = 'none') {
1622
+ fs.mkdirSync(featureDir, { recursive: true });
1623
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
1624
+ fs.writeFileSync(path.join(featureDir, 'task-01-x.md'),
1625
+ `# Task 01\n\nStatus: ${taskStatus}\n\n## Dependencies\n\n- ${dependency}\n\n## Verification Plan\n\n- Command: pnpm test\n`);
1626
+ }
2848
1627
 
2849
1628
  test('P0 Claude and Codex reject configured specs roots outside the project', () => {
2850
1629
  const claudeResolver = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
@@ -2852,15 +1631,11 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
2852
1631
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-external-specs-'));
2853
1632
  const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-configured-specs-'));
2854
1633
  try {
2855
- const local = path.join(tmp, 'specs', 'local');
2856
- const external = path.join(externalRoot, 'remote');
2857
- fs.mkdirSync(local, { recursive: true });
2858
- fs.mkdirSync(external, { recursive: true });
2859
- fs.writeFileSync(path.join(local, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'local' }));
2860
- fs.writeFileSync(path.join(external, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'remote' }));
1634
+ writeWorkflowPacketAt(path.join(tmp, 'specs', 'local'));
1635
+ writeWorkflowPacketAt(path.join(externalRoot, 'remote'));
2861
1636
  const runtime = { paths: { specs: externalRoot } };
2862
- const claude = claudeResolver.resolveActiveSpec({ projectRoot: tmp, runtime });
2863
- const codex = codexUtils.resolveActiveSpec(tmp, runtime, null, null);
1637
+ const claude = claudeResolver.resolveWorkflowCandidate({ projectRoot: tmp, runtime });
1638
+ const codex = codexUtils.resolveWorkflowCandidate(tmp, runtime, null, null);
2864
1639
  assert.equal(claude.error, 'invalid_specs');
2865
1640
  assert.equal(codex.error, 'invalid_specs');
2866
1641
  assert.match(claude.reason, /escapes project root/);
@@ -2871,59 +1646,31 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
2871
1646
  }
2872
1647
  });
2873
1648
 
2874
- test('P0 caller-owned downgrade flags have no canonical capability', () => {
2875
- for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
2876
- assert.throws(
2877
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
2878
- (error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
2879
- );
2880
- }
2881
- assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
2882
- assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
2883
- });
2884
-
2885
- test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
2886
- assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
2887
- assert.equal(POLICY.isValidOverrideReceipt, undefined);
2888
- assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
2889
- assert.throws(
2890
- () => POLICY.classifyLane({
2891
- riskSignals: { privacy: true },
2892
- override: 'Direct',
2893
- overrideReceipt: { verifiedByRuntime: true },
2894
- }),
2895
- (error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
2896
- );
2897
- });
2898
-
2899
1649
  test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
2900
1650
  const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
2901
1651
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-'));
2902
1652
  try {
2903
1653
  const specsDir = path.join(tmp, 'specs');
2904
- fs.mkdirSync(specsDir, { recursive: true });
2905
- // Create two active specs
2906
- for (const name of ['alpha', 'beta']) {
2907
- fs.mkdirSync(path.join(specsDir, name), { recursive: true });
2908
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name, current_phase: 'design' }));
2909
- }
2910
- const amb = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1654
+ for (const name of ['alpha', 'beta']) writeWorkflowPacketAt(path.join(specsDir, name));
1655
+ const amb = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
2911
1656
  assert.equal(amb.error, 'multiple_active');
2912
1657
  assert.deepEqual(amb.candidates.sort(), ['alpha', 'beta']);
2913
1658
  // Explicit feature resolves deterministically
2914
- const alpha = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
1659
+ const alpha = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
2915
1660
  assert.equal(alpha.featureName, 'alpha');
2916
1661
  // Explicit not-found fail-closed
2917
- const notFound = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
1662
+ const notFound = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
2918
1663
  assert.equal(notFound.error, 'explicit_not_found');
2919
- // Explicit path containment: try to escape via ../
2920
- const escape = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitPath: path.join(tmp, 'specs', '..', 'etc', 'passwd') });
1664
+ // Explicit path containment: a plan.md reached through ../ is outside the specs root
1665
+ fs.mkdirSync(path.join(tmp, 'etc'), { recursive: true });
1666
+ fs.writeFileSync(path.join(tmp, 'etc', 'plan.md'), '# Outside\n');
1667
+ const escape = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(tmp, 'specs', '..', 'etc', 'plan.md') });
2921
1668
  assert.equal(escape.error, 'explicit_malformed');
2922
1669
  // Malformed feature name with slash must be rejected
2923
- const malformed = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
1670
+ const malformed = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
2924
1671
  assert.equal(malformed.error, 'explicit_malformed');
2925
1672
  // Valid explicit path inside root
2926
- const validPath = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'beta', 'spec.json') });
1673
+ const validPath = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'beta', 'plan.md') });
2927
1674
  assert.equal(validPath.featureName, 'beta');
2928
1675
  } finally {
2929
1676
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -2942,30 +1689,23 @@ test('P0 parity Claude/Codex: canonical receipt and multi-active', () => {
2942
1689
  const codexUtils = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
2943
1690
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-resolver-'));
2944
1691
  try {
2945
- const specsDir = path.join(tmp, 'specs');
2946
- fs.mkdirSync(specsDir, { recursive: true });
2947
- for (const name of ['x', 'y']) {
2948
- fs.mkdirSync(path.join(specsDir, name), { recursive: true });
2949
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name }));
2950
- }
2951
- const amb = codexUtils.resolveActiveSpec(tmp, {}, null, null);
1692
+ for (const name of ['x', 'y']) writeWorkflowPacketAt(path.join(tmp, 'specs', name));
1693
+ const amb = codexUtils.resolveWorkflowCandidate(tmp, {}, null, null);
2952
1694
  assert.equal(amb.error, 'multiple_active');
2953
- const esc = codexUtils.resolveActiveSpec(tmp, {}, null, path.join(tmp, 'specs', '..', 'outside', 'spec.json'));
1695
+ fs.mkdirSync(path.join(tmp, 'outside'), { recursive: true });
1696
+ fs.writeFileSync(path.join(tmp, 'outside', 'plan.md'), '# Outside\n');
1697
+ const esc = codexUtils.resolveWorkflowCandidate(tmp, {}, null, path.join(tmp, 'specs', '..', 'outside', 'plan.md'));
2954
1698
  assert.equal(esc.error, 'explicit_malformed');
2955
1699
  } finally {
2956
1700
  fs.rmSync(tmp, { recursive: true, force: true });
2957
1701
  }
2958
1702
  });
2959
1703
 
2960
- test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forged are blocked', () => {
2961
- // Probe A: explicit failure outcomes must fail canonical validator and not promote flash
1704
+ test('P0 regression: placeholder, explicit failure and artifact receipts are blocked', () => {
1705
+ // Probe A: explicit failure outcomes must fail the canonical validator
2962
1706
  const bodyTestsFailed = 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nTests failed: 1\n';
2963
1707
  assert.ok(POLICY.validateCanonicalReceipt(bodyTestsFailed).length > 0, 'Tests failed: 1 must be rejected');
2964
1708
  assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nResult: FAIL\nResult: PASS\n').includes('exit_result'), 'Result FAIL then PASS must be rejected');
2965
- const flashTask = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', blocker: 'awaiting', dependencyBlocked: true, unblocks: false };
2966
- const notPromoted = POLICY.promoteFlashTask(flashTask, 'PASS', bodyTestsFailed);
2967
- assert.equal(notPromoted.readyForSync, false, 'flash with Tests failed should not promote');
2968
- assert.equal(notPromoted.receipt, 'FLASH_UNVERIFIED');
2969
1709
 
2970
1710
  // Probe B: placeholder tokens must be rejected, but substring todo must pass
2971
1711
  assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: TODO\nExit: 0\nBase: a\nHead: b\n').includes('command'), 'Command TODO must be rejected');
@@ -2980,24 +1720,6 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
2980
1720
  assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: bundle\nsha256: abc123\n').includes('artifact_hash'), 'short artifact sha must fail');
2981
1721
  assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact produced sha256:deadbeef\n').includes('artifact_hash'), 'short inline sha256 must fail');
2982
1722
  assert.deepEqual(POLICY.validateCanonicalReceipt(`Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: 0123456789abcdef0123456789abcdef01234567\nHead: 89abcdef0123456789abcdef0123456789abcdef\nArtifact: bundle\nsha256: ${'c'.repeat(64)}\n`), [], '64-hex artifact sha should pass');
2983
-
2984
- // Probe F: lanePolicy forged plain object must not bypass classification
2985
- const { spawnSync } = require('node:child_process');
2986
- const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
2987
- const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
2988
- assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
2989
- assert.match(forged.stderr, /downgrade.*not permitted/i);
2990
- const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
2991
- assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
2992
-
2993
- const trusted = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true });
2994
- assert.equal(trusted.lane, 'Direct');
2995
- const viaPolicy = POLICY.lanePolicy(trusted);
2996
- assert.equal(viaPolicy.lane, 'Direct', 'trusted Direct should be respected');
2997
- assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
2998
- const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
2999
- const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
3000
- assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
3001
1723
  });
3002
1724
 
3003
1725
  test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
@@ -3005,33 +1727,29 @@ test('P0 regression: symlink spec/task containment and malformed spec handling',
3005
1727
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-regression-symlink-'));
3006
1728
  try {
3007
1729
  const specsDir = path.join(tmp, 'specs');
3008
- fs.mkdirSync(specsDir, { recursive: true });
3009
- fs.mkdirSync(path.join(specsDir, 'valid'), { recursive: true });
3010
- fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'valid' }));
1730
+ writeWorkflowPacketAt(path.join(specsDir, 'valid'));
3011
1731
  const outside = path.join(tmp, 'outside');
3012
- fs.mkdirSync(outside, { recursive: true });
3013
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
3014
- const link = path.join(specsDir, 'linked');
3015
- fs.symlinkSync(outside, link);
3016
- const e1 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
1732
+ writeWorkflowPacketAt(outside);
1733
+ fs.symlinkSync(outside, path.join(specsDir, 'linked'));
1734
+ const e1 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'linked' });
3017
1735
  assert.equal(e1.error, 'explicit_malformed');
3018
- const e2 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'linked', 'spec.json') });
1736
+ const e2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitPath: path.join(specsDir, 'linked', 'plan.md') });
3019
1737
  assert.equal(e2.error, 'explicit_malformed');
3020
- const nonExplicit = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1738
+ const nonExplicit = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3021
1739
  assert.equal(nonExplicit.error, 'invalid_specs');
3022
1740
  assert.ok(nonExplicit.candidates.includes('linked'));
3023
- const featDir = path.join(specsDir, 'valid');
3024
- fs.mkdirSync(path.join(featDir, 'tasks'), { recursive: true });
1741
+ fs.rmSync(path.join(specsDir, 'linked'));
1742
+ // A task file that is a symlink to a file outside the packet is refused.
3025
1743
  const taskOutside = path.join(tmp, 'outside-task.md');
3026
- fs.writeFileSync(taskOutside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
3027
- const taskLink = path.join(featDir, 'tasks', 'task.md');
3028
- fs.symlinkSync(taskOutside, taskLink);
3029
- const codexReceipt = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
3030
- const fails = codexReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
3031
- assert.ok(fails.includes('a'), 'task symlink outside should be rejected as check a');
3032
- fs.mkdirSync(path.join(specsDir, 'bad'), { recursive: true });
3033
- fs.writeFileSync(path.join(specsDir, 'bad', 'spec.json'), '{ malformed');
3034
- const mal = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1744
+ fs.copyFileSync(path.join(specsDir, 'valid', 'task-01-x.md'), taskOutside);
1745
+ fs.symlinkSync(taskOutside, path.join(specsDir, 'valid', 'task-02-link.md'));
1746
+ const linkedTask = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
1747
+ assert.equal(linkedTask.error, 'invalid_specs');
1748
+ assert.ok(linkedTask.candidates.includes('valid'));
1749
+ fs.rmSync(path.join(specsDir, 'valid', 'task-02-link.md'));
1750
+ // A malformed packet (dependency on a missing task) is reported, not skipped.
1751
+ writeWorkflowPacketAt(path.join(specsDir, 'bad'), 'pending', 'task-09-missing.md');
1752
+ const mal = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3035
1753
  assert.equal(mal.error, 'invalid_specs');
3036
1754
  assert.ok(mal.candidates.includes('bad'));
3037
1755
  } finally {
@@ -3102,18 +1820,18 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
3102
1820
  assert.match(codexSrc, /getSharedValidate/);
3103
1821
  assert.match(codexSrc, /workflow-policy\.cjs/);
3104
1822
  assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
3105
- // safeTaskFile fail-closed: simulate realpathSync throwing
1823
+ // The task-file read fails closed when realpath throws
3106
1824
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
3107
1825
  try {
3108
1826
  const featDir = path2.join(tmp, 'feat');
3109
- fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
3110
- fs2.writeFileSync(path2.join(featDir, 'tasks', 'task.md'), '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1827
+ writeWorkflowPacketAt(featDir, 'done');
1828
+ fs2.appendFileSync(path2.join(featDir, 'task-01-x.md'), '\n## Receipt\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n```\npnpm test\n```\n');
1829
+ const codexReceipt = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
3111
1830
  const orig = fs2.realpathSync;
3112
1831
  fs2.realpathSync = () => { throw new Error('simulated realpath failure'); };
3113
1832
  try {
3114
- const codexReceipt = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
3115
- const fails = codexReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
3116
- assert.ok(fails.includes('a'), 'realpath failure should be fail-closed with check a');
1833
+ const proof = codexReceipt.checkWorkflowReceiptDetails(featDir, 'task-01-x.md', null);
1834
+ assert.ok(proof.failures.includes('unsafe_path'), `realpath failure must fail closed, got ${proof.failures}`);
3117
1835
  } finally {
3118
1836
  fs2.realpathSync = orig;
3119
1837
  }
@@ -3130,15 +1848,12 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3130
1848
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-resolver-fail-'));
3131
1849
  try {
3132
1850
  const specsDir = path2.join(tmp, 'specs');
3133
- fs2.mkdirSync(path2.join(specsDir, 'demo'), { recursive: true });
3134
- fs2.writeFileSync(path2.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo' }));
3135
- // Monkey patch realpathSync to always throw for explicitFeature
1851
+ writeWorkflowPacketAt(path2.join(specsDir, 'demo'));
3136
1852
  const origRealpath = fs2.realpathSync;
3137
1853
  fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
3138
1854
  try {
3139
- const res = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
1855
+ const res = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
3140
1856
  assert.equal(res.error, 'explicit_malformed', 'explicitFeature realpath failure should be explicit_malformed');
3141
- assert.match(res.reason, /canonicalization error/);
3142
1857
  } finally {
3143
1858
  fs2.realpathSync = origRealpath;
3144
1859
  }
@@ -3149,7 +1864,7 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3149
1864
  return origRealpath2(p);
3150
1865
  };
3151
1866
  try {
3152
- const res2 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1867
+ const res2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3153
1868
  assert.equal(res2.error, 'invalid_specs');
3154
1869
  assert.ok(res2.candidates.includes('<specs>'));
3155
1870
  } finally {
@@ -3162,21 +1877,17 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3162
1877
  return origLstat(p);
3163
1878
  };
3164
1879
  try {
3165
- const res3 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1880
+ const res3 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3166
1881
  assert.equal(res3.error, 'invalid_specs');
3167
1882
  assert.ok(res3.candidates.includes('demo'));
3168
1883
  } finally {
3169
1884
  fs2.lstatSync = origLstat;
3170
1885
  }
3171
- // Dangling spec.json symlink should be invalid_specs
3172
- const outside = path2.join(tmp, 'outside-spec2');
3173
- fs2.mkdirSync(outside, { recursive: true });
3174
- fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1886
+ // A dangling plan.md symlink is invalid
3175
1887
  const featDir = path2.join(specsDir, 'dangle');
3176
1888
  fs2.mkdirSync(featDir, { recursive: true });
3177
- const specLink = path2.join(featDir, 'spec.json');
3178
- fs2.symlinkSync(path2.join(tmp, 'nonexistent-target'), specLink);
3179
- const res4 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1889
+ fs2.symlinkSync(path2.join(tmp, 'nonexistent-target'), path2.join(featDir, 'plan.md'));
1890
+ const res4 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3180
1891
  assert.equal(res4.error, 'invalid_specs');
3181
1892
  assert.ok(res4.candidates.includes('dangle'));
3182
1893
  } finally {
@@ -3186,13 +1897,11 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3186
1897
  const codexUtils = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
3187
1898
  const tmp2 = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-fail-'));
3188
1899
  try {
3189
- const specsDir2 = path2.join(tmp2, 'specs');
3190
- fs2.mkdirSync(path2.join(specsDir2, 'demo2'), { recursive: true });
3191
- fs2.writeFileSync(path2.join(specsDir2, 'demo2', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo2' }));
1900
+ writeWorkflowPacketAt(path2.join(tmp2, 'specs', 'demo2'));
3192
1901
  const orig = fs2.realpathSync;
3193
1902
  fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
3194
1903
  try {
3195
- const r = codexUtils.resolveActiveSpec(tmp2, {}, 'demo2', null);
1904
+ const r = codexUtils.resolveWorkflowCandidate(tmp2, {}, 'demo2', null);
3196
1905
  assert.equal(r.error, 'explicit_malformed');
3197
1906
  } finally {
3198
1907
  fs2.realpathSync = orig;