@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
@@ -0,0 +1,421 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+ const test = require('node:test');
9
+
10
+ const ROOT = path.resolve(__dirname, '../..');
11
+ const SCRIPTS = path.join(ROOT, 'src/claude/scripts');
12
+ const POLICY = require(path.join(SCRIPTS, 'workflow-policy.cjs'));
13
+ const PROVENANCE = require(path.join(SCRIPTS, 'provenance.cjs'));
14
+ const RECEIPT = require(path.join(SCRIPTS, 'spec-receipt.cjs'));
15
+ const SCAFFOLD = path.join(SCRIPTS, 'spec-scaffold.cjs');
16
+ const VALIDATOR = path.join(SCRIPTS, 'validate-spec-output.cjs');
17
+ const SEMANTIC_MODEL = require(path.join(SCRIPTS, 'spec-semantic-model.cjs'));
18
+ const CLAUDE_CHECK = require(path.join(ROOT, 'src/claude/hooks/completion-authority-check.cjs'));
19
+ const CODEX_CHECK = require(path.join(ROOT, 'src/codex/hooks/completion-authority-check.cjs'));
20
+ const FEATURE = 'closeout-demo';
21
+ const TASK = 'tasks/task-R1-01-closeout.md';
22
+ const TEMP_RUN_ID = String(process.env.CAFEKIT_CLOSEOUT_TEMP_RUN_ID || `${process.pid}-${Date.now()}`)
23
+ .replace(/[^a-zA-Z0-9_-]/g, '-');
24
+ const TEMP_PREFIX = `cafekit-v2-closeout-owned-${TEMP_RUN_ID}`;
25
+
26
+ function withTempResources(callback) {
27
+ const ownedPaths = [];
28
+ const originalHome = Object.hasOwn(process.env, 'HOME')
29
+ ? { present: true, value: process.env.HOME }
30
+ : { present: false };
31
+ function ownedDirectory(label) {
32
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), `${TEMP_PREFIX}-${label}-`));
33
+ ownedPaths.push(directory);
34
+ return directory;
35
+ }
36
+ const resources = {
37
+ directory: ownedDirectory,
38
+ useHome(label) {
39
+ const directory = ownedDirectory(label);
40
+ process.env.HOME = directory;
41
+ return directory;
42
+ },
43
+ };
44
+ try {
45
+ return callback(resources);
46
+ } finally {
47
+ if (originalHome.present) process.env.HOME = originalHome.value;
48
+ else delete process.env.HOME;
49
+ for (const ownedPath of ownedPaths.reverse()) {
50
+ fs.rmSync(ownedPath, { recursive: true, force: true });
51
+ }
52
+ }
53
+ }
54
+
55
+ test('temp resource scope cleans exact owned paths and restores HOME after failure', () => {
56
+ const originalHome = Object.hasOwn(process.env, 'HOME')
57
+ ? { present: true, value: process.env.HOME }
58
+ : { present: false };
59
+ let root;
60
+ let home;
61
+ assert.throws(() => withTempResources((resources) => {
62
+ root = resources.directory('failure-root');
63
+ home = resources.useHome('failure-home');
64
+ throw new Error('intentional resource-scope failure');
65
+ }), /intentional resource-scope failure/);
66
+ assert.equal(fs.existsSync(root), false);
67
+ assert.equal(fs.existsSync(home), false);
68
+ assert.equal(Object.hasOwn(process.env, 'HOME'), originalHome.present);
69
+ if (originalHome.present) assert.equal(process.env.HOME, originalHome.value);
70
+ });
71
+
72
+ function gitRoot(resources) {
73
+ const root = resources.directory('git-root');
74
+ for (const args of [['init', '-q'], ['config', 'user.email', 'test@example.invalid'], ['config', 'user.name', 'Test'], ['commit', '--allow-empty', '-qm', 'base']]) {
75
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
76
+ assert.equal(result.status, 0, result.stderr);
77
+ }
78
+ return root;
79
+ }
80
+
81
+ function runtime(root, session = 'execution-session') {
82
+ return PROVENANCE.deriveRuntimeContext({
83
+ projectRoot: root,
84
+ specsRoot: path.join(root, 'specs'),
85
+ specFile: path.join(root, 'specs', FEATURE, 'spec.json'),
86
+ featureName: FEATURE,
87
+ runtimeSession: session,
88
+ });
89
+ }
90
+
91
+ function receiptBody(ctx, extra = '') {
92
+ return [
93
+ 'Verification: PASS',
94
+ 'Command: node --test',
95
+ 'Exit: 0',
96
+ 'Result: PASS',
97
+ 'Expected: focused verification passes',
98
+ 'Observed: focused verification passed',
99
+ `Base: ${ctx.base}`,
100
+ `Head: ${ctx.head}`,
101
+ extra,
102
+ ].filter(Boolean).join('\n') + '\n';
103
+ }
104
+
105
+ function prepareCanonicalTaskless(root, { specStatus, featureReceipt }) {
106
+ const scaffold = spawnSync(process.execPath, [SCAFFOLD, FEATURE, '--title', 'Closeout fixture'], { cwd: root, encoding: 'utf8' });
107
+ assert.equal(scaffold.status, 0, scaffold.stderr);
108
+ const featureDir = path.join(root, 'specs', FEATURE);
109
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
110
+ fs.mkdirSync(path.join(root, 'test'), { recursive: true });
111
+ fs.writeFileSync(path.join(root, 'src/final-state.js'), 'module.exports = { ready: true };\n');
112
+ fs.writeFileSync(path.join(root, 'test/final-state.test.js'), "require('node:test')('final state', () => {});\n");
113
+ fs.writeFileSync(path.join(featureDir, 'requirements.md'), `# Requirements
114
+
115
+ ## Requirements
116
+ ### Requirement 1: Closeout state
117
+ - **R1.1**: When closeout is requested, the system shall accept current validated state and reject stale semantic state.
118
+ - **R1.2**: The system shall prove the current digest through the grounded final-state boundary.
119
+ `);
120
+ fs.writeFileSync(path.join(featureDir, 'design.md'), `# Design
121
+
122
+ ## Architecture
123
+ The final-state authority recomputes current semantic state before closeout.
124
+
125
+ ## Typed Anchors
126
+ | ID | Type | Target | Role | Access | Action |
127
+ |---|---|---|---|---|---|
128
+ | A-D-01 | file | \`src/final-state.js\` | design boundary | read | read |
129
+ | A-D-02 | file | \`test/final-state.test.js\` | separate proof boundary | read | read |
130
+
131
+ ## Canonical Contracts & Invariants
132
+ ### D1 — Current-state decision
133
+ The current persisted artifacts are authoritative.
134
+ ### I1 — Stale-state invariant
135
+ A changed semantic artifact cannot retain readiness.
136
+ ### C1 — Closeout contract
137
+ Only a validated and grounded current digest reaches closeout.
138
+
139
+ ## Verification Definitions
140
+ - **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 inspection \`src/final-state.js\` via A-D-01; Expected current validated state reaches closeout with the reviewed digest; Negative/failure stale semantic state is rejected before execution closeout; Reachability/grounding entrypoint \`src/final-state.js\` via A-D-01, A-D-02.
141
+
142
+ ## Verification
143
+ R1.1 and R1.2 are verified by V1 against D1, I1, and C1.
144
+ `);
145
+ const specPath = path.join(featureDir, 'spec.json');
146
+ const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
147
+ spec.authoring.requirements = 'validated';
148
+ spec.authoring.design = 'validated';
149
+ spec.validation.status = 'completed';
150
+ spec.validation.semantic_review = {
151
+ status: 'completed', semantic_digest: null, reviewed_criteria: ['R1.1', 'R1.2'],
152
+ counterexamples: [{
153
+ criterion: 'R1.1', case_kind: 'failure',
154
+ scenario: 'A requirements artifact changes after the reviewed digest was recorded.',
155
+ expected: 'Final-state authority rejects closeout until the current digest is reviewed.',
156
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
157
+ }, {
158
+ criterion: 'R1.2', case_kind: 'adversarial',
159
+ scenario: 'A closeout supplies execution provenance while retaining a digest for older authored artifacts.',
160
+ expected: 'Final-state authority rejects the stale digest despite valid execution provenance.',
161
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
162
+ }],
163
+ };
164
+ const projection = SEMANTIC_MODEL.modelFromMarkdown(featureDir, spec);
165
+ assert.deepEqual(projection.errors, []);
166
+ spec.semantic_model = projection.model;
167
+ fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
168
+ const digest = spawnSync(process.execPath, [VALIDATOR, featureDir, '--semantic-digest'], { cwd: root, encoding: 'utf8' });
169
+ assert.equal(digest.status, 0, `${digest.stdout}\n${digest.stderr}`);
170
+ spec.validation.semantic_review.semantic_digest = digest.stdout.trim();
171
+ spec.ready_for_implementation = true;
172
+ spec.status = specStatus === 'complete' ? 'done' : specStatus;
173
+ delete spec.current_phase;
174
+ delete spec.phase;
175
+ fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
176
+ const ctx = runtime(root);
177
+ if (featureReceipt) fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(ctx, `Feature: ${FEATURE}`));
178
+ return { featureDir, ctx };
179
+ }
180
+
181
+ function prepare(root, { taskless = false, specStatus = 'in_progress', taskStatus = 'done', taskReceipt = true, legacy = false, featureReceipt = false } = {}) {
182
+ if (taskless) return prepareCanonicalTaskless(root, { specStatus, featureReceipt });
183
+ const featureDir = path.join(root, 'specs', FEATURE);
184
+ fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
185
+ const policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' });
186
+ const spec = {
187
+ status: specStatus === 'complete' ? 'in_progress' : specStatus,
188
+ current_phase: specStatus === 'complete' ? 'closeout' : 'implementation',
189
+ feature_name: FEATURE, workflow_policy: policy, task_registry: {},
190
+ };
191
+ if (!taskless) spec.task_registry[TASK] = { status: taskStatus, completed_at: '2026-08-13T00:00:00.000Z', dependencies: [] };
192
+ fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
193
+ const ctx = runtime(root);
194
+ if (!taskless) {
195
+ const plan = [
196
+ '# Task R1-01: closeout', `**Status:** ${taskStatus}`, '## Outcome', 'Closeout works.',
197
+ '## Verification Plan', '- **Command:** `node --test`', '- **Expected:** focused verification passes',
198
+ '- **Negative path:** not relevant', '- **Reachability:** not relevant',
199
+ ].join('\n') + (legacy ? `\n## Evidence\n\n${receiptBody(ctx)}` : '\n');
200
+ fs.writeFileSync(path.join(featureDir, TASK), plan);
201
+ if (taskReceipt) {
202
+ fs.mkdirSync(path.join(featureDir, 'receipts'), { recursive: true });
203
+ fs.writeFileSync(path.join(featureDir, 'receipts', path.basename(TASK)), receiptBody(ctx, `Task: ${path.basename(TASK)}\nTask path: ${TASK}`));
204
+ }
205
+ }
206
+ if (featureReceipt) fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(ctx, `Feature: ${FEATURE}`));
207
+ return { featureDir, ctx };
208
+ }
209
+
210
+ function installGate(root, kind) {
211
+ const runtimeDir = path.join(root, kind === 'claude' ? '.claude' : '.codex');
212
+ fs.mkdirSync(path.join(runtimeDir, 'hooks'), { recursive: true });
213
+ fs.mkdirSync(path.join(runtimeDir, 'hooks', 'lib'), { recursive: true });
214
+ fs.mkdirSync(path.join(runtimeDir, 'scripts'), { recursive: true });
215
+ for (const name of [
216
+ 'workflow-policy.cjs', 'provenance.cjs', 'spec-resolver.cjs', 'spec-receipt.cjs',
217
+ 'validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs',
218
+ ]) {
219
+ fs.copyFileSync(path.join(SCRIPTS, name), path.join(runtimeDir, 'scripts', name));
220
+ }
221
+ if (kind === 'claude') {
222
+ for (const name of [
223
+ 'spec-gate.cjs', 'completion-authority-check.cjs', 'completion-authority-state.cjs',
224
+ 'semantic-review-authority.cjs',
225
+ ]) fs.copyFileSync(path.join(ROOT, 'src/claude/hooks', name), path.join(runtimeDir, 'hooks', name));
226
+ } else {
227
+ fs.cpSync(path.join(ROOT, 'src/codex/hooks'), path.join(runtimeDir, 'hooks'), { recursive: true });
228
+ }
229
+ // The real installer materializes this canonical helper for both adapters.
230
+ // Keep this narrow runtime fixture faithful to the same dependency closure.
231
+ fs.copyFileSync(
232
+ path.join(ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
233
+ path.join(runtimeDir, 'hooks/lib/runtime-path-safety.cjs'),
234
+ );
235
+ return path.join(runtimeDir, 'hooks/spec-gate.cjs');
236
+ }
237
+
238
+ function gate(root, kind) {
239
+ const home = path.join(root, '.claude', 'hooks', '.logs', 'test-home');
240
+ fs.mkdirSync(home, { recursive: true });
241
+ const hook = installGate(root, kind);
242
+ const featureReceipt = path.join(root, 'specs', FEATURE, 'feature-receipt.md');
243
+ if (fs.existsSync(featureReceipt)) {
244
+ const current = runtime(root);
245
+ fs.writeFileSync(featureReceipt, receiptBody(current, `Feature: ${FEATURE}`));
246
+ assert.deepEqual(RECEIPT.checkFeatureReceipt(path.dirname(featureReceipt), current, POLICY).failures, []);
247
+ }
248
+ const result = spawnSync(process.execPath, [hook], {
249
+ cwd: root,
250
+ env: { ...process.env, HOME: home, USERPROFILE: home, ...(kind === 'claude' ? { CLAUDE_PROJECT_DIR: root } : {}) },
251
+ input: JSON.stringify({ cwd: root, session_id: 'execution-session', hook_event_name: 'Stop', stop_hook_active: false }),
252
+ encoding: 'utf8',
253
+ });
254
+ assert.equal(result.status, 0, result.stderr);
255
+ return result.stdout.trim();
256
+ }
257
+
258
+ test('separate receipt passes; missing, traversal, symlink, and conflicting legacy proof fail closed', () => {
259
+ withTempResources((resources) => {
260
+ const root = gitRoot(resources);
261
+ const { featureDir, ctx } = prepare(root);
262
+ assert.deepEqual(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures, []);
263
+ fs.unlinkSync(path.join(featureDir, 'receipts', path.basename(TASK)));
264
+ assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('missing_receipt'));
265
+ assert.equal(RECEIPT.canonicalTaskReceiptPath('../task-x.md'), null);
266
+ const outside = path.join(root, 'outside.md');
267
+ fs.writeFileSync(outside, receiptBody(ctx));
268
+ fs.mkdirSync(path.join(featureDir, 'receipts'), { recursive: true });
269
+ fs.symlinkSync(outside, path.join(featureDir, 'receipts', path.basename(TASK)));
270
+ assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('unsafe_path'));
271
+ fs.unlinkSync(path.join(featureDir, 'receipts', path.basename(TASK)));
272
+ const taskFile = path.join(featureDir, TASK);
273
+ fs.appendFileSync(taskFile, `\n## Evidence\n\n${receiptBody(ctx)}`);
274
+ assert.equal(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).source, 'legacy');
275
+ fs.writeFileSync(path.join(featureDir, 'receipts', path.basename(TASK)), receiptBody(ctx, `Task: ${path.basename(TASK)}\nTask path: ${TASK}`).replace('Command: node --test', 'Command: node --test changed'));
276
+ assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('receipt_conflict'));
277
+ });
278
+ });
279
+
280
+ test('Claude and Codex gates require task proof at every Stop and feature proof only at durable closeout', () => {
281
+ withTempResources((resources) => {
282
+ for (const kind of ['claude', 'codex']) {
283
+ const early = gitRoot(resources);
284
+ const invalidAlias = gitRoot(resources);
285
+ const taskless = gitRoot(resources);
286
+ const taskBearing = gitRoot(resources);
287
+ prepare(early, { taskless: true, specStatus: 'in_progress' });
288
+ assert.equal(gate(early, kind), '', `${kind} pre-closeout`);
289
+ const { featureDir: invalidAliasDir } = prepare(invalidAlias, { taskless: true, specStatus: 'in_progress' });
290
+ const invalidAliasFile = path.join(invalidAliasDir, 'spec.json');
291
+ const invalidAliasSpec = JSON.parse(fs.readFileSync(invalidAliasFile, 'utf8'));
292
+ invalidAliasSpec.status = 'completed';
293
+ fs.writeFileSync(invalidAliasFile, `${JSON.stringify(invalidAliasSpec, null, 2)}\n`);
294
+ assert.match(gate(invalidAlias, kind), /spec status is invalid: completed/, `${kind} rejects legacy aliases in schema 2.1`);
295
+ prepare(taskless, { taskless: true, specStatus: 'complete', featureReceipt: true });
296
+ assert.equal(gate(taskless, kind), '', `${kind} taskless closeout`);
297
+ fs.appendFileSync(path.join(taskless, 'specs', FEATURE, 'requirements.md'), '\nSemantic mutation after review.\n');
298
+ assert.match(gate(taskless, kind), /validation failed|semantic digest|stale/i, `${kind} rejects stale semantics with unchanged execution provenance`);
299
+ prepare(taskBearing, { taskReceipt: false, featureReceipt: false });
300
+ assert.match(gate(taskBearing, kind), new RegExp(path.basename(TASK)), `${kind} unproven done task before closeout`);
301
+ prepare(taskBearing, { taskReceipt: true, featureReceipt: false });
302
+ assert.equal(gate(taskBearing, kind), '', `${kind} proven done task before closeout`);
303
+ assert.equal(fs.existsSync(path.join(taskBearing, 'specs', FEATURE, 'feature-receipt.md')), false);
304
+ prepare(taskBearing, { specStatus: 'complete', featureReceipt: false });
305
+ assert.match(gate(taskBearing, kind), /feature-receipt\.md/);
306
+ prepare(taskBearing, { specStatus: 'complete', featureReceipt: true });
307
+ assert.equal(gate(taskBearing, kind), '');
308
+ fs.appendFileSync(path.join(taskBearing, 'specs', FEATURE, 'receipts', path.basename(TASK)), 'Exit: 1\n');
309
+ assert.match(gate(taskBearing, kind), new RegExp(path.basename(TASK)));
310
+ }
311
+ });
312
+ });
313
+
314
+ test('an unproven done dependency cannot advance a dependent task', () => {
315
+ withTempResources((resources) => {
316
+ for (const kind of ['claude', 'codex']) {
317
+ const root = gitRoot(resources);
318
+ const { featureDir } = prepare(root, { taskReceipt: false });
319
+ const dependent = 'tasks/task-R1-02-dependent.md';
320
+ const specFile = path.join(featureDir, 'spec.json');
321
+ const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
322
+ spec.task_registry[dependent] = { status: 'pending', completed_at: null, dependencies: [TASK] };
323
+ fs.writeFileSync(specFile, JSON.stringify(spec));
324
+ fs.writeFileSync(path.join(featureDir, dependent), '# Task R1-02: dependent\n\n**Status:** pending\n');
325
+ const blocked = gate(root, kind);
326
+ assert.match(blocked, new RegExp(path.basename(TASK)), `${kind} must reject the unproven done dependency`);
327
+ assert.equal(JSON.parse(fs.readFileSync(specFile, 'utf8')).task_registry[dependent].status, 'pending');
328
+ }
329
+ });
330
+ });
331
+
332
+ test('completion authority supports taskless v2 and binds task plan plus receipt bytes', () => {
333
+ for (const [kind, checker] of [['claude', CLAUDE_CHECK], ['codex', CODEX_CHECK]]) {
334
+ withTempResources((resources) => {
335
+ const root = gitRoot(resources);
336
+ resources.useHome(`${kind}-home`);
337
+ prepare(root, { taskless: true, specStatus: 'complete', featureReceipt: true });
338
+ const args = { policy: POLICY, projectRoot: root, runtime: {}, payload: { session_id: 'execution-session' } };
339
+ if (kind === 'claude') args.resolver = require(path.join(SCRIPTS, 'spec-resolver.cjs'));
340
+ const result = checker.evaluateCloseout(args);
341
+ assert.equal(result.ok, true, result.reason);
342
+ assert.match(result.binding.feature_receipt_digest, /^[a-f0-9]{64}$/);
343
+ });
344
+ }
345
+ });
346
+
347
+ test('shared taskless terminal predicate requires canonical 2.1 policy and physical tasklessness', () => {
348
+ withTempResources((resources) => {
349
+ const root = gitRoot(resources);
350
+ const { featureDir } = prepare(root, { taskless: true, specStatus: 'complete' });
351
+ const specPath = path.join(featureDir, 'spec.json');
352
+ const canonical = JSON.parse(fs.readFileSync(specPath, 'utf8'));
353
+ assert.equal(POLICY.isCanonicalTasklessTerminalSpec(canonical, featureDir), true);
354
+
355
+ const legacy = {
356
+ ...canonical,
357
+ schema_version: '2.0',
358
+ workflow_policy: POLICY.workflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' }),
359
+ };
360
+ assert.equal(POLICY.isCanonicalTasklessTerminalSpec(legacy, featureDir), false);
361
+
362
+ fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
363
+ fs.writeFileSync(path.join(featureDir, 'tasks', 'hidden.md'), '# physical task\n');
364
+ assert.equal(POLICY.isCanonicalTasklessTerminalSpec(canonical, featureDir), false);
365
+ });
366
+ });
367
+
368
+ test('completion authority activates only at durable closeout and honors the Stop loop guard', () => {
369
+ for (const [kind, checker] of [['claude', CLAUDE_CHECK], ['codex', CODEX_CHECK]]) {
370
+ withTempResources((resources) => {
371
+ const root = gitRoot(resources);
372
+ resources.useHome(`${kind}-home`);
373
+ prepare(root, { featureReceipt: false });
374
+ const args = { policy: POLICY, projectRoot: root, runtime: {}, payload: { session_id: 'execution-session' } };
375
+ if (kind === 'claude') args.resolver = require(path.join(SCRIPTS, 'spec-resolver.cjs'));
376
+ const inactive = checker.evaluateCloseout(args);
377
+ assert.equal(inactive.ok, true, inactive.reason);
378
+ assert.equal(inactive.active, false);
379
+
380
+ args.payload.stop_hook_active = true;
381
+ const loop = checker.evaluateCloseout(args);
382
+ assert.equal(loop.ok, true, loop.reason);
383
+ assert.equal(loop.active, false);
384
+
385
+ prepare(root, { specStatus: 'complete', featureReceipt: false });
386
+ args.payload.stop_hook_active = false;
387
+ const blocked = checker.evaluateCloseout(args);
388
+ assert.equal(blocked.ok, false);
389
+ assert.match(blocked.reason, /feature-receipt\.md/);
390
+ });
391
+ }
392
+ });
393
+
394
+ test('v2 floor merge keeps axes independent, planning controls artifact profile, and risks raise assurance floor', () => {
395
+ const policy = {
396
+ PLANNING_DEPTHS: ['None', 'Compact', 'Full'], ASSURANCE_LEVELS: ['Routine', 'Elevated', 'Strict'],
397
+ compatibilityLane: POLICY.compatibilityLane, planningObligationsFor: POLICY.planningObligationsFor,
398
+ obligationsForAssurance: POLICY.obligationsForAssurance, actorNeedsFor: POLICY.actorNeedsFor,
399
+ classifyLane: POLICY.classifyLane,
400
+ };
401
+ const floor = { version: '2', planning_depth: 'Full', automatic_planning_depth: 'Full', assurance_level: 'Routine', automatic_assurance_level: 'Routine', lane: 'Standard', automatic_lane: 'Standard', risks: [], artifact_profile: 'strict', planning_obligations: [], proof_obligations: [], actor_needs: [] };
402
+ const current = { ...floor, planning_depth: 'Compact', automatic_planning_depth: 'Compact', risks: ['privacy'], artifact_profile: 'bounded' };
403
+ for (const checker of [CLAUDE_CHECK, CODEX_CHECK]) {
404
+ const merged = checker.mergePolicyFloor(policy, floor, current);
405
+ assert.equal(merged.planning_depth, 'Full');
406
+ assert.equal(merged.artifact_profile, 'strict');
407
+ assert.equal(merged.automatic_assurance_level, 'Elevated');
408
+ assert.equal(merged.assurance_level, 'Elevated');
409
+ assert.ok(merged.proof_obligations.includes('needsExecutionProof'));
410
+ }
411
+ });
412
+
413
+ test('scaffold creates planning artifacts but no task or feature receipts', () => {
414
+ withTempResources((resources) => {
415
+ const root = resources.directory('scaffold-root');
416
+ const result = spawnSync(process.execPath, [path.join(SCRIPTS, 'spec-scaffold.cjs'), 'no-receipt', '--specs-root', 'specs'], { cwd: root, encoding: 'utf8' });
417
+ assert.equal(result.status, 0, result.stderr);
418
+ const files = fs.readdirSync(path.join(root, 'specs', 'no-receipt'), { recursive: true }).map(String);
419
+ assert.equal(files.some((file) => file.includes('receipt')), false);
420
+ });
421
+ });