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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/bin/__tests__/codex-hooks.test.js +444 -14
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +615 -185
  4. package/bin/__tests__/installer-safety.test.js +39 -0
  5. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  6. package/bin/__tests__/package-inventory.test.js +687 -13
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +1914 -221
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +143 -99
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  22. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  23. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  24. package/src/claude/hooks/completion-authority.cjs +13 -7
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/spec-gate.cjs +47 -117
  31. package/src/claude/hooks/spec-state.cjs +8 -2
  32. package/src/claude/migration-manifest.json +9 -1
  33. package/src/claude/rules/state-sync.md +52 -21
  34. package/src/claude/rules/workflow.md +17 -9
  35. package/src/claude/scripts/spec-final-state.cjs +244 -0
  36. package/src/claude/scripts/spec-ground.cjs +535 -173
  37. package/src/claude/scripts/spec-readiness.cjs +118 -0
  38. package/src/claude/scripts/spec-receipt.cjs +162 -0
  39. package/src/claude/scripts/spec-resolver.cjs +67 -16
  40. package/src/claude/scripts/spec-scaffold.cjs +1354 -139
  41. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  42. package/src/claude/scripts/validate-spec-output.cjs +2782 -142
  43. package/src/claude/scripts/workflow-policy.cjs +501 -183
  44. package/src/claude/settings/settings.json +4 -0
  45. package/src/claude/skills/code-review/SKILL.md +12 -3
  46. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  47. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  48. package/src/claude/skills/develop/SKILL.md +47 -60
  49. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  50. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  51. package/src/claude/skills/specs/SKILL.md +183 -177
  52. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  53. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  54. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  55. package/src/claude/skills/specs/references/review.md +200 -260
  56. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  57. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  58. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  59. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  60. package/src/claude/skills/specs/rules/design-review.md +42 -97
  61. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  62. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  63. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  64. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  65. package/src/claude/skills/specs/templates/design.md +49 -263
  66. package/src/claude/skills/specs/templates/requirements.md +26 -36
  67. package/src/claude/skills/specs/templates/research.md +9 -98
  68. package/src/claude/skills/specs/templates/spec-state.json +23 -85
  69. package/src/claude/skills/specs/templates/task.md +30 -102
  70. package/src/claude/skills/sync/SKILL.md +33 -10
  71. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  72. package/src/claude/skills/test/SKILL.md +17 -7
  73. package/src/codex/AGENTS.md +6 -0
  74. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  75. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  76. package/src/codex/hooks/completion-authority.cjs +13 -7
  77. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  78. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  79. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  80. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  81. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  82. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  83. package/src/codex/hooks/privacy-approval.cjs +1 -1
  84. package/src/codex/hooks/privacy-block.cjs +67 -11
  85. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  86. package/src/codex/hooks/spec-gate.cjs +49 -34
  87. package/src/codex/hooks/spec-state.cjs +10 -3
  88. package/src/codex/hooks/state.cjs +2 -0
  89. package/src/codex/hooks.json +6 -1
  90. package/src/codex/rules/state-sync.md +47 -27
  91. package/src/common/AGENTS.md +1 -1
@@ -14,18 +14,45 @@ const SCAFFOLD = path.join(ROOT, 'src/claude/scripts/spec-scaffold.cjs');
14
14
  const RESOLVER = path.join(ROOT, 'src/claude/scripts/spec-resolver.cjs');
15
15
  const POLICY = require(path.join(ROOT, 'src/claude/scripts/workflow-policy.cjs'));
16
16
  const SPEC_RESOLVER = require(RESOLVER);
17
+ const { normalizeCodexBody } = require(path.join(ROOT, 'bin/lib/codex-install.js'));
18
+ const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
17
19
 
18
20
  function write(filePath, content) {
19
21
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
22
  fs.writeFileSync(filePath, content);
21
23
  }
22
24
 
25
+ function writeRaw(filePath, content) {
26
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
+ fs.writeFileSync(filePath, content);
28
+ }
29
+
30
+ function copyCodexRuntime(adapterRoot, entrypoints) {
31
+ const files = copyClaudeTestRuntime(ROOT, adapterRoot, entrypoints);
32
+ for (const relative of files) {
33
+ if (!/\.(?:cjs|js)$/.test(relative)) continue;
34
+ const target = path.join(adapterRoot, relative);
35
+ writeRaw(
36
+ target,
37
+ normalizeCodexBody(fs.readFileSync(target, 'utf8'), `src/claude/${relative}`),
38
+ );
39
+ }
40
+ return files;
41
+ }
42
+
23
43
  function run(script, args, cwd, options = {}) {
44
+ const fixtureRoot = script === VALIDATOR && typeof args[0] === 'string'
45
+ ? path.dirname(path.dirname(path.resolve(args[0])))
46
+ : null;
24
47
  return spawnSync(process.execPath, [script, ...args], {
25
48
  cwd,
26
49
  encoding: 'utf8',
27
50
  ...options,
28
- env: options.env ? { ...process.env, ...options.env } : process.env,
51
+ env: {
52
+ ...process.env,
53
+ ...(fixtureRoot ? { HOME: path.join(fixtureRoot, '.home'), USERPROFILE: path.join(fixtureRoot, '.home') } : {}),
54
+ ...(options.env || {}),
55
+ },
29
56
  });
30
57
  }
31
58
 
@@ -49,33 +76,208 @@ function snapshotTree(root) {
49
76
  return entries.sort().join('\n');
50
77
  }
51
78
 
52
- function task({ file = 'task-R1-01-one.md', mapping = '1.1', related = '`src/one.js` | Modify', extra = '' } = {}) {
53
- return `# Task R1-01: One\n\n## Context\n- Existing source.\n\n## Constraints\n- Keep scope.\n\n## Steps\n- [ ] Update source\n - _Requirements: ${mapping}_\n\n## Requirements\n- 1.1 — One behavior\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| ${related} | Source | test |\n\n## Completion Criteria\n- [ ] Behavior works.\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/one.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n${extra}`;
79
+ function task({
80
+ id = 'R1-01',
81
+ mapping = '1.1',
82
+ related = '`src/one.js` | Modify',
83
+ relatedRows = null,
84
+ dependencies = 'none',
85
+ status = 'pending',
86
+ parallel = false,
87
+ extra = '',
88
+ } = {}) {
89
+ const relatedPath = related.match(/`([^`]+)`/)?.[1] || 'src/one.js';
90
+ const action = related.match(/\|\s*([A-Za-z]+)\b/i)?.[1] || 'Modify';
91
+ const acceptanceId = mapping.match(/\d+\.\d+/)?.[0];
92
+ const lifecycleRows = relatedRows === null ? (action.toLowerCase() === 'modify'
93
+ ? `| \`${relatedPath}\` | Read | Inspect existing implementation |\n| ${related} | Update implementation |`
94
+ : `| ${related} | Apply declared lifecycle action |`) : relatedRows;
95
+ return `# Task ${id}: One\n\n**Status:** ${status}\n\n## Outcome\n\nCalling the real fixture entrypoint returns status value 1.\n\n## Scope and Typed Anchors\n\n- **In scope:** Implement the mapped observable behavior.\n- **Out of scope:** Unrelated runtime behavior.\n- **Contracts/Invariants:** none\n- **Canonical design anchors consumed:** A-D-01\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n| A-${id}-01 | command | \`node --test test/one.test.js\` | planned verification command |\n\n## Changes\n\n- [ ] ${parallel ? '(P) ' : ''}Update the source boundary and preserve its negative path.${mapping ? ` _Requirements: ${mapping}_` : ''}\n\n## Acceptance\n\n${acceptanceId ? `- **R${acceptanceId}:**` : '-'} Calling \`${relatedPath}\` returns an observable result with status value 1.\n\n## Dependencies\n\n- ${dependencies}\n\n## Verification Plan\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and one assertion for status value 1\n- **Negative path:** invalid input returns a deterministic error result\n- **Reachability:** \`${relatedPath}\` is invoked by the focused test\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|---|\n${lifecycleRows}\n${extra}`;
96
+ }
97
+
98
+ function legacyWorkflowPolicy(lane = 'Standard') {
99
+ const proofObligations = lane === 'Critical'
100
+ ? ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding']
101
+ : ['needsInspection', 'needsExecutionProof'];
102
+ return {
103
+ version: '1',
104
+ lane,
105
+ automatic_lane: lane,
106
+ risks: lane === 'Critical' ? ['auth'] : [],
107
+ artifact_profile: lane === 'Critical' ? 'strict' : 'bounded',
108
+ proof_obligations: proofObligations,
109
+ actor_needs: proofObligations.map((obligation) => ({
110
+ capability: {
111
+ needsInspection: 'inspection',
112
+ needsExecutionProof: 'execution-proof',
113
+ needsIndependentAudit: 'audit',
114
+ needsResearchGrounding: 'research-grounding',
115
+ }[obligation],
116
+ independence: obligation === 'needsIndependentAudit' ? 'independent' : 'same-session',
117
+ })),
118
+ override_receipt: null,
119
+ };
120
+ }
121
+
122
+ function legacyTask({ relatedRows = '| `src/one.js` | Read | Inspect existing behavior |', extra = '' } = {}) {
123
+ return `# Task R1-01: One\n\n**Status:** pending\n**Dependencies:** none\n\n## Context\n\nThe existing fixture service returns one deterministic result.\n\n## Constraints\n\nPreserve the stable result contract and its invalid-input behavior.\n\n## Steps\n\n- [ ] Inspect the declared boundary and preserve status value 1. _Requirements: 1.1_\n\n## Requirements\n\n- 1.1 — Return status value 1 from the fixture service.\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|\n${relatedRows}\n\n## Completion Criteria\n\n- Calling \`src/one.js\` returns result status value 1.\n\n## Evidence\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and status value 1\n- **Inspect:** \`src/one.js\` runtime result\n- Runtime reachability verification\n - **Entrypoint/caller:** \`src/one.js\`\n\n## Risk Assessment\n\n| Risk | Severity | Mitigation |\n|---|---|---|\n| regression | low | focused test |\n${extra}`;
124
+ }
125
+
126
+ function lifecycleTimestamps({ tasks = true, research = false, review = false } = {}) {
127
+ return {
128
+ init: '2026-08-11T00:00:00+07:00',
129
+ requirements_done: '2026-08-11T00:01:00+07:00',
130
+ research_done: research ? '2026-08-11T00:02:00+07:00' : null,
131
+ design_done: '2026-08-11T00:03:00+07:00',
132
+ tasks_done: tasks ? '2026-08-11T00:04:00+07:00' : null,
133
+ code_done: null,
134
+ test_done: null,
135
+ review_done: review ? '2026-08-11T00:05:00+07:00' : null,
136
+ validation_done: '2026-08-11T00:06:00+07:00',
137
+ };
138
+ }
139
+
140
+ function semanticReviewNotRun() {
141
+ return {
142
+ status: 'not-run', reviewed_artifact_digest: null, reviewed_criteria: [], counterexamples: [],
143
+ };
144
+ }
145
+
146
+ function semanticDigest(specDir) {
147
+ const result = run(VALIDATOR, [specDir, '--semantic-digest'], ROOT);
148
+ assert.equal(result.status, 0, output(result));
149
+ return result.stdout.trim();
150
+ }
151
+
152
+ function completeSemanticReview(specDir, independence = 'same-session') {
153
+ const statePath = path.join(specDir, 'spec.json');
154
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
155
+ state.validation = isPlainObjectForTest(state.validation) ? state.validation : {};
156
+ state.validation.semantic_review = {
157
+ status: 'completed',
158
+ reviewed_artifact_digest: semanticDigest(specDir),
159
+ reviewed_criteria: ['R1.1'],
160
+ counterexamples: [{
161
+ criterion: 'R1.1',
162
+ scenario: 'The fixture receives invalid input at its runtime boundary.',
163
+ expected: 'The fixture returns the deterministic negative-path result.',
164
+ design_reference: 'D1',
165
+ }],
166
+ };
167
+ write(statePath, JSON.stringify(state, null, 2));
168
+ if (independence === 'independent') {
169
+ const root = path.dirname(path.dirname(specDir));
170
+ const featureName = state.feature_name;
171
+ const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({
172
+ feature_name: featureName,
173
+ spec_file: path.relative(root, statePath),
174
+ semantic_digest: state.validation.semantic_review.reviewed_artifact_digest,
175
+ verdict: 'PASS',
176
+ })}`;
177
+ const result = spawnSync(process.execPath, [path.join(ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
178
+ cwd: root,
179
+ env: { ...process.env, HOME: path.join(root, '.home'), USERPROFILE: path.join(root, '.home'), PROJECT_ROOT: root },
180
+ input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'review-agent', agent_type: 'code_auditor', last_assistant_message: claim, cwd: root }),
181
+ encoding: 'utf8',
182
+ });
183
+ assert.equal(result.status, 0, output(result));
184
+ assert.equal(result.stderr, '');
185
+ }
186
+ return state;
187
+ }
188
+
189
+ function isPlainObjectForTest(value) {
190
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
191
+ }
192
+
193
+ function obligationClosureBlock(obligation) {
194
+ const consumers = obligation.consumer_tasks.length > 0
195
+ ? obligation.consumer_tasks.join(', ')
196
+ : 'none';
197
+ return `<!-- implementation-obligation:${obligation.id} -->\n- Requirements: ${obligation.requirements.join(', ')}\n- Canonical state: ${obligation.canonical_state}\n- Authority: ${obligation.authority}\n- Recovery: ${obligation.recovery}\n- Owner task: ${obligation.owner_task}\n- Consumers: ${consumers}\n- Verification: ${obligation.verification}\n- Evidence: ${obligation.evidence}\n`;
198
+ }
199
+
200
+ function executionClosure(obligation) {
201
+ return `\n## Execution Closure\n\n${obligationClosureBlock(obligation)}`;
202
+ }
203
+
204
+ function executionClosureTable(obligation) {
205
+ const consumers = obligation.consumer_tasks.length > 0
206
+ ? obligation.consumer_tasks.join(', ')
207
+ : 'none';
208
+ return `\n## Execution Closure\n\n<!-- implementation-obligation:${obligation.id} -->\n` +
209
+ '| Obligation ID / Requirements | Canonical State | Authority | Crash / Retry Recovery | Owner Task | Consumer Tasks / Dependency Order | Verification / Evidence |\n' +
210
+ '|---|---|---|---|---|---|---|\n' +
211
+ `| ${obligation.id} / ${obligation.requirements.join(', ')} | ${obligation.canonical_state} | ${obligation.authority} | ${obligation.recovery} | \`${obligation.owner_task}\` | ${consumers} | \`${obligation.verification}\` → ${obligation.evidence} |\n`;
212
+ }
213
+
214
+ function criticalNoObligationDesign() {
215
+ return designFixture() +
216
+ '## Execution Closure\n\n' +
217
+ 'N/A — this fixture has no cross-task state, retry, migration, or authority obligation.\n';
218
+ }
219
+
220
+ function designFixture() {
221
+ return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
222
+ '## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
223
+ '| A-D-01 | file | `src/one.js` | existing owner and runtime entrypoint |\n\n' +
224
+ '## Decisions and Invariants\n\n### D1 — Stable result\n\n- **Decision:** Return status value 1.\n- **Negative path:** Invalid input returns a deterministic error.\n- **Anchors:** A-D-01\n\n' +
225
+ '## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
226
+ '| R1.1 | `node --test test/one.test.js` | exit code 0 and status value 1 | invalid input through `src/one.js` |\n\n';
227
+ }
228
+
229
+ function designFixture21() {
230
+ return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
231
+ '## Typed Anchors\n\n| ID | Type | Target | Role | Access | Action |\n|---|---|---|---|---|---|\n' +
232
+ '| A-D-01 | file | `src/one.js` | runtime entrypoint | read | read |\n' +
233
+ '| A-D-02 | command | `node --test test/one.test.js` | proof command | read | read |\n\n' +
234
+ '## Decisions and Invariants\n\n### D1 — Stable result\n\nReturn status value 1 and reject invalid input.\n\n' +
235
+ '### I1 — Invalid-state invariant\n\nInvalid input never reports the success status.\n\n' +
236
+ '### C1 — Result contract\n\nThe result contains one numeric status.\n\n' +
237
+ '## Verification Definitions\n\n' +
238
+ '- **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/one.test.js`; Expected exit code 0 and status value 1; Negative/failure invalid input returns a deterministic error; Reachability/grounding entrypoint `src/one.js` via A-D-01, A-D-02.\n';
54
239
  }
55
240
 
56
241
  function createSpec(root, options = {}) {
57
- const specDir = path.join(root, options.name || 'spec');
242
+ const featureName = options.name || 'spec';
243
+ const specDir = path.join(root, 'specs', featureName);
58
244
  const taskPath = 'tasks/task-R1-01-one.md';
59
- write(path.join(specDir, 'requirements.md'), options.requirements || '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** Do one thing.\n');
60
- write(path.join(specDir, 'design.md'), options.design || '# Design\n');
61
- write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n- Internal evidence.\n');
62
- write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n');
245
+ write(
246
+ path.join(specDir, 'requirements.md'),
247
+ options.requirements || '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return one deterministic result.\n',
248
+ );
249
+ write(
250
+ path.join(specDir, 'design.md'),
251
+ options.design || designFixture(),
252
+ );
63
253
  write(path.join(specDir, taskPath), options.task || task());
254
+ write(path.join(root, 'test/one.test.js'), "'use strict';\n// Separate proof artifact for fixture validation.\n");
64
255
  write(path.join(specDir, 'spec.json'), JSON.stringify({
65
256
  schema_version: '2.0',
66
- feature_name: 'fixture',
257
+ feature_name: featureName,
258
+ created_at: '2026-08-11T00:00:00+07:00',
259
+ updated_at: '2026-08-11T00:05:00+07:00',
260
+ language: 'en',
67
261
  status: 'in_progress',
68
262
  current_phase: 'tasks',
69
263
  scope_lock: { source: 'fixture', in_scope: ['1'], out_of_scope: [], expansion_policy: 'requires-user-approval' },
264
+ coordination: {
265
+ tasks_required: true,
266
+ phases_required: false,
267
+ reason: 'task_topology',
268
+ task_triggers: ['separate_proof'],
269
+ },
70
270
  task_files: [taskPath],
71
271
  task_registry: {
72
272
  [taskPath]: {
73
273
  id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null,
74
274
  started_at: null, completed_at: null, last_updated_at: null,
275
+ artifacts: ['test/one.test.js'],
75
276
  },
76
277
  },
77
278
  approvals: { requirements: { generated: true, agent_validated: true, user_approved: true }, design: { generated: true, agent_validated: true, user_approved: true }, tasks: { generated: true, agent_validated: true, user_approved: true } },
78
- validation: { status: 'not-run' },
279
+ validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
280
+ timestamps: { ...lifecycleTimestamps(), validation_done: null },
79
281
  ready_for_implementation: false,
80
282
  workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
81
283
  ...(options.spec || {}),
@@ -86,29 +288,67 @@ function createSpec(root, options = {}) {
86
288
  function createBoundedSpec(root, name = 'bounded') {
87
289
  const specDir = path.join(root, name);
88
290
  const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
89
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\nBounded requirements are filled by the feature owner.\n');
90
- write(path.join(specDir, 'design.md'), '# Design\n\nBounded design is filled by the feature owner.\n');
91
- write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n\nBlocker: awaiting execution proof\n');
291
+ write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return status value 1.\n');
292
+ write(path.join(specDir, 'design.md'), designFixture());
92
293
  write(path.join(specDir, 'spec.json'), JSON.stringify({
93
294
  schema_version: '2.0',
94
295
  feature_name: name,
296
+ created_at: '2026-08-11T00:00:00+07:00',
297
+ updated_at: '2026-08-11T00:03:00+07:00',
95
298
  status: 'in_progress',
96
299
  current_phase: 'design',
97
300
  scope_lock: { source: 'fixture', in_scope: [], out_of_scope: [], expansion_policy: 'requires-user-approval' },
301
+ coordination: { tasks_required: false, phases_required: false, reason: 'taskless_requested' },
98
302
  approvals: {
99
303
  requirements: { generated: true, agent_validated: true, user_approved: false },
100
304
  design: { generated: true, agent_validated: true, user_approved: false },
101
305
  },
102
- validation: { status: 'not-run' },
306
+ validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
307
+ timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
103
308
  ready_for_implementation: false,
104
309
  workflow_policy: policy,
105
310
  }, null, 2));
106
311
  return specDir;
107
312
  }
108
313
 
109
- function createPolicyOnlySpec(root, name, policy) {
314
+ function legacyTasklessSpec20State(name, policy) {
315
+ return {
316
+ schema_version: '2.0',
317
+ feature_name: name,
318
+ created_at: '2026-08-11T00:00:00+07:00',
319
+ updated_at: '2026-08-11T00:03:00+07:00',
320
+ language: 'en',
321
+ status: 'in_progress',
322
+ current_phase: 'design',
323
+ scope_lock: {
324
+ source: 'legacy fixture',
325
+ in_scope: [],
326
+ out_of_scope: [],
327
+ expansion_policy: 'requires-user-approval',
328
+ },
329
+ coordination: {
330
+ tasks_required: false,
331
+ phases_required: false,
332
+ reason: 'taskless_requested',
333
+ },
334
+ validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
335
+ timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
336
+ ready_for_implementation: false,
337
+ workflow_policy: policy,
338
+ };
339
+ }
340
+
341
+ function createLegacyTasklessSpec20(root, name, policy) {
110
342
  const specDir = path.join(root, 'specs', name);
111
- write(path.join(specDir, 'spec.json'), JSON.stringify({ workflow_policy: policy }, null, 2));
343
+ write(
344
+ path.join(specDir, 'requirements.md'),
345
+ '# Requirements\n\n### Requirement 1: Legacy baseline\n\n- **R1.1** The fixture shall preserve one deterministic result.\n',
346
+ );
347
+ write(path.join(specDir, 'design.md'), designFixture());
348
+ write(
349
+ path.join(specDir, 'spec.json'),
350
+ JSON.stringify(legacyTasklessSpec20State(name, policy), null, 2),
351
+ );
112
352
  return specDir;
113
353
  }
114
354
 
@@ -117,19 +357,219 @@ function output(result) {
117
357
  }
118
358
 
119
359
  function completeScaffoldedSpec(specDir) {
120
- const taskPath = path.join(specDir, 'tasks', 'task-R1-01-artifact.md');
121
- let taskText = fs.readFileSync(taskPath, 'utf8')
122
- .replace(/\{\{[^}\n]+\}\}/g, 'filled')
123
- .replace(/_Requirements:\s*[^_]+_/g, '_Requirements: 1.1_')
124
- .replace(/\| `filled` \| Create \/ Modify \/ Delete \| filled \|/g, '| `src/one.js` | Read | inspect source |')
125
- .replace('Entrypoint/caller: filled', 'Entrypoint/caller: src/one.js');
126
- fs.writeFileSync(taskPath, taskText);
360
+ const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
361
+ for (const [index, taskFile] of (state.task_files || []).entries()) {
362
+ const entry = state.task_registry[taskFile];
363
+ write(path.join(specDir, taskFile), task({
364
+ id: entry.id,
365
+ related: `\`src/${index === 0 ? 'one' : `task-${index + 1}`}.js\` | Read`,
366
+ dependencies: entry.dependencies.length > 0 ? entry.dependencies.join(',') : 'none',
367
+ }));
368
+ }
127
369
  write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: Artifact\n\n- **R1.1** The task is verifiable.\n');
128
- write(path.join(specDir, 'design.md'), '# Design\n');
129
- write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n- Producer fixture.\n');
130
- return JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
370
+ write(
371
+ path.join(specDir, 'design.md'),
372
+ state.workflow_policy?.assurance_level === 'Strict' ? criticalNoObligationDesign() : designFixture(),
373
+ );
374
+ if (state.research === 'research.md') {
375
+ write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n- Producer fixture.\n');
376
+ }
377
+ return state;
131
378
  }
132
379
 
380
+ test('canonical legacy v2.0 taskless migration fixture is valid without execution evidence', () => {
381
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-legacy-taskless-baseline-'));
382
+ try {
383
+ const specDir = createLegacyTasklessSpec20(
384
+ root,
385
+ 'legacy-taskless-baseline',
386
+ POLICY.workflowPolicySnapshot({ riskSignals: {} }),
387
+ );
388
+ const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
389
+ assert.equal(state.schema_version, '2.0');
390
+ assert.equal(state.ready_for_implementation, false);
391
+ assert.equal(Object.hasOwn(state, 'task_files'), false);
392
+ assert.equal(Object.hasOwn(state, 'task_registry'), false);
393
+ assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
394
+ assert.equal(fs.existsSync(path.join(specDir, 'receipts')), false);
395
+ state.override_receipt = { legacy: true };
396
+ write(path.join(specDir, 'spec.json'), JSON.stringify(state, null, 2));
397
+ const validation = run(VALIDATOR, [specDir], ROOT);
398
+ assert.equal(validation.status, 0, output(validation));
399
+ } finally {
400
+ fs.rmSync(root, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ function completeScaffoldedSpec21(specDir) {
405
+ const statePath = path.join(specDir, 'spec.json');
406
+ let state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
407
+ const fixtureRoot = path.dirname(path.dirname(specDir));
408
+ const criteria = (state.task_files || []).map((_, index) => `R1.${index + 1}`);
409
+ write(path.join(fixtureRoot, 'test/one.test.js'), "require('node:test')('one', () => {});\n");
410
+ fs.mkdirSync(path.join(fixtureRoot, 'artifacts'), { recursive: true });
411
+ for (const [index, taskFile] of (state.task_files || []).entries()) {
412
+ const entry = state.task_registry[taskFile];
413
+ const ownedPath = `src/task-${index + 1}.js`;
414
+ const criterion = criteria[index];
415
+ const taskRole = index === 0 ? 'subject' : 'verifier';
416
+ write(path.join(fixtureRoot, ownedPath), 'module.exports = { status: 1 };\n');
417
+ write(path.join(specDir, taskFile), `# Task ${entry.id}: ${entry.title}
418
+ **Status:** pending
419
+
420
+ ## Outcome
421
+ Return observable status value 1 through the real fixture entrypoint.
422
+
423
+ ## Scope
424
+ - **In scope:** Exact behavior for ${ownedPath}.
425
+ - **Out of scope:** Unrelated runtime behavior.
426
+
427
+ ## Anchors and Ownership
428
+ | ID | Type | Target | Role | Access | Action |
429
+ |---|---|---|---|---|---|
430
+ | A-${entry.id}-01 | file | \`${ownedPath}\` | owner | write | modify |
431
+ ${taskRole === 'verifier' ? `| A-${entry.id}-02 | artifact | \`artifacts/${entry.id}.json\` | verifier | write | create |` : ''}
432
+
433
+ ## Changes
434
+ - [ ] ${taskRole === 'verifier' ? 'Implement the separate proof behavior.' : 'Implement the exact owned behavior.'} _Requirements: ${criterion.slice(1)}_
435
+
436
+ ## Acceptance
437
+ - **${criterion}:** ${taskRole === 'verifier' ? 'The focused proof reports status value 1 and rejects an invalid result.' : 'The fixture returns status value 1 and rejects invalid input.'}
438
+
439
+ ## Dependencies
440
+ - ${entry.dependencies.length > 0 ? entry.dependencies.join(', ') : 'none'}
441
+
442
+ ## Verification Plan
443
+ - **Verification ref:** V1
444
+ - **Task role:** ${taskRole}
445
+ - **Command:** \`node --test test/one.test.js\`
446
+ - **Expected:** Exit code 0 and exact status value 1.
447
+ - **Negative path:** Invalid input returns rejected state.
448
+ - **Reachability:** \`${ownedPath}\`
449
+ `);
450
+ }
451
+ write(path.join(specDir, 'requirements.md'), `# Requirements
452
+
453
+ ### Requirement 1: Artifact
454
+
455
+ ${criteria.map((criterion, index) => `- **${criterion}**: Task ${index + 1} shall expose its distinct observable status or proof outcome and reject invalid input.`).join('\n')}
456
+ `);
457
+ write(path.join(specDir, 'design.md'), `# Design
458
+
459
+ ## Architecture
460
+ The fixture entrypoint delegates to the owned task boundary.
461
+
462
+ ## Typed Anchors
463
+ | ID | Type | Target | Role | Access | Action |
464
+ |---|---|---|---|---|---|
465
+ | A-D-01 | command | \`node --test test/one.test.js\` | verification command | read | read |
466
+
467
+ ## Canonical Contracts & Invariants
468
+ ### D1 — Delegation decision
469
+ The entrypoint delegates exactly once.
470
+ ### I1 — Invalid-state invariant
471
+ Invalid input never returns status value 1.
472
+ ### C1 — Result contract
473
+ The result contains a numeric status.
474
+ ### T1 — Artifact transition
475
+ The transition records success, failure, and recovery states.
476
+
477
+ ## Verification Definitions
478
+ - **V1**: Subject criteria ${criteria[0]}; Subject owner ${state.task_registry[state.task_files[0]].id}; Proof criteria ${criteria.slice(1).join(', ')}; Proof owner ${state.task_registry[state.task_files[1]].id}; Evidence anchor A-${state.task_registry[state.task_files[1]].id}-02; Decision refs D1, I1, C1; Method command \`node --test test/one.test.js\`; Expected exit code 0 and exact status value 1; Negative/failure invalid input returns rejected state; Reachability/grounding entrypoint \`src/task-1.js\` via A-D-01, A-${state.task_registry[state.task_files[1]].id}-02.
479
+ `);
480
+ const promoted = run(
481
+ SCAFFOLD,
482
+ [path.basename(specDir), '--sync-semantic-model'],
483
+ path.dirname(path.dirname(specDir)),
484
+ );
485
+ assert.equal(promoted.status, 0, output(promoted));
486
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
487
+ state.authoring.requirements = 'validated';
488
+ state.authoring.design = 'validated';
489
+ state.authoring.tasks = state.task_files?.length ? 'validated' : 'absent';
490
+ state.validation.status = 'completed';
491
+ state.validation.semantic_review = {
492
+ status: 'completed', semantic_digest: null, reviewed_criteria: criteria,
493
+ counterexamples: criteria.map((criterion, index) => ({
494
+ criterion, case_kind: index % 2 === 0 ? 'failure' : 'adversarial',
495
+ scenario: `Task ${index + 1} receives a distinct malformed status payload at its declared boundary.`,
496
+ expected: `Task ${index + 1} rejects that payload without publishing its distinct success outcome.`,
497
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
498
+ })),
499
+ };
500
+ write(statePath, JSON.stringify(state, null, 2));
501
+ state.validation.semantic_review.semantic_digest = semanticDigest(specDir);
502
+ state.ready_for_implementation = true;
503
+ write(statePath, JSON.stringify(state, null, 2));
504
+ return state;
505
+ }
506
+
507
+ test('Codex installer transform preserves all owned spec assets with idempotent runtime parity', () => {
508
+ for (const relative of [
509
+ 'src/claude/skills/specs/SKILL.md',
510
+ 'src/claude/skills/specs/rules/tasks-generation.md',
511
+ 'src/claude/skills/specs/rules/design-principles.md',
512
+ 'src/claude/skills/specs/references/review.md',
513
+ 'src/claude/skills/specs/templates/design.md',
514
+ 'src/claude/skills/specs/templates/task.md',
515
+ 'src/claude/scripts/spec-scaffold.cjs',
516
+ 'src/claude/scripts/spec-receipt.cjs',
517
+ 'src/claude/scripts/validate-spec-output.cjs',
518
+ ]) {
519
+ const source = fs.readFileSync(path.join(ROOT, relative), 'utf8');
520
+ const transformed = normalizeCodexBody(source, relative);
521
+ assert.equal(normalizeCodexBody(transformed, relative), transformed, `${relative} must normalize idempotently`);
522
+ assert.doesNotMatch(transformed, /node \.claude\/scripts\//, `${relative} retained a Claude executable path`);
523
+ if (/node \.claude\/scripts\//.test(source)) {
524
+ assert.match(transformed, /node \.codex\/scripts\//, `${relative} lost its Codex executable path`);
525
+ }
526
+ if (/implementation-obligation:/.test(source)) {
527
+ assert.match(transformed, /implementation-obligation:/, `${relative} lost closure semantics`);
528
+ }
529
+ }
530
+
531
+ const skill = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/SKILL.md'), 'utf8');
532
+ const review = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/references/review.md'), 'utf8');
533
+ const designRules = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/rules/design-principles.md'), 'utf8');
534
+ const taskRules = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/rules/tasks-generation.md'), 'utf8');
535
+ assert.match(skill, /\| `Compact` \| `spec\.json`, `requirements\.md`, `design\.md` \| only on a research trigger \| only on a typed topology trigger \|/);
536
+ assert.match(skill, /\| `Full` \| `spec\.json`, `requirements\.md`, `design\.md` \| only on a research trigger \| only on a typed topology trigger \|/);
537
+ assert.doesNotMatch(review, /Standard[^\n]{0,100}(?:must not|forbidden|never)[^\n]{0,60}tasks?/i);
538
+ assert.match(designRules, /## Conditional detail[\s\S]*Migration:[\s\S]*only when existing state or public contracts change/);
539
+ assert.match(taskRules, /distinct ownership, a durable[\s\S]{0,180}parallel coordination/);
540
+ assert.match(taskRules, /receipts\/<task-basename>\.md|execution closeout/);
541
+
542
+ const parityRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-validator-parity-'));
543
+ try {
544
+ const installedScripts = path.join(parityRoot, '.codex/scripts');
545
+ const installedValidator = path.join(installedScripts, 'validate-spec-output.cjs');
546
+ copyCodexRuntime(path.dirname(installedScripts), ['scripts/validate-spec-output.cjs']);
547
+
548
+ const scaffolded = run(SCAFFOLD, ['runtime-parity'], parityRoot);
549
+ assert.equal(scaffolded.status, 0, output(scaffolded));
550
+ const specDir = path.join(parityRoot, 'specs/runtime-parity');
551
+ write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The runtime shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
552
+ write(path.join(specDir, 'design.md'), designFixture21());
553
+ const promoted = run(SCAFFOLD, ['runtime-parity', '--sync-semantic-model'], parityRoot);
554
+ assert.equal(promoted.status, 0, output(promoted));
555
+ const sourcePending = run(VALIDATOR, [specDir], ROOT);
556
+ const codexPending = run(installedValidator, [specDir], ROOT);
557
+ assert.equal(sourcePending.status, 0, output(sourcePending));
558
+ assert.equal(codexPending.status, sourcePending.status, output(codexPending));
559
+
560
+ const statePath = path.join(specDir, 'spec.json');
561
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
562
+ state.status = 'paused';
563
+ writeRaw(statePath, JSON.stringify(state, null, 2));
564
+ const sourceDonePending = run(VALIDATOR, [specDir], ROOT);
565
+ const codexDonePending = run(installedValidator, [specDir], ROOT);
566
+ assert.equal(sourceDonePending.status, 0, output(sourceDonePending));
567
+ assert.equal(codexDonePending.status, sourceDonePending.status, output(codexDonePending));
568
+ } finally {
569
+ fs.rmSync(parityRoot, { recursive: true, force: true });
570
+ }
571
+ });
572
+
133
573
  test('resolver binds feature identity and rejects cross-feature spec aliases', () => {
134
574
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-identity-'));
135
575
  try {
@@ -193,17 +633,47 @@ test('validator rejects non-Markdown task artifacts instead of ignoring them', (
193
633
  }
194
634
  });
195
635
 
196
- test('validator never treats a completed feature receipt as execution authority', () => {
636
+ test('validator keeps authoring receipt-free and validates taskless execution closeout proof', () => {
197
637
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-feature-receipt-'));
198
638
  try {
199
- const specDir = createBoundedSpec(root, 'completed-feature-receipt');
639
+ const specDir = createBoundedSpec(root, 'weak-completed-feature-receipt');
200
640
  write(path.join(specDir, 'feature-receipt.md'), [
201
641
  '# Feature Verification Receipt', '', 'Verification: PASS',
202
642
  'Command: npm test', 'Exit: 0', 'Base: a', 'Head: b',
203
643
  ].join('\n'));
204
- const result = run(VALIDATOR, [specDir], ROOT);
205
- assert.notEqual(result.status, 0);
206
- assert.match(output(result), /feature-receipt\.md: informational only/);
644
+ const premature = run(VALIDATOR, [specDir], ROOT);
645
+ assert.notEqual(premature.status, 0);
646
+ assert.match(output(premature), /completed execution receipt is valid only on terminal status done, never for readiness/);
647
+
648
+ const weakSpecPath = path.join(specDir, 'spec.json');
649
+ const weakSpec = JSON.parse(fs.readFileSync(weakSpecPath, 'utf8'));
650
+ weakSpec.status = 'done';
651
+ weakSpec.current_phase = 'done';
652
+ write(weakSpecPath, JSON.stringify(weakSpec, null, 2));
653
+ const weak = run(VALIDATOR, [specDir], ROOT);
654
+ assert.notEqual(weak.status, 0);
655
+ assert.match(output(weak), /completed proof and spec\.json must both use status done/);
656
+ assert.match(output(weak), /observable expected result/);
657
+ assert.match(output(weak), /distinct 40- or 64-hex Base and Head bindings/);
658
+
659
+ const completedDir = createBoundedSpec(root, 'completed-feature-receipt');
660
+ const completedSpecPath = path.join(completedDir, 'spec.json');
661
+ const completedSpec = JSON.parse(fs.readFileSync(completedSpecPath, 'utf8'));
662
+ completedSpec.status = 'done';
663
+ completedSpec.current_phase = 'done';
664
+ write(completedSpecPath, JSON.stringify(completedSpec, null, 2));
665
+ write(path.join(completedDir, 'feature-receipt.md'), [
666
+ '# Feature Verification Receipt', '', 'Verification: PASS', 'Status: done',
667
+ 'Command: `node --test test/one.test.js`',
668
+ 'Expected result: exit code 0 and one test passes',
669
+ 'Observed result: exit code 0 and one test passed',
670
+ 'Artifact/runtime proof: `src/one.js` persisted state value 1',
671
+ `Base: ${'a'.repeat(40)}`,
672
+ `Head: ${'b'.repeat(40)}`,
673
+ ].join('\n'));
674
+ const completed = run(VALIDATOR, [completedDir], ROOT);
675
+ assert.equal(completed.status, 0, output(completed));
676
+
207
677
  } finally {
208
678
  fs.rmSync(root, { recursive: true, force: true });
209
679
  }
@@ -222,7 +692,10 @@ test('validator rejects required and optional spec artifacts that symlink outsid
222
692
  assert.match(output(requiredResult), /requirements\.md: symlink is not allowed/);
223
693
 
224
694
  const optional = createSpec(root, { name: 'optional-artifact-link' });
225
- fs.unlinkSync(path.join(optional, 'research.md'));
695
+ const optionalSpecPath = path.join(optional, 'spec.json');
696
+ const optionalSpec = JSON.parse(fs.readFileSync(optionalSpecPath, 'utf8'));
697
+ optionalSpec.research = 'research.md';
698
+ write(optionalSpecPath, JSON.stringify(optionalSpec, null, 2));
226
699
  write(path.join(external, 'research.md'), '# External research\n');
227
700
  fs.symlinkSync(path.join(external, 'research.md'), path.join(optional, 'research.md'));
228
701
  const optionalResult = run(VALIDATOR, [optional], ROOT);
@@ -230,7 +703,6 @@ test('validator rejects required and optional spec artifacts that symlink outsid
230
703
  assert.match(output(optionalResult), /research\.md: symlink is not allowed/);
231
704
 
232
705
  const bounded = createBoundedSpec(root, 'receipt-artifact-link');
233
- fs.unlinkSync(path.join(bounded, 'feature-receipt.md'));
234
706
  write(path.join(external, 'feature-receipt.md'), '# External receipt\n');
235
707
  fs.symlinkSync(path.join(external, 'feature-receipt.md'), path.join(bounded, 'feature-receipt.md'));
236
708
  const boundedResult = run(VALIDATOR, [bounded], ROOT);
@@ -242,7 +714,7 @@ test('validator rejects required and optional spec artifacts that symlink outsid
242
714
  }
243
715
  });
244
716
 
245
- test('Standard bounded readiness rejects scope and document placeholders but allows a pending receipt', () => {
717
+ test('Standard bounded readiness requires canonical testable requirements and a concrete design boundary', () => {
246
718
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-standard-placeholders-'));
247
719
  try {
248
720
  const specDir = createBoundedSpec(root, 'standard-placeholders');
@@ -252,7 +724,8 @@ test('Standard bounded readiness rejects scope and document placeholders but all
252
724
  spec.approvals.requirements.user_approved = true;
253
725
  spec.approvals.design.user_approved = true;
254
726
  spec.validation = { status: 'completed' };
255
- spec.timestamps = { validation_done: '2026-08-11T00:00:00+07:00' };
727
+ spec.updated_at = '2026-08-11T00:06:00+07:00';
728
+ spec.timestamps = lifecycleTimestamps({ tasks: false });
256
729
  spec.scope_lock.source = '{{PROJECT_DESCRIPTION}}';
257
730
  fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
258
731
  write(path.join(specDir, 'requirements.md'), '# Requirements\n\n{{REQUIREMENTS}}\n');
@@ -261,14 +734,36 @@ test('Standard bounded readiness rejects scope and document placeholders but all
261
734
  const rejected = run(VALIDATOR, [specDir], ROOT);
262
735
  assert.notEqual(rejected.status, 0);
263
736
  assert.match(output(rejected), /scope_lock\.source.*concrete scope source/);
264
- assert.match(output(rejected), /requirements\.md: Standard bounded readiness rejects placeholders/);
265
- assert.match(output(rejected), /design\.md: Standard bounded readiness rejects placeholders/);
737
+ assert.match(output(rejected), /requirements\.md: Standard readiness rejects placeholders/);
738
+ assert.match(output(rejected), /design\.md: Standard readiness rejects placeholders/);
266
739
  assert.doesNotMatch(output(rejected), /feature-receipt\.md: canonical feature receipt required/);
267
740
 
268
741
  spec.scope_lock.source = 'The bounded feature scope is the one described here';
269
742
  fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
270
743
  write(path.join(specDir, 'requirements.md'), '# Requirements\n\nThe bounded behavior is explicitly defined.\n');
271
744
  write(path.join(specDir, 'design.md'), '# Design\n\nThe bounded implementation boundary is explicitly defined.\n');
745
+ const generic = run(VALIDATOR, [specDir], ROOT);
746
+ assert.notEqual(generic.status, 0);
747
+ assert.match(output(generic), /canonical numeric requirement id/);
748
+ assert.match(output(generic), /testable acceptance criterion using RN\.M/);
749
+ assert.match(output(generic), /grounded path\/code identifier/);
750
+
751
+ write(
752
+ path.join(specDir, 'requirements.md'),
753
+ '# Requirements\n\n## Requirements\n\n### Requirement 1: Locale persistence\n\n' +
754
+ '- **R1.1** When a locale is saved, the CLI shall write the selected value to `.codex/runtime.json` and return exit code 0.\n',
755
+ );
756
+ write(
757
+ path.join(specDir, 'design.md'),
758
+ '# Design\n\n## Boundary\n\nThe `.codex/runtime.json` file is owned by `saveLocale`; the CLI calls it before returning success.\n\n' +
759
+ '## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
760
+ '| A-D-01 | file | `.codex/runtime.json` | locale persistence target |\n' +
761
+ '| A-D-02 | symbol | `src/runtime.js#saveLocale` | locale writer |\n\n' +
762
+ '## Decisions and Invariants\n\n### D1 — Persist selected locale\n\n- **Decision:** `saveLocale` writes the selected value.\n- **Negative path:** Invalid locale leaves state unchanged.\n- **Anchors:** A-D-01, A-D-02\n\n' +
763
+ '## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
764
+ '| R1.1 | `node --test test/runtime.test.js` | exit code 0 and saved locale | invalid locale through `saveLocale` |\n',
765
+ );
766
+ completeSemanticReview(specDir);
272
767
  const accepted = run(VALIDATOR, [specDir], ROOT);
273
768
  assert.equal(accepted.status, 0, output(accepted));
274
769
  } finally {
@@ -280,19 +775,29 @@ test('tasks-only validates the persisted policy before any filesystem mutation',
280
775
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-preflight-'));
281
776
  try {
282
777
  const standardPolicy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
778
+ const boundaries = JSON.stringify([{
779
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01'],
780
+ write_sets: { 'R1-01': ['src/one.js'] },
781
+ }]);
283
782
  const cases = [
284
- ['malformed', '{', /existing spec\.json is invalid JSON/],
285
- ['null-state', 'null', /existing spec\.json must contain a JSON object/],
286
- ['missing-snapshot', JSON.stringify({}), /existing spec must contain a persisted workflow_policy/],
287
- ['null-snapshot', JSON.stringify({ workflow_policy: null }), /existing workflow_policy is invalid/],
288
- ['extra-field', JSON.stringify({ workflow_policy: { ...standardPolicy, extra: true } }), /existing workflow_policy is invalid/],
783
+ ['malformed', () => '{', /existing spec\.json is invalid JSON/],
784
+ ['null-state', () => 'null', /existing spec\.json must contain a JSON object/],
785
+ ['missing-snapshot', (state) => { delete state.workflow_policy; return JSON.stringify(state, null, 2); }, /existing spec must contain a persisted workflow_policy/],
786
+ ['null-snapshot', (state) => { state.workflow_policy = null; return JSON.stringify(state, null, 2); }, /existing workflow_policy is invalid/],
787
+ ['extra-field', (state) => {
788
+ state.workflow_policy = { ...standardPolicy, extra: true };
789
+ return JSON.stringify(state, null, 2);
790
+ }, /schema 2\.0 workflow_policy contains unsupported legacy authority field\(s\): extra/],
289
791
  ];
290
- for (const [name, body, expected] of cases) {
291
- const specDir = path.join(root, 'specs', name);
792
+ for (const [name, mutate, expected] of cases) {
793
+ const specDir = createLegacyTasklessSpec20(root, name, standardPolicy);
292
794
  const specPath = path.join(specDir, 'spec.json');
293
- write(specPath, body);
795
+ const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
796
+ write(specPath, mutate(state));
294
797
  const before = fs.readFileSync(specPath, 'utf8');
295
- const result = run(SCAFFOLD, [name, '--tasks', 'R1-01-one', '--tasks-only'], root);
798
+ const result = run(SCAFFOLD, [
799
+ name, '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', boundaries,
800
+ ], root);
296
801
  assert.equal(result.status, 2, output(result));
297
802
  assert.match(output(result), expected);
298
803
  assert.equal(fs.readFileSync(specPath, 'utf8'), before);
@@ -314,6 +819,21 @@ test('scaffold accepts only one safe alphanumeric-starting feature segment', ()
314
819
  assert.match(output(result), /feature|safe relative|path segment/i);
315
820
  assert.equal(snapshotTree(root), before, `${feature} must not mutate the workspace`);
316
821
  }
822
+
823
+ const beforeFreshSequence = snapshotTree(root);
824
+ const badFreshSequence = run(SCAFFOLD, ['bad-fresh-sequence', '--tasks', 'R1-1-one'], root);
825
+ assert.equal(badFreshSequence.status, 2, output(badFreshSequence));
826
+ assert.match(output(badFreshSequence), /SEQ 2 digits/);
827
+ assert.equal(snapshotTree(root), beforeFreshSequence, 'one-digit fresh task sequence mutated the workspace');
828
+
829
+ createLegacyTasklessSpec20(root, 'bad-tasks-only-sequence', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
830
+ const beforeTasksOnlySequence = snapshotTree(root);
831
+ const badTasksOnlySequence = run(SCAFFOLD, [
832
+ 'bad-tasks-only-sequence', '--tasks', 'R1-1-one', '--tasks-only',
833
+ ], root);
834
+ assert.equal(badTasksOnlySequence.status, 2, output(badTasksOnlySequence));
835
+ assert.match(output(badTasksOnlySequence), /SEQ 2 digits/);
836
+ assert.equal(snapshotTree(root), beforeTasksOnlySequence, 'one-digit tasks-only sequence mutated the workspace');
317
837
  } finally {
318
838
  fs.rmSync(root, { recursive: true, force: true });
319
839
  }
@@ -328,9 +848,9 @@ test('validator requires a strict persisted workflow-policy snapshot and never f
328
848
  ['explicit-null', (state) => { state.workflow_policy = null; }, /workflow_policy must be an object/],
329
849
  ['malformed', (state) => { state.workflow_policy = { ...policy, proof_obligations: { needsExecutionProof: true } }; }, /proof_obligations must be an array/],
330
850
  ['extra-field', (state) => { state.workflow_policy = { ...policy, extra: true }; }, /workflow_policy fields must be exactly/],
331
- ['lane-mismatch', (state) => { state.workflow_policy = { ...policy, lane: 'Standard', automatic_lane: 'Critical' }; }, /lane cannot be lower than automatic_lane/],
332
- ['risk-mismatch', (state) => { state.workflow_policy = { ...policy, risks: ['auth'] }; }, /lane must be at least Critical for its risks/],
333
- ['obligation-mismatch', (state) => { state.workflow_policy = { ...policy, proof_obligations: ['needsExecutionProof'], actor_needs: [{ capability: 'execution-proof', independence: 'same-session' }] }; }, /proof_obligations do not match lane and risks/],
851
+ ['lane-mismatch', (state) => { state.workflow_policy = { ...policy, lane: 'Critical', automatic_lane: 'Critical' }; }, /lane is not the derived compatibility lane|automatic_lane is not the derived compatibility lane/],
852
+ ['risk-mismatch', (state) => { state.workflow_policy = { ...policy, risks: ['auth'] }; }, /assurance_level must be at least Elevated/],
853
+ ['obligation-mismatch', (state) => { state.workflow_policy = { ...policy, proof_obligations: ['needsInspection', 'needsExecutionProof'], actor_needs: [{ capability: 'inspection', independence: 'same-session' }, { capability: 'execution-proof', independence: 'same-session' }] }; }, /proof_obligations do not match assurance_level and risks/],
334
854
  ];
335
855
  for (const [name, mutate, expected] of cases) {
336
856
  const specDir = createSpec(root, { name, spec: { workflow_policy: policy } });
@@ -357,7 +877,8 @@ test('validator requires a strict persisted workflow-policy snapshot and never f
357
877
  tasks: { generated: true, agent_validated: true, user_approved: true },
358
878
  },
359
879
  validation: { status: 'completed' },
360
- timestamps: { validation_done: '2026-08-11T00:00:00+07:00' },
880
+ updated_at: '2026-08-11T00:06:00+07:00',
881
+ timestamps: lifecycleTimestamps(),
361
882
  },
362
883
  });
363
884
  const legacyBefore = snapshotTree(legacyDir);
@@ -388,40 +909,66 @@ test('tasks-only preflights malformed topology, duplicates, and symlinks without
388
909
  ['unsafe-research-path', { workflow_policy: policy, research: '../outside' }, /spec\.research must be a safe relative path/],
389
910
  ];
390
911
  for (const [name, state, expected] of cases) {
391
- const specDir = createPolicyOnlySpec(root, name, policy);
392
- write(path.join(specDir, 'spec.json'), JSON.stringify(state, null, 2));
912
+ const specDir = createLegacyTasklessSpec20(root, name, policy);
913
+ write(path.join(specDir, 'spec.json'), JSON.stringify({
914
+ ...legacyTasklessSpec20State(name, policy),
915
+ ...state,
916
+ }, null, 2));
917
+ for (const declaredTask of new Set(Array.isArray(state.task_files) ? state.task_files : [])) {
918
+ write(path.join(specDir, declaredTask), task());
919
+ }
920
+ const boundaryTasks = Array.isArray(state.task_files) && state.task_files.includes(taskPath)
921
+ ? ['R1-01', 'R1-02', 'R1-03'] : ['R1-02', 'R1-03'];
922
+ const boundaries = JSON.stringify([{
923
+ id: 'B-OWN', type: 'ownership', tasks: boundaryTasks,
924
+ write_sets: Object.fromEntries(boundaryTasks.map((id) => [id, [`src/${id.toLowerCase()}.js`]])),
925
+ }]);
393
926
  const before = snapshotTree(root);
394
- const result = run(SCAFFOLD, [name, '--tasks', 'R1-02-two', '--tasks-only'], root);
927
+ const result = run(SCAFFOLD, [
928
+ name, '--tasks', 'R1-02-two,R1-03-three', '--tasks-only',
929
+ '--boundaries', boundaries,
930
+ ], root);
395
931
  assert.equal(result.status, 2, output(result));
396
932
  assert.match(output(result), expected);
397
933
  assert.equal(snapshotTree(root), before, `${name} mutated the existing topology`);
398
934
  }
399
935
 
400
- const conflictDir = createPolicyOnlySpec(root, 'conflicting-duplicate', policy);
936
+ const conflictDir = createLegacyTasklessSpec20(root, 'conflicting-duplicate', policy);
401
937
  write(path.join(conflictDir, 'tasks/task-R1-01-one.md'), 'already filled\n');
402
938
  write(path.join(conflictDir, 'spec.json'), JSON.stringify({
403
- workflow_policy: policy,
939
+ ...legacyTasklessSpec20State('conflicting-duplicate', policy),
404
940
  task_files: [taskPath],
405
941
  task_registry: { [taskPath]: { ...registryEntry, title: 'Conflicting title' } },
406
942
  }, null, 2));
407
943
  const conflictBefore = snapshotTree(root);
408
- const conflict = run(SCAFFOLD, ['conflicting-duplicate', '--tasks', 'R1-01-one', '--tasks-only'], root);
944
+ const singleBoundary = JSON.stringify([{
945
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01'], write_sets: { 'R1-01': ['src/one.js'] },
946
+ }]);
947
+ const conflict = run(SCAFFOLD, [
948
+ 'conflicting-duplicate', '--tasks', 'R1-01-one', '--tasks-only',
949
+ '--boundaries', singleBoundary,
950
+ ], root);
409
951
  assert.equal(conflict.status, 2, output(conflict));
410
952
  assert.match(output(conflict), /conflicting duplicate/);
411
953
  assert.equal(snapshotTree(root), conflictBefore);
412
954
 
413
- const duplicateDir = createPolicyOnlySpec(root, 'duplicate-cli', policy);
955
+ const duplicateDir = createLegacyTasklessSpec20(root, 'duplicate-cli', policy);
414
956
  const duplicateBefore = snapshotTree(root);
415
- const duplicate = run(SCAFFOLD, ['duplicate-cli', '--tasks', 'R1-01-one,R1-01-two', '--tasks-only'], root);
957
+ const duplicate = run(SCAFFOLD, [
958
+ 'duplicate-cli', '--tasks', 'R1-01-one,R1-01-two', '--tasks-only',
959
+ '--boundaries', singleBoundary,
960
+ ], root);
416
961
  assert.equal(duplicate.status, 2, output(duplicate));
417
962
  assert.match(output(duplicate), /duplicate task id.*R1-01/);
418
963
  assert.equal(snapshotTree(root), duplicateBefore);
419
964
 
420
- const symlinkDir = createPolicyOnlySpec(root, 'symlink-tasks', policy);
965
+ const symlinkDir = createLegacyTasklessSpec20(root, 'symlink-tasks', policy);
421
966
  const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-symlink-target-'));
422
967
  fs.symlinkSync(outside, path.join(symlinkDir, 'tasks'), 'dir');
423
968
  const symlinkBefore = snapshotTree(root);
424
- const symlinkResult = run(SCAFFOLD, ['symlink-tasks', '--tasks', 'R1-01-one', '--tasks-only'], root);
969
+ const symlinkResult = run(SCAFFOLD, [
970
+ 'symlink-tasks', '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', singleBoundary,
971
+ ], root);
425
972
  assert.equal(symlinkResult.status, 2, output(symlinkResult));
426
973
  assert.match(output(symlinkResult), /symlink/);
427
974
  assert.equal(snapshotTree(root), symlinkBefore);
@@ -435,11 +982,14 @@ test('tasks-only commits transactionally and preserves exact bytes after an inje
435
982
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-transaction-'));
436
983
  try {
437
984
  const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
438
- createPolicyOnlySpec(root, 'partial-write', policy);
985
+ createLegacyTasklessSpec20(root, 'partial-write', policy);
439
986
  const before = snapshotTree(root);
440
987
  const result = run(
441
988
  SCAFFOLD,
442
- ['partial-write', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only'],
989
+ ['partial-write', '--tasks', 'R1-01-one,R1-02-two', '--boundaries', JSON.stringify([{
990
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
991
+ write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
992
+ }]), '--tasks-only'],
443
993
  root,
444
994
  { env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '1' } },
445
995
  );
@@ -451,51 +1001,261 @@ test('tasks-only commits transactionally and preserves exact bytes after an inje
451
1001
  }
452
1002
  });
453
1003
 
1004
+ test('tasks-only migration derives only absent feature identity and rejects present conflicts atomically', () => {
1005
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-legacy-identity-'));
1006
+ const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
1007
+ const boundaries = JSON.stringify([{
1008
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1009
+ write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1010
+ }]);
1011
+ const migrate = (feature) => run(SCAFFOLD, [
1012
+ feature, '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', '--boundaries', boundaries,
1013
+ ], root);
1014
+ try {
1015
+ const absentFeature = 'legacy-identity-absent';
1016
+ const absentDir = createLegacyTasklessSpec20(root, absentFeature, policy);
1017
+ const absentPath = path.join(absentDir, 'spec.json');
1018
+ const absentState = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
1019
+ delete absentState.feature_name;
1020
+ write(absentPath, JSON.stringify(absentState, null, 2));
1021
+
1022
+ const migrated = migrate(absentFeature);
1023
+ assert.equal(migrated.status, 0, output(migrated));
1024
+ const canonical = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
1025
+ assert.equal(canonical.schema_version, '2.1');
1026
+ assert.equal(canonical.feature_name, absentFeature);
1027
+ assert.equal(canonical.created_at, '2026-08-11T00:00:00+07:00');
1028
+ assert.equal(canonical.language, 'en');
1029
+ assert.equal(canonical.status, 'in_progress');
1030
+ assert.deepEqual(canonical.scope_lock, absentState.scope_lock);
1031
+ assert.deepEqual(canonical.authoring, {
1032
+ requirements: 'draft', design: 'draft', research: 'absent', tasks: 'draft',
1033
+ });
1034
+ assert.equal(canonical.ready_for_implementation, false);
1035
+ assert.equal(canonical.validation.status, 'not-run');
1036
+ assert.equal(canonical.validation.semantic_review.status, 'not-run');
1037
+ assert.equal(Object.hasOwn(canonical, 'research'), false);
1038
+ assert.equal(fs.existsSync(path.join(absentDir, 'research.md')), false);
1039
+ assert.equal(fs.existsSync(path.join(absentDir, 'receipts')), false);
1040
+ assert.equal(fs.existsSync(path.join(absentDir, 'reports')), false);
1041
+ for (const entry of Object.values(canonical.task_registry)) {
1042
+ assert.deepEqual(
1043
+ [entry.status, entry.blocker, entry.started_at, entry.completed_at, entry.last_updated_at],
1044
+ ['pending', null, null, null, null],
1045
+ );
1046
+ }
1047
+
1048
+ for (const [label, featureName] of [
1049
+ ['empty', ''], ['non-string', 42], ['different', 'another-feature'],
1050
+ ]) {
1051
+ const feature = `legacy-identity-${label}`;
1052
+ const specDir = createLegacyTasklessSpec20(root, feature, policy);
1053
+ const specPath = path.join(specDir, 'spec.json');
1054
+ const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1055
+ state.feature_name = featureName;
1056
+ write(specPath, JSON.stringify(state, null, 2));
1057
+ const before = snapshotTree(specDir);
1058
+ const rejected = migrate(feature);
1059
+ assert.equal(rejected.status, 2, `${label}: ${output(rejected)}`);
1060
+ assert.match(output(rejected), /feature_name conflicts with the scaffold feature identity/);
1061
+ assert.equal(snapshotTree(specDir), before, `${label} identity rejection mutated the fixture`);
1062
+ }
1063
+ } finally {
1064
+ fs.rmSync(root, { recursive: true, force: true });
1065
+ }
1066
+ });
1067
+
1068
+ test('fresh scaffold rolls back authority rejection and injected partial writes without deleting pre-existing content', () => {
1069
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-fresh-transaction-'));
1070
+ try {
1071
+ const codexRoot = path.join(root, '.codex');
1072
+ const installedScripts = path.join(codexRoot, 'scripts');
1073
+ const installedHooks = path.join(codexRoot, 'hooks');
1074
+ const installedTemplates = path.join(codexRoot, 'skills/specs/templates');
1075
+ fs.mkdirSync(installedScripts, { recursive: true });
1076
+ fs.mkdirSync(installedHooks, { recursive: true });
1077
+ fs.mkdirSync(path.dirname(installedTemplates), { recursive: true });
1078
+ fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), installedTemplates, { recursive: true });
1079
+ copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
1080
+ write(path.join(installedHooks, 'completion-authority-state.cjs'), 'module.exports = {};\n');
1081
+ write(
1082
+ path.join(installedHooks, 'completion-authority-check.cjs'),
1083
+ 'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture authority reject" }; } };\n',
1084
+ );
1085
+ write(path.join(root, 'specs/keep.txt'), 'pre-existing\n');
1086
+
1087
+ const beforeAuthority = snapshotTree(root);
1088
+ const authority = run(
1089
+ path.join(installedScripts, 'spec-scaffold.cjs'),
1090
+ ['authority-reject', '--lane', 'Standard'],
1091
+ root,
1092
+ );
1093
+ assert.equal(authority.status, 2, output(authority));
1094
+ assert.match(output(authority), /fixture authority reject.*transaction rolled back/);
1095
+ assert.equal(snapshotTree(root), beforeAuthority);
1096
+ assert.equal(fs.existsSync(path.join(root, 'specs/authority-reject')), false);
1097
+
1098
+ write(
1099
+ path.join(installedHooks, 'completion-authority-check.cjs'),
1100
+ 'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
1101
+ );
1102
+ const seeded = run(
1103
+ path.join(installedScripts, 'spec-scaffold.cjs'),
1104
+ ['tasks-authority', '--lane', 'Standard'],
1105
+ root,
1106
+ );
1107
+ assert.equal(seeded.status, 0, output(seeded));
1108
+ write(
1109
+ path.join(installedHooks, 'completion-authority-check.cjs'),
1110
+ 'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture tasks-only authority reject" }; } };\n',
1111
+ );
1112
+ const beforeTasksAuthority = snapshotTree(root);
1113
+ const tasksAuthority = run(
1114
+ path.join(installedScripts, 'spec-scaffold.cjs'),
1115
+ ['tasks-authority', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only',
1116
+ '--boundaries', JSON.stringify([{
1117
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1118
+ write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1119
+ }])],
1120
+ root,
1121
+ );
1122
+ assert.equal(tasksAuthority.status, 2, output(tasksAuthority));
1123
+ assert.match(output(tasksAuthority), /fixture tasks-only authority reject.*transaction rolled back/);
1124
+ assert.equal(snapshotTree(root), beforeTasksAuthority);
1125
+ assert.equal(fs.existsSync(path.join(root, 'specs/tasks-authority/tasks/task-R1-01-one.md')), false);
1126
+
1127
+ const beforeInjected = snapshotTree(root);
1128
+ const injected = run(
1129
+ SCAFFOLD,
1130
+ ['fresh-injected', '--lane', 'Standard'],
1131
+ root,
1132
+ { env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '2' } },
1133
+ );
1134
+ assert.equal(injected.status, 2, output(injected));
1135
+ assert.match(output(injected), /injected failure.*transaction rolled back/);
1136
+ assert.equal(snapshotTree(root), beforeInjected);
1137
+ assert.equal(fs.readFileSync(path.join(root, 'specs/keep.txt'), 'utf8'), 'pre-existing\n');
1138
+ } finally {
1139
+ fs.rmSync(root, { recursive: true, force: true });
1140
+ }
1141
+ });
1142
+
1143
+ test('installed Codex scaffold rejects a symlinked template parent before creating a spec', () => {
1144
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-template-symlink-'));
1145
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-template-outside-'));
1146
+ try {
1147
+ const codexRoot = path.join(root, '.codex');
1148
+ copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
1149
+ write(
1150
+ path.join(codexRoot, 'hooks/completion-authority-check.cjs'),
1151
+ 'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
1152
+ );
1153
+ write(path.join(codexRoot, 'hooks/completion-authority-state.cjs'), 'module.exports = {};\n');
1154
+ fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), outside, { recursive: true });
1155
+ const templatesParent = path.join(root, '.agents/skills/specs');
1156
+ fs.mkdirSync(templatesParent, { recursive: true });
1157
+ fs.symlinkSync(outside, path.join(templatesParent, 'templates'));
1158
+
1159
+ const result = run(
1160
+ path.join(codexRoot, 'scripts/spec-scaffold.cjs'),
1161
+ ['template-parent-escape', '--lane', 'Standard'],
1162
+ root,
1163
+ );
1164
+ assert.equal(result.status, 2, output(result));
1165
+ assert.match(output(result), /template check failed|symlink component rejected/i);
1166
+ assert.equal(fs.existsSync(path.join(root, 'specs/template-parent-escape')), false);
1167
+ } finally {
1168
+ fs.rmSync(root, { recursive: true, force: true });
1169
+ fs.rmSync(outside, { recursive: true, force: true });
1170
+ }
1171
+ });
1172
+
454
1173
  test('tasks-only is idempotent for an identical task request and preserves the snapshot bytes', () => {
455
1174
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-idempotent-'));
456
1175
  try {
457
1176
  const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
458
- createPolicyOnlySpec(root, 'idempotent', policy);
459
- const first = run(SCAFFOLD, ['idempotent', '--tasks', 'R1-01-one', '--tasks-only'], root);
1177
+ createLegacyTasklessSpec20(root, 'idempotent', policy);
1178
+ const boundaryArgs = ['--boundaries', JSON.stringify([{
1179
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1180
+ write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1181
+ }])];
1182
+ const idempotentArgs = ['idempotent', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...boundaryArgs];
1183
+ const first = run(SCAFFOLD, idempotentArgs, root);
460
1184
  assert.equal(first.status, 0, output(first));
461
1185
  const beforeSecond = snapshotTree(root);
462
- const second = run(SCAFFOLD, ['idempotent', '--tasks', 'R1-01-one', '--tasks-only'], root);
1186
+ const second = run(SCAFFOLD, idempotentArgs, root);
463
1187
  assert.equal(second.status, 0, output(second));
464
1188
  assert.match(output(second), /0 new task stub\(s\)/);
465
1189
  assert.equal(snapshotTree(root), beforeSecond);
1190
+
1191
+ const artifactBase = run(SCAFFOLD, ['idempotent-artifact', '--lane', 'Standard'], root);
1192
+ assert.equal(artifactBase.status, 0, output(artifactBase));
1193
+ const proofArgs = ['--boundaries', JSON.stringify([{
1194
+ id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
1195
+ verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
1196
+ }])];
1197
+ const artifactArgs = [
1198
+ 'idempotent-artifact', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...proofArgs,
1199
+ ];
1200
+ const artifactFirst = run(SCAFFOLD, artifactArgs, root);
1201
+ assert.equal(artifactFirst.status, 0, output(artifactFirst));
1202
+ const artifactDir = path.join(root, 'specs/idempotent-artifact');
1203
+ completeScaffoldedSpec21(artifactDir);
1204
+ const artifactValidation = run(VALIDATOR, [artifactDir], ROOT);
1205
+ assert.equal(artifactValidation.status, 0, output(artifactValidation));
1206
+ const beforeArtifactSecond = snapshotTree(root);
1207
+ const artifactSecond = run(SCAFFOLD, artifactArgs, root);
1208
+ assert.equal(artifactSecond.status, 0, output(artifactSecond));
1209
+ assert.match(output(artifactSecond), /0 new task stub\(s\)/);
1210
+ assert.equal(snapshotTree(root), beforeArtifactSecond);
466
1211
  } finally {
467
1212
  fs.rmSync(root, { recursive: true, force: true });
468
1213
  }
469
1214
  });
470
1215
 
471
- test('tasks-only consumes the policy snapshot and escalates Standard auth monotonically', () => {
1216
+ test('tasks-only consumes the policy snapshot and escalates auth to Elevated monotonically', () => {
472
1217
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-policy-'));
473
1218
  try {
474
- const specDir = createPolicyOnlySpec(root, 'existing', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
1219
+ const specDir = createLegacyTasklessSpec20(root, 'existing', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
1220
+ const boundaries = JSON.stringify([{
1221
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1222
+ write_sets: { 'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'] },
1223
+ }]);
475
1224
  const escalated = run(SCAFFOLD, [
476
- 'existing', '--tasks', 'R1-01-auth', '--risks', 'auth', '--tasks-only',
1225
+ 'existing', '--tasks', 'R1-01-auth,R1-02-next', '--risks', 'auth', '--tasks-only',
1226
+ '--boundaries', boundaries,
477
1227
  ], root);
478
1228
  assert.equal(escalated.status, 0, output(escalated));
479
- assert.match(output(escalated), /1 new task stub\(s\)/);
480
- assert.match(output(escalated), /research\.md created for needsResearchGrounding/);
1229
+ assert.match(output(escalated), /2 new task stub\(s\)/);
1230
+ assert.doesNotMatch(output(escalated), /research\.md created/);
481
1231
  const first = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
482
- assert.equal(first.workflow_policy.lane, 'Critical');
1232
+ assert.equal(POLICY.readWorkflowPolicySnapshot(first).lane, 'Standard');
1233
+ assert.equal(POLICY.readWorkflowPolicySnapshot(first).assurance_level, 'Elevated');
483
1234
  assert.deepEqual(first.workflow_policy.risks, ['auth']);
484
- assert.equal(first.workflow_policy.artifact_profile, 'strict');
485
- assert.ok(first.workflow_policy.proof_obligations.includes('needsResearchGrounding'));
486
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), true);
1235
+ assert.equal(POLICY.readWorkflowPolicySnapshot(first).artifact_profile, 'bounded');
1236
+ assert.equal(POLICY.readWorkflowPolicySnapshot(first).proof_obligations.includes('needsResearchGrounding'), false);
1237
+ assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
487
1238
  assert.equal(fs.existsSync(path.join(specDir, 'reports')), false);
488
1239
 
489
1240
  const policyBytes = JSON.stringify(first.workflow_policy);
490
- const noNewRisk = run(SCAFFOLD, ['existing', '--tasks', 'R1-02-next', '--tasks-only'], root);
1241
+ const noNewRisk = run(SCAFFOLD, [
1242
+ 'existing', '--tasks', 'R1-01-auth,R1-02-next', '--tasks-only', '--boundaries', boundaries,
1243
+ ], root);
491
1244
  assert.equal(noNewRisk.status, 0, output(noNewRisk));
492
- assert.match(output(noNewRisk), /1 new task stub\(s\)/);
1245
+ assert.match(output(noNewRisk), /0 new task stub\(s\)/);
493
1246
  const second = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
494
1247
  assert.equal(JSON.stringify(second.workflow_policy), policyBytes);
495
1248
 
496
1249
  const beforeRejectedLane = fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8');
1250
+ const rejectedBoundaries = JSON.stringify([{
1251
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02', 'R1-03'],
1252
+ write_sets: {
1253
+ 'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'], 'R1-03': ['src/invalid.js'],
1254
+ },
1255
+ }]);
497
1256
  const rejectedLane = run(SCAFFOLD, [
498
- 'existing', '--tasks', 'R1-03-invalid', '--lane', 'Standard', '--tasks-only',
1257
+ 'existing', '--tasks', 'R1-03-invalid', '--lane', 'Critical', '--tasks-only',
1258
+ '--boundaries', rejectedBoundaries,
499
1259
  ], root);
500
1260
  assert.equal(rejectedLane.status, 2, output(rejectedLane));
501
1261
  assert.match(output(rejectedLane), /does not match persisted workflow_policy\.lane/);
@@ -506,54 +1266,25 @@ test('tasks-only consumes the policy snapshot and escalates Standard auth monoto
506
1266
  }
507
1267
  });
508
1268
 
509
- test('scaffold emits bounded artifacts on demand without default research or reports ceremony', () => {
1269
+ test('scaffold emits minimal 2.1 artifacts without default research or reports ceremony', () => {
510
1270
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-artifact-profile-'));
511
1271
  try {
512
1272
  const standardDefault = run(SCAFFOLD, ['standard-default', '--lane', 'Standard'], root);
513
1273
  assert.equal(standardDefault.status, 0, output(standardDefault));
514
1274
  const standardDir = path.join(root, 'specs', 'standard-default');
515
1275
  assert.deepEqual(fs.readdirSync(standardDir).sort(), [
516
- 'design.md', 'feature-receipt.md', 'requirements.md', 'spec.json',
1276
+ 'design.md', 'requirements.md', 'spec.json',
517
1277
  ]);
518
1278
 
519
- for (const args of [
520
- ['standard-tasks', '--lane', 'Standard', '--tasks', 'R1-01-one'],
521
- ['critical-no-research', '--lane', 'Critical', '--tasks', 'R1-01-one'],
522
- ]) {
523
- const result = run(SCAFFOLD, args, root);
524
- assert.equal(result.status, 0, output(result));
525
- const specDir = path.join(root, 'specs', args[0]);
526
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
527
- assert.equal(fs.existsSync(path.join(specDir, 'reports')), false);
528
- assert.equal(fs.existsSync(path.join(specDir, 'tasks/task-R1-01-one.md')), true);
529
- }
530
-
531
- for (const [name, risk] of [['critical-auth-case', 'Auth'], ['critical-auth-alias', 'authentication']]) {
532
- const result = run(SCAFFOLD, [name, '--risks', risk, '--tasks', 'R1-01-one'], root);
533
- assert.equal(result.status, 0, output(result));
534
- const specDir = path.join(root, 'specs', name);
535
- const spec = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
536
- assert.equal(spec.workflow_policy.lane, 'Critical');
537
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), true);
538
- }
539
-
540
- const unknown = run(SCAFFOLD, ['unknown-risk', '--risks', 'unclassified-risk'], root);
541
- assert.equal(unknown.status, 0, output(unknown));
542
- const unknownSpec = JSON.parse(fs.readFileSync(path.join(root, 'specs/unknown-risk/spec.json'), 'utf8'));
543
- assert.equal(unknownSpec.workflow_policy.lane, 'Standard');
544
- assert.equal(fs.existsSync(path.join(root, 'specs/unknown-risk/research.md')), false);
545
-
546
- const authTemplate = run(SCAFFOLD, [
547
- 'critical-auth-template', '--risks', 'auth', '--tasks', 'R1-01-artifact',
548
- ], root);
549
- assert.equal(authTemplate.status, 0, output(authTemplate));
550
- const authTemplateDir = path.join(root, 'specs', 'critical-auth-template');
551
- const unfilled = run(VALIDATOR, [authTemplateDir], ROOT);
552
- assert.notEqual(unfilled.status, 0);
553
- assert.match(output(unfilled), /Evidence Summary must contain concrete evidence/);
554
- completeScaffoldedSpec(authTemplateDir);
555
- const filled = run(VALIDATOR, [authTemplateDir], ROOT);
556
- assert.equal(filled.status, 0, output(filled));
1279
+ const strict = run(SCAFFOLD, ['strict-taskless', '--risks', 'auth', '--assurance-level', 'Strict'], root);
1280
+ assert.equal(strict.status, 0, output(strict));
1281
+ const strictDir = path.join(root, 'specs', 'strict-taskless');
1282
+ const strictSpec = JSON.parse(fs.readFileSync(path.join(strictDir, 'spec.json'), 'utf8'));
1283
+ assert.equal(strictSpec.schema_version, '2.1');
1284
+ assert.equal(POLICY.readWorkflowPolicySnapshot(strictSpec).lane, 'Critical');
1285
+ assert.equal(strictSpec.authoring.tasks, 'absent');
1286
+ assert.equal(fs.existsSync(path.join(strictDir, 'research.md')), false);
1287
+ assert.equal(fs.existsSync(path.join(strictDir, 'reports')), false);
557
1288
  } finally {
558
1289
  fs.rmSync(root, { recursive: true, force: true });
559
1290
  }
@@ -562,7 +1293,7 @@ test('scaffold emits bounded artifacts on demand without default research or rep
562
1293
  test('strict validator requires numeric IDs, rejects phantom mappings, and grounds research content', () => {
563
1294
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-strict-grounding-'));
564
1295
  try {
565
- const strictPolicy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } });
1296
+ const strictPolicy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
566
1297
  const noIds = createSpec(root, {
567
1298
  name: 'no-numeric-ids',
568
1299
  requirements: '# Requirements\n\nThe behavior has no numeric identifier.\n',
@@ -572,6 +1303,15 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
572
1303
  assert.notEqual(noIdsResult.status, 0);
573
1304
  assert.match(output(noIdsResult), /strict workflow requires numeric requirement IDs/);
574
1305
 
1306
+ const legacyDialect = createSpec(root, {
1307
+ name: 'legacy-req-dialect',
1308
+ requirements: '# Requirements\n\n## Requirements\n\n### REQ-01: One\n\n- **REQ-01.1** The service shall return one result.\n',
1309
+ spec: { workflow_policy: strictPolicy },
1310
+ });
1311
+ const legacyDialectResult = run(VALIDATOR, [legacyDialect], ROOT);
1312
+ assert.notEqual(legacyDialectResult.status, 0);
1313
+ assert.match(output(legacyDialectResult), /REQ-01 is not a canonical requirement id/);
1314
+
575
1315
  const phantom = createSpec(root, {
576
1316
  name: 'phantom-mapping',
577
1317
  task: task({ mapping: '9.1' }),
@@ -584,7 +1324,8 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
584
1324
  const researchPlaceholder = createSpec(root, {
585
1325
  name: 'research-placeholder',
586
1326
  task: task({ related: '`src/strict-only.js` | Read' }),
587
- spec: { workflow_policy: strictPolicy },
1327
+ design: criticalNoObligationDesign(),
1328
+ spec: { workflow_policy: strictPolicy, research: 'research.md' },
588
1329
  });
589
1330
  write(path.join(researchPlaceholder, 'research.md'), '# Research\n\n## Evidence Summary\n- TBD\n');
590
1331
  const placeholderResult = run(VALIDATOR, [researchPlaceholder], ROOT);
@@ -599,7 +1340,211 @@ test('strict validator requires numeric IDs, rejects phantom mappings, and groun
599
1340
  }
600
1341
  });
601
1342
 
602
- test('spec-ready accepts pending execution evidence without an audit receipt', () => {
1343
+ test('validator rejects unsupported or duplicate structured requirement IDs without mining incidental prose', () => {
1344
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-requirement-inventory-'));
1345
+ try {
1346
+ const unsupportedHeading = createSpec(root, {
1347
+ name: 'unsupported-heading',
1348
+ requirements: '# Requirements\n\n### R1A: One\n\n- **R1.1** The service shall return status value 1.\n',
1349
+ });
1350
+ let result = run(VALIDATOR, [unsupportedHeading], ROOT);
1351
+ assert.notEqual(result.status, 0);
1352
+ assert.match(output(result), /unsupported requirement id R1A/);
1353
+
1354
+ const unsupportedCriterion = createSpec(root, {
1355
+ name: 'unsupported-criterion',
1356
+ requirements: '# Requirements\n\n### Requirement 1: One\n\n- **R1-1** The service shall return status value 1.\n',
1357
+ });
1358
+ result = run(VALIDATOR, [unsupportedCriterion], ROOT);
1359
+ assert.notEqual(result.status, 0);
1360
+ assert.match(output(result), /unsupported acceptance criterion id R1-1/);
1361
+
1362
+ const duplicates = createSpec(root, {
1363
+ name: 'duplicate-inventory',
1364
+ requirements: '# Requirements\n\n### Requirement 1: First\n\n- **R1.1** The service shall return status value 1.\n\n' +
1365
+ '### R1: Duplicate\n\n- **R1.1** The service shall return status value 2.\n',
1366
+ });
1367
+ result = run(VALIDATOR, [duplicates], ROOT);
1368
+ assert.notEqual(result.status, 0);
1369
+ assert.match(output(result), /duplicate canonical requirement id R1/);
1370
+ assert.match(output(result), /duplicate canonical acceptance criterion id R1\.1/);
1371
+
1372
+ const incidental = createSpec(root, {
1373
+ name: 'incidental-rfc-adr-and-reference',
1374
+ task: task({ related: '`src/one.js` | Read' }),
1375
+ requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
1376
+ '- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
1377
+ 'See RFC 9110, ADR-001, and R7.2 for historical context only.\n',
1378
+ });
1379
+ result = run(VALIDATOR, [incidental], ROOT);
1380
+ assert.equal(result.status, 0, output(result));
1381
+
1382
+ const ignoredSyntax = createSpec(root, {
1383
+ name: 'ignored-fenced-and-commented-inventory',
1384
+ task: task({ related: '`src/one.js` | Read' }),
1385
+ requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
1386
+ '- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
1387
+ '<!-- ### Requirement 1: commented duplicate\n- **R1.1** commented duplicate -->\n\n' +
1388
+ '```markdown\n### Requirement 1: example duplicate\n- **R1.1** example duplicate\n```\n',
1389
+ });
1390
+ result = run(VALIDATOR, [ignoredSyntax], ROOT);
1391
+ assert.equal(result.status, 0, output(result));
1392
+
1393
+ for (const [name, heading] of [
1394
+ ['numeric-heading', '### 1: One'],
1395
+ ['bracket-heading', '### [R1]: One'],
1396
+ ]) {
1397
+ const invalidHeading = createSpec(root, {
1398
+ name,
1399
+ requirements: `# Requirements\n\n${heading}\n\n- **R1.1** The service shall return status value 1.\n`,
1400
+ });
1401
+ result = run(VALIDATOR, [invalidHeading], ROOT);
1402
+ assert.notEqual(result.status, 0);
1403
+ assert.match(output(result), /unsupported requirement id/);
1404
+ }
1405
+ } finally {
1406
+ fs.rmSync(root, { recursive: true, force: true });
1407
+ }
1408
+ });
1409
+
1410
+ test('validator requires substantive Specs v2 task semantics and planned executable proof', () => {
1411
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-task-semantics-'));
1412
+ try {
1413
+ const invalid = createSpec(root, { name: 'weak-task-semantics' });
1414
+ const weakTask = task({ related: '`src/one.js` | Read' })
1415
+ .replace('Calling the real fixture entrypoint returns status value 1.', 'TBD')
1416
+ .replace('- **In scope:** Implement the mapped observable behavior.', '- **In scope:** TBD')
1417
+ .replace('- **Out of scope:** Unrelated runtime behavior.', '- **Out of scope:** N/A')
1418
+ .replace('- **R1.1:** Calling `src/one.js` returns an observable result with status value 1.', '- **R1.1:** Works')
1419
+ .replace('| A-R1-01-01 | command | `node --test test/one.test.js` |', '| A-R1-01-01 | command | `npm run fixture-check` |')
1420
+ .replace('- **Command:** `node --test test/one.test.js`', '- **Command:** run tests')
1421
+ .replace('exit code 0 and one assertion for status value 1', 'looks good')
1422
+ .replace('invalid input returns a deterministic error result', 'TBD')
1423
+ .replace('`src/one.js` is invoked by the focused test', 'TBD');
1424
+ writeRaw(path.join(invalid, 'tasks/task-R1-01-one.md'), weakTask);
1425
+ const rejected = run(VALIDATOR, [invalid], ROOT);
1426
+ assert.notEqual(rejected.status, 0);
1427
+ assert.match(output(rejected), /Outcome must be substantive/);
1428
+ assert.match(output(rejected), /Verification Plan requires a command-shaped invocation or justified N\/A/);
1429
+ assert.match(output(rejected), /Verification Plan requires an observable Expected result/);
1430
+ assert.match(output(rejected), /Verification Plan requires a negative-path disposition/);
1431
+ assert.match(output(rejected), /Verification Plan requires a concrete reachability target or justified N\/A/);
1432
+
1433
+ const emptyExecution = createSpec(root, {
1434
+ name: 'empty-steps-mapping-only-requirements',
1435
+ task: task({ related: '`src/empty.js` | Read' })
1436
+ .replace(/## Changes\n[\s\S]*?\n## Acceptance/, '## Changes\n\n## Acceptance'),
1437
+ });
1438
+ const emptyExecutionResult = run(VALIDATOR, [emptyExecution], ROOT);
1439
+ assert.notEqual(emptyExecutionResult.status, 0);
1440
+ assert.match(output(emptyExecutionResult), /Changes must be substantive|Changes must contain at least one substantive actionable checkbox/);
1441
+
1442
+ const justified = createSpec(root, {
1443
+ name: 'justified-command-na',
1444
+ task: task({ related: '`src/static-contract.js` | Read' }).replace(
1445
+ '- **Command:** `node --test test/one.test.js`',
1446
+ '- Command: N/A — no executable runtime exists because this task only inspects a static contract',
1447
+ ),
1448
+ });
1449
+ const accepted = run(VALIDATOR, [justified], ROOT);
1450
+ assert.equal(accepted.status, 0, output(accepted));
1451
+
1452
+ const projectSpecific = createSpec(root, {
1453
+ name: 'project-specific-command-shape',
1454
+ task: task({ related: '`src/project-specific.js` | Read' })
1455
+ .replace('node --test test/one.test.js', 'npm run project-specific-check'),
1456
+ });
1457
+ const projectSpecificResult = run(VALIDATOR, [projectSpecific], ROOT);
1458
+ assert.equal(projectSpecificResult.status, 0, output(projectSpecificResult));
1459
+ } finally {
1460
+ fs.rmSync(root, { recursive: true, force: true });
1461
+ }
1462
+ });
1463
+
1464
+ test('all ready non-Direct specs require semantic design and coherent authoring timestamps', () => {
1465
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-ready-lifecycle-'));
1466
+
1467
+ function readyFixture(name, mutate = () => {}) {
1468
+ const specDir = createSpec(root, {
1469
+ name,
1470
+ task: task({ related: '`src/one.js` | Read' }),
1471
+ spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
1472
+ });
1473
+ const specPath = path.join(specDir, 'spec.json');
1474
+ const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1475
+ state.ready_for_implementation = true;
1476
+ state.validation = { status: 'completed' };
1477
+ state.updated_at = '2026-08-11T00:06:00+07:00';
1478
+ state.timestamps = lifecycleTimestamps();
1479
+ mutate(state, specDir);
1480
+ write(specPath, JSON.stringify(state, null, 2));
1481
+ completeSemanticReview(
1482
+ specDir,
1483
+ state.workflow_policy.assurance_level === 'Strict' ? 'independent' : 'same-session',
1484
+ );
1485
+ return specDir;
1486
+ }
1487
+
1488
+ try {
1489
+ const critical = readyFixture('critical-generic-design', (state, specDir) => {
1490
+ state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1491
+ state.timestamps = lifecycleTimestamps({ research: true });
1492
+ write(path.join(specDir, 'design.md'), '# Design\n\n## Architecture\n\nThe implementation boundary is defined and appropriate.\n');
1493
+ });
1494
+ let result = run(VALIDATOR, [critical], ROOT);
1495
+ assert.notEqual(result.status, 0);
1496
+ assert.match(output(result), /design\.md: Critical readiness requires a grounded path\/code identifier/);
1497
+
1498
+ const strictAuthoring = readyFixture('critical-one-task-authoring', (state, specDir) => {
1499
+ state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1500
+ state.timestamps = lifecycleTimestamps({ research: true });
1501
+ write(path.join(specDir, 'design.md'), criticalNoObligationDesign());
1502
+ });
1503
+ result = run(VALIDATOR, [strictAuthoring], ROOT);
1504
+ assert.equal(result.status, 0, output(result));
1505
+
1506
+ const invalidCases = [
1507
+ ['boolean-created-at', (state) => { state.created_at = true; }, /created_at: must be an ISO 8601 timestamp|created_at: ready handoff requires/],
1508
+ ['boolean-task-time', (state) => { state.timestamps.tasks_done = true; }, /timestamps\.tasks_done: must be null or an ISO 8601 timestamp|ready handoff requires/],
1509
+ ['missing-validation-time', (state) => { delete state.timestamps.validation_done; }, /timestamps\.validation_done: ready handoff requires/],
1510
+ ['reversed-phase-time', (state) => { state.timestamps.design_done = '2026-08-11T00:00:30+07:00'; }, /final lifecycle timestamps must be ordered/],
1511
+ ['mismatched-final-state', (state) => { state.updated_at = '2026-08-11T00:07:00+07:00'; }, /updated_at must describe the same final state/],
1512
+ ];
1513
+ for (const [name, mutate, expected] of invalidCases) {
1514
+ const specDir = readyFixture(name, mutate);
1515
+ result = run(VALIDATOR, [specDir], ROOT);
1516
+ assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
1517
+ assert.match(output(result), expected);
1518
+ }
1519
+
1520
+ const reviewDir = readyFixture('five-task-review-time', (state, specDir) => {
1521
+ state.design_context = { validation_recommended: true };
1522
+ for (let index = 2; index <= 5; index += 1) {
1523
+ const taskPath = `tasks/task-R1-0${index}-review.md`;
1524
+ state.task_files.push(taskPath);
1525
+ state.task_registry[taskPath] = {
1526
+ id: `R1-0${index}`, title: `Review ${index}`, status: 'pending', dependencies: [], blocker: null,
1527
+ started_at: null, completed_at: null, last_updated_at: null,
1528
+ };
1529
+ write(path.join(specDir, taskPath), task({ id: `R1-0${index}`, related: `\`src/review-${index}.js\` | Read` }));
1530
+ }
1531
+ });
1532
+ result = run(VALIDATOR, [reviewDir], ROOT);
1533
+ assert.notEqual(result.status, 0);
1534
+ assert.match(output(result), /timestamps\.review_done: required after validation for a large task graph/);
1535
+
1536
+ const reviewPath = path.join(reviewDir, 'spec.json');
1537
+ const reviewState = JSON.parse(fs.readFileSync(reviewPath, 'utf8'));
1538
+ reviewState.timestamps.review_done = '2026-08-11T00:05:00+07:00';
1539
+ write(reviewPath, JSON.stringify(reviewState, null, 2));
1540
+ result = run(VALIDATOR, [reviewDir], ROOT);
1541
+ assert.equal(result.status, 0, output(result));
1542
+ } finally {
1543
+ fs.rmSync(root, { recursive: true, force: true });
1544
+ }
1545
+ });
1546
+
1547
+ test('spec-ready excludes execution receipts while preserving Strict review state', () => {
603
1548
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-spec-ready-'));
604
1549
  try {
605
1550
  const standardDir = createSpec(root, {
@@ -614,19 +1559,20 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
614
1559
  tasks: { generated: true, agent_validated: true, user_approved: true },
615
1560
  },
616
1561
  validation: { status: 'completed' },
617
- timestamps: { validation_done: '2026-08-11T00:00:00+07:00' },
1562
+ updated_at: '2026-08-11T00:06:00+07:00',
1563
+ timestamps: lifecycleTimestamps(),
618
1564
  },
619
1565
  });
620
- write(path.join(standardDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n');
621
- write(path.join(standardDir, 'tasks/task-R1-01-one.md'), `${fs.readFileSync(path.join(standardDir, 'tasks/task-R1-01-one.md'), 'utf8')}\nVerification: PENDING\n`);
1566
+ completeSemanticReview(standardDir);
622
1567
  const standardResult = run(VALIDATOR, [standardDir], ROOT);
623
1568
  assert.equal(standardResult.status, 0, output(standardResult));
624
1569
 
625
1570
  const criticalDir = createSpec(root, {
626
1571
  name: 'pending-critical',
627
1572
  task: task({ related: '`src/pending-only.js` | Read' }),
1573
+ design: criticalNoObligationDesign(),
628
1574
  spec: {
629
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } }),
1575
+ workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
630
1576
  ready_for_implementation: true,
631
1577
  approvals: {
632
1578
  requirements: { generated: true, agent_validated: true, user_approved: true },
@@ -634,9 +1580,11 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
634
1580
  tasks: { generated: true, agent_validated: true, user_approved: true },
635
1581
  },
636
1582
  validation: { status: 'completed' },
637
- timestamps: { validation_done: '2026-08-11T00:00:00+07:00' },
1583
+ updated_at: '2026-08-11T00:06:00+07:00',
1584
+ timestamps: lifecycleTimestamps({ research: true, review: true }),
638
1585
  },
639
1586
  });
1587
+ completeSemanticReview(criticalDir, 'independent');
640
1588
  const criticalResult = run(VALIDATOR, [criticalDir], ROOT);
641
1589
  assert.equal(criticalResult.status, 0, output(criticalResult));
642
1590
  } finally {
@@ -644,7 +1592,7 @@ test('spec-ready accepts pending execution evidence without an audit receipt', (
644
1592
  }
645
1593
  });
646
1594
 
647
- test('P1 bounded Standard validates without a task registry and fails closed on receipt or policy shape errors', () => {
1595
+ test('Specs v2 Compact validates without task registry or premature receipt and fails closed on policy shape errors', () => {
648
1596
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-bounded-'));
649
1597
  try {
650
1598
  const specDir = createBoundedSpec(root);
@@ -655,12 +1603,11 @@ test('P1 bounded Standard validates without a task registry and fails closed on
655
1603
  assert.equal(Object.hasOwn(persisted, 'task_files'), false);
656
1604
  assert.equal(Object.hasOwn(persisted, 'task_registry'), false);
657
1605
 
658
- fs.unlinkSync(path.join(specDir, 'feature-receipt.md'));
659
- const missingReceipt = run(VALIDATOR, [specDir], ROOT);
660
- assert.notEqual(missingReceipt.status, 0);
661
- assert.match(output(missingReceipt), /feature-receipt\.md: required/);
662
-
663
1606
  write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n\nBlocker: awaiting execution proof\n');
1607
+ const prematureReceipt = run(VALIDATOR, [specDir], ROOT);
1608
+ assert.notEqual(prematureReceipt.status, 0);
1609
+ assert.match(output(prematureReceipt), /premature execution receipt/);
1610
+ fs.unlinkSync(path.join(specDir, 'feature-receipt.md'));
664
1611
  const malformed = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
665
1612
  malformed.workflow_policy.extra = true;
666
1613
  fs.writeFileSync(path.join(specDir, 'spec.json'), JSON.stringify(malformed, null, 2));
@@ -672,7 +1619,7 @@ test('P1 bounded Standard validates without a task registry and fails closed on
672
1619
  }
673
1620
  });
674
1621
 
675
- test('P1 Standard scaffold remains bounded while Critical risk requires a task bundle', () => {
1622
+ test('Specs v2 scaffold keeps planning artifacts independent from assurance and task topology', () => {
676
1623
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-lanes-'));
677
1624
  try {
678
1625
  const scaffoldSource = fs.readFileSync(SCAFFOLD, 'utf8');
@@ -687,75 +1634,89 @@ test('P1 Standard scaffold remains bounded while Critical risk requires a task b
687
1634
  assert.equal(standard.status, 0, output(standard));
688
1635
  const standardDir = path.join(root, 'specs', 'bounded-standard');
689
1636
  const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
690
- assert.equal(standardSpec.workflow_policy.lane, 'Standard');
691
- assert.deepEqual(standardSpec.workflow_policy.proof_obligations, ['needsInspection', 'needsExecutionProof']);
1637
+ assert.equal(POLICY.readWorkflowPolicySnapshot(standardSpec).lane, 'Standard');
1638
+ assert.deepEqual(POLICY.readWorkflowPolicySnapshot(standardSpec).proof_obligations, ['needsExecutionProof']);
692
1639
  assert.equal(fs.existsSync(path.join(standardDir, 'tasks')), false);
693
1640
  assert.equal(fs.existsSync(path.join(standardDir, 'task_registry')), false);
1641
+ assert.equal(standardSpec.validation.semantic_review.status, 'not-run');
1642
+ assert.equal(fs.existsSync(path.join(standardDir, 'semantic-review.md')), false);
1643
+ write(path.join(standardDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The scaffolded command shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
1644
+ write(path.join(standardDir, 'design.md'), designFixture21());
1645
+ const standardPromoted = run(SCAFFOLD, ['bounded-standard', '--sync-semantic-model'], root);
1646
+ assert.equal(standardPromoted.status, 0, output(standardPromoted));
694
1647
  const standardValidation = run(VALIDATOR, [standardDir], ROOT);
695
1648
  assert.equal(standardValidation.status, 0, output(standardValidation));
696
1649
 
697
- const critical = run(SCAFFOLD, ['critical-risk', '--risks', 'auth'], root);
698
- assert.equal(critical.status, 2, output(critical));
699
- assert.match(output(critical), /Critical workflow policy requires a task bundle/);
1650
+ const elevated = run(SCAFFOLD, ['elevated-risk', '--risks', 'auth'], root);
1651
+ assert.equal(elevated.status, 0, output(elevated));
1652
+ const elevatedTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/elevated-risk/spec.json'), 'utf8'));
1653
+ assert.equal(elevatedTaskless.workflow_policy.planning_depth, 'Compact');
1654
+ assert.equal(elevatedTaskless.workflow_policy.assurance_level, 'Elevated');
1655
+ assert.equal(elevatedTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
1656
+ assert.deepEqual(elevatedTaskless.coordination, { boundaries: [] });
1657
+ assert.equal(fs.existsSync(path.join(root, 'specs/elevated-risk/research.md')), false);
1658
+ assert.equal(elevatedTaskless.validation.semantic_review.status, 'not-run');
1659
+ assert.equal(elevatedTaskless.authoring.tasks, 'absent');
1660
+
1661
+ const strict = run(SCAFFOLD, ['strict-opt-in', '--risks', 'auth', '--assurance-level', 'Strict'], root);
1662
+ assert.equal(strict.status, 0, output(strict));
1663
+ const strictTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/strict-opt-in/spec.json'), 'utf8'));
1664
+ assert.equal(strictTaskless.workflow_policy.assurance_level, 'Strict');
1665
+ assert.equal(strictTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
1666
+ assert.equal(strictTaskless.authoring.tasks, 'absent');
1667
+ } finally {
1668
+ fs.rmSync(root, { recursive: true, force: true });
1669
+ }
1670
+ });
700
1671
 
701
- const criticalHappy = run(SCAFFOLD, [
702
- 'critical-happy',
703
- '--risks', 'auth',
704
- '--tasks', 'R1-01-artifact',
1672
+ test('Full phase scaffold fills canonical 2.1 artifacts and passes E2E validation', () => {
1673
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-full-phase-'));
1674
+ try {
1675
+ const phase = [{
1676
+ id: 'delivery',
1677
+ task_ids: ['R1-01', 'R1-02'],
1678
+ entry_condition: 'requirements and design are approved',
1679
+ exit_condition: 'task verification plan is complete',
1680
+ owner_boundary: 'B-T',
1681
+ }];
1682
+ const result = run(SCAFFOLD, [
1683
+ 'full-phase', '--planning-depth', 'Full', '--tasks', 'R1-01-artifact,R1-02-consumer',
1684
+ '--boundaries', JSON.stringify([{
1685
+ id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'],
1686
+ precondition: 'Input is ready for transition', postcondition: 'Output is durably complete',
1687
+ failure: 'Failure state is recorded', recovery: 'Recovery resumes from recorded state',
1688
+ }]), '--phases', JSON.stringify(phase),
705
1689
  ], root);
706
- assert.equal(criticalHappy.status, 0, output(criticalHappy));
707
- const criticalDir = path.join(root, 'specs', 'critical-happy');
708
- const criticalSpec = JSON.parse(fs.readFileSync(path.join(criticalDir, 'spec.json'), 'utf8'));
709
- assert.equal(criticalSpec.workflow_policy.lane, 'Critical');
710
- assert.equal(criticalSpec.workflow_policy.artifact_profile, 'strict');
711
- assert.deepEqual(criticalSpec.workflow_policy.proof_obligations, [
712
- 'needsInspection',
713
- 'needsExecutionProof',
714
- 'needsIndependentAudit',
715
- 'needsResearchGrounding',
716
- ]);
717
- assert.deepEqual(criticalSpec.task_files, ['tasks/task-R1-01-artifact.md']);
718
- assert.equal(fs.existsSync(path.join(criticalDir, 'research.md')), true);
719
- completeScaffoldedSpec(criticalDir);
720
- const criticalValidation = run(VALIDATOR, [criticalDir], ROOT);
721
- assert.equal(criticalValidation.status, 0, output(criticalValidation));
1690
+ assert.equal(result.status, 0, output(result));
1691
+ const specDir = path.join(root, 'specs/full-phase');
1692
+ const spec = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1693
+ assert.deepEqual(spec.coordination.phases, phase);
1694
+ assert.equal(spec.coordination.boundaries[0].type, 'transition');
1695
+ completeScaffoldedSpec21(specDir);
1696
+ const validation = run(VALIDATOR, [specDir], ROOT);
1697
+ assert.equal(validation.status, 0, output(validation));
722
1698
  } finally {
723
1699
  fs.rmSync(root, { recursive: true, force: true });
724
1700
  }
725
1701
  });
726
1702
 
727
- test('producer scaffold emits optional artifacts and validator accepts or rejects the canonical shape', () => {
1703
+ test('producer scaffold projects typed dependency deliverable into registry edges', () => {
728
1704
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-artifacts-'));
729
1705
  try {
730
- const ordinary = run(SCAFFOLD, ['ordinary', '--tasks', 'R1-01-artifact'], root);
1706
+ const ordinary = run(SCAFFOLD, [
1707
+ 'ordinary', '--tasks', 'R1-01-artifact,R1-02-consumer',
1708
+ '--boundaries', JSON.stringify([{
1709
+ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
1710
+ deliverable: 'artifacts/output.json',
1711
+ }]),
1712
+ ], root);
731
1713
  assert.equal(ordinary.status, 0, output(ordinary));
732
1714
  const ordinarySpec = JSON.parse(fs.readFileSync(path.join(root, 'specs/ordinary/spec.json'), 'utf8'));
733
1715
  assert.equal(Object.hasOwn(ordinarySpec.task_registry['tasks/task-R1-01-artifact.md'], 'artifacts'), false);
734
- completeScaffoldedSpec(path.join(root, 'specs/ordinary'));
735
- const ordinaryAfterFill = run(VALIDATOR, [path.join(root, 'specs/ordinary')], ROOT);
736
- assert.equal(ordinaryAfterFill.status, 0, output(ordinaryAfterFill));
737
-
738
- const declared = run(SCAFFOLD, [
739
- 'declared',
740
- '--tasks',
741
- 'R1-01-artifact',
742
- '--artifacts',
743
- JSON.stringify({ 'R1-01-artifact': ['dist/output.js'] }),
744
- ], root);
745
- assert.equal(declared.status, 0, output(declared));
746
- const declaredDir = path.join(root, 'specs/declared');
747
- const declaredSpec = completeScaffoldedSpec(declaredDir);
748
- assert.deepEqual(declaredSpec.task_registry['tasks/task-R1-01-artifact.md'].artifacts, ['dist/output.js']);
749
- const declaredValidation = run(VALIDATOR, [declaredDir], ROOT);
750
- assert.equal(declaredValidation.status, 0, output(declaredValidation));
751
-
752
- const malformedPath = path.join(declaredDir, 'spec.json');
753
- const malformedSpec = JSON.parse(fs.readFileSync(malformedPath, 'utf8'));
754
- malformedSpec.task_registry['tasks/task-R1-01-artifact.md'].artifacts = ['../outside'];
755
- fs.writeFileSync(malformedPath, JSON.stringify(malformedSpec, null, 2));
756
- const malformedValidation = run(VALIDATOR, [declaredDir], ROOT);
757
- assert.notEqual(malformedValidation.status, 0);
758
- assert.match(output(malformedValidation), /task_registry.*artifacts.*safe relative path/);
1716
+ assert.deepEqual(ordinarySpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies, [
1717
+ 'tasks/task-R1-01-artifact.md',
1718
+ ]);
1719
+ assert.equal(ordinarySpec.coordination.boundaries[0].deliverable, 'artifacts/output.json');
759
1720
  } finally {
760
1721
  fs.rmSync(root, { recursive: true, force: true });
761
1722
  }
@@ -784,46 +1745,84 @@ test('validator rejects missing explicit requirement and sub-criterion mappings'
784
1745
  const partialResult = run(VALIDATOR, [partial], ROOT);
785
1746
  assert.notEqual(partialResult.status, 0);
786
1747
  assert.match(output(partialResult), /R1\.2: acceptance criterion not covered/);
1748
+
1749
+ const standardUnknown = createSpec(root, {
1750
+ name: 'standard-unknown-mapping',
1751
+ task: task({ mapping: '1.1, 9.1' }),
1752
+ spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
1753
+ });
1754
+ const standardUnknownResult = run(VALIDATOR, [standardUnknown], ROOT);
1755
+ assert.notEqual(standardUnknownResult.status, 0);
1756
+ assert.match(output(standardUnknownResult), /references unknown requirement R9/);
787
1757
  } finally {
788
1758
  fs.rmSync(root, { recursive: true, force: true });
789
1759
  }
790
1760
  });
791
1761
 
792
- test('validator rejects unknown, missing, and divergent contract copies', () => {
1762
+ test('v1 adapter rejects unknown, missing, and divergent contract copies', () => {
793
1763
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-'));
794
- const design = '# Design\n\n<!-- contract:PAYLOAD -->\n```json\n{ "id": 1 }\n```\n';
795
- try {
796
- const unknown = createSpec(root, {
797
- name: 'unknown', design,
798
- task: task({ extra: 'Contracts: MISSING\n' }),
1764
+ const design = `${designFixture()}<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`;
1765
+ function contractFixture(name, { designText = design, extra = '' } = {}) {
1766
+ return createSpec(root, {
1767
+ name,
1768
+ design: designText,
1769
+ task: legacyTask({ extra }),
1770
+ spec: { workflow_policy: legacyWorkflowPolicy() },
799
1771
  });
1772
+ }
1773
+ try {
1774
+ const unknown = contractFixture('unknown', { extra: 'Contracts: MISSING\n' });
800
1775
  let result = run(VALIDATOR, [unknown], ROOT);
801
1776
  assert.notEqual(result.status, 0);
802
1777
  assert.match(output(result), /unknown contract "MISSING"/);
803
1778
 
804
- const noCanonical = createSpec(root, {
805
- name: 'no-canonical',
806
- task: task({ extra: 'Contracts: PAYLOAD\n' }),
1779
+ const noCanonical = contractFixture('no-canonical', {
1780
+ designText: designFixture(),
1781
+ extra: 'Contracts: PAYLOAD\n',
807
1782
  });
808
1783
  result = run(VALIDATOR, [noCanonical], ROOT);
809
1784
  assert.notEqual(result.status, 0);
810
1785
  assert.match(output(result), /defines no canonical contract blocks/);
811
1786
 
812
- const missing = createSpec(root, {
813
- name: 'missing', design,
814
- task: task({ extra: 'Contracts: PAYLOAD\n' }),
815
- });
1787
+ const missing = contractFixture('missing', { extra: 'Contracts: PAYLOAD\n' });
816
1788
  result = run(VALIDATOR, [missing], ROOT);
817
1789
  assert.notEqual(result.status, 0);
818
1790
  assert.match(output(result), /contract "PAYLOAD" is missing/);
819
1791
 
820
- const divergent = createSpec(root, {
821
- name: 'divergent', design,
822
- task: task({ extra: 'Contracts: PAYLOAD\n\n```json\n{ "id": 2 }\n```\n' }),
1792
+ const divergent = contractFixture('divergent', {
1793
+ extra: 'Contracts: PAYLOAD\n\n```json\n{ "id": 2 }\n```\n',
823
1794
  });
824
1795
  result = run(VALIDATOR, [divergent], ROOT);
825
1796
  assert.notEqual(result.status, 0);
826
1797
  assert.match(output(result), /contract "PAYLOAD" body diverges/);
1798
+
1799
+ const duplicateDesign = contractFixture('duplicate-design-contract', {
1800
+ designText: `${design}\n<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`,
1801
+ });
1802
+ result = run(VALIDATOR, [duplicateDesign], ROOT);
1803
+ assert.notEqual(result.status, 0);
1804
+ assert.match(output(result), /duplicate canonical contract definition "PAYLOAD"/);
1805
+
1806
+ const repeatedDeclaration = contractFixture('repeated-task-contract-line', {
1807
+ extra: 'Contracts: PAYLOAD\nContracts: PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
1808
+ });
1809
+ result = run(VALIDATOR, [repeatedDeclaration], ROOT);
1810
+ assert.notEqual(result.status, 0);
1811
+ assert.match(output(result), /Contracts declaration must appear at most once/);
1812
+
1813
+ const duplicateName = contractFixture('duplicate-task-contract-name', {
1814
+ extra: 'Contracts: PAYLOAD, PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
1815
+ });
1816
+ result = run(VALIDATOR, [duplicateName], ROOT);
1817
+ assert.notEqual(result.status, 0);
1818
+ assert.match(output(result), /Contracts declaration contains duplicate name "PAYLOAD"/);
1819
+
1820
+ const orphanTagged = contractFixture('orphan-tagged-contract', {
1821
+ extra: '<!-- contract:PAYLOAD -->\n```json\n{ "id": 1 }\n```\n',
1822
+ });
1823
+ result = run(VALIDATOR, [orphanTagged], ROOT);
1824
+ assert.notEqual(result.status, 0);
1825
+ assert.match(output(result), /must declare it on one Contracts: line/);
827
1826
  } finally {
828
1827
  fs.rmSync(root, { recursive: true, force: true });
829
1828
  }
@@ -841,8 +1840,8 @@ test('validator rejects invalid ready approval and validation transitions', () =
841
1840
  });
842
1841
  const result = run(VALIDATOR, [specDir], ROOT);
843
1842
  assert.notEqual(result.status, 0);
844
- assert.match(output(result), /requires generated and agent_validated and user_approved requirements evidence/);
845
- assert.match(output(result), /requires generated and agent_validated and user_approved design evidence/);
1843
+ assert.match(output(result), /requires generated and agent_validated requirements evidence/);
1844
+ assert.match(output(result), /requires generated and agent_validated design evidence/);
846
1845
  assert.match(output(result), /validation evidence is incomplete/);
847
1846
 
848
1847
  // Legacy approved field must fail closed with migration guidance
@@ -875,6 +1874,37 @@ test('validator rejects invalid ready approval and validation transitions', () =
875
1874
  }
876
1875
  });
877
1876
 
1877
+ test('validator rejects precise square-bracket placeholders without flagging links or checkboxes', () => {
1878
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-square-placeholders-'));
1879
+ try {
1880
+ for (const [name, placeholder] of [
1881
+ ['specific-value', '[specific value]'],
1882
+ ['target-users', '[target users]'],
1883
+ ['component-name', '[Component Name]'],
1884
+ ]) {
1885
+ const specDir = createSpec(root, {
1886
+ name,
1887
+ task: task({ related: '`src/placeholder.js` | Read' }).replace(
1888
+ 'Implement the mapped observable behavior.',
1889
+ `Implement the mapped ${placeholder} behavior.`,
1890
+ ),
1891
+ });
1892
+ const result = run(VALIDATOR, [specDir], ROOT);
1893
+ assert.notEqual(result.status, 0);
1894
+ assert.match(output(result), /unresolved square-bracket placeholder/);
1895
+ }
1896
+
1897
+ const safe = createSpec(root, {
1898
+ name: 'safe-link-checkbox',
1899
+ task: `${task({ related: '`src/safe.js` | Read' })}\n[Component Name](https://example.invalid)\n- [ ] Keep this real checkbox.\n`,
1900
+ });
1901
+ const safeResult = run(VALIDATOR, [safe], ROOT);
1902
+ assert.equal(safeResult.status, 0, output(safeResult));
1903
+ } finally {
1904
+ fs.rmSync(root, { recursive: true, force: true });
1905
+ }
1906
+ });
1907
+
878
1908
  test('validator rejects malformed Related Files and dependency topology', () => {
879
1909
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-shape-'));
880
1910
  try {
@@ -886,7 +1916,8 @@ test('validator rejects malformed Related Files and dependency topology', () =>
886
1916
  ]) {
887
1917
  const specDir = createSpec(root, {
888
1918
  name,
889
- task: related ? task({ related }) : task({ related: 'Path | Action | Description\n|---|---|---|' }),
1919
+ task: legacyTask({ relatedRows: related ? `| ${related} | Invalid fixture row |` : '' }),
1920
+ spec: { workflow_policy: legacyWorkflowPolicy() },
890
1921
  });
891
1922
  const result = run(VALIDATOR, [specDir], ROOT);
892
1923
  assert.notEqual(result.status, 0);
@@ -896,10 +1927,19 @@ test('validator rejects malformed Related Files and dependency topology', () =>
896
1927
  const cycle = createSpec(root, { name: 'cycle' });
897
1928
  const statePath = path.join(cycle, 'spec.json');
898
1929
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
1930
+ state.coordination.task_triggers = ['distinct_ownership', 'real_dependency'];
899
1931
  state.task_files.push('tasks/task-R1-02-two.md');
900
1932
  state.task_registry['tasks/task-R1-01-one.md'].dependencies = ['tasks/task-R1-02-two.md'];
901
1933
  state.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: ['tasks/task-R1-01-one.md'], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
902
- write(path.join(cycle, 'tasks/task-R1-02-two.md'), task({ related: '`src/two.js` | Modify' }));
1934
+ write(path.join(cycle, 'tasks/task-R1-01-one.md'), task({
1935
+ related: '`src/one.js` | Read',
1936
+ dependencies: 'tasks/task-R1-02-two.md',
1937
+ }));
1938
+ write(path.join(cycle, 'tasks/task-R1-02-two.md'), task({
1939
+ id: 'R1-02',
1940
+ related: '`src/two.js` | Read',
1941
+ dependencies: 'tasks/task-R1-01-one.md',
1942
+ }));
903
1943
  write(statePath, JSON.stringify(state, null, 2));
904
1944
  const cycleResult = run(VALIDATOR, [cycle], ROOT);
905
1945
  assert.notEqual(cycleResult.status, 0);
@@ -913,7 +1953,11 @@ test('validator rejects malformed Related Files and dependency topology', () =>
913
1953
  const orderingState = JSON.parse(fs.readFileSync(orderingStatePath, 'utf8'));
914
1954
  orderingState.task_files.push('tasks/task-R1-02-two.md');
915
1955
  orderingState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
916
- write(path.join(ordering, 'tasks/task-R1-02-two.md'), task({ related: '`src/generated.js` | Modify' }));
1956
+ write(path.join(ordering, 'tasks/task-R1-02-two.md'), task({
1957
+ id: 'R1-02',
1958
+ related: '`src/generated.js` | Modify',
1959
+ relatedRows: '| `src/generated.js` | Modify | Consume generated implementation |',
1960
+ }));
917
1961
  write(orderingStatePath, JSON.stringify(orderingState, null, 2));
918
1962
  const orderingResult = run(VALIDATOR, [ordering], ROOT);
919
1963
  assert.notEqual(orderingResult.status, 0);
@@ -923,6 +1967,608 @@ test('validator rejects malformed Related Files and dependency topology', () =>
923
1967
  }
924
1968
  });
925
1969
 
1970
+ test('scaffold validates one dependency map before writes and persists canonical registry edges', () => {
1971
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-dependencies-'));
1972
+ const taskList = 'R1-01-owner,R1-02-consumer';
1973
+ const dependencyMap = JSON.stringify([{
1974
+ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
1975
+ deliverable: 'artifacts/owner-output.json',
1976
+ }]);
1977
+ try {
1978
+ const standard = run(SCAFFOLD, [
1979
+ 'dependency-positive', '--lane', 'Standard', '--tasks', taskList,
1980
+ '--boundaries', dependencyMap,
1981
+ ], root);
1982
+ assert.equal(standard.status, 0, output(standard));
1983
+ const standardDir = path.join(root, 'specs/dependency-positive');
1984
+ const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
1985
+ assert.deepEqual(
1986
+ standardSpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies,
1987
+ ['tasks/task-R1-01-owner.md'],
1988
+ );
1989
+ assert.deepEqual(standardSpec.coordination.boundaries, JSON.parse(dependencyMap));
1990
+ const standardTask = fs.readFileSync(
1991
+ path.join(standardDir, 'tasks/task-R1-02-consumer.md'),
1992
+ 'utf8',
1993
+ );
1994
+ assert.doesNotMatch(standardTask, /^## Execution Closure/m);
1995
+ assert.doesNotMatch(
1996
+ fs.readFileSync(path.join(standardDir, 'design.md'), 'utf8'),
1997
+ /^## Execution Closure/m,
1998
+ );
1999
+
2000
+ const invalid = run(SCAFFOLD, [
2001
+ 'dependency-invalid', '--tasks', taskList, '--boundaries', JSON.stringify([{
2002
+ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
2003
+ }]),
2004
+ ], root);
2005
+ assert.equal(invalid.status, 2, output(invalid));
2006
+ assert.match(output(invalid), /fields must be exactly.*deliverable|dependency fields must be exactly/);
2007
+ assert.equal(fs.existsSync(path.join(root, 'specs/dependency-invalid')), false);
2008
+
2009
+ } finally {
2010
+ fs.rmSync(root, { recursive: true, force: true });
2011
+ }
2012
+ });
2013
+
2014
+ test('task-level (P) requires dependency-free and non-overlapping task ownership', () => {
2015
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-parallel-'));
2016
+ const ownerPath = 'tasks/task-R1-01-one.md';
2017
+ const siblingPath = 'tasks/task-R1-02-two.md';
2018
+
2019
+ function parallelFixture(name, { dependency = false, overlap = false } = {}) {
2020
+ const ownerTask = task({
2021
+ related: overlap ? '`src/shared.js` | Read' : '`src/parallel-a.js` | Read',
2022
+ relatedRows: overlap
2023
+ ? '| `src/shared.js` | Read | Inspect existing |\n| `src/shared.js` | Modify | Update owned behavior |'
2024
+ : null,
2025
+ parallel: true,
2026
+ });
2027
+ const specDir = createSpec(root, { name, task: ownerTask });
2028
+ const statePath = path.join(specDir, 'spec.json');
2029
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
2030
+ state.coordination.task_triggers = dependency
2031
+ ? ['parallel_coordination', 'real_dependency', 'separate_proof']
2032
+ : ['parallel_coordination', 'separate_proof'];
2033
+ state.task_files.push(siblingPath);
2034
+ state.task_registry[siblingPath] = {
2035
+ id: 'R1-02', title: 'Two', status: 'pending',
2036
+ dependencies: dependency ? [ownerPath] : [],
2037
+ blocker: null, started_at: null, completed_at: null, last_updated_at: null,
2038
+ };
2039
+ const sibling = task({
2040
+ id: 'R1-02',
2041
+ related: overlap ? '`src/shared.js` | Read' : '`src/parallel-b.js` | Read',
2042
+ dependencies: dependency ? ownerPath : 'none',
2043
+ parallel: dependency,
2044
+ });
2045
+ write(path.join(specDir, siblingPath), sibling);
2046
+ write(statePath, JSON.stringify(state, null, 2));
2047
+ return specDir;
2048
+ }
2049
+
2050
+ try {
2051
+ const valid = parallelFixture('parallel-valid');
2052
+ assert.equal(run(VALIDATOR, [valid], ROOT).status, 0);
2053
+
2054
+ const dependency = parallelFixture('parallel-dependency', { dependency: true });
2055
+ const dependencyResult = run(VALIDATOR, [dependency], ROOT);
2056
+ assert.notEqual(dependencyResult.status, 0);
2057
+ assert.match(output(dependencyResult), /task-level \(P\) cannot declare sibling dependencies/);
2058
+
2059
+ const overlap = parallelFixture('parallel-overlap', { overlap: true });
2060
+ const overlapResult = run(VALIDATOR, [overlap], ROOT);
2061
+ assert.notEqual(overlapResult.status, 0);
2062
+ assert.match(output(overlapResult), /task-level \(P\) has Related Files contention on src\/shared\.js/);
2063
+ } finally {
2064
+ fs.rmSync(root, { recursive: true, force: true });
2065
+ }
2066
+ });
2067
+
2068
+ test('validator rejects dependency drift and non-canonical readiness statuses', () => {
2069
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-readiness-'));
2070
+ const ownerPath = 'tasks/task-R1-01-one.md';
2071
+ const consumerPath = 'tasks/task-R1-02-two.md';
2072
+ try {
2073
+ const driftDir = createSpec(root, {
2074
+ name: 'dependency-aligned',
2075
+ task: task({ related: '`src/owner.js` | Read' }),
2076
+ });
2077
+ const driftStatePath = path.join(driftDir, 'spec.json');
2078
+ const driftState = JSON.parse(fs.readFileSync(driftStatePath, 'utf8'));
2079
+ driftState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2080
+ driftState.task_files.push(consumerPath);
2081
+ driftState.task_registry[consumerPath] = {
2082
+ id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
2083
+ started_at: null, completed_at: null, last_updated_at: null,
2084
+ };
2085
+ write(
2086
+ path.join(driftDir, consumerPath),
2087
+ task({
2088
+ id: 'R1-02',
2089
+ related: '`src/consumer.js` | Read',
2090
+ dependencies: ownerPath,
2091
+ }),
2092
+ );
2093
+ write(driftStatePath, JSON.stringify(driftState, null, 2));
2094
+ const aligned = run(VALIDATOR, [driftDir], ROOT);
2095
+ assert.equal(aligned.status, 0, output(aligned));
2096
+
2097
+ const consumerFile = path.join(driftDir, consumerPath);
2098
+ write(
2099
+ consumerFile,
2100
+ fs.readFileSync(consumerFile, 'utf8').replace(
2101
+ `- ${ownerPath}\n\n## Verification Plan`,
2102
+ '- none\n\n## Verification Plan',
2103
+ ),
2104
+ );
2105
+ const drifted = run(VALIDATOR, [driftDir], ROOT);
2106
+ assert.notEqual(drifted.status, 0);
2107
+ assert.match(output(drifted), /dependency drift between spec\.json task_registry/);
2108
+
2109
+ const duplicateHeader = createSpec(root, {
2110
+ name: 'duplicate-dependency-section',
2111
+ task: `${task({ related: '`src/duplicate.js` | Read' })}\n## Dependencies\n\n- none\n`,
2112
+ });
2113
+ const duplicateHeaderResult = run(VALIDATOR, [duplicateHeader], ROOT);
2114
+ assert.notEqual(duplicateHeaderResult.status, 0);
2115
+ assert.match(output(duplicateHeaderResult), /machine-read section Dependencies.*must appear at most once/);
2116
+
2117
+ const duplicateSections = createSpec(root, {
2118
+ name: 'duplicate-machine-sections',
2119
+ task: `${task({ related: '`src/duplicate-sections.js` | Read' })}\n` +
2120
+ '## Verification Plan\n- **Command:** `npm test`\n\n' +
2121
+ '## Acceptance\n- **R1.1:** conflicting duplicate mapping\n',
2122
+ });
2123
+ const duplicateSectionsResult = run(VALIDATOR, [duplicateSections], ROOT);
2124
+ assert.notEqual(duplicateSectionsResult.status, 0);
2125
+ assert.match(output(duplicateSectionsResult), /machine-read section Verification Plan.*must appear at most once/);
2126
+ assert.match(output(duplicateSectionsResult), /machine-read section Acceptance.*must appear at most once/);
2127
+
2128
+ const ordinary = createSpec(root, {
2129
+ name: 'ordinary-in-progress',
2130
+ task: task({ related: '`src/ordinary.js` | Read', status: 'in_progress' }),
2131
+ spec: {
2132
+ task_registry: {
2133
+ [ownerPath]: {
2134
+ id: 'R1-01', title: 'One', status: 'in_progress', dependencies: [], blocker: null,
2135
+ started_at: '2026-08-12T00:00:00+07:00', completed_at: null,
2136
+ last_updated_at: '2026-08-12T00:01:00+07:00',
2137
+ artifacts: ['test/one.test.js'],
2138
+ },
2139
+ },
2140
+ },
2141
+ });
2142
+ const ordinaryResult = run(VALIDATOR, [ordinary], ROOT);
2143
+ assert.equal(ordinaryResult.status, 0, output(ordinaryResult));
2144
+
2145
+ const ordinaryStatePath = path.join(ordinary, 'spec.json');
2146
+ const ordinaryState = JSON.parse(fs.readFileSync(ordinaryStatePath, 'utf8'));
2147
+ const ordinaryTaskPath = path.join(ordinary, ownerPath);
2148
+ write(
2149
+ ordinaryTaskPath,
2150
+ fs.readFileSync(ordinaryTaskPath, 'utf8')
2151
+ .replace(/\n## Dependencies\n[\s\S]*?(?=\n## Verification Plan)/, ''),
2152
+ );
2153
+ ordinaryState.ready_for_implementation = true;
2154
+ ordinaryState.validation = { status: 'completed' };
2155
+ ordinaryState.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2156
+ write(ordinaryStatePath, JSON.stringify(ordinaryState, null, 2));
2157
+ const readyWithoutHeader = run(VALIDATOR, [ordinary], ROOT);
2158
+ assert.notEqual(readyWithoutHeader.status, 0);
2159
+ assert.match(output(readyWithoutHeader), /missing Dependencies/);
2160
+
2161
+ const invalidCases = [
2162
+ ['bad-ready-type', (state) => { state.ready_for_implementation = 'true'; }, /ready_for_implementation: must be a boolean/],
2163
+ ['bad-spec-status', (state) => { state.status = 'ready'; }, /spec\.json\.status: must be one of/],
2164
+ ['bad-task-status', (state) => { state.task_registry[ownerPath].status = 'ready'; }, /task_registry.*status: must be one of/],
2165
+ ['bad-validation-shape', (state) => { state.validation = 'not-run'; }, /validation: must be an object/],
2166
+ [
2167
+ 'registry-id-path-drift',
2168
+ (state) => { state.task_registry[ownerPath].id = 'R9-99'; },
2169
+ /task_registry id must match path-derived id R1-01/,
2170
+ ],
2171
+ [
2172
+ 'markdown-heading-id-drift',
2173
+ (_state, specDir) => {
2174
+ const taskFile = path.join(specDir, ownerPath);
2175
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('# Task R1-01:', '# Task R9-99:'));
2176
+ },
2177
+ /Markdown heading id R9-99 must match path-derived id R1-01/,
2178
+ ],
2179
+ [
2180
+ 'registry-markdown-status-drift',
2181
+ (state) => {
2182
+ state.task_registry[ownerPath].status = 'done';
2183
+ state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
2184
+ state.task_registry[ownerPath].completed_at = '2026-08-12T00:01:00+07:00';
2185
+ state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2186
+ },
2187
+ /status drift between spec\.json task_registry done and Markdown Status header pending/,
2188
+ ],
2189
+ [
2190
+ 'done-missing-lifecycle-timestamps',
2191
+ (state, specDir) => {
2192
+ state.task_registry[ownerPath].status = 'done';
2193
+ const taskFile = path.join(specDir, ownerPath);
2194
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** done'));
2195
+ },
2196
+ /done status requires started_at, completed_at, and last_updated_at timestamps/,
2197
+ ],
2198
+ [
2199
+ 'blocked-missing-blocker',
2200
+ (state, specDir) => {
2201
+ state.task_registry[ownerPath].status = 'blocked';
2202
+ state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2203
+ const taskFile = path.join(specDir, ownerPath);
2204
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2205
+ },
2206
+ /blocked status requires a non-empty blocker/,
2207
+ ],
2208
+ [
2209
+ 'in-progress-with-blocker',
2210
+ (state, specDir) => {
2211
+ state.task_registry[ownerPath].status = 'in_progress';
2212
+ state.task_registry[ownerPath].blocker = 'waiting on owner';
2213
+ state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
2214
+ state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2215
+ const taskFile = path.join(specDir, ownerPath);
2216
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
2217
+ },
2218
+ /in_progress status requires blocker null/,
2219
+ ],
2220
+ [
2221
+ 'in-progress-reversed-timestamps',
2222
+ (state, specDir) => {
2223
+ state.task_registry[ownerPath].status = 'in_progress';
2224
+ state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
2225
+ state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2226
+ const taskFile = path.join(specDir, ownerPath);
2227
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
2228
+ },
2229
+ /in_progress timestamps must satisfy started_at <= last_updated_at/,
2230
+ ],
2231
+ [
2232
+ 'started-blocked-reversed-timestamps',
2233
+ (state, specDir) => {
2234
+ state.task_registry[ownerPath].status = 'blocked';
2235
+ state.task_registry[ownerPath].blocker = 'waiting for the declared external authority';
2236
+ state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
2237
+ state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2238
+ const taskFile = path.join(specDir, ownerPath);
2239
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2240
+ },
2241
+ /started blocked timestamps must satisfy started_at <= last_updated_at/,
2242
+ ],
2243
+ [
2244
+ 'bad-validation-status',
2245
+ (state) => {
2246
+ state.ready_for_implementation = true;
2247
+ state.validation = { status: 'ready' };
2248
+ state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2249
+ },
2250
+ /validation\.status: must be one of|requires validation\.status completed/,
2251
+ ],
2252
+ [
2253
+ 'blocked-ready',
2254
+ (state, specDir) => {
2255
+ state.ready_for_implementation = true;
2256
+ state.status = 'blocked';
2257
+ state.blocker = 'owner unavailable';
2258
+ state.validation = { status: 'completed' };
2259
+ state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2260
+ state.task_registry[ownerPath].status = 'blocked';
2261
+ const taskFile = path.join(specDir, ownerPath);
2262
+ write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2263
+ },
2264
+ /cannot be blocked while ready_for_implementation is true/,
2265
+ ],
2266
+ ];
2267
+ for (const [name, mutate, expected] of invalidCases) {
2268
+ const specDir = createSpec(root, {
2269
+ name,
2270
+ task: task({ related: '`src/status.js` | Read' }),
2271
+ });
2272
+ const specPath = path.join(specDir, 'spec.json');
2273
+ const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
2274
+ mutate(state, specDir);
2275
+ write(specPath, JSON.stringify(state, null, 2));
2276
+ const result = run(VALIDATOR, [specDir], ROOT);
2277
+ assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
2278
+ assert.match(output(result), expected);
2279
+ }
2280
+ } finally {
2281
+ fs.rmSync(root, { recursive: true, force: true });
2282
+ }
2283
+ });
2284
+
2285
+ test('v1 Critical implementation obligations validate ownership, references, closure, and proof', () => {
2286
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-obligations-'));
2287
+ const ownerPath = 'tasks/task-R1-01-one.md';
2288
+ const consumerPath = 'tasks/task-R1-02-two.md';
2289
+ const criticalPolicy = legacyWorkflowPolicy('Critical');
2290
+
2291
+ function makeFixture(name, mutate = () => {}, policy = criticalPolicy) {
2292
+ const obligation = {
2293
+ id: 'canonical-transition',
2294
+ requirements: ['R1.1'],
2295
+ canonical_state: 'Persisted transition state and allowed values',
2296
+ authority: 'Owner module is the sole transition authority',
2297
+ recovery: 'Retry reconciles the persisted state before continuing',
2298
+ owner_task: ownerPath,
2299
+ consumer_tasks: [consumerPath],
2300
+ verification: 'node --test focused.test.js',
2301
+ evidence: 'Exit 0 and the declared state transition is observed',
2302
+ };
2303
+ const specDir = createSpec(root, {
2304
+ name,
2305
+ task: task({ related: '`src/owner.js` | Read' }),
2306
+ spec: { workflow_policy: policy },
2307
+ });
2308
+ const specPath = path.join(specDir, 'spec.json');
2309
+ write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
2310
+ const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
2311
+ state.task_files.push(consumerPath);
2312
+ state.task_registry[consumerPath] = {
2313
+ id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
2314
+ started_at: null, completed_at: null, last_updated_at: null,
2315
+ };
2316
+ write(
2317
+ path.join(specDir, consumerPath),
2318
+ task({
2319
+ id: 'R1-02',
2320
+ related: '`src/consumer.js` | Read',
2321
+ dependencies: ownerPath,
2322
+ extra: executionClosure(obligation),
2323
+ })
2324
+ .replace('# Task R1-01: One', '# Task R1-02: Two'),
2325
+ );
2326
+ const ownerFile = path.join(specDir, ownerPath);
2327
+ write(
2328
+ ownerFile,
2329
+ `${fs.readFileSync(ownerFile, 'utf8')}${executionClosure(obligation)}`,
2330
+ );
2331
+ const designFile = path.join(specDir, 'design.md');
2332
+ write(
2333
+ designFile,
2334
+ `${fs.readFileSync(designFile, 'utf8')}${executionClosure(obligation)}`,
2335
+ );
2336
+ state.implementation_obligations = [obligation];
2337
+ mutate(state, specDir);
2338
+ write(specPath, JSON.stringify(state, null, 2));
2339
+ return specDir;
2340
+ }
2341
+
2342
+ try {
2343
+ const omitted = createSpec(root, {
2344
+ name: 'critical-omitted-obligation-decision',
2345
+ spec: { workflow_policy: criticalPolicy },
2346
+ });
2347
+ write(path.join(omitted, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
2348
+ const omittedResult = run(VALIDATOR, [omitted], ROOT);
2349
+ assert.notEqual(omittedResult.status, 0);
2350
+ assert.match(output(omittedResult), /Critical scope must declare non-empty.*or one justified N\/A/);
2351
+
2352
+ const valid = makeFixture('valid-obligation');
2353
+ const validResult = run(VALIDATOR, [valid], ROOT);
2354
+ assert.equal(validResult.status, 0, output(validResult));
2355
+
2356
+ const tableClosure = makeFixture('valid-table-closure', (state, specDir) => {
2357
+ const designFile = path.join(specDir, 'design.md');
2358
+ const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
2359
+ write(designFile, `${designPrefix}${executionClosureTable(state.implementation_obligations[0])}`);
2360
+ });
2361
+ const tableClosureResult = run(VALIDATOR, [tableClosure], ROOT);
2362
+ assert.equal(tableClosureResult.status, 0, output(tableClosureResult));
2363
+
2364
+ const swapped = makeFixture('marker-local-swapped-obligations', (state, specDir) => {
2365
+ const first = state.implementation_obligations[0];
2366
+ const second = {
2367
+ ...first,
2368
+ id: 'secondary-transition',
2369
+ canonical_state: 'Secondary persisted state and terminal values',
2370
+ authority: 'Secondary module is the sole transition authority',
2371
+ recovery: 'Restart rolls the secondary state back before retry',
2372
+ verification: 'node --test secondary-focused.test.js',
2373
+ evidence: 'Exit 0 and the secondary persisted transition is observed',
2374
+ };
2375
+ state.implementation_obligations.push(second);
2376
+ for (const taskFile of [ownerPath, consumerPath]) {
2377
+ const filePath = path.join(specDir, taskFile);
2378
+ write(filePath, `${fs.readFileSync(filePath, 'utf8')}\n${obligationClosureBlock(second)}`);
2379
+ }
2380
+ const designFile = path.join(specDir, 'design.md');
2381
+ const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
2382
+ const firstWithSecondValues = { ...second, id: first.id };
2383
+ const secondWithFirstValues = { ...first, id: second.id };
2384
+ write(
2385
+ designFile,
2386
+ `${designPrefix}\n## Execution Closure\n\n` +
2387
+ `${obligationClosureBlock(firstWithSecondValues)}\n${obligationClosureBlock(secondWithFirstValues)}`,
2388
+ );
2389
+ });
2390
+ const swappedResult = run(VALIDATOR, [swapped], ROOT);
2391
+ assert.notEqual(swappedResult.status, 0);
2392
+ assert.match(output(swappedResult), /marker-local field/);
2393
+
2394
+ const cases = [
2395
+ [
2396
+ 'standard-only-rejected',
2397
+ () => {},
2398
+ legacyWorkflowPolicy(),
2399
+ /allowed only for Critical workflow policy/,
2400
+ ],
2401
+ [
2402
+ 'owner-must-be-one-task',
2403
+ (state) => { state.implementation_obligations[0].owner_task = [ownerPath]; },
2404
+ criticalPolicy,
2405
+ /owner_task: must name exactly one registered task/,
2406
+ ],
2407
+ [
2408
+ 'unknown-requirement',
2409
+ (state) => { state.implementation_obligations[0].requirements = ['R9.1']; },
2410
+ criticalPolicy,
2411
+ /unknown requirement reference R9\.1/,
2412
+ ],
2413
+ [
2414
+ 'unknown-consumer',
2415
+ (state) => { state.implementation_obligations[0].consumer_tasks = ['tasks/task-R1-99-missing.md']; },
2416
+ criticalPolicy,
2417
+ /unknown task reference tasks\/task-R1-99-missing\.md/,
2418
+ ],
2419
+ [
2420
+ 'missing-dependency-closure',
2421
+ (state, specDir) => {
2422
+ state.task_registry[consumerPath].dependencies = [];
2423
+ const taskFile = path.join(specDir, consumerPath);
2424
+ write(
2425
+ taskFile,
2426
+ fs.readFileSync(taskFile, 'utf8').replace(
2427
+ `- ${ownerPath}\n\n## Verification Plan`,
2428
+ '- none\n\n## Verification Plan',
2429
+ ),
2430
+ );
2431
+ },
2432
+ criticalPolicy,
2433
+ /must depend on owner tasks\/task-R1-01-one\.md/,
2434
+ ],
2435
+ [
2436
+ 'missing-proof',
2437
+ (state) => {
2438
+ state.implementation_obligations[0].verification = 'TBD';
2439
+ state.implementation_obligations[0].evidence = 'PENDING';
2440
+ },
2441
+ criticalPolicy,
2442
+ /verification: must be an executable command|evidence: must name an observable result/,
2443
+ ],
2444
+ [
2445
+ 'missing-state-authority-recovery',
2446
+ (state) => {
2447
+ delete state.implementation_obligations[0].canonical_state;
2448
+ state.implementation_obligations[0].authority = 'authority';
2449
+ state.implementation_obligations[0].recovery = 'recovery';
2450
+ },
2451
+ criticalPolicy,
2452
+ /canonical_state: must be concrete|authority: must be concrete|recovery: must be concrete/,
2453
+ ],
2454
+ [
2455
+ 'missing-design-correspondence',
2456
+ (_state, specDir) => {
2457
+ const designFile = path.join(specDir, 'design.md');
2458
+ write(
2459
+ designFile,
2460
+ fs.readFileSync(designFile, 'utf8').replace(
2461
+ '\n<!-- implementation-obligation:canonical-transition -->\n',
2462
+ '\n',
2463
+ ),
2464
+ );
2465
+ },
2466
+ criticalPolicy,
2467
+ /design\.md: missing implementation obligation marker canonical-transition/,
2468
+ ],
2469
+ [
2470
+ 'missing-owner-correspondence',
2471
+ (_state, specDir) => {
2472
+ const ownerFile = path.join(specDir, ownerPath);
2473
+ write(
2474
+ ownerFile,
2475
+ fs.readFileSync(ownerFile, 'utf8').replace(
2476
+ '\n<!-- implementation-obligation:canonical-transition -->\n',
2477
+ '\n',
2478
+ ),
2479
+ );
2480
+ },
2481
+ criticalPolicy,
2482
+ /task-R1-01-one\.md: missing implementation obligation marker canonical-transition/,
2483
+ ],
2484
+ [
2485
+ 'marker-outside-execution-closure',
2486
+ (_state, specDir) => {
2487
+ const designFile = path.join(specDir, 'design.md');
2488
+ const body = fs.readFileSync(designFile, 'utf8')
2489
+ .replace('\n<!-- implementation-obligation:canonical-transition -->\n', '\n');
2490
+ write(designFile, `${body}\n## Outside Closure\n\n<!-- implementation-obligation:canonical-transition -->\n`);
2491
+ },
2492
+ criticalPolicy,
2493
+ /implementation obligation marker canonical-transition must be inside Execution Closure/,
2494
+ ],
2495
+ [
2496
+ 'duplicate-closure-marker',
2497
+ (_state, specDir) => {
2498
+ const designFile = path.join(specDir, 'design.md');
2499
+ write(
2500
+ designFile,
2501
+ fs.readFileSync(designFile, 'utf8').replace(
2502
+ '<!-- implementation-obligation:canonical-transition -->',
2503
+ '<!-- implementation-obligation:canonical-transition -->\n<!-- implementation-obligation:canonical-transition -->',
2504
+ ),
2505
+ );
2506
+ },
2507
+ criticalPolicy,
2508
+ /duplicate implementation obligation marker canonical-transition/,
2509
+ ],
2510
+ [
2511
+ 'duplicate-execution-closure',
2512
+ (_state, specDir) => {
2513
+ const designFile = path.join(specDir, 'design.md');
2514
+ write(designFile, `${fs.readFileSync(designFile, 'utf8')}\n## Execution Closure\n\nNo duplicate allowed.\n`);
2515
+ },
2516
+ criticalPolicy,
2517
+ /implementation obligation markers require exactly one Execution Closure section/,
2518
+ ],
2519
+ [
2520
+ 'closure-json-drift',
2521
+ (_state, specDir) => {
2522
+ const ownerFile = path.join(specDir, ownerPath);
2523
+ write(
2524
+ ownerFile,
2525
+ fs.readFileSync(ownerFile, 'utf8').replace(
2526
+ 'Persisted transition state and allowed values',
2527
+ 'A divergent local transition description',
2528
+ ),
2529
+ );
2530
+ },
2531
+ criticalPolicy,
2532
+ /task-R1-01-one\.md: Execution Closure for canonical-transition must mirror spec\.json canonical_state/,
2533
+ ],
2534
+ [
2535
+ 'closure-requirements-drift',
2536
+ (_state, specDir) => {
2537
+ const designFile = path.join(specDir, 'design.md');
2538
+ write(
2539
+ designFile,
2540
+ fs.readFileSync(designFile, 'utf8').replace('- Requirements: R1.1', '- Requirements: R9.1'),
2541
+ );
2542
+ },
2543
+ criticalPolicy,
2544
+ /design\.md: Execution Closure for canonical-transition must mirror spec\.json requirements/,
2545
+ ],
2546
+ [
2547
+ 'duplicate-obligation-id',
2548
+ (state) => {
2549
+ state.implementation_obligations.push({ ...state.implementation_obligations[0] });
2550
+ },
2551
+ criticalPolicy,
2552
+ /duplicate obligation id canonical-transition/,
2553
+ ],
2554
+ [
2555
+ 'whitespace-obligation-id',
2556
+ (state) => { state.implementation_obligations[0].id = ' canonical-transition '; },
2557
+ criticalPolicy,
2558
+ /must be lowercase without surrounding whitespace/,
2559
+ ],
2560
+ ];
2561
+ for (const [name, mutate, policy, expected] of cases) {
2562
+ const specDir = makeFixture(name, mutate, policy);
2563
+ const result = run(VALIDATOR, [specDir], ROOT);
2564
+ assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
2565
+ assert.match(output(result), expected);
2566
+ }
2567
+ } finally {
2568
+ fs.rmSync(root, { recursive: true, force: true });
2569
+ }
2570
+ });
2571
+
926
2572
  test('grounding rejects empty sections, unsafe paths, unsupported actions, and zero-match globs', () => {
927
2573
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-'));
928
2574
  try {
@@ -958,7 +2604,10 @@ test('validator enforces Read before Modify for existing implementation', () =>
958
2604
  // Existing file Modify without preceding Read must fail
959
2605
  const missingRead = createSpec(root, {
960
2606
  name: 'missing-read',
961
- task: task({ related: '`src/existing.js` | Modify' }),
2607
+ task: task({
2608
+ related: '`src/existing.js` | Modify',
2609
+ relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
2610
+ }),
962
2611
  });
963
2612
  const missingResult = run(VALIDATOR, [missingRead], ROOT);
964
2613
  assert.notEqual(missingResult.status, 0);
@@ -967,7 +2616,7 @@ test('validator enforces Read before Modify for existing implementation', () =>
967
2616
  // Same path with Read in same task earlier row should pass
968
2617
  const withReadSameTask = createSpec(root, {
969
2618
  name: 'with-read-same',
970
- task: `# Task R1-01: One\n\n## Context\n- Existing.\n\n## Constraints\n- Keep.\n\n## Steps\n- [ ] Update\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing.js\` | Read | Inspect existing |\n| \`src/existing.js\` | Modify | Update |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/existing.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2619
+ task: task({ related: '`src/existing.js` | Modify' }),
971
2620
  });
972
2621
  const sameResult = run(VALIDATOR, [withReadSameTask], ROOT);
973
2622
  assert.equal(sameResult.status, 0, output(sameResult));
@@ -979,8 +2628,13 @@ test('validator enforces Read before Modify for existing implementation', () =>
979
2628
  depState.task_files.push('tasks/task-R1-02-two.md');
980
2629
  depState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
981
2630
  // Task 1 has Read, Task 2 modifies without depending
982
- write(path.join(depRead, 'tasks/task-R1-01-one.md'), `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Read\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing.js\` | Read | Inspect |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/existing.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
983
- write(path.join(depRead, 'tasks/task-R1-02-two.md'), task({ related: '`src/existing.js` | Modify' }));
2631
+ write(path.join(depRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/existing.js` | Read' }));
2632
+ write(path.join(depRead, 'tasks/task-R1-02-two.md'), task({
2633
+ id: 'R1-02',
2634
+ related: '`src/existing.js` | Modify',
2635
+ relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
2636
+ dependencies: 'tasks/task-R1-01-one.md',
2637
+ }));
984
2638
  write(depStatePath, JSON.stringify(depState, null, 2));
985
2639
  const depMissingResult = run(VALIDATOR, [depRead], ROOT);
986
2640
  assert.notEqual(depMissingResult.status, 0);
@@ -988,6 +2642,7 @@ test('validator enforces Read before Modify for existing implementation', () =>
988
2642
 
989
2643
  // If Modify task depends on Read task, should pass
990
2644
  depState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2645
+ depState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
991
2646
  write(depStatePath, JSON.stringify(depState, null, 2));
992
2647
  const depPass = run(VALIDATOR, [depRead], ROOT);
993
2648
  assert.equal(depPass.status, 0, output(depPass));
@@ -1018,17 +2673,22 @@ test('validator rejects reachability without concrete anchor and accepts with pa
1018
2673
  }
1019
2674
  });
1020
2675
 
1021
- test('validator fails closed when 5+ tasks have no contract blocks', () => {
2676
+ test('validator keeps contract blocks opt-in regardless of task count', () => {
1022
2677
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-closed-'));
1023
2678
  try {
1024
2679
  const specDir = createSpec(root, { name: 'five-no-contract' });
1025
2680
  const statePath = path.join(specDir, 'spec.json');
1026
2681
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
2682
+ state.design_context = { validation_recommended: true };
1027
2683
  for (let i = 2; i <= 5; i += 1) {
1028
2684
  const taskFile = `tasks/task-R1-0${i}-extra.md`;
1029
2685
  state.task_files.push(taskFile);
1030
2686
  state.task_registry[taskFile] = { id: `R1-0${i}`, title: `Extra ${i}`, status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
1031
- write(path.join(specDir, taskFile), task({ related: '`src/extra.js` | Modify', mapping: '1.1' }));
2687
+ write(path.join(specDir, taskFile), task({
2688
+ id: `R1-0${i}`,
2689
+ related: '`src/extra.js` | Modify',
2690
+ mapping: '1.1',
2691
+ }));
1032
2692
  }
1033
2693
  // Need to ensure each extra task has Read before Modify to avoid lifecycle error
1034
2694
  for (let i = 2; i <= 5; i += 1) {
@@ -1042,8 +2702,7 @@ test('validator fails closed when 5+ tasks have no contract blocks', () => {
1042
2702
  write(path.join(specDir, 'tasks/task-R1-01-one.md'), first);
1043
2703
  write(statePath, JSON.stringify(state, null, 2));
1044
2704
  const result = run(VALIDATOR, [specDir], ROOT);
1045
- assert.notEqual(result.status, 0);
1046
- assert.match(output(result), /5\+ task spec requires contract blocks/);
2705
+ assert.equal(result.status, 0, output(result));
1047
2706
  } finally {
1048
2707
  fs.rmSync(root, { recursive: true, force: true });
1049
2708
  }
@@ -1085,7 +2744,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1085
2744
  // Same-task: Create after Read must fail (dead-code regression)
1086
2745
  const createAfterRead = createSpec(root, {
1087
2746
  name: 'create-after-read',
1088
- task: `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Do work\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/new.js\` | Read | Read before create |\n| \`src/new.js\` | Create | Create after read |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/new.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2747
+ task: task({
2748
+ related: '`src/new.js` | Read',
2749
+ relatedRows: '| `src/new.js` | Read | Read before create |\n| `src/new.js` | Create | Create after read |',
2750
+ }),
1089
2751
  });
1090
2752
  const carResult = run(VALIDATOR, [createAfterRead], ROOT);
1091
2753
  assert.notEqual(carResult.status, 0);
@@ -1094,7 +2756,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1094
2756
  // Same-task: Create after Modify must fail
1095
2757
  const createAfterModify = createSpec(root, {
1096
2758
  name: 'create-after-modify',
1097
- task: `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Do work\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/new.js\` | Modify | Modify before create |\n| \`src/new.js\` | Create | Create after modify |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/new.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2759
+ task: task({
2760
+ related: '`src/new.js` | Modify',
2761
+ relatedRows: '| `src/new.js` | Modify | Modify before create |\n| `src/new.js` | Create | Create after modify |',
2762
+ }),
1098
2763
  });
1099
2764
  const camResult = run(VALIDATOR, [createAfterModify], ROOT);
1100
2765
  assert.notEqual(camResult.status, 0);
@@ -1103,7 +2768,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1103
2768
  // Same-task: Create before Read/Modify must pass; repeated Reads must not be rejected
1104
2769
  const createBefore = createSpec(root, {
1105
2770
  name: 'create-before',
1106
- task: `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Do work\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/new.js\` | Create | Create first |\n| \`src/new.js\` | Read | Read after create |\n| \`src/new.js\` | Modify | Modify after create |\n| \`src/new.js\` | Read | Second read |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/new.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2771
+ task: task({
2772
+ related: '`src/new.js` | Create',
2773
+ relatedRows: '| `src/new.js` | Create | Create first |\n| `src/new.js` | Read | Read after create |\n| `src/new.js` | Modify | Modify after create |\n| `src/new.js` | Read | Second read |',
2774
+ }),
1107
2775
  });
1108
2776
  const cbResult = run(VALIDATOR, [createBefore], ROOT);
1109
2777
  assert.equal(cbResult.status, 0, output(cbResult));
@@ -1111,7 +2779,10 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1111
2779
  // Valid same-task repeated reads without create must pass (not rejected unnecessarily)
1112
2780
  const repeatedReads = createSpec(root, {
1113
2781
  name: 'repeated-reads',
1114
- task: `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Do work\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing.js\` | Read | First read |\n| \`src/existing.js\` | Read | Second read |\n| \`src/existing.js\` | Modify | Modify after reads |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/existing.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2782
+ task: task({
2783
+ related: '`src/existing.js` | Read',
2784
+ relatedRows: '| `src/existing.js` | Read | First read |\n| `src/existing.js` | Read | Second read |\n| `src/existing.js` | Modify | Modify after reads |',
2785
+ }),
1115
2786
  });
1116
2787
  const rrResult = run(VALIDATOR, [repeatedReads], ROOT);
1117
2788
  assert.equal(rrResult.status, 0, output(rrResult));
@@ -1121,10 +2792,13 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1121
2792
  const crossReadStatePath = path.join(crossRead, 'spec.json');
1122
2793
  const crossReadState = JSON.parse(fs.readFileSync(crossReadStatePath, 'utf8'));
1123
2794
  // Task1 creates src/gen.js
1124
- write(path.join(crossRead, 'tasks/task-R1-01-one.md'), `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Create\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Create | Create gen |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/gen.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
2795
+ write(path.join(crossRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen.js` | Create' }));
1125
2796
  crossReadState.task_files.push('tasks/task-R1-02-two.md');
1126
2797
  crossReadState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
1127
- write(path.join(crossRead, 'tasks/task-R1-02-two.md'), `# Task R1-02: Two\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Read gen\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Read | Read gen without dep |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/gen.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
2798
+ write(path.join(crossRead, 'tasks/task-R1-02-two.md'), task({
2799
+ id: 'R1-02',
2800
+ related: '`src/gen.js` | Read',
2801
+ }));
1128
2802
  write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
1129
2803
  const crossReadFail = run(VALIDATOR, [crossRead], ROOT);
1130
2804
  assert.notEqual(crossReadFail.status, 0);
@@ -1132,6 +2806,13 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1132
2806
 
1133
2807
  // Cross-task Read with dependency must pass
1134
2808
  crossReadState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2809
+ crossReadState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2810
+ const crossReadTaskPath = path.join(crossRead, 'tasks/task-R1-02-two.md');
2811
+ write(crossReadTaskPath, task({
2812
+ id: 'R1-02',
2813
+ related: '`src/gen.js` | Read',
2814
+ dependencies: 'tasks/task-R1-01-one.md',
2815
+ }));
1135
2816
  write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
1136
2817
  const crossReadPass = run(VALIDATOR, [crossRead], ROOT);
1137
2818
  assert.equal(crossReadPass.status, 0, output(crossReadPass));
@@ -1140,15 +2821,27 @@ test('validator enforces Create before Read/Modify/Delete within same task and c
1140
2821
  const crossMod = createSpec(root, { name: 'cross-mod-base' });
1141
2822
  const crossModStatePath = path.join(crossMod, 'spec.json');
1142
2823
  const crossModState = JSON.parse(fs.readFileSync(crossModStatePath, 'utf8'));
1143
- write(path.join(crossMod, 'tasks/task-R1-01-one.md'), `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Create\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen2.js\` | Create | Create gen2 |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/gen2.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
2824
+ write(path.join(crossMod, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen2.js` | Create' }));
1144
2825
  crossModState.task_files.push('tasks/task-R1-02-two.md');
1145
2826
  crossModState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
1146
- write(path.join(crossMod, 'tasks/task-R1-02-two.md'), `# Task R1-02: Two\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Modify gen2\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen2.js\` | Modify | Modify without dep |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/gen2.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
2827
+ write(path.join(crossMod, 'tasks/task-R1-02-two.md'), task({
2828
+ id: 'R1-02',
2829
+ related: '`src/gen2.js` | Modify',
2830
+ relatedRows: '| `src/gen2.js` | Modify | Modify without dependency |',
2831
+ }));
1147
2832
  write(crossModStatePath, JSON.stringify(crossModState, null, 2));
1148
2833
  const crossModFail = run(VALIDATOR, [crossMod], ROOT);
1149
2834
  assert.notEqual(crossModFail.status, 0);
1150
2835
  assert.match(output(crossModFail), /modify of src\/gen2\.js must depend on creator/);
1151
2836
  crossModState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2837
+ crossModState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2838
+ const crossModTaskPath = path.join(crossMod, 'tasks/task-R1-02-two.md');
2839
+ write(crossModTaskPath, task({
2840
+ id: 'R1-02',
2841
+ related: '`src/gen2.js` | Modify',
2842
+ relatedRows: '| `src/gen2.js` | Modify | Modify generated implementation |',
2843
+ dependencies: 'tasks/task-R1-01-one.md',
2844
+ }));
1152
2845
  write(crossModStatePath, JSON.stringify(crossModState, null, 2));
1153
2846
  const crossModPass = run(VALIDATOR, [crossMod], ROOT);
1154
2847
  assert.equal(crossModPass.status, 0, output(crossModPass));