@haposoft/cafekit 0.18.0 → 0.18.2

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 (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -14,15 +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 READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
21
- const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
22
17
  const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
23
18
  const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
24
19
  const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
25
- const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
26
20
  const GATE = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/quality-gate.md');
27
21
  const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
28
22
  const TEST_SOURCE_ROOT = path.dirname(TEST_SKILL);
@@ -64,7 +58,6 @@ after(() => fs.rmSync(RUNTIME_ROOT, { recursive: true, force: true }));
64
58
  const EXPECTED_BASE = RUNTIME_CONTEXT.base;
65
59
  const EXPECTED_HEAD = RUNTIME_CONTEXT.head;
66
60
  const EXPECTED_PROVENANCE = { base: EXPECTED_BASE, head: EXPECTED_HEAD };
67
- const RECEIPT_BINDING = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
68
61
 
69
62
  function initFixtureGit(root) {
70
63
  for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['add', '-A'], ['commit', '-qm', 'fixture']]) {
@@ -85,7 +78,7 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
85
78
  fs.copyFileSync(path.join(hooksRoot, hook), target);
86
79
  }
87
80
  // Hook helpers the raw-copied hooks require at runtime.
88
- for (const helper of ['hook-state-dir.cjs', 'runtime-dir.cjs', 'hook-payload.cjs']) {
81
+ for (const helper of ['hook-state-dir.cjs', 'runtime-dir.cjs', 'hook-payload.cjs', 'project-root.cjs']) {
89
82
  const target = path.join(destinationRoot, 'hooks', 'lib', helper);
90
83
  fs.mkdirSync(path.dirname(target), { recursive: true });
91
84
  fs.copyFileSync(path.join(hooksRoot, 'lib', helper), target);
@@ -159,182 +152,10 @@ function assertVocabularyIsLegacyOnly(filePath, regions) {
159
152
  );
160
153
  }
161
154
 
162
- function runNode(root, script, args) {
163
- return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
164
- }
165
-
166
- function commandOutput(result) {
167
- return `${result.stdout}\n${result.stderr}`;
168
- }
169
-
170
- function createCanonicalAuthoringFixture() {
171
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-develop-contract-'));
172
- fs.mkdirSync(path.join(root, 'src'), { recursive: true });
173
- fs.mkdirSync(path.join(root, 'test'), { recursive: true });
174
- fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
175
- fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { enabled: true };\n');
176
- fs.writeFileSync(path.join(root, 'test', 'service.test.js'), 'require("node:test")("service", () => {});\n');
177
- const scaffold = runNode(root, SCAFFOLD, ['feature']);
178
- assert.equal(scaffold.status, 0, commandOutput(scaffold));
179
- const specDir = path.join(root, 'specs', 'feature');
180
- const specPath = path.join(specDir, 'spec.json');
181
- fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
182
-
183
- ## Requirements
184
- ### Requirement 1: Observable service behavior
185
- - **R1.1**: When valid input arrives, the service shall return enabled state; invalid input shall return rejected state.
186
- - **R1.2**: The verifier shall produce separate observable proof for enabled and rejected states.
187
- `);
188
- fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
189
-
190
- ## Architecture
191
- The entrypoint exposes the bounded service behavior and a separate proof artifact.
192
-
193
- ## Typed Anchors
194
- | ID | Type | Target | Role | Access | Action |
195
- |---|---|---|---|---|---|
196
- | A-D-01 | file | \`src/entry.js\` | service boundary | read | read |
197
- | A-D-02 | artifact | \`artifacts/design-proof.json\` | proof evidence | write | create |
198
-
199
- ## Canonical Contracts & Invariants
200
- ### D1 — Dispatch decision
201
- The entrypoint dispatches exactly once.
202
- ### I1 — Invalid-state invariant
203
- Invalid input never returns enabled state.
204
- ### C1 — Result contract
205
- The result contains one enabled boolean.
206
-
207
- ## Verification Definitions
208
- - **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.
209
- `);
210
- return { root, specDir, specPath };
211
- }
212
-
213
- function createOrdinaryAuthoringFixture() {
214
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ordinary-readiness-'));
215
- fs.mkdirSync(path.join(root, 'src'), { recursive: true });
216
- fs.mkdirSync(path.join(root, 'test'), { recursive: true });
217
- fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
218
- fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), 'require("node:test")("entry", () => {});\n');
219
- const scaffold = runNode(root, SCAFFOLD, ['ordinary']);
220
- assert.equal(scaffold.status, 0, commandOutput(scaffold));
221
- const specDir = path.join(root, 'specs', 'ordinary');
222
- const specPath = path.join(specDir, 'spec.json');
223
- fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
224
-
225
- ## Requirements
226
- ### Requirement 1: Delete behavior
227
- - **R1.1**: When deletion is requested, the service shall delete the selected record and report deleted state.
228
- `);
229
- fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
230
-
231
- ## Typed Anchors
232
- | ID | Type | Target | Role | Access | Action |
233
- |---|---|---|---|---|---|
234
- | A-D-01 | file | \`src/entry.js\` | behavior owner | read | read |
235
-
236
- ## Canonical Contracts & Invariants
237
- ### D1 — Delete decision
238
- The selected record is deleted rather than archived.
239
- ### I1 — State invariant
240
- Deleted state is reported only after deletion succeeds.
241
- ### C1 — Result contract
242
- The result contains the literal deleted state.
243
-
244
- ## Verification Definitions
245
- - **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.
246
- `);
247
- const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
248
- assert.equal(semantic.status, 0, commandOutput(semantic));
249
- const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
250
- assert.equal(authoring.status, 0, commandOutput(authoring));
251
- initFixtureGit(root);
252
- return { root, specDir, specPath };
253
- }
254
-
255
- function ordinaryReview() {
256
- return canonicalPassReview(['R1.1'], [{
257
- criterion: 'R1.1', case_kind: 'failure',
258
- scenario: 'Deletion targets a record that does not exist in the current store.',
259
- expected: 'The service returns not-found and never reports the record as deleted.',
260
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
261
- }]);
262
- }
263
-
264
- function canonicalPassReview(reviewedCriteria, counterexamples) {
265
- return {
266
- verdict: 'PASS',
267
- findings: [],
268
- unresolved_decisions: [],
269
- graph_coverage: [
270
- 'criterion_local', 'cross_criterion', 'runtime_path',
271
- 'assumption_provenance', 'compatibility_migration',
272
- ].map((surface) => ({
273
- surface, covered: true,
274
- notes: 'The review covers this semantic surface against the canonical model.',
275
- })),
276
- reviewed_criteria: reviewedCriteria,
277
- counterexamples,
278
- reviewer_evidence: null,
279
- };
280
- }
281
-
282
- function promoteCanonicalModel(fixture) {
283
- const result = runNode(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
284
- assert.equal(result.status, 0, commandOutput(result));
285
- }
286
-
287
- function makeCanonicalReady(fixture) {
288
- const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
289
- assert.equal(authoring.status, 0, commandOutput(authoring));
290
- const result = READINESS.finalizeReadiness({
291
- specDir: fixture.specDir,
292
- projectRoot: fixture.root,
293
- reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
294
- criterion: 'R1.1', case_kind: 'failure',
295
- scenario: 'The service receives invalid input without its required discriminator.',
296
- expected: 'The service returns rejected state and never emits enabled true.',
297
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
298
- }, {
299
- criterion: 'R1.2', case_kind: 'adversarial',
300
- scenario: 'The service reports enabled state without producing separate proof evidence.',
301
- expected: 'Verification fails because the independent proof artifact is absent.',
302
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
303
- }]),
304
- });
305
- return result.spec;
306
- }
307
-
308
- function bindFixtureReceipt(root, value, feature = 'demo', session = 'test') {
309
- const context = PROVENANCE_HELPER.deriveRuntimeContext({
310
- projectRoot: root,
311
- specsRoot: path.join(root, 'specs'),
312
- specFile: path.join(root, 'specs', feature, 'spec.json'),
313
- featureName: feature,
314
- runtimeSession: session,
315
- });
316
- return value.replaceAll('0123456789abcdef0123456789abcdef01234567', context.base)
317
- .replaceAll('89abcdef0123456789abcdef0123456789abcdef', context.head);
318
- }
319
-
320
155
  function canonicalReceipt(command = 'pnpm test', extra = '') {
321
156
  return `Verification: PASS\nCommand: ${command}\nExit: 0\nBase: ${EXPECTED_BASE}\nHead: ${EXPECTED_HEAD}\n${extra}`;
322
157
  }
323
158
 
324
- function canonicalFlashTask(overrides = {}) {
325
- return {
326
- status: 'in_progress',
327
- receipt: 'FLASH_UNVERIFIED',
328
- blocker: 'awaiting /cf:test <feature>',
329
- dependencyBlocked: true,
330
- unblocks: false,
331
- feature_name: 'demo',
332
- runtime_context: RUNTIME_CONTEXT,
333
- expected_provenance: { ...EXPECTED_PROVENANCE },
334
- ...overrides,
335
- };
336
- }
337
-
338
159
  function read(filePath) {
339
160
  return fs.readFileSync(filePath, 'utf8');
340
161
  }
@@ -377,380 +198,17 @@ test('develop flags retain their process-v3 contracts', () => {
377
198
  assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
378
199
  });
379
200
 
380
- test('delegation plan consumes legacy tiers as obligations without an agent chain', () => {
381
- const light = POLICY.delegationPlan({ tier: 'Light', mode: 'full-spec', taskCount: 2 });
382
- const standard = POLICY.delegationPlan({ tier: 'Standard', mode: 'specific-task' });
383
- const deep = POLICY.delegationPlan({ tier: 'Deep', mode: 'full-spec', taskCount: 2 });
384
- assert.deepEqual(light.proof_obligations, ['needsExecutionProof']);
385
- assert.deepEqual(standard.proof_obligations, ['needsExecutionProof']);
386
- assert.deepEqual(deep.proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
387
- for (const plan of [light, standard, deep]) {
388
- assert.equal(Object.hasOwn(plan, 'delegated'), false);
389
- assert.doesNotMatch(JSON.stringify(plan), /inspector|implementer|test-runner|code-auditor/);
390
- }
201
+ test('review and sync skills keep the verdict vocabulary and the sync-finalize path', () => {
391
202
  assert.doesNotMatch(read(GATE), /spec-review|quality-review/);
392
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/);
393
209
  });
394
210
 
395
- test('lane classifier selects Direct for explicit reversible low-risk work', () => {
396
- const result = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
397
- assert.equal(result.lane, 'Direct');
398
- assert.equal(result.automaticLane, 'Direct');
399
- assert.deepEqual(result.risks, []);
400
- const policy = POLICY.lanePolicy(result);
401
- assert.deepEqual(policy.proof_obligations, ['needsExecutionProof']);
402
- assert.equal(policy.artifact_profile, 'targeted');
403
- assert.equal(policy.requiresSpec, false);
404
- assert.equal(policy.requiresState, false);
405
- assert.equal(policy.proof_obligations.includes('needsDurableTaskState'), false);
406
- });
407
-
408
- test('lane classifier raises named risks to Elevated without inventing independent-audit authority', () => {
409
- const result = POLICY.classifyLane({
410
- reversible: true,
411
- riskSignals: { auth: true, migration: true, publicContract: true },
412
- });
413
- assert.equal(result.lane, 'Standard');
414
- assert.equal(result.assuranceLevel, 'Elevated');
415
- assert.equal(result.automaticAssuranceLevel, 'Elevated');
416
- assert.deepEqual(result.risks, ['auth', 'migration', 'publicContract']);
417
- assert.deepEqual(POLICY.lanePolicy(result).proof_obligations, [
418
- 'needsInspection', 'needsExecutionProof',
419
- ]);
420
-
421
- const strict = POLICY.classifyLane({
422
- riskSignals: { auth: true },
423
- assurance_level: 'Strict',
424
- });
425
- assert.equal(strict.lane, 'Critical');
426
- assert.equal(strict.automaticLane, 'Standard');
427
- assert.equal(strict.assuranceLevel, 'Strict');
428
- assert.equal(strict.automaticAssuranceLevel, 'Elevated');
429
- assert.ok(POLICY.lanePolicy(strict).proof_obligations.includes('needsIndependentAudit'));
430
- });
431
-
432
- test('P1 persists the minimal v2.1 policy and derives compatibility views', () => {
433
- const initial = POLICY.persistWorkflowPolicySnapshot(
434
- { feature_name: 'bounded' },
435
- { riskSignals: {} },
436
- );
437
- const snapshot = initial.workflow_policy;
438
- assert.deepEqual(Object.keys(snapshot).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
439
- assert.equal(snapshot.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
440
- assert.equal(snapshot.planning_depth, 'Compact');
441
- assert.equal(snapshot.assurance_level, 'Routine');
442
- assert.deepEqual(snapshot.classified_minimum, {
443
- planning_depth: 'Compact',
444
- assurance_level: 'Routine',
445
- });
446
- for (const derivedField of ['lane', 'automatic_lane', 'artifact_profile', 'proof_obligations', 'actor_needs']) {
447
- assert.equal(Object.hasOwn(snapshot, derivedField), false, `${derivedField} must not be persisted`);
448
- }
449
- const view = POLICY.readWorkflowPolicySnapshot(initial);
450
- assert.equal(view.lane, 'Standard');
451
- assert.equal(view.automatic_lane, 'Standard');
452
- assert.equal(view.artifact_profile, 'bounded');
453
- assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
454
- assert.equal(Object.hasOwn(initial, 'override_' + 'receipt'), false);
455
- assert.equal(POLICY.validateWorkflowPolicySnapshot(snapshot).valid, true);
456
-
457
- const persistedAgain = POLICY.persistWorkflowPolicySnapshot(initial, {
458
- riskSignals: { auth: true },
459
- override: 'Direct',
460
- });
461
- assert.deepEqual(persistedAgain.workflow_policy, snapshot, 'persist-once must not reclassify an existing snapshot');
462
-
463
- const malformed = { ...snapshot, lane: 'Critical' };
464
- const validation = POLICY.validateWorkflowPolicySnapshot(malformed);
465
- assert.equal(validation.valid, false);
466
- assert.match(validation.errors.join('; '), /v2\.1 fields must be exactly/);
467
- assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: malformed }), /Invalid workflow policy snapshot/);
468
-
469
- const malformedShape = { ...snapshot, classified_minimum: { planning_depth: 'Compact' } };
470
- const shapeValidation = POLICY.validateWorkflowPolicySnapshot(malformedShape);
471
- assert.equal(shapeValidation.valid, false);
472
- assert.match(shapeValidation.errors.join('; '), /classified_minimum must contain exactly/);
473
-
474
- const malformedRisks = { ...snapshot, risks: { auth: true } };
475
- const risksValidation = POLICY.validateWorkflowPolicySnapshot(malformedRisks);
476
- assert.equal(risksValidation.valid, false);
477
- assert.match(risksValidation.errors.join('; '), /risks must be an array/);
478
- });
479
-
480
- test('P1 workflow-policy validation is semantic and explicit workflow_policy values never reclassify', () => {
481
- const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
482
- const standardUnknown = POLICY.escalateWorkflowPolicy(direct, { risks: ['unclassified-risk'] });
483
- const elevatedAuth = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
484
- const escalatedElevated = POLICY.escalateWorkflowPolicy(direct, { risks: ['auth'] });
485
-
486
- for (const legitimate of [direct, standardUnknown, elevatedAuth, escalatedElevated]) {
487
- assert.equal(POLICY.validateWorkflowPolicySnapshot(legitimate).valid, true);
488
- }
489
- assert.equal(escalatedElevated.lane, 'Standard');
490
- assert.equal(escalatedElevated.automatic_lane, 'Standard');
491
- assert.equal(escalatedElevated.planning_depth, 'None');
492
- assert.equal(escalatedElevated.assurance_level, 'Elevated');
493
-
494
- const forgedDirectAuth = { ...direct, risks: ['auth'] };
495
- const forgedDirectUnknown = { ...direct, risks: ['unclassified-risk'] };
496
- const forgedStandardAuth = { ...standardUnknown, risks: ['auth'] };
497
- for (const forged of [forgedDirectAuth, forgedDirectUnknown]) {
498
- const result = POLICY.validateWorkflowPolicySnapshot(forged);
499
- assert.equal(result.valid, false);
500
- assert.match(result.errors.join('; '), /at least (Elevated|Strict)/);
501
- }
502
- assert.equal(
503
- POLICY.validateWorkflowPolicySnapshot(forgedStandardAuth).valid,
504
- true,
505
- 'changing one normalized risk to another does not invent a higher assurance minimum',
506
- );
507
- for (const riskAlias of ['Auth', 'authentication', 'public_contract']) {
508
- const escalated = POLICY.escalateWorkflowPolicy(direct, { risks: [riskAlias] });
509
- assert.equal(escalated.lane, 'Standard', `${riskAlias} must classify as Elevated`);
510
- assert.equal(escalated.assurance_level, 'Elevated');
511
- assert.equal(POLICY.validateWorkflowPolicySnapshot({ ...direct, risks: [riskAlias] }).valid, false);
512
- }
513
- assert.throws(() => POLICY.assertWorkflowPolicySnapshot(forgedDirectAuth), /Invalid workflow policy snapshot/);
514
- assert.throws(() => POLICY.readWorkflowPolicySnapshot({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
515
- assert.throws(() => POLICY.lanePolicy({ workflow_policy: forgedDirectAuth }), /Invalid workflow policy snapshot/);
516
-
517
- for (const explicitValue of [null, undefined]) {
518
- const state = { workflow_policy: explicitValue, reversible: true, lowRisk: true, isolated: true };
519
- assert.throws(() => POLICY.workflowPolicySnapshot(state), /Invalid workflow policy snapshot/);
520
- assert.throws(() => POLICY.escalateWorkflowPolicy(state, {}), /Invalid workflow policy snapshot/);
521
- assert.throws(() => POLICY.lanePolicy(state), /Invalid workflow policy snapshot/);
522
- }
523
-
524
- const forgedLanePolicyCli = spawnSync(process.execPath, [
525
- POLICY_PATH,
526
- '--lane-policy',
527
- '--task-json',
528
- JSON.stringify({ workflow_policy: forgedDirectAuth }),
529
- '--json',
530
- ], { encoding: 'utf8' });
531
- assert.equal(forgedLanePolicyCli.status, 2, `${forgedLanePolicyCli.stdout}\n${forgedLanePolicyCli.stderr}`);
532
- assert.match(forgedLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
533
-
534
- const nullLanePolicyCli = spawnSync(process.execPath, [
535
- POLICY_PATH,
536
- '--lane-policy',
537
- '--task-json',
538
- JSON.stringify({ workflow_policy: null, reversible: true, lowRisk: true, isolated: true }),
539
- '--json',
540
- ], { encoding: 'utf8' });
541
- assert.equal(nullLanePolicyCli.status, 2, `${nullLanePolicyCli.stdout}\n${nullLanePolicyCli.stderr}`);
542
- assert.match(nullLanePolicyCli.stderr, /Invalid workflow policy snapshot/);
543
-
544
- const validatePolicyCli = spawnSync(process.execPath, [
545
- POLICY_PATH,
546
- '--validate-policy',
547
- '--task-json',
548
- JSON.stringify({ workflow_policy: forgedDirectAuth }),
549
- '--json',
550
- ], { encoding: 'utf8' });
551
- assert.equal(validatePolicyCli.status, 2, `${validatePolicyCli.stdout}\n${validatePolicyCli.stderr}`);
552
- assert.equal(JSON.parse(validatePolicyCli.stdout).valid, false);
553
-
554
- const validateNullPolicyCli = spawnSync(process.execPath, [
555
- POLICY_PATH,
556
- '--validate-policy',
557
- '--task-json',
558
- JSON.stringify({ workflow_policy: null }),
559
- '--json',
560
- ], { encoding: 'utf8' });
561
- assert.equal(validateNullPolicyCli.status, 2, `${validateNullPolicyCli.stdout}\n${validateNullPolicyCli.stderr}`);
562
- assert.equal(JSON.parse(validateNullPolicyCli.stdout).valid, false);
563
- });
564
-
565
- test('P1 workflow policy escalation is monotonic across all newly classified risks', () => {
566
- const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
567
- assert.equal(direct.lane, 'Direct');
568
- assert.deepEqual(POLICY.escalateWorkflowPolicy(direct, {}), direct, 'no new risks must not escalate');
569
-
570
- const standard = POLICY.escalateWorkflowPolicy(direct, { risks: ['new-standard-risk'], riskLevel: 'standard' });
571
- assert.equal(standard.lane, 'Standard');
572
- assert.equal(standard.artifact_profile, 'targeted');
573
- assert.equal(standard.planning_depth, 'None');
574
- assert.equal(standard.assurance_level, 'Elevated');
575
- assert.equal(standard.risks.includes('new-standard-risk'), true);
576
- assert.deepEqual(standard.proof_obligations, ['needsInspection', 'needsExecutionProof']);
577
-
578
- const ambiguityElevated = POLICY.escalateWorkflowPolicy(direct, { riskSignals: { ambiguity: true } });
579
- assert.equal(ambiguityElevated.lane, 'Standard', 'risk discovery must require inspection without inventing an audit');
580
- assert.equal(ambiguityElevated.assurance_level, 'Elevated');
581
-
582
- const criticalSeverity = POLICY.escalateWorkflowPolicy(direct, { risks: ['payment'], riskLevel: 'critical' });
583
- assert.equal(criticalSeverity.assurance_level, 'Elevated', 'severity labels must not create independent-audit authority');
584
- assert.equal(criticalSeverity.proof_obligations.includes('needsIndependentAudit'), false);
585
-
586
- const critical = POLICY.escalateWorkflowPolicy(standard, { risks: ['auth'], assurance_level: 'Strict' });
587
- assert.equal(critical.lane, 'Critical');
588
- assert.equal(critical.artifact_profile, 'targeted');
589
- assert.equal(critical.planning_depth, 'None');
590
- assert.equal(critical.assurance_level, 'Strict');
591
- assert.equal(critical.automatic_assurance_level, 'Elevated');
592
- assert.equal(critical.proof_obligations.includes('needsIndependentAudit'), true);
593
- assert.equal(critical.proof_obligations.includes('needsResearchGrounding'), false);
594
-
595
- const noDowngrade = POLICY.escalateWorkflowPolicy(critical, { risks: ['another-standard-risk'], riskLevel: 'standard' });
596
- assert.equal(noDowngrade.lane, 'Critical');
597
- assert.equal(noDowngrade.proof_obligations.includes('needsIndependentAudit'), true);
598
- assert.equal(noDowngrade.proof_obligations.includes('needsResearchGrounding'), false);
599
-
600
- const criticalAgain = POLICY.escalateWorkflowPolicy(critical, { risks: ['auth'] });
601
- assert.deepEqual(criticalAgain, critical, 'repeating known risks must not create a new escalation');
602
- });
603
-
604
- test('P1 legacy execution tier is read-compatible without becoming emitted policy authority', () => {
605
- const legacy = { design_context: { execution_tier: 'Deep' } };
606
- const snapshot = POLICY.workflowPolicySnapshot(legacy);
607
- assert.equal(snapshot.lane, 'Critical');
608
- assert.equal(snapshot.artifact_profile, 'strict');
609
- const policy = POLICY.lanePolicy(legacy);
610
- assert.equal(policy.lane, 'Critical');
611
- assert.equal(Object.hasOwn(policy, 'execution_tier'), false);
612
- assert.equal(Object.hasOwn(policy, 'executionTier'), false);
613
- const legacyPlan = POLICY.delegationPlan({ tier: 'Deep' });
614
- assert.equal(Object.hasOwn(legacyPlan, 'execution_tier'), false);
615
- assert.equal(Object.hasOwn(legacyPlan, 'executionTier'), false);
616
- assert.deepEqual(legacyPlan.proof_obligations, snapshot.proof_obligations);
617
- assert.equal(POLICY.workflowPolicySnapshot({ design_context: { execution_tier: 'standard' } }).lane, 'Standard');
618
- });
619
-
620
- test('legacy v1 policy is read-compatible, immutable on read, and explicitly migrates to v2.1', () => {
621
- const cases = [
622
- { lane: 'Direct', risks: [], obligations: ['needsExecutionProof'] },
623
- { lane: 'Standard', risks: [], obligations: ['needsInspection', 'needsExecutionProof'] },
624
- { lane: 'Critical', risks: ['auth'], obligations: ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding'] },
625
- ];
626
- for (const fixture of cases) {
627
- const v1 = {
628
- version: '1',
629
- lane: fixture.lane,
630
- automatic_lane: fixture.lane,
631
- risks: fixture.risks,
632
- artifact_profile: { Direct: 'targeted', Standard: 'bounded', Critical: 'strict' }[fixture.lane],
633
- proof_obligations: fixture.obligations,
634
- actor_needs: POLICY.actorNeedsFor(fixture.obligations),
635
- override_receipt: null,
636
- };
637
- assert.equal(POLICY.validateWorkflowPolicySnapshot(v1).valid, true, fixture.lane);
638
- const source = { feature_name: `legacy-${fixture.lane}`, workflow_policy: v1 };
639
- const before = JSON.stringify(source);
640
- const adapted = POLICY.readWorkflowPolicySnapshot(source);
641
- assert.equal(adapted.version, '2');
642
- assert.deepEqual(adapted.proof_obligations, fixture.obligations, fixture.lane);
643
- assert.deepEqual(adapted.actor_needs, v1.actor_needs, fixture.lane);
644
- assert.equal(POLICY.validateWorkflowPolicySnapshot(adapted).valid, true, fixture.lane);
645
- assert.equal(JSON.stringify(source), before, 'read adapter must not mutate persisted v1 state');
646
- const migrated = POLICY.persistWorkflowPolicySnapshot(source).workflow_policy;
647
- assert.equal(migrated.version, POLICY.CANONICAL_WORKFLOW_POLICY_VERSION);
648
- assert.deepEqual(Object.keys(migrated).sort(), [...POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS].sort());
649
- const migratedView = POLICY.readWorkflowPolicySnapshot({ workflow_policy: migrated });
650
- assert.equal(migratedView.lane, adapted.lane, fixture.lane);
651
- assert.equal(migratedView.artifact_profile, adapted.artifact_profile, fixture.lane);
652
- assert.deepEqual(migratedView.proof_obligations, POLICY.obligationsForAssurance(
653
- migrated.assurance_level,
654
- migrated.risks,
655
- ), fixture.lane);
656
- }
657
- });
658
-
659
- test('post-classification downgrade is unsupported', () => {
660
- assert.throws(
661
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
662
- (error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
663
- );
664
- });
665
-
666
- test('caller-requested downgrade is blocked before persistence', () => {
667
- assert.throws(
668
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct' }),
669
- (error) => /planning_depth downgrade/.test(error.message) && /not permitted/.test(error.message),
670
- );
671
- // also via CLI
672
- const cli = spawnSync(process.execPath, [
673
- POLICY_PATH,
674
- '--classify-lane',
675
- '--task-json',
676
- JSON.stringify({ riskSignals: { auth: true }, override: 'Direct' }),
677
- '--json',
678
- ], { encoding: 'utf8' });
679
- assert.equal(cli.status, 2);
680
- assert.match(cli.stderr, /planning_depth downgrade.*not permitted/i);
681
- });
682
-
683
- test('forged approval via legacy approved field is rejected', () => {
684
- assert.throws(() => POLICY.approvalState({ generated: true, approved: true }), /Legacy approval state/);
685
- const cli = spawnSync(process.execPath, [
686
- POLICY_PATH,
687
- '--approval-state',
688
- '--task-json',
689
- JSON.stringify({ generated: true, approved: true }),
690
- '--json',
691
- ], { encoding: 'utf8' });
692
- assert.equal(cli.status, 2);
693
- assert.match(cli.stderr, /Legacy approval state/);
694
- const legacyCheck = POLICY.validateApprovalSchema({ approvals: { requirements: { generated: true, approved: true } } });
695
- assert.equal(legacyCheck.valid, false);
696
- assert.equal(legacyCheck.legacy, true);
697
- assert.match(legacyCheck.error, /Legacy/);
698
- });
699
-
700
- test('technical approval readiness ignores legacy user_approved', () => {
701
- const state = POLICY.approvalState({ generated: true, agent_validated: true });
702
- assert.equal(state.generated, true);
703
- assert.equal(state.agent_validated, true);
704
- assert.equal(state.ready, true);
705
- assert.equal(state.schema_version, '2.0');
706
- assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true }).ready, true);
707
- assert.equal(POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false }).ready, true);
708
- const cli = spawnSync(process.execPath, [
709
- POLICY_PATH,
710
- '--approval-state',
711
- '--task-json',
712
- JSON.stringify({ generated: true, agent_validated: true }),
713
- '--json',
714
- ], { encoding: 'utf8' });
715
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
716
- assert.equal(JSON.parse(cli.stdout).state.ready, true);
717
- });
718
-
719
- test('approval schema fails closed for explicit null, array, empty, and malformed states', () => {
720
- const absent = POLICY.validateApprovalSchema({});
721
- assert.equal(absent.valid, true);
722
- assert.equal(absent.absent, true);
723
- assert.equal(POLICY.approvalState({}).present, false);
724
-
725
- const invalidSpecs = [
726
- [],
727
- { approvals: null },
728
- { approvals: [] },
729
- { approvals: {} },
730
- { approvals: { requirements: null } },
731
- { approvals: { requirements: { generated: true, agent_validated: true, user_approved: null } } },
732
- { approvals: { requirements: { generated: true, agent_validated: true, user_approved: true, extra: false } } },
733
- ];
734
- for (const spec of invalidSpecs) {
735
- assert.equal(POLICY.validateApprovalSchema(spec).valid, false, `invalid approval should fail: ${JSON.stringify(spec)}`);
736
- assert.throws(() => POLICY.approvalState(spec), /approval|approvals|schema/i);
737
- }
738
- assert.throws(() => POLICY.approvalState({ schema_version: null }), /schema_version/);
739
- assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
740
- });
741
-
742
- test('CLI exposes a derived lane without making it primary Develop authority', () => {
743
- const cli = spawnSync(process.execPath, [
744
- POLICY_PATH,
745
- '--classify-lane',
746
- '--task-json',
747
- JSON.stringify({ reversible: true, lowRisk: true, isolated: true }),
748
- '--json',
749
- ], { encoding: 'utf8' });
750
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
751
- const payload = JSON.parse(cli.stdout);
752
- assert.equal(payload.classification.lane, 'Direct');
753
- assert.equal(payload.policy.requiresSpec, false);
211
+ test('Develop names the process-first packet as its only authority', () => {
754
212
  const develop = read(DEVELOP);
755
213
  const regions = markdownLegacyRegions(develop);
756
214
  assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
@@ -764,18 +222,6 @@ test('CLI exposes a derived lane without making it primary Develop authority', (
764
222
  assert.match(develop, /--notes.*opt-in/i);
765
223
  assert.doesNotMatch(develop, /--no-notes/);
766
224
  assert.doesNotMatch(develop, /planner|implementer|test-runner|code-auditor|docs-keeper/i);
767
- const fullRoutine = POLICY.readWorkflowPolicySnapshot({
768
- workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Routine' }),
769
- });
770
- const compactStrict = POLICY.readWorkflowPolicySnapshot({
771
- workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' }),
772
- });
773
- assert.equal(fullRoutine.planning_depth, 'Full');
774
- assert.equal(fullRoutine.assurance_level, 'Routine');
775
- assert.equal(compactStrict.planning_depth, 'Compact');
776
- assert.equal(compactStrict.assurance_level, 'Strict');
777
- assert.notEqual(fullRoutine.artifact_profile, compactStrict.artifact_profile);
778
- assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
779
225
  });
780
226
 
781
227
  test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
@@ -1107,7 +553,7 @@ test('Claude installed Test preserves plan-native proof and references', () => {
1107
553
  const installedReview = fs.readFileSync(path.join(root, '.claude/skills/code-review/SKILL.md'), 'utf8');
1108
554
  assert.equal(installedReview, fs.readFileSync(CODE_REVIEW_SKILL, 'utf8'));
1109
555
  assert.match(installedReview, /controller-validated `test-proof-v1`/);
1110
- 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);
1111
557
  assert.equal(fs.existsSync(path.join(root, '.agents/skills/test')), false);
1112
558
  } finally {
1113
559
  fs.rmSync(root, { recursive: true, force: true });
@@ -1121,411 +567,24 @@ test('specs-usage-guide documents Test proof handoff without timing claims', ()
1121
567
  assert.match(section, /Test không ghi `Status:` hay inline[\s\S]*Develop controller là writer duy nhất/i);
1122
568
  assert.match(section, /tracked\/untracked\/ignored drift/i);
1123
569
  assert.match(section, /HTTPS\/localhost origin[\s\S]*fresh consent/i);
1124
- assert.match(section, /separate-receipt adapter/i);
570
+ assert.match(section, /chỉ nằm trong `## Receipt` inline/i);
1125
571
  assert.match(section, /không phải benchmark thời gian/i);
1126
572
  assert.match(section, /không chứng minh live adherence/i);
1127
573
  assert.doesNotMatch(section, /\b\d+(?:\.\d+)?\s*(?:ms|milliseconds?|seconds?|minutes?)\b/i);
1128
574
  assert.doesNotMatch(section, /\[(?:LIVE_)?VERIFIED\]/i);
1129
575
  });
1130
576
 
1131
- test('canonical verdict adapter handles completion and unfinished decisions', () => {
1132
- assert.deepEqual(POLICY.REVIEW_VERDICTS, ['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
1133
- assert.doesNotThrow(() => POLICY.assertVerdict('PASS_WITH_WARNINGS'));
1134
- assert.throws(() => POLICY.assertVerdict('PARTIAL'), /Unsupported canonical verdict/);
1135
- assert.equal(POLICY.consumeReviewVerdict('PASS').completion, 'unfinished');
1136
- assert.equal(POLICY.consumeReviewVerdict('PASS').unfinished, true);
1137
- assert.equal(POLICY.consumeReviewVerdict('PASS').createsCompletion, false);
1138
- assert.equal(POLICY.consumeReviewVerdict('PASS_WITH_WARNINGS').unfinished, true);
1139
- assert.equal(POLICY.consumeReviewVerdict('FAIL').retry, true);
1140
- assert.equal(POLICY.consumeReviewVerdict('BLOCKED').retry, false);
1141
- assert.equal(POLICY.consumeReviewVerdict('BLOCKED').unfinished, true);
1142
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').completion, 'unfinished');
1143
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').completion, 'unfinished');
1144
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').verdict, 'BLOCKED');
1145
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').verdict, 'BLOCKED');
1146
- assert.equal(POLICY.consumeReviewVerdict('PARTIAL').retry, false);
1147
- assert.equal(POLICY.consumeReviewVerdict('NO_TESTS').retry, false);
1148
- assert.throws(() => POLICY.consumeReviewVerdict('UNKNOWN'), /Unsupported canonical verdict/);
1149
- const cli = spawnSync(process.execPath, [POLICY_PATH, '--consume-verdict', '--verdict', 'BLOCKED', '--json'], { encoding: 'utf8' });
1150
- assert.equal(cli.status, 0, `${cli.stdout}\n${cli.stderr}`);
1151
- assert.deepEqual(JSON.parse(cli.stdout).action, 'stop');
1152
- assert.match(read(GATE), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1153
- assert.match(read(CODE_REVIEW_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1154
- assert.match(read(TEST_SKILL), /PASS \| PASS_WITH_WARNINGS \| FAIL \| BLOCKED/);
1155
- });
1156
-
1157
- test('completion decision requires canonical execution receipt and every workflow obligation', () => {
1158
- const receipt = canonicalReceipt();
1159
- const critical = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1160
- const audit = {
1161
- schema_version: '1',
1162
- reviewer_session_id: 'review-session-1',
1163
- implementation_session_id: 'implementation-session-1',
1164
- expected_provenance: { ...EXPECTED_PROVENANCE },
1165
- evidence: 'independent audit report for the bound change',
1166
- verdict: 'PASS',
1167
- };
1168
- const missing = POLICY.completionDecision('PASS', {
1169
- workflow_policy: critical,
1170
- receipt_binding: RECEIPT_BINDING,
1171
- execution_receipt: receipt,
1172
- proofs: {
1173
- needsInspection: { evidence: 'inspection receipt' },
1174
- needsResearchGrounding: { evidence: 'research receipt' },
1175
- },
1176
- });
1177
- assert.equal(missing.completion, 'unfinished');
1178
- assert.ok(missing.missingProof.includes('needsIndependentAudit'));
1179
- assert.equal(missing.unfinished, true);
1180
-
1181
- const complete = POLICY.completionDecision('PASS', {
1182
- workflow_policy: critical,
1183
- receipt_binding: RECEIPT_BINDING,
1184
- execution_receipt: receipt,
1185
- proofs: {
1186
- needsInspection: { evidence: 'inspection receipt' },
1187
- needsResearchGrounding: { evidence: 'research receipt' },
1188
- needsIndependentAudit: audit,
1189
- },
1190
- });
1191
- assert.equal(complete.completion, 'complete');
1192
- assert.equal(complete.unfinished, false);
1193
- assert.equal(complete.warnings, undefined);
1194
- const warnings = POLICY.completionDecision('PASS_WITH_WARNINGS', {
1195
- workflow_policy: critical,
1196
- receipt_binding: RECEIPT_BINDING,
1197
- execution_receipt: receipt,
1198
- proofs: {
1199
- needsInspection: { evidence: 'inspection receipt' },
1200
- needsResearchGrounding: { evidence: 'research receipt' },
1201
- needsIndependentAudit: audit,
1202
- },
1203
- });
1204
- assert.equal(warnings.completion, 'unfinished');
1205
- assert.equal(warnings.unfinished, true);
1206
- assert.match(warnings.blocker, /literal PASS/);
1207
- assert.equal(POLICY.completionDecision('PASS', {
1208
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1209
- receipt_binding: RECEIPT_BINDING,
1210
- execution_receipt: receipt,
1211
- }).completion, 'unfinished');
1212
- assert.equal(POLICY.completionDecision('PASS', {
1213
- workflow_policy: {},
1214
- receipt_binding: RECEIPT_BINDING,
1215
- execution_receipt: receipt,
1216
- }).completion, 'unfinished');
1217
- assert.equal(POLICY.completionDecision('PASS', {
1218
- workflow_policy: critical,
1219
- execution_receipt: receipt,
1220
- }).completion, 'unfinished', 'arbitrary Base/Head without runtime binding must not complete');
1221
- assert.equal(POLICY.completionDecision('PASS', {
1222
- workflow_policy: critical,
1223
- receipt_binding: RECEIPT_BINDING,
1224
- execution_receipt: receipt,
1225
- proofs: {
1226
- needsInspection: { evidence: 'inspection receipt' },
1227
- needsResearchGrounding: { evidence: 'research receipt' },
1228
- needsIndependentAudit: { independent: true, evidence: 'marker only', verdict: 'PASS' },
1229
- },
1230
- }).completion, 'unfinished', 'independent marker must not satisfy audit');
1231
- assert.equal(POLICY.validateIndependentAuditEvidence({ independent: true }, EXPECTED_PROVENANCE).valid, false);
1232
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, verdict: 'PASS_WITH_WARNINGS' }, EXPECTED_PROVENANCE).valid, false);
1233
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, reviewer_session_id: audit.implementation_session_id }, EXPECTED_PROVENANCE).valid, false);
1234
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, expected_provenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }, EXPECTED_PROVENANCE).valid, false);
1235
- assert.equal(POLICY.validateIndependentAuditEvidence({ ...audit, extra: 'forged' }, EXPECTED_PROVENANCE).valid, false);
1236
- assert.equal(POLICY.validateIndependentAuditEvidence([audit], EXPECTED_PROVENANCE).valid, false);
1237
- assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, RUNTIME_CONTEXT).valid, true);
1238
- assert.equal(POLICY.validateIndependentAuditEvidence(audit, EXPECTED_PROVENANCE, { ...RUNTIME_CONTEXT }).valid, false, 'forged runtime session context must not satisfy audit binding');
1239
- assert.equal(POLICY.completionDecision('PASS', {}).completion, 'unfinished');
1240
- const artifactContext = {
1241
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
1242
- receipt_binding: RECEIPT_BINDING,
1243
- task_context: { artifacts: ['output/bundle.js'], expected_provenance: { ...EXPECTED_PROVENANCE } },
1244
- execution_receipt: receipt,
1245
- proofs: { needsInspection: { evidence: 'inspection receipt' } },
1246
- receipt_options: { requireArtifactHash: false, artifactPaths: [] },
1247
- };
1248
- const artifactBlocked = POLICY.completionDecision('PASS', artifactContext);
1249
- assert.equal(artifactBlocked.completion, 'unfinished', 'caller receipt options must not weaken task artifact requirements');
1250
- assert.ok(artifactBlocked.missingProof.some((item) => item.includes('artifact_hash')));
1251
- });
1252
-
1253
- test('auto authoring reaches readiness only after promotion, validation, grounding, and current review', () => {
1254
- const fixture = createCanonicalAuthoringFixture();
1255
- try {
1256
- let spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1257
- assert.equal(spec.semantic_model, null);
1258
- spec.ready_for_implementation = true;
1259
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1260
- let validation = VALIDATOR.validateSpec(fixture.specDir);
1261
- assert.ok(validation.errors.some((error) => error.includes('requires explicit promoted machine semantic authority')));
1262
-
1263
- spec.ready_for_implementation = false;
1264
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1265
- promoteCanonicalModel(fixture);
1266
- spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1267
- assert.ok(spec.semantic_model && spec.semantic_model.criteria.length === 2);
1268
- spec.ready_for_implementation = true;
1269
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1270
- validation = VALIDATOR.validateSpec(fixture.specDir);
1271
- assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
1272
- assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
1273
-
1274
- spec.ready_for_implementation = false;
1275
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1276
- spec = makeCanonicalReady(fixture);
1277
- validation = VALIDATOR.validateSpec(fixture.specDir);
1278
- assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
1279
- assert.equal(GROUNDER.groundSpec({ specDir: fixture.specDir, root: fixture.root, spec }).errors.length, 0);
1280
-
1281
- const specs = read(SPECS);
1282
- const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
1283
- assert.ok(executableBlocks.every((block) => !/cf:?develop/i.test(block)), 'Specs must never execute Develop');
1284
- assert.match(specs, /An implementation workflow owns execution/i);
1285
- assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
1286
- assert.match(specs, /The user decides\s+at GATE-DONE whether the feature is done/i);
1287
- } finally {
1288
- fs.rmSync(fixture.root, { recursive: true, force: true });
1289
- }
1290
- });
1291
-
1292
- test('atomic readiness supports one-criterion Routine specs and binds semantic text', () => {
1293
- const fixture = createOrdinaryAuthoringFixture();
1294
- try {
1295
- const authored = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1296
- authored.decisions = [
1297
- { 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.' },
1298
- { 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.' },
1299
- ];
1300
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(authored, null, 2)}\n`);
1301
- const result = READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() });
1302
- assert.equal(result.spec.ready_for_implementation, true);
1303
- assert.equal(result.spec.semantic_model.criteria.length, 1);
1304
- assert.match(result.spec.semantic_model.criteria[0].text, /delete the selected record/);
1305
- assert.deepEqual(VALIDATOR.validateSpec(fixture.specDir).errors, []);
1306
-
1307
- const beforeDigest = result.semantic_digest;
1308
- const design = path.join(fixture.specDir, 'design.md');
1309
- const originalDesign = fs.readFileSync(design, 'utf8');
1310
- fs.writeFileSync(design, originalDesign.replace('deleted rather than archived', 'archived rather than deleted'));
1311
- const decisionProjection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
1312
- assert.notDeepEqual(decisionProjection.model.design_records, result.spec.semantic_model.design_records);
1313
- const decisionDigest = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: decisionProjection.model });
1314
- assert.equal(decisionDigest.errors.length, 0, decisionDigest.errors.join('\n'));
1315
- assert.notEqual(decisionDigest.digest, beforeDigest);
1316
- fs.writeFileSync(design, originalDesign);
1317
-
1318
- const requirements = path.join(fixture.specDir, 'requirements.md');
1319
- fs.writeFileSync(requirements, fs.readFileSync(requirements, 'utf8').replace('delete the selected record', 'archive the selected record'));
1320
- const projection = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-semantic-model.cjs')).modelFromMarkdown(fixture.specDir, result.spec);
1321
- assert.notEqual(projection.model.criteria[0].text, result.spec.semantic_model.criteria[0].text);
1322
- const changed = VALIDATOR.computeSemanticDigest21(fixture.specDir, { ...result.spec, semantic_model: projection.model });
1323
- assert.equal(changed.errors.length, 0, changed.errors.join('\n'));
1324
- assert.notEqual(changed.digest, beforeDigest);
1325
- assert.ok(VALIDATOR.validateSpec(fixture.specDir).errors.some((error) => /projection|stale/.test(error)));
1326
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1327
- });
1328
-
1329
- test('atomic readiness preserves exact bytes on review, grounding, decision, and Strict authority failures', () => {
1330
- const fixture = createOrdinaryAuthoringFixture();
1331
- try {
1332
- const original = fs.readFileSync(fixture.specPath);
1333
- for (const mutate of [
1334
- (review) => ({ ...review, reviewed_criteria: [] }),
1335
- (review) => ({ ...review, counterexamples: [] }),
1336
- ]) {
1337
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: mutate(ordinaryReview()) }), /readiness validation/);
1338
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1339
- }
1340
- fs.unlinkSync(path.join(fixture.root, 'src', 'entry.js'));
1341
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /grounding|validation/);
1342
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1343
- fs.writeFileSync(path.join(fixture.root, 'src', 'entry.js'), 'module.exports = { action: "delete" };\n');
1344
-
1345
- process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME = '1';
1346
- try {
1347
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /injected finalization failure/);
1348
- assert.equal(fs.readFileSync(fixture.specPath).equals(original), true);
1349
- } finally { delete process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME; }
1350
-
1351
- const blocked = JSON.parse(original);
1352
- blocked.decisions = [{ id: 'Q1', classification: 'user_owned', statement: 'Choose permanent deletion or retention archive behavior.', status: 'unresolved', evidence: null }];
1353
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
1354
- const blockedBytes = fs.readFileSync(fixture.specPath);
1355
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /unresolved material user-owned decision/);
1356
- assert.equal(fs.readFileSync(fixture.specPath).equals(blockedBytes), true);
1357
-
1358
- delete blocked.decisions;
1359
- blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
1360
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
1361
- const strictBytes = fs.readFileSync(fixture.specPath);
1362
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
1363
- assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
1364
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1365
- });
1366
-
1367
- test('Strict readiness is refused as no longer supported', () => {
1368
- const fixture = createOrdinaryAuthoringFixture();
1369
- try {
1370
- const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
1371
- spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
1372
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1373
- const before = fs.readFileSync(fixture.specPath);
1374
- assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
1375
- assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
1376
- } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
1377
- });
1378
-
1379
- test('done is a final-state request bound to current semantic model and digest', () => {
1380
- const fixture = createCanonicalAuthoringFixture();
1381
- try {
1382
- promoteCanonicalModel(fixture);
1383
- const spec = makeCanonicalReady(fixture);
1384
- spec.status = 'done';
1385
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
1386
- const candidate = {
1387
- featureDir: fixture.specDir,
1388
- featureName: 'feature',
1389
- specFile: fixture.specPath,
1390
- spec,
1391
- };
1392
- const dependencies = () => ({
1393
- validator: VALIDATOR,
1394
- grounder: GROUNDER,
1395
- semanticAuthority: {},
1396
- });
1397
- const current = FINAL_STATE.validateCanonicalFinalState({
1398
- policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
1399
- });
1400
- assert.equal(current.ok, true, current.reason);
1401
- assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
1402
-
1403
- const failedReceipt = structuredClone(spec);
1404
- failedReceipt.validation.semantic_review.verdict = 'FAIL';
1405
- const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
1406
- policy: POLICY,
1407
- projectRoot: fixture.root,
1408
- candidate: { ...candidate, spec: failedReceipt },
1409
- dependencies,
1410
- });
1411
- assert.equal(receiptMutation.ok, false);
1412
- assert.match(receiptMutation.reason, /PASS CONTINUE/);
1413
-
1414
- const mismatchedHistory = structuredClone(spec);
1415
- const latest = mismatchedHistory.validation.semantic_review_history.entries[
1416
- mismatchedHistory.validation.semantic_review_history.entries.length - 1
1417
- ];
1418
- latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
1419
- const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
1420
- policy: POLICY,
1421
- projectRoot: fixture.root,
1422
- candidate: { ...candidate, spec: mismatchedHistory },
1423
- dependencies,
1424
- });
1425
- assert.equal(digestMismatch.ok, false);
1426
- assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
1427
-
1428
- const requirementsPath = path.join(fixture.specDir, 'requirements.md');
1429
- const requirements = fs.readFileSync(requirementsPath, 'utf8');
1430
- fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
1431
- const staleDigest = FINAL_STATE.validateCanonicalFinalState({
1432
- policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
1433
- });
1434
- assert.equal(staleDigest.ok, false);
1435
- assert.match(staleDigest.reason, /stale|differs from Markdown projection/i);
1436
-
1437
- fs.writeFileSync(requirementsPath, requirements);
1438
- const staleModel = structuredClone(spec);
1439
- staleModel.semantic_model.criteria[0].owner = 'A-D-02';
1440
- fs.writeFileSync(fixture.specPath, `${JSON.stringify(staleModel, null, 2)}\n`);
1441
- const invalidModel = FINAL_STATE.validateCanonicalFinalState({
1442
- policy: POLICY,
1443
- projectRoot: fixture.root,
1444
- candidate: { ...candidate, spec: staleModel },
1445
- dependencies,
1446
- });
1447
- assert.equal(invalidModel.ok, false);
1448
- assert.match(invalidModel.reason, /semantic projection|semantic_model|validation failed/i);
1449
- } finally {
1450
- fs.rmSync(fixture.root, { recursive: true, force: true });
1451
- }
1452
- });
1453
-
1454
- test('flash PASS promotes proof; only trusted sync-finalize completes', () => {
1455
- const initial = canonicalFlashTask();
1456
- for (const verdict of ['FAIL', 'BLOCKED', 'PARTIAL', 'NO_TESTS', 'PASS_WITH_WARNINGS']) {
1457
- const result = POLICY.promoteFlashTask(initial, verdict);
1458
- assert.equal(result.status, 'in_progress');
1459
- assert.equal(result.receipt, 'FLASH_UNVERIFIED');
1460
- assert.equal(result.dependencyBlocked, true);
1461
- assert.equal(result.unblocks, false);
1462
- assert.match(result.blocker, /verification|test proof/);
1463
- }
1464
- const promoted = POLICY.promoteFlashTask(initial, 'PASS', canonicalReceipt());
1465
- assert.equal(promoted.status, 'in_progress');
1466
- assert.equal(promoted.receipt.startsWith('Verification: PASS'), true);
1467
- assert.equal(promoted.blocker, null);
1468
- assert.equal(promoted.dependencyBlocked, true);
1469
- assert.equal(promoted.unblocks, false);
1470
- assert.equal(promoted.readyForSync, true);
1471
- assert.equal(POLICY.finalizeFlashTask(promoted, 'sync'), promoted);
1472
- const forgedFinalize = POLICY.finalizeFlashTask(promoted, 'sync-finalize');
1473
- assert.equal(forgedFinalize.status, 'in_progress');
1474
- assert.equal(forgedFinalize.dependencyBlocked, true);
1475
- assert.equal(forgedFinalize.unblocks, false);
1476
- const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', promoted.receipt);
1477
- assert.equal(finalized.status, 'done');
1478
- assert.equal(finalized.dependencyBlocked, false);
1479
- assert.equal(finalized.unblocks, true);
1480
- assert.equal(finalized.readyForSync, false);
1481
- assert.equal(POLICY.isStaleFlashDone({ status: 'done', receipt: 'FLASH_UNVERIFIED' }), true);
1482
- assert.match(read(TEST_SKILL), /only explicit .*sync-finalize/i);
1483
- assert.match(read(SYNC_SKILL), /sync-finalize/);
1484
- });
1485
-
1486
- test('spec-gate rejects stale FLASH_UNVERIFIED done state', () => {
1487
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-spec-gate-'));
1488
- const specGate = installClaudeRuntimeClosure(root);
1489
- assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'spec-final-state.cjs')), true);
1490
- assert.equal(fs.existsSync(path.join(root, '.claude', 'scripts', 'validate-spec-output.cjs')), true);
1491
- fs.mkdirSync(path.join(root, 'specs', 'demo', 'tasks'), { recursive: true });
1492
- fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
1493
- status: 'in_progress',
1494
- current_phase: 'closeout',
1495
- feature_name: 'demo',
1496
- task_registry: { 'tasks/task.md': { status: 'done', receipt: 'FLASH_UNVERIFIED' } },
1497
- }));
1498
- initFixtureGit(root);
1499
- try {
1500
- const result = spawnSync(process.execPath, [specGate], {
1501
- cwd: root,
1502
- env: { ...process.env, PROJECT_ROOT: root },
1503
- input: JSON.stringify({ cwd: root, session_id: 'test' }),
1504
- encoding: 'utf8',
1505
- });
1506
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
1507
- const payload = JSON.parse(result.stdout);
1508
- assert.equal(payload.decision, 'block');
1509
- assert.match(payload.reason, /FLASH_UNVERIFIED/);
1510
- } finally {
1511
- fs.rmSync(root, { recursive: true, force: true });
1512
- }
1513
- });
1514
-
1515
577
  test('first-run cache bypass is blocked - canonical receipt required even without cache', () => {
1516
578
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-first-run-'));
1517
579
  const claude = path.join(root, '.claude');
1518
580
  const specGate = installClaudeRuntimeClosure(root);
1519
- const specDir = path.join(root, 'specs', 'demo', 'tasks');
1520
- fs.mkdirSync(specDir, { recursive: true });
1521
- // Done task without canonical receipt (missing Command, provenance)
1522
- fs.writeFileSync(path.join(root, 'specs', 'demo', 'spec.json'), JSON.stringify({
1523
- status: 'done',
1524
- current_phase: 'closeout',
1525
- feature_name: 'demo',
1526
- task_registry: { 'tasks/task-R1-01-one.md': { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } },
1527
- }));
1528
- 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`);
1529
588
  initFixtureGit(root);
1530
589
  try {
1531
590
  assert.equal(fs.existsSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')), false);
@@ -1538,22 +597,18 @@ test('first-run cache bypass is blocked - canonical receipt required even withou
1538
597
  assert.equal(result.status, 0);
1539
598
  const payload = JSON.parse(result.stdout);
1540
599
  assert.equal(payload.decision, 'block', 'first-run without canonical receipt must block');
1541
- assert.match(payload.reason, /tasks\/task-R1-01-one\.md/);
600
+ assert.match(payload.reason, /task-01-one\.md/);
1542
601
  assert.match(payload.reason, /receipt|command|provenance/i);
1543
- // Also test that with canonical receipt it passes
1544
- 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');
1545
- fs.mkdirSync(path.join(root, 'specs', 'demo', 'receipts'), { recursive: true });
1546
- fs.writeFileSync(
1547
- path.join(root, 'specs', 'demo', 'receipts', 'task-R1-01-one.md'),
1548
- 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'),
1549
- );
1550
- fs.writeFileSync(
1551
- path.join(root, 'specs', 'demo', 'feature-receipt.md'),
1552
- 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'),
1553
- );
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`);
1554
610
  // Clear cache to simulate fresh first-run with valid receipt
1555
611
  try { fs.unlinkSync(path.join(claude, 'hooks', '.logs', 'spec-gate-last.json')); } catch {}
1556
- // Re-run with valid receipt - should not block
1557
612
  const result2 = spawnSync(process.execPath, [specGate], {
1558
613
  cwd: root,
1559
614
  env: { ...process.env, PROJECT_ROOT: root },
@@ -1576,10 +631,6 @@ test('canonical receipt requires command, exit, provenance and unambiguous PASS'
1576
631
  assert.ok(missingVerification.includes('verification_state'));
1577
632
  const artifactWithoutHash = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: dist/bundle.js\n');
1578
633
  assert.ok(artifactWithoutHash.includes('artifact_hash'));
1579
- // CLI validation
1580
- 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' });
1581
- assert.equal(cli.status, 0);
1582
- assert.equal(JSON.parse(cli.stdout).ok, true);
1583
634
  });
1584
635
 
1585
636
  test('canonical receipt provenance requires both Base and Head (or both base_sha and head_sha)', () => {
@@ -1629,27 +680,10 @@ test('canonical receipt provenance requires both Base and Head (or both base_sha
1629
680
  assert.deepEqual(validWithValues, [], 'valid Base and Head with values should pass');
1630
681
  const validShaWithValues = POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: 0123456789abcdef0123456789abcdef01234567\nhead_sha: 89abcdef0123456789abcdef0123456789abcdef\n');
1631
682
  assert.deepEqual(validShaWithValues, [], 'valid base_sha and head_sha with values should pass');
1632
- // CLI: only Base via --validate-receipt should fail
1633
- 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' });
1634
- assert.equal(cliOnlyBase.status, 2);
1635
- assert.equal(JSON.parse(cliOnlyBase.stdout).ok, false);
1636
- assert.ok(JSON.parse(cliOnlyBase.stdout).failures.includes('provenance'));
1637
- 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' });
1638
- assert.equal(cliBoth.status, 0);
1639
- assert.equal(JSON.parse(cliBoth.stdout).ok, true);
1640
- // CLI empty Base should fail
1641
- 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' });
1642
- assert.equal(cliEmptyBase.status, 2);
1643
- assert.ok(JSON.parse(cliEmptyBase.stdout).failures.includes('provenance'));
1644
683
  });
1645
684
 
1646
685
  test('canonical receipt binds expected provenance when the runtime supplies it', () => {
1647
686
  const body = canonicalReceipt();
1648
- const binding = POLICY.createReceiptBinding(RUNTIME_CONTEXT);
1649
- assert.deepEqual(binding.expectedProvenance, EXPECTED_PROVENANCE);
1650
- assert.equal(binding.requireProvenanceBinding, true);
1651
- assert.equal(binding.runtimeContext.context_id, RUNTIME_CONTEXT.context_id);
1652
- assert.throws(() => POLICY.createReceiptBinding(EXPECTED_PROVENANCE), /runtime-derived provenance context/);
1653
687
  assert.ok(POLICY.validateCanonicalReceipt(body, { requireProvenanceBinding: true }).includes('provenance'));
1654
688
  assert.deepEqual(POLICY.validateCanonicalReceipt(body, { expectedProvenance: EXPECTED_PROVENANCE }), []);
1655
689
  assert.ok(POLICY.validateCanonicalReceipt(body, { expectedProvenance: { base: EXPECTED_BASE, head: 'fedcba9876543210fedcba9876543210fedcba98' } }).includes('provenance'));
@@ -1743,37 +777,24 @@ test('runtime provenance derives exact Git evidence, CLI context, and stale/forg
1743
777
  assert.equal(withIgnoredCache.head, initial.head, 'nested ignored node_modules/cache files must not enter Head');
1744
778
  assert.equal(withChangedIgnoredCache.head, initial.head, 'changing nested ignored cache data must not change Head');
1745
779
 
1746
- const forgedContext = { ...initial };
1747
- const forgedDecision = POLICY.completionDecision('PASS', {
1748
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1749
- runtime_context: forgedContext,
1750
- execution_receipt: exactReceipt,
1751
- });
1752
- assert.equal(forgedDecision.completion, 'unfinished');
1753
- assert.deepEqual(forgedDecision.missingProof, ['runtime_provenance']);
1754
-
1755
- const flash = {
1756
- status: 'in_progress',
1757
- receipt: 'FLASH_UNVERIFIED',
1758
- blocker: 'awaiting test proof',
1759
- dependencyBlocked: true,
1760
- unblocks: false,
1761
- runtime_context: initial,
1762
- };
1763
- const promoted = POLICY.promoteFlashTask(flash, 'PASS', exactReceipt);
1764
- assert.equal(promoted.readyForSync, true);
1765
-
780
+ // A source change after the receipt was written leaves its Base/Head stale.
1766
781
  fs.writeFileSync(sourceFile, 'mutated source\n');
1767
- const stale = POLICY.completionDecision('PASS', {
1768
- workflow_policy: { proof_obligations: ['needsExecutionProof'] },
1769
- receipt_binding: POLICY.createReceiptBinding(initial),
1770
- execution_receipt: exactReceipt,
1771
- });
1772
- assert.equal(stale.completion, 'unfinished');
1773
- assert.ok(stale.missingProof.some((item) => item.includes('execution_receipt:provenance')));
1774
- const staleFinalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', exactReceipt, initial);
1775
- assert.equal(staleFinalized.status, 'in_progress');
1776
- 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
+ );
1777
798
 
1778
799
  fs.writeFileSync(path.join(root, 'src', 'untracked.js'), 'untracked source\n');
1779
800
  const withUntracked = PROVENANCE_HELPER.deriveRuntimeContext(input);
@@ -1867,76 +888,6 @@ test('runtime provenance chooses a deterministic root when all history is Specs-
1867
888
  }
1868
889
  });
1869
890
 
1870
- test('lane traces - Direct, Standard, explicit Strict classification, override, state mutation and completion', () => {
1871
- // Direct: isolated reversible low-risk
1872
- const direct = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1 });
1873
- assert.equal(direct.lane, 'Direct');
1874
- assert.equal(direct.automaticLane, 'Direct');
1875
- assert.deepEqual(POLICY.lanePolicy(direct).proof_obligations, ['needsExecutionProof']);
1876
- assert.equal(POLICY.lanePolicy(direct).requiresSpec, false);
1877
- assert.equal(POLICY.lanePolicy(direct).shipPoint, 'task');
1878
- // Standard: default
1879
- const standard = POLICY.classifyLane({ title: 'add pagination to list', taskCount: 1 });
1880
- assert.equal(standard.lane, 'Standard');
1881
- assert.deepEqual(POLICY.lanePolicy(standard).proof_obligations, ['needsExecutionProof']);
1882
- assert.equal(POLICY.lanePolicy(standard).shipPoint, 'feature');
1883
- // Risk alone is Elevated; Critical requires an explicit Strict assurance choice.
1884
- const elevated = POLICY.classifyLane({ riskSignals: { auth: true }, taskCount: 1 });
1885
- assert.equal(elevated.lane, 'Standard');
1886
- assert.deepEqual(POLICY.lanePolicy(elevated).proof_obligations, ['needsInspection', 'needsExecutionProof']);
1887
- const critical = POLICY.classifyLane({ riskSignals: { auth: true }, assurance_level: 'Strict', taskCount: 1 });
1888
- assert.equal(critical.lane, 'Critical');
1889
- assert.ok(critical.risks.includes('auth'));
1890
- assert.deepEqual(POLICY.lanePolicy(critical).proof_obligations, ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit']);
1891
- // Override: Direct requesting Critical is allowed (upgrade) without extra auth
1892
- const upgrade = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true, taskCount: 1, override: 'Critical' });
1893
- assert.equal(upgrade.lane, 'Critical');
1894
- assert.equal(upgrade.automaticLane, 'Direct');
1895
- // Downgrade without auth is blocked
1896
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard' }), /assurance_level downgrade.*blocked/i);
1897
- // Caller-owned booleans cannot weaken the classified minimum.
1898
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', userAuthorized: true }), /assurance_level downgrade.*blocked/i);
1899
- assert.throws(() => POLICY.classifyLane({ riskSignals: { payment: true }, override: 'Standard', user_approved: true }), /assurance_level downgrade.*blocked/i);
1900
- // State mutation: approvalState
1901
- const pendingApproval = POLICY.approvalState({ generated: true, agent_validated: false, user_approved: false });
1902
- assert.equal(pendingApproval.ready, false);
1903
- const agentValidated = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: false });
1904
- assert.equal(agentValidated.ready, true, 'generated plus agent validation is technical readiness');
1905
- const approved = POLICY.approvalState({ generated: true, agent_validated: true, user_approved: true });
1906
- assert.equal(approved.ready, true);
1907
- // Completion: flash work remains in_progress and does not unblock
1908
- const flashTask = canonicalFlashTask();
1909
- const failRemains = POLICY.promoteFlashTask(flashTask, 'FAIL');
1910
- assert.equal(failRemains.status, 'in_progress');
1911
- assert.equal(failRemains.unblocks, false);
1912
- const blockedRemains = POLICY.promoteFlashTask(flashTask, 'BLOCKED');
1913
- assert.equal(blockedRemains.unblocks, false);
1914
- const noTestsRemains = POLICY.promoteFlashTask(flashTask, 'NO_TESTS');
1915
- assert.equal(noTestsRemains.unblocks, false);
1916
- const promoted = POLICY.promoteFlashTask(flashTask, 'PASS', canonicalReceipt());
1917
- assert.equal(promoted.readyForSync, true);
1918
- assert.equal(promoted.unblocks, false, 'promoted flash must not unblock until sync-finalize');
1919
- const finalized = POLICY.syncFinalizeFlashTask(flashTask, 'PASS', promoted.receipt);
1920
- assert.equal(finalized.status, 'done');
1921
- assert.equal(finalized.unblocks, true);
1922
- });
1923
-
1924
- test('flash selective promotion - only specific task is promoted, not blanket', () => {
1925
- const registry = {
1926
- 'tasks/task-a.md': canonicalFlashTask(),
1927
- 'tasks/task-b.md': canonicalFlashTask(),
1928
- };
1929
- const promotedA = POLICY.promoteFlashTask(registry['tasks/task-a.md'], 'PASS', canonicalReceipt());
1930
- const notPromotedB = registry['tasks/task-b.md']; // unchanged
1931
- assert.equal(promotedA.readyForSync, true);
1932
- assert.equal(notPromotedB.receipt, 'FLASH_UNVERIFIED');
1933
- assert.equal(notPromotedB.readyForSync, undefined);
1934
- // FAIL on B does not affect A
1935
- const failB = POLICY.promoteFlashTask(notPromotedB, 'FAIL');
1936
- assert.equal(failB.receipt, 'FLASH_UNVERIFIED');
1937
- assert.equal(promotedA.receipt.startsWith('Verification: PASS'), true);
1938
- });
1939
-
1940
891
  test('parallel waves require immutable provenance receipts and safe recovery', () => {
1941
892
  const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
1942
893
  assert.match(waves, /only for explicit parallel execution/i);
@@ -2644,36 +1595,6 @@ test('P0 receipt fail-closed: Exit 1/-1/abc/conflict/empty command rejected', ()
2644
1595
  assert.deepEqual(fails, [], `case ${desc} should pass, got ${fails}`);
2645
1596
  }
2646
1597
  }
2647
- // CLI parity: Exit 1 must be rejected via CLI
2648
- const cli = spawnSync(process.execPath, [POLICY_PATH, '--validate-receipt', '--task-json', JSON.stringify({ body: `${base}Exit: 1\n` }), '--json'], { encoding: 'utf8' });
2649
- assert.equal(cli.status, 2);
2650
- assert.equal(JSON.parse(cli.stdout).ok, false);
2651
- });
2652
-
2653
- test('P0 flash marker-only must not promote or finalize', () => {
2654
- const flash = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
2655
- const markerOnly = 'Verification: PASS';
2656
- const promoted = POLICY.promoteFlashTask(flash, 'PASS', markerOnly);
2657
- assert.equal(promoted.status, 'in_progress');
2658
- assert.equal(promoted.receipt, 'FLASH_UNVERIFIED');
2659
- assert.equal(promoted.readyForSync, false);
2660
- assert.equal(promoted.dependencyBlocked, true);
2661
- assert.equal(promoted.unblocks, false);
2662
- assert.match(promoted.blocker, /canonical receipt/);
2663
- // Even with PASS marker but missing command/exit/provenance, finalize must not complete
2664
- const fakePromoted = { status: 'in_progress', receipt: markerOnly, readyForSync: true, dependencyBlocked: true, unblocks: false };
2665
- const notFinalized = POLICY.finalizeFlashTask(fakePromoted, 'sync-finalize');
2666
- assert.equal(notFinalized.status, 'in_progress', 'marker-only receipt must not finalize');
2667
- // Valid receipt promotes correctly
2668
- const valid = canonicalReceipt();
2669
- const good = POLICY.promoteFlashTask(flash, 'PASS', valid);
2670
- assert.equal(good.readyForSync, true);
2671
- assert.equal(good.receipt, valid.trim());
2672
- const forgedFinalize = POLICY.finalizeFlashTask(good, 'sync-finalize');
2673
- assert.equal(forgedFinalize.status, 'in_progress');
2674
- assert.equal(forgedFinalize.unblocks, false);
2675
- const finalized = POLICY.syncFinalizeFlashTask(flash, 'PASS', valid);
2676
- assert.equal(finalized.status, 'done');
2677
1598
  });
2678
1599
 
2679
1600
  test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact receipts compatible', () => {
@@ -2697,123 +1618,12 @@ test('P0 canonical artifacts declaration requires SHA-256 and keeps no-artifact
2697
1618
  }
2698
1619
  });
2699
1620
 
2700
- test('P0 sync-finalize derives promotion from current FLASH_UNVERIFIED plus explicit PASS proof', () => {
2701
- const receipt = canonicalReceipt('node --test');
2702
- const forged = {
2703
- status: 'in_progress',
2704
- receipt,
2705
- readyForSync: true,
2706
- dependencyBlocked: true,
2707
- unblocks: false,
2708
- flashTransition: 'promoted',
2709
- promotionReceipt: receipt,
2710
- };
2711
- const direct = spawnSync(process.execPath, [
2712
- POLICY_PATH,
2713
- '--sync-finalize',
2714
- '--task-json',
2715
- JSON.stringify(forged),
2716
- '--json',
2717
- ], { encoding: 'utf8' });
2718
- assert.equal(direct.status, 2, `${direct.stdout}\n${direct.stderr}`);
2719
- const directPayload = JSON.parse(direct.stdout);
2720
- assert.equal(directPayload.ok, false);
2721
- assert.equal(directPayload.task.status, 'in_progress');
2722
- assert.equal(directPayload.task.receipt, 'FLASH_UNVERIFIED');
2723
- assert.equal(directPayload.task.readyForSync, false);
2724
- assert.equal(directPayload.task.dependencyBlocked, true);
2725
- assert.equal(directPayload.task.unblocks, false);
2726
- assert.match(directPayload.message, /current task must be FLASH_UNVERIFIED.*canonical/i);
2727
-
2728
- const minimal = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED' };
2729
- const minimalResult = POLICY.syncFinalizeFlashTask(minimal, 'PASS', receipt);
2730
- assert.equal(minimalResult.status, 'in_progress');
2731
- assert.match(minimalResult.blocker, /exact canonical stored state/);
2732
- const forgedOpen = { ...canonicalFlashTask(), readyForSync: true };
2733
- const forgedOpenResult = POLICY.syncFinalizeFlashTask(forgedOpen, 'PASS', receipt);
2734
- assert.equal(forgedOpenResult.status, 'in_progress');
2735
- assert.equal(forgedOpenResult.unblocks, false);
2736
-
2737
- const current = canonicalFlashTask({ blocker: 'awaiting /cf:test' });
2738
- const supported = spawnSync(process.execPath, [
2739
- POLICY_PATH,
2740
- '--sync-finalize',
2741
- '--task-json',
2742
- JSON.stringify(current),
2743
- '--verdict',
2744
- 'PASS',
2745
- '--proof',
2746
- receipt,
2747
- '--project-root',
2748
- RUNTIME_ROOT,
2749
- '--specs-root',
2750
- path.join(RUNTIME_ROOT, 'specs'),
2751
- '--spec-file',
2752
- RUNTIME_SPEC,
2753
- '--feature-name',
2754
- 'demo',
2755
- '--runtime-session',
2756
- 'implementation-session-1',
2757
- '--json',
2758
- ], { encoding: 'utf8' });
2759
- assert.equal(supported.status, 0, `${supported.stdout}\n${supported.stderr}`);
2760
- assert.equal(JSON.parse(supported.stdout).task.status, 'done');
2761
- const warningFinalize = POLICY.syncFinalizeFlashTask(current, 'PASS_WITH_WARNINGS', receipt);
2762
- assert.equal(warningFinalize.status, 'in_progress');
2763
- assert.equal(warningFinalize.unblocks, false);
2764
-
2765
- const initial = canonicalFlashTask();
2766
- const promoted = POLICY.promoteFlashTask(initial, 'PASS', receipt);
2767
- const persisted = JSON.parse(JSON.stringify(promoted));
2768
- assert.equal(persisted.flashTransition, 'promoted');
2769
- assert.equal(persisted.promotionReceipt, receipt.trim());
2770
- const forgedPersistedFinalize = POLICY.finalizeFlashTask(persisted, 'sync-finalize');
2771
- assert.equal(forgedPersistedFinalize.status, 'in_progress');
2772
- assert.equal(forgedPersistedFinalize.unblocks, false);
2773
- const finalized = POLICY.syncFinalizeFlashTask(initial, 'PASS', receipt);
2774
- assert.equal(finalized.status, 'done');
2775
- assert.equal(finalized.flashTransition, 'finalized');
2776
-
2777
- const artifactInitial = { ...initial, artifacts: ['output/bundle.js'] };
2778
- const artifactPromotion = POLICY.promoteFlashTask(artifactInitial, 'PASS', receipt);
2779
- assert.equal(artifactPromotion.readyForSync, false, 'flash promotion must apply task artifact requirements');
2780
-
2781
- const badProof = spawnSync(process.execPath, [
2782
- POLICY_PATH,
2783
- '--sync-finalize',
2784
- '--task-json',
2785
- JSON.stringify(current),
2786
- '--verdict',
2787
- 'PASS',
2788
- '--proof',
2789
- 'Verification: PASS',
2790
- '--json',
2791
- ], { encoding: 'utf8' });
2792
- assert.equal(badProof.status, 2);
2793
- assert.equal(JSON.parse(badProof.stdout).task.status, 'in_progress');
2794
-
2795
- const badArtifact = spawnSync(process.execPath, [
2796
- POLICY_PATH,
2797
- '--sync-finalize',
2798
- '--task-json',
2799
- JSON.stringify({ ...current, artifacts: ['output/bundle.js'] }),
2800
- '--verdict',
2801
- 'PASS',
2802
- '--proof',
2803
- receipt,
2804
- '--json',
2805
- ], { encoding: 'utf8' });
2806
- assert.equal(badArtifact.status, 2);
2807
- assert.equal(JSON.parse(badArtifact.stdout).task.status, 'in_progress');
2808
-
2809
- const markerOnly = {
2810
- ...forged,
2811
- flashTransition: 'promoted',
2812
- promotionReceipt: 'Verification: PASS',
2813
- receipt: 'Verification: PASS',
2814
- };
2815
- assert.equal(POLICY.finalizeFlashTask(markerOnly, 'sync-finalize').status, 'in_progress');
2816
- });
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
+ }
2817
1627
 
2818
1628
  test('P0 Claude and Codex reject configured specs roots outside the project', () => {
2819
1629
  const claudeResolver = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
@@ -2821,15 +1631,11 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
2821
1631
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-external-specs-'));
2822
1632
  const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-configured-specs-'));
2823
1633
  try {
2824
- const local = path.join(tmp, 'specs', 'local');
2825
- const external = path.join(externalRoot, 'remote');
2826
- fs.mkdirSync(local, { recursive: true });
2827
- fs.mkdirSync(external, { recursive: true });
2828
- fs.writeFileSync(path.join(local, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'local' }));
2829
- 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'));
2830
1636
  const runtime = { paths: { specs: externalRoot } };
2831
- const claude = claudeResolver.resolveActiveSpec({ projectRoot: tmp, runtime });
2832
- 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);
2833
1639
  assert.equal(claude.error, 'invalid_specs');
2834
1640
  assert.equal(codex.error, 'invalid_specs');
2835
1641
  assert.match(claude.reason, /escapes project root/);
@@ -2840,59 +1646,31 @@ test('P0 Claude and Codex reject configured specs roots outside the project', ()
2840
1646
  }
2841
1647
  });
2842
1648
 
2843
- test('P0 caller-owned downgrade flags have no canonical capability', () => {
2844
- for (const callerClaim of [{ user_approved: true }, { userApproved: true }, { userAuthorized: true }]) {
2845
- assert.throws(
2846
- () => POLICY.classifyLane({ riskSignals: { privacy: true }, override: 'Direct', ...callerClaim }),
2847
- (error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
2848
- );
2849
- }
2850
- assert.throws(() => POLICY.classifyLane({ riskSignals: { auth: true }, override: 'Standard', confirmDowngrade: true }), /assurance_level downgrade.*blocked/i);
2851
- assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
2852
- });
2853
-
2854
- test('P0 canonical policy exposes no caller-owned downgrade authority', () => {
2855
- assert.equal(POLICY.isUserAuthorizedForDowngrade, undefined);
2856
- assert.equal(POLICY.isValidOverrideReceipt, undefined);
2857
- assert.equal(POLICY.CANONICAL_WORKFLOW_POLICY_FIELDS.includes('override_' + 'receipt'), false);
2858
- assert.throws(
2859
- () => POLICY.classifyLane({
2860
- riskSignals: { privacy: true },
2861
- override: 'Direct',
2862
- overrideReceipt: { verifiedByRuntime: true },
2863
- }),
2864
- (error) => /downgrade/.test(error.message) && /not permitted/.test(error.message),
2865
- );
2866
- });
2867
-
2868
1649
  test('P0 active spec deterministic: multiple active ambiguity and explicit target/path containment', () => {
2869
1650
  const RESOLVER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'));
2870
1651
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-'));
2871
1652
  try {
2872
1653
  const specsDir = path.join(tmp, 'specs');
2873
- fs.mkdirSync(specsDir, { recursive: true });
2874
- // Create two active specs
2875
- for (const name of ['alpha', 'beta']) {
2876
- fs.mkdirSync(path.join(specsDir, name), { recursive: true });
2877
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name, current_phase: 'design' }));
2878
- }
2879
- 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: {} });
2880
1656
  assert.equal(amb.error, 'multiple_active');
2881
1657
  assert.deepEqual(amb.candidates.sort(), ['alpha', 'beta']);
2882
1658
  // Explicit feature resolves deterministically
2883
- const alpha = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
1659
+ const alpha = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'alpha' });
2884
1660
  assert.equal(alpha.featureName, 'alpha');
2885
1661
  // Explicit not-found fail-closed
2886
- const notFound = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
1662
+ const notFound = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'gamma' });
2887
1663
  assert.equal(notFound.error, 'explicit_not_found');
2888
- // Explicit path containment: try to escape via ../
2889
- 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') });
2890
1668
  assert.equal(escape.error, 'explicit_malformed');
2891
1669
  // Malformed feature name with slash must be rejected
2892
- const malformed = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
1670
+ const malformed = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: '../alpha' });
2893
1671
  assert.equal(malformed.error, 'explicit_malformed');
2894
1672
  // Valid explicit path inside root
2895
- 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') });
2896
1674
  assert.equal(validPath.featureName, 'beta');
2897
1675
  } finally {
2898
1676
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -2911,30 +1689,23 @@ test('P0 parity Claude/Codex: canonical receipt and multi-active', () => {
2911
1689
  const codexUtils = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
2912
1690
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-resolver-'));
2913
1691
  try {
2914
- const specsDir = path.join(tmp, 'specs');
2915
- fs.mkdirSync(specsDir, { recursive: true });
2916
- for (const name of ['x', 'y']) {
2917
- fs.mkdirSync(path.join(specsDir, name), { recursive: true });
2918
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: name }));
2919
- }
2920
- 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);
2921
1694
  assert.equal(amb.error, 'multiple_active');
2922
- 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'));
2923
1698
  assert.equal(esc.error, 'explicit_malformed');
2924
1699
  } finally {
2925
1700
  fs.rmSync(tmp, { recursive: true, force: true });
2926
1701
  }
2927
1702
  });
2928
1703
 
2929
- test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forged are blocked', () => {
2930
- // 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
2931
1706
  const bodyTestsFailed = 'Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nTests failed: 1\n';
2932
1707
  assert.ok(POLICY.validateCanonicalReceipt(bodyTestsFailed).length > 0, 'Tests failed: 1 must be rejected');
2933
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');
2934
- const flashTask = { status: 'in_progress', receipt: 'FLASH_UNVERIFIED', blocker: 'awaiting', dependencyBlocked: true, unblocks: false };
2935
- const notPromoted = POLICY.promoteFlashTask(flashTask, 'PASS', bodyTestsFailed);
2936
- assert.equal(notPromoted.readyForSync, false, 'flash with Tests failed should not promote');
2937
- assert.equal(notPromoted.receipt, 'FLASH_UNVERIFIED');
2938
1709
 
2939
1710
  // Probe B: placeholder tokens must be rejected, but substring todo must pass
2940
1711
  assert.ok(POLICY.validateCanonicalReceipt('Verification: PASS\nCommand: TODO\nExit: 0\nBase: a\nHead: b\n').includes('command'), 'Command TODO must be rejected');
@@ -2949,24 +1720,6 @@ test('P0 regression: placeholder, explicit failure, artifact and lanePolicy forg
2949
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');
2950
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');
2951
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');
2952
-
2953
- // Probe F: lanePolicy forged plain object must not bypass classification
2954
- const { spawnSync } = require('node:child_process');
2955
- const POLICY_PATH = path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs');
2956
- const forged = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct', riskSignals: { auth: true } }), '--json'], { encoding: 'utf8' });
2957
- assert.equal(forged.status, 2, 'forged Direct with auth should be blocked via lanePolicy');
2958
- assert.match(forged.stderr, /downgrade.*not permitted/i);
2959
- const forged2 = spawnSync(process.execPath, [POLICY_PATH, '--lane-policy', '--task-json', JSON.stringify({ lane: 'Direct', automaticLane: 'Direct' }), '--json'], { encoding: 'utf8' });
2960
- assert.equal(forged2.status, 2, 'plain Direct without justification should be blocked');
2961
-
2962
- const trusted = POLICY.classifyLane({ reversible: true, lowRisk: true, isolated: true });
2963
- assert.equal(trusted.lane, 'Direct');
2964
- const viaPolicy = POLICY.lanePolicy(trusted);
2965
- assert.equal(viaPolicy.lane, 'Direct', 'trusted Direct should be respected');
2966
- assert.throws(() => { 'use strict'; trusted.lane = 'Direct'; }, /read only|Cannot assign|TypeError/);
2967
- const paymentTrusted = POLICY.classifyLane({ riskSignals: { payment: true } });
2968
- const forgedCopy = { ...paymentTrusted, lane: 'Direct' };
2969
- assert.equal(POLICY.lanePolicy(forgedCopy).lane, 'Standard', 'compatibility lane is derived from authoritative axes');
2970
1723
  });
2971
1724
 
2972
1725
  test('P0 regression: symlink spec/task containment and malformed spec handling', () => {
@@ -2974,33 +1727,29 @@ test('P0 regression: symlink spec/task containment and malformed spec handling',
2974
1727
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-regression-symlink-'));
2975
1728
  try {
2976
1729
  const specsDir = path.join(tmp, 'specs');
2977
- fs.mkdirSync(specsDir, { recursive: true });
2978
- fs.mkdirSync(path.join(specsDir, 'valid'), { recursive: true });
2979
- fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'valid' }));
1730
+ writeWorkflowPacketAt(path.join(specsDir, 'valid'));
2980
1731
  const outside = path.join(tmp, 'outside');
2981
- fs.mkdirSync(outside, { recursive: true });
2982
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
2983
- const link = path.join(specsDir, 'linked');
2984
- fs.symlinkSync(outside, link);
2985
- 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' });
2986
1735
  assert.equal(e1.error, 'explicit_malformed');
2987
- 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') });
2988
1737
  assert.equal(e2.error, 'explicit_malformed');
2989
- const nonExplicit = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1738
+ const nonExplicit = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
2990
1739
  assert.equal(nonExplicit.error, 'invalid_specs');
2991
1740
  assert.ok(nonExplicit.candidates.includes('linked'));
2992
- const featDir = path.join(specsDir, 'valid');
2993
- 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.
2994
1743
  const taskOutside = path.join(tmp, 'outside-task.md');
2995
- fs.writeFileSync(taskOutside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
2996
- const taskLink = path.join(featDir, 'tasks', 'task.md');
2997
- fs.symlinkSync(taskOutside, taskLink);
2998
- const codexReceipt = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
2999
- const fails = codexReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
3000
- assert.ok(fails.includes('a'), 'task symlink outside should be rejected as check a');
3001
- fs.mkdirSync(path.join(specsDir, 'bad'), { recursive: true });
3002
- fs.writeFileSync(path.join(specsDir, 'bad', 'spec.json'), '{ malformed');
3003
- 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: {} });
3004
1753
  assert.equal(mal.error, 'invalid_specs');
3005
1754
  assert.ok(mal.candidates.includes('bad'));
3006
1755
  } finally {
@@ -3071,18 +1820,18 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
3071
1820
  assert.match(codexSrc, /getSharedValidate/);
3072
1821
  assert.match(codexSrc, /workflow-policy\.cjs/);
3073
1822
  assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
3074
- // safeTaskFile fail-closed: simulate realpathSync throwing
1823
+ // The task-file read fails closed when realpath throws
3075
1824
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
3076
1825
  try {
3077
1826
  const featDir = path2.join(tmp, 'feat');
3078
- fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
3079
- 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'));
3080
1830
  const orig = fs2.realpathSync;
3081
1831
  fs2.realpathSync = () => { throw new Error('simulated realpath failure'); };
3082
1832
  try {
3083
- const codexReceipt = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'));
3084
- const fails = codexReceipt.checkReceipt(featDir, 'tasks/task.md', { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
3085
- 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}`);
3086
1835
  } finally {
3087
1836
  fs2.realpathSync = orig;
3088
1837
  }
@@ -3099,15 +1848,12 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3099
1848
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-resolver-fail-'));
3100
1849
  try {
3101
1850
  const specsDir = path2.join(tmp, 'specs');
3102
- fs2.mkdirSync(path2.join(specsDir, 'demo'), { recursive: true });
3103
- fs2.writeFileSync(path2.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo' }));
3104
- // Monkey patch realpathSync to always throw for explicitFeature
1851
+ writeWorkflowPacketAt(path2.join(specsDir, 'demo'));
3105
1852
  const origRealpath = fs2.realpathSync;
3106
1853
  fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
3107
1854
  try {
3108
- const res = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
1855
+ const res = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {}, explicitFeature: 'demo' });
3109
1856
  assert.equal(res.error, 'explicit_malformed', 'explicitFeature realpath failure should be explicit_malformed');
3110
- assert.match(res.reason, /canonicalization error/);
3111
1857
  } finally {
3112
1858
  fs2.realpathSync = origRealpath;
3113
1859
  }
@@ -3118,7 +1864,7 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3118
1864
  return origRealpath2(p);
3119
1865
  };
3120
1866
  try {
3121
- const res2 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1867
+ const res2 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3122
1868
  assert.equal(res2.error, 'invalid_specs');
3123
1869
  assert.ok(res2.candidates.includes('<specs>'));
3124
1870
  } finally {
@@ -3131,21 +1877,17 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3131
1877
  return origLstat(p);
3132
1878
  };
3133
1879
  try {
3134
- const res3 = RESOLVER.resolveActiveSpec({ projectRoot: tmp, runtime: {} });
1880
+ const res3 = RESOLVER.resolveWorkflowCandidate({ projectRoot: tmp, runtime: {} });
3135
1881
  assert.equal(res3.error, 'invalid_specs');
3136
1882
  assert.ok(res3.candidates.includes('demo'));
3137
1883
  } finally {
3138
1884
  fs2.lstatSync = origLstat;
3139
1885
  }
3140
- // Dangling spec.json symlink should be invalid_specs
3141
- const outside = path2.join(tmp, 'outside-spec2');
3142
- fs2.mkdirSync(outside, { recursive: true });
3143
- fs2.writeFileSync(path2.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1886
+ // A dangling plan.md symlink is invalid
3144
1887
  const featDir = path2.join(specsDir, 'dangle');
3145
1888
  fs2.mkdirSync(featDir, { recursive: true });
3146
- const specLink = path2.join(featDir, 'spec.json');
3147
- fs2.symlinkSync(path2.join(tmp, 'nonexistent-target'), specLink);
3148
- 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: {} });
3149
1891
  assert.equal(res4.error, 'invalid_specs');
3150
1892
  assert.ok(res4.candidates.includes('dangle'));
3151
1893
  } finally {
@@ -3155,13 +1897,11 @@ test('P0 regression: resolver fail-closed on canonicalization and lstat errors',
3155
1897
  const codexUtils = require(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
3156
1898
  const tmp2 = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-fail-'));
3157
1899
  try {
3158
- const specsDir2 = path2.join(tmp2, 'specs');
3159
- fs2.mkdirSync(path2.join(specsDir2, 'demo2'), { recursive: true });
3160
- fs2.writeFileSync(path2.join(specsDir2, 'demo2', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'demo2' }));
1900
+ writeWorkflowPacketAt(path2.join(tmp2, 'specs', 'demo2'));
3161
1901
  const orig = fs2.realpathSync;
3162
1902
  fs2.realpathSync = () => { throw Object.assign(new Error('simulated EACCES'), { code: 'EACCES' }); };
3163
1903
  try {
3164
- const r = codexUtils.resolveActiveSpec(tmp2, {}, 'demo2', null);
1904
+ const r = codexUtils.resolveWorkflowCandidate(tmp2, {}, 'demo2', null);
3165
1905
  assert.equal(r.error, 'explicit_malformed');
3166
1906
  } finally {
3167
1907
  fs2.realpathSync = orig;