@haposoft/cafekit 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -5,21 +5,8 @@ const crypto = require('node:crypto');
5
5
  const fs = require('node:fs');
6
6
  const os = require('node:os');
7
7
  const path = require('node:path');
8
- const { spawnSync } = require('node:child_process');
9
8
  const test = require('node:test');
10
9
 
11
- const ROOT = path.resolve(__dirname, '../..');
12
- const SCRIPTS = path.join(ROOT, 'src/claude/scripts');
13
- const POLICY = require(path.join(SCRIPTS, 'workflow-policy.cjs'));
14
- const PROVENANCE = require(path.join(SCRIPTS, 'provenance.cjs'));
15
- const RECEIPT = require(path.join(SCRIPTS, 'spec-receipt.cjs'));
16
- const SCAFFOLD = path.join(SCRIPTS, 'spec-scaffold.cjs');
17
- const VALIDATOR = path.join(SCRIPTS, 'validate-spec-output.cjs');
18
- const AUTHORING_VALIDATION = path.join(SCRIPTS, 'spec-authoring-validation.cjs');
19
- const RUNTIME_CLOSURE = require('./test-runtime-dependency-closure.cjs');
20
- const SEMANTIC_MODEL = require(path.join(SCRIPTS, 'spec-semantic-model.cjs'));
21
- const FEATURE = 'closeout-demo';
22
- const TASK = 'tasks/task-R1-01-closeout.md';
23
10
  const TEMP_RUN_ID = String(process.env.CAFEKIT_CLOSEOUT_TEMP_RUN_ID || `${process.pid}-${Date.now()}`)
24
11
  .replace(/[^a-zA-Z0-9_-]/g, '-');
25
12
  const TEMP_PREFIX = `cafekit-v2-closeout-owned-${TEMP_RUN_ID}`;
@@ -69,362 +56,3 @@ test('temp resource scope cleans exact owned paths and restores HOME after failu
69
56
  assert.equal(Object.hasOwn(process.env, 'HOME'), originalHome.present);
70
57
  if (originalHome.present) assert.equal(process.env.HOME, originalHome.value);
71
58
  });
72
-
73
- function gitRoot(resources) {
74
- const root = resources.directory('git-root');
75
- for (const args of [['init', '-q'], ['config', 'user.email', 'test@example.invalid'], ['config', 'user.name', 'Test'], ['commit', '--allow-empty', '-qm', 'base']]) {
76
- const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
77
- assert.equal(result.status, 0, result.stderr);
78
- }
79
- return root;
80
- }
81
-
82
- function runtime(root, session = 'execution-session') {
83
- return PROVENANCE.deriveRuntimeContext({
84
- projectRoot: root,
85
- specsRoot: path.join(root, 'specs'),
86
- specFile: path.join(root, 'specs', FEATURE, 'spec.json'),
87
- featureName: FEATURE,
88
- runtimeSession: session,
89
- });
90
- }
91
-
92
- function receiptBody(ctx, extra = '') {
93
- return [
94
- 'Verification: PASS',
95
- 'Command: node --test',
96
- 'Exit: 0',
97
- 'Result: PASS',
98
- 'Expected: focused verification passes',
99
- 'Observed: focused verification passed',
100
- `Base: ${ctx.base}`,
101
- `Head: ${ctx.head}`,
102
- extra,
103
- ].filter(Boolean).join('\n') + '\n';
104
- }
105
-
106
- function prepareCanonicalTaskless(root, { specStatus, featureReceipt }) {
107
- const scaffold = spawnSync(process.execPath, [SCAFFOLD, FEATURE, '--title', 'Closeout fixture'], { cwd: root, encoding: 'utf8' });
108
- assert.equal(scaffold.status, 0, scaffold.stderr);
109
- const featureDir = path.join(root, 'specs', FEATURE);
110
- fs.mkdirSync(path.join(root, 'src'), { recursive: true });
111
- fs.mkdirSync(path.join(root, 'test'), { recursive: true });
112
- fs.writeFileSync(path.join(root, 'src/final-state.js'), 'module.exports = { ready: true };\n');
113
- fs.writeFileSync(path.join(root, 'test/final-state.test.js'), "require('node:test')('final state', () => {});\n");
114
- fs.writeFileSync(path.join(featureDir, 'requirements.md'), `# Requirements
115
-
116
- ## Requirements
117
- ### Requirement 1: Closeout state
118
- - **R1.1**: When closeout is requested, the system shall accept current validated state and reject stale semantic state.
119
- - **R1.2**: The system shall prove the current digest through the grounded final-state boundary.
120
- `);
121
- fs.writeFileSync(path.join(featureDir, 'design.md'), `# Design
122
-
123
- ## Architecture
124
- The final-state authority recomputes current semantic state before closeout.
125
-
126
- ## Typed Anchors
127
- | ID | Type | Target | Role | Access | Action |
128
- |---|---|---|---|---|---|
129
- | A-D-01 | file | \`src/final-state.js\` | design boundary | read | read |
130
- | A-D-02 | file | \`test/final-state.test.js\` | separate proof boundary | read | read |
131
-
132
- ## Canonical Contracts & Invariants
133
- ### D1 — Current-state decision
134
- The current persisted artifacts are authoritative.
135
- ### I1 — Stale-state invariant
136
- A changed semantic artifact cannot retain readiness.
137
- ### C1 — Closeout contract
138
- Only a validated and grounded current digest reaches closeout.
139
-
140
- ## Verification Definitions
141
- - **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.
142
-
143
- ## Verification
144
- R1.1 and R1.2 are verified by V1 against D1, I1, and C1.
145
- `);
146
- const specPath = path.join(featureDir, 'spec.json');
147
- const seed = JSON.parse(fs.readFileSync(specPath, 'utf8'));
148
- const seedProjection = SEMANTIC_MODEL.modelFromMarkdown(featureDir, seed);
149
- assert.deepEqual(seedProjection.errors, []);
150
- seed.semantic_model = seedProjection.model;
151
- fs.writeFileSync(specPath, `${JSON.stringify(seed, null, 2)}\n`);
152
- // I21: only the C16 coordinator may set an authoring stage to `validated`,
153
- // and only together with the digest receipt that proves it fresh. The fixture
154
- // runs the real command instead of hand-authoring the enum.
155
- const authored = spawnSync(
156
- process.execPath,
157
- [AUTHORING_VALIDATION, featureDir, '--root', root],
158
- { cwd: root, encoding: 'utf8' },
159
- );
160
- assert.equal(authored.status, 0, `${authored.stdout}\n${authored.stderr}`);
161
- const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
162
- assert.equal(spec.authoring.requirements, 'validated');
163
- assert.equal(spec.authoring.design, 'validated');
164
- spec.validation.status = 'completed';
165
- spec.validation.semantic_review = {
166
- status: 'completed',
167
- semantic_digest: null,
168
- verdict: 'PASS',
169
- lifecycle_disposition: 'CONTINUE',
170
- findings: [],
171
- unresolved_decisions: [],
172
- graph_coverage: [
173
- 'criterion_local', 'cross_criterion', 'runtime_path',
174
- 'assumption_provenance', 'compatibility_migration',
175
- ].map((surface) => ({ surface, covered: true, notes: `${surface} reviewed for the closeout fixture.` })),
176
- repair_round: 0,
177
- reviewer_evidence: {
178
- assurance: 'Routine',
179
- summary: 'Closeout fixture reviewer confirmed both criteria against the grounded final-state boundary.',
180
- },
181
- reviewed_criteria: ['R1.1', 'R1.2'],
182
- counterexamples: [{
183
- criterion: 'R1.1', case_kind: 'failure',
184
- scenario: 'A requirements artifact changes after the reviewed digest was recorded.',
185
- expected: 'Final-state authority rejects closeout until the current digest is reviewed.',
186
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
187
- }, {
188
- criterion: 'R1.2', case_kind: 'adversarial',
189
- scenario: 'A closeout supplies execution provenance while retaining a digest for older authored artifacts.',
190
- expected: 'Final-state authority rejects the stale digest despite valid execution provenance.',
191
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
192
- }],
193
- };
194
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
195
- const digest = spawnSync(process.execPath, [VALIDATOR, featureDir, '--semantic-digest'], { cwd: root, encoding: 'utf8' });
196
- assert.equal(digest.status, 0, `${digest.stdout}\n${digest.stderr}`);
197
- spec.validation.semantic_review.semantic_digest = digest.stdout.trim();
198
- // C13: a completed receipt is only terminal when the durable history records
199
- // it. The entry binds the same semantic digest, the receipt's own canonical
200
- // digest, and the attempt index the receipt reports as repair_round.
201
- spec.validation.semantic_review_history.entries = [{
202
- sequence: 0,
203
- semantic_digest: spec.validation.semantic_review.semantic_digest,
204
- review_receipt_digest: `sha256:${crypto.createHash('sha256')
205
- .update(SEMANTIC_MODEL.stableJson(spec.validation.semantic_review), 'utf8').digest('hex')}`,
206
- verdict: 'PASS',
207
- lifecycle_disposition: 'CONTINUE',
208
- blocking_count: 0,
209
- attempt_index: spec.validation.semantic_review.repair_round,
210
- review_epoch: 0,
211
- }];
212
- spec.ready_for_implementation = true;
213
- spec.status = specStatus === 'complete' ? 'done' : specStatus;
214
- delete spec.current_phase;
215
- delete spec.phase;
216
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
217
- const ctx = runtime(root);
218
- if (featureReceipt) fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(ctx, `Feature: ${FEATURE}`));
219
- return { featureDir, ctx };
220
- }
221
-
222
- function prepare(root, { taskless = false, specStatus = 'in_progress', taskStatus = 'done', taskReceipt = true, legacy = false, featureReceipt = false } = {}) {
223
- if (taskless) return prepareCanonicalTaskless(root, { specStatus, featureReceipt });
224
- const featureDir = path.join(root, 'specs', FEATURE);
225
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
226
- const policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' });
227
- const spec = {
228
- status: specStatus === 'complete' ? 'in_progress' : specStatus,
229
- current_phase: specStatus === 'complete' ? 'closeout' : 'implementation',
230
- feature_name: FEATURE, workflow_policy: policy, task_registry: {},
231
- };
232
- if (!taskless) spec.task_registry[TASK] = { status: taskStatus, completed_at: '2026-08-13T00:00:00.000Z', dependencies: [] };
233
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
234
- const ctx = runtime(root);
235
- if (!taskless) {
236
- const plan = [
237
- '# Task R1-01: closeout', `**Status:** ${taskStatus}`, '## Outcome', 'Closeout works.',
238
- '## Verification Plan', '- **Command:** `node --test`', '- **Expected:** focused verification passes',
239
- '- **Negative path:** not relevant', '- **Reachability:** not relevant',
240
- ].join('\n') + (legacy ? `\n## Evidence\n\n${receiptBody(ctx)}` : '\n');
241
- fs.writeFileSync(path.join(featureDir, TASK), plan);
242
- if (taskReceipt) {
243
- fs.mkdirSync(path.join(featureDir, 'receipts'), { recursive: true });
244
- fs.writeFileSync(path.join(featureDir, 'receipts', path.basename(TASK)), receiptBody(ctx, `Task: ${path.basename(TASK)}\nTask path: ${TASK}`));
245
- }
246
- }
247
- if (featureReceipt) fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(ctx, `Feature: ${FEATURE}`));
248
- return { featureDir, ctx };
249
- }
250
-
251
- function installGate(root, kind) {
252
- const runtimeDir = path.join(root, kind === 'claude' ? '.claude' : '.codex');
253
- fs.mkdirSync(path.join(runtimeDir, 'hooks'), { recursive: true });
254
- fs.mkdirSync(path.join(runtimeDir, 'hooks', 'lib'), { recursive: true });
255
- fs.mkdirSync(path.join(runtimeDir, 'scripts'), { recursive: true });
256
- // Copy the real transitive require closure rather than a hand-listed subset.
257
- // A hand-listed subset silently rots the moment one of these entrypoints
258
- // gains a new local dependency: the fixture keeps building, and the gate only
259
- // fails later with a bare MODULE_NOT_FOUND from inside the installed runtime.
260
- RUNTIME_CLOSURE.copyClaudeTestRuntime(ROOT, runtimeDir, [
261
- 'scripts/workflow-policy.cjs', 'scripts/provenance.cjs', 'scripts/spec-resolver.cjs',
262
- 'scripts/spec-receipt.cjs', 'scripts/validate-spec-output.cjs', 'scripts/spec-ground.cjs',
263
- 'scripts/spec-semantic-model.cjs', 'scripts/spec-final-state.cjs',
264
- 'hooks/lib/runtime-path-safety.cjs', 'hooks/lib/hook-state-dir.cjs', 'hooks/lib/runtime-dir.cjs',
265
- 'hooks/lib/hook-payload.cjs',
266
- ]);
267
- if (kind === 'claude') {
268
- fs.copyFileSync(path.join(ROOT, 'src/claude/hooks', 'spec-gate.cjs'), path.join(runtimeDir, 'hooks', 'spec-gate.cjs'));
269
- } else {
270
- fs.cpSync(path.join(ROOT, 'src/codex/hooks'), path.join(runtimeDir, 'hooks'), { recursive: true });
271
- }
272
- // The real installer materializes this canonical helper for both adapters.
273
- // Keep this narrow runtime fixture faithful to the same dependency closure.
274
- fs.copyFileSync(
275
- path.join(ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
276
- path.join(runtimeDir, 'hooks/lib/runtime-path-safety.cjs'),
277
- );
278
- return path.join(runtimeDir, 'hooks/spec-gate.cjs');
279
- }
280
-
281
- function gate(root, kind) {
282
- const home = path.join(root, '.claude', 'hooks', '.logs', 'test-home');
283
- fs.mkdirSync(home, { recursive: true });
284
- const hook = installGate(root, kind);
285
- const featureReceipt = path.join(root, 'specs', FEATURE, 'feature-receipt.md');
286
- if (fs.existsSync(featureReceipt)) {
287
- const current = runtime(root);
288
- fs.writeFileSync(featureReceipt, receiptBody(current, `Feature: ${FEATURE}`));
289
- assert.deepEqual(RECEIPT.checkFeatureReceipt(path.dirname(featureReceipt), current, POLICY).failures, []);
290
- }
291
- const result = spawnSync(process.execPath, [hook], {
292
- cwd: root,
293
- env: { ...process.env, HOME: home, USERPROFILE: home, ...(kind === 'claude' ? { CLAUDE_PROJECT_DIR: root } : {}) },
294
- input: JSON.stringify({ cwd: root, session_id: 'execution-session', hook_event_name: 'Stop', stop_hook_active: false }),
295
- encoding: 'utf8',
296
- });
297
- assert.equal(result.status, 0, result.stderr);
298
- return result.stdout.trim();
299
- }
300
-
301
- test('separate receipt passes; missing, traversal, symlink, and conflicting legacy proof fail closed', () => {
302
- withTempResources((resources) => {
303
- const root = gitRoot(resources);
304
- const { featureDir, ctx } = prepare(root);
305
- assert.deepEqual(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures, []);
306
- fs.unlinkSync(path.join(featureDir, 'receipts', path.basename(TASK)));
307
- assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('missing_receipt'));
308
- assert.equal(RECEIPT.canonicalTaskReceiptPath('../task-x.md'), null);
309
- const outside = path.join(root, 'outside.md');
310
- fs.writeFileSync(outside, receiptBody(ctx));
311
- fs.mkdirSync(path.join(featureDir, 'receipts'), { recursive: true });
312
- fs.symlinkSync(outside, path.join(featureDir, 'receipts', path.basename(TASK)));
313
- assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('unsafe_path'));
314
- fs.unlinkSync(path.join(featureDir, 'receipts', path.basename(TASK)));
315
- const taskFile = path.join(featureDir, TASK);
316
- fs.appendFileSync(taskFile, `\n## Evidence\n\n${receiptBody(ctx)}`);
317
- assert.equal(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).source, 'legacy');
318
- 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'));
319
- assert.ok(RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures.includes('receipt_conflict'));
320
- });
321
- });
322
-
323
- test('Claude and Codex gates require task proof at every Stop and feature proof only at durable closeout', () => {
324
- withTempResources((resources) => {
325
- for (const kind of ['claude', 'codex']) {
326
- const early = gitRoot(resources);
327
- const invalidAlias = gitRoot(resources);
328
- const taskless = gitRoot(resources);
329
- const taskBearing = gitRoot(resources);
330
- prepare(early, { taskless: true, specStatus: 'in_progress' });
331
- assert.equal(gate(early, kind), '', `${kind} pre-closeout`);
332
- const { featureDir: invalidAliasDir } = prepare(invalidAlias, { taskless: true, specStatus: 'in_progress' });
333
- const invalidAliasFile = path.join(invalidAliasDir, 'spec.json');
334
- const invalidAliasSpec = JSON.parse(fs.readFileSync(invalidAliasFile, 'utf8'));
335
- invalidAliasSpec.status = 'completed';
336
- fs.writeFileSync(invalidAliasFile, `${JSON.stringify(invalidAliasSpec, null, 2)}\n`);
337
- assert.match(gate(invalidAlias, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} blocks legacy 2.1 closeout with the migration message`);
338
- prepare(taskless, { taskless: true, specStatus: 'complete', featureReceipt: true });
339
- assert.match(gate(taskless, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} taskless 2.1 closeout is retired`);
340
- fs.appendFileSync(path.join(taskless, 'specs', FEATURE, 'requirements.md'), '\nSemantic mutation after review.\n');
341
- assert.match(gate(taskless, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} still blocks a changed 2.1 packet`);
342
- prepare(taskBearing, { taskReceipt: false, featureReceipt: false });
343
- assert.match(gate(taskBearing, kind), new RegExp(path.basename(TASK)), `${kind} unproven done task before closeout`);
344
- prepare(taskBearing, { taskReceipt: true, featureReceipt: false });
345
- assert.equal(gate(taskBearing, kind), '', `${kind} proven done task before closeout`);
346
- assert.equal(fs.existsSync(path.join(taskBearing, 'specs', FEATURE, 'feature-receipt.md')), false);
347
- prepare(taskBearing, { specStatus: 'complete', featureReceipt: false });
348
- assert.match(gate(taskBearing, kind), /feature-receipt\.md/);
349
- prepare(taskBearing, { specStatus: 'complete', featureReceipt: true });
350
- assert.equal(gate(taskBearing, kind), '');
351
- fs.appendFileSync(path.join(taskBearing, 'specs', FEATURE, 'receipts', path.basename(TASK)), 'Exit: 1\n');
352
- assert.match(gate(taskBearing, kind), new RegExp(path.basename(TASK)));
353
- }
354
- });
355
- });
356
-
357
- test('an unproven done dependency cannot advance a dependent task', () => {
358
- withTempResources((resources) => {
359
- for (const kind of ['claude', 'codex']) {
360
- const root = gitRoot(resources);
361
- const { featureDir } = prepare(root, { taskReceipt: false });
362
- const dependent = 'tasks/task-R1-02-dependent.md';
363
- const specFile = path.join(featureDir, 'spec.json');
364
- const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
365
- spec.task_registry[dependent] = { status: 'pending', completed_at: null, dependencies: [TASK] };
366
- fs.writeFileSync(specFile, JSON.stringify(spec));
367
- fs.writeFileSync(path.join(featureDir, dependent), '# Task R1-02: dependent\n\n**Status:** pending\n');
368
- const blocked = gate(root, kind);
369
- assert.match(blocked, new RegExp(path.basename(TASK)), `${kind} must reject the unproven done dependency`);
370
- assert.equal(JSON.parse(fs.readFileSync(specFile, 'utf8')).task_registry[dependent].status, 'pending');
371
- }
372
- });
373
- });
374
-
375
- test('shared taskless terminal predicate requires canonical 2.1 policy and physical tasklessness', () => {
376
- withTempResources((resources) => {
377
- const root = gitRoot(resources);
378
- const { featureDir } = prepare(root, { taskless: true, specStatus: 'complete' });
379
- const specPath = path.join(featureDir, 'spec.json');
380
- const canonical = JSON.parse(fs.readFileSync(specPath, 'utf8'));
381
- assert.equal(POLICY.isCanonicalTasklessTerminalSpec(canonical, featureDir), true);
382
-
383
- const legacy = {
384
- ...canonical,
385
- schema_version: '2.0',
386
- workflow_policy: POLICY.workflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' }),
387
- };
388
- assert.equal(POLICY.isCanonicalTasklessTerminalSpec(legacy, featureDir), false);
389
-
390
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
391
- fs.writeFileSync(path.join(featureDir, 'tasks', 'hidden.md'), '# physical task\n');
392
- assert.equal(POLICY.isCanonicalTasklessTerminalSpec(canonical, featureDir), false);
393
- });
394
- });
395
-
396
- test('scaffold creates planning artifacts but no task or feature receipts', () => {
397
- withTempResources((resources) => {
398
- const root = resources.directory('scaffold-root');
399
- const result = spawnSync(process.execPath, [path.join(SCRIPTS, 'spec-scaffold.cjs'), 'no-receipt', '--specs-root', 'specs'], { cwd: root, encoding: 'utf8' });
400
- assert.equal(result.status, 0, result.stderr);
401
- const files = fs.readdirSync(path.join(root, 'specs', 'no-receipt'), { recursive: true }).map(String);
402
- assert.equal(files.some((file) => file.includes('receipt')), false);
403
- });
404
- });
405
-
406
- test('legacy receipt still requires provenance under a clean committed tree', () => {
407
- // The workflow task path may validate a committed, unchanged receipt on
408
- // structure alone; the legacy task and feature receipt paths must not.
409
- withTempResources((resources) => {
410
- const root = gitRoot(resources);
411
- const { featureDir } = prepare(root);
412
- const stale = { base: 'f'.repeat(40), head: 'e'.repeat(40) };
413
- fs.writeFileSync(
414
- path.join(featureDir, 'receipts', path.basename(TASK)),
415
- receiptBody(stale, `Task: ${path.basename(TASK)}\nTask path: ${TASK}`),
416
- );
417
- fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(stale, `Feature: ${FEATURE}`));
418
- for (const args of [['add', '-A'], ['commit', '-qm', 'committed legacy receipts']]) {
419
- const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
420
- assert.equal(result.status, 0, result.stderr);
421
- }
422
- const clean = spawnSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
423
- assert.equal(clean.stdout.trim(), '', 'fixture tree must be clean before the check');
424
- const ctx = runtime(root);
425
- const taskFailures = RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures;
426
- assert.ok(taskFailures.includes('provenance'), `legacy task receipts keep provenance binding when committed and clean; got ${JSON.stringify(taskFailures)}`);
427
- const featureFailures = RECEIPT.checkFeatureReceipt(featureDir, ctx, POLICY).failures;
428
- assert.ok(featureFailures.includes('provenance'), `feature receipts keep provenance binding when committed and clean; got ${JSON.stringify(featureFailures)}`);
429
- });
430
- });
@@ -6,10 +6,9 @@ const os = require('node:os');
6
6
  const path = require('node:path');
7
7
 
8
8
  const CLAUDE_RUNTIME_ENTRYPOINTS = Object.freeze([
9
- 'scripts/spec-final-state.cjs',
10
9
  'scripts/spec-receipt.cjs',
11
10
  'scripts/spec-resolver.cjs',
12
- 'scripts/validate-spec-output.cjs',
11
+ 'scripts/workflow-policy.cjs',
13
12
  ]);
14
13
 
15
14
  function inside(root, target) {
@@ -38,6 +38,7 @@ function install(root, platforms) {
38
38
  const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
39
39
  { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
40
40
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
41
+ return result;
41
42
  }
42
43
 
43
44
  function commands(settings) {
@@ -55,7 +56,7 @@ test('fresh claude install ships no usage hook', () => {
55
56
  });
56
57
  });
57
58
 
58
- test('upgrade removes an installed usage hook and its CafeKit entries but keeps a foreign one', () => {
59
+ test('upgrade preserves an edited usage hook while removing its CafeKit entries', () => {
59
60
  inTempProject((root) => {
60
61
  install(root, ['claude']);
61
62
  const settingsPath = path.join(root, '.claude', 'settings.json');
@@ -74,8 +75,9 @@ test('upgrade removes an installed usage hook and its CafeKit entries but keeps
74
75
  });
75
76
  fs.writeFileSync(settingsPath, JSON.stringify(before, null, 2));
76
77
 
77
- install(root, ['claude']);
78
- assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', 'usage.cjs')), false, 'an edited copy is removed too');
78
+ const upgrade = install(root, ['claude']);
79
+ assert.equal(fs.readFileSync(path.join(root, '.claude', 'hooks', 'usage.cjs'), 'utf8'), '// user edit\n');
80
+ assert.match(upgrade.stdout + upgrade.stderr, /Preserved 1 obsolete runtime/);
79
81
  const after = commands(JSON.parse(fs.readFileSync(settingsPath, 'utf8')));
80
82
  assert.equal(after.includes(CAFEKIT_USAGE_COMMAND), false, 'both CafeKit entries are pruned');
81
83
  assert.equal(after.includes(FOREIGN_USAGE_COMMAND), true, 'a hook outside .claude/hooks/ survives');