@haposoft/cafekit 0.16.0-rc.2 → 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 (98) hide show
  1. package/bin/__tests__/codex-hooks.test.js +794 -21
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +1838 -112
  4. package/bin/__tests__/installer-safety.test.js +82 -29
  5. package/bin/__tests__/opencode-spec-gate.test.js +799 -0
  6. package/bin/__tests__/package-inventory.test.js +841 -10
  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 +2469 -71
  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 +153 -202
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
  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 +604 -56
  22. package/src/claude/hooks/completion-authority-check.cjs +56 -0
  23. package/src/claude/hooks/completion-authority-state.cjs +527 -0
  24. package/src/claude/hooks/completion-authority.cjs +154 -0
  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/session.cjs +33 -19
  31. package/src/claude/hooks/spec-gate.cjs +181 -139
  32. package/src/claude/hooks/spec-state.cjs +112 -47
  33. package/src/claude/migration-manifest.json +15 -2
  34. package/src/claude/rules/state-sync.md +52 -21
  35. package/src/claude/rules/workflow.md +17 -9
  36. package/src/claude/scripts/provenance.cjs +315 -0
  37. package/src/claude/scripts/spec-final-state.cjs +244 -0
  38. package/src/claude/scripts/spec-ground.cjs +535 -173
  39. package/src/claude/scripts/spec-readiness.cjs +118 -0
  40. package/src/claude/scripts/spec-receipt.cjs +162 -0
  41. package/src/claude/scripts/spec-resolver.cjs +501 -0
  42. package/src/claude/scripts/spec-scaffold.cjs +1945 -55
  43. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  44. package/src/claude/scripts/validate-spec-output.cjs +3204 -262
  45. package/src/claude/scripts/workflow-policy.cjs +1888 -203
  46. package/src/claude/settings/settings.json +12 -0
  47. package/src/claude/skills/code-review/SKILL.md +94 -66
  48. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  49. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  50. package/src/claude/skills/develop/SKILL.md +138 -283
  51. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  52. package/src/claude/skills/develop/references/quality-gate.md +123 -148
  53. package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
  54. package/src/claude/skills/specs/SKILL.md +192 -397
  55. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  56. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  57. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  58. package/src/claude/skills/specs/references/review.md +200 -260
  59. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  60. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  61. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  62. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  63. package/src/claude/skills/specs/rules/design-review.md +42 -97
  64. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  65. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  66. package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
  67. package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
  68. package/src/claude/skills/specs/templates/design.md +49 -263
  69. package/src/claude/skills/specs/templates/requirements.md +26 -36
  70. package/src/claude/skills/specs/templates/research.md +9 -98
  71. package/src/claude/skills/specs/templates/spec-state.json +26 -67
  72. package/src/claude/skills/specs/templates/task.md +30 -82
  73. package/src/claude/skills/sync/SKILL.md +45 -15
  74. package/src/claude/skills/sync/references/sync-protocols.md +34 -16
  75. package/src/claude/skills/test/SKILL.md +151 -167
  76. package/src/codex/AGENTS.md +6 -0
  77. package/src/codex/hooks/completion-authority-check.cjs +63 -0
  78. package/src/codex/hooks/completion-authority-state.cjs +527 -0
  79. package/src/codex/hooks/completion-authority.cjs +106 -0
  80. package/src/codex/hooks/lib/hook-context.cjs +58 -4
  81. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  82. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  83. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  84. package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
  85. package/src/codex/hooks/lib/spec-utils.cjs +104 -27
  86. package/src/codex/hooks/privacy-approval.cjs +1 -1
  87. package/src/codex/hooks/privacy-block.cjs +67 -11
  88. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  89. package/src/codex/hooks/session.cjs +2 -0
  90. package/src/codex/hooks/spec-gate.cjs +151 -24
  91. package/src/codex/hooks/spec-state.cjs +65 -12
  92. package/src/codex/hooks/state.cjs +2 -0
  93. package/src/codex/hooks.json +16 -1
  94. package/src/codex/rules/state-sync.md +47 -27
  95. package/src/common/AGENTS.md +1 -1
  96. package/src/opencode/AGENTS.md +4 -2
  97. package/src/opencode/plugins/spec-gate.ts +296 -122
  98. package/src/opencode/plugins/spec-state.ts +183 -116
@@ -104,6 +104,22 @@ for (const command of [
104
104
  'cat .env*',
105
105
  'sed -n 1p .env',
106
106
  "bash -c 'sed -n 1p .env'",
107
+ "python3 -c \"open('.env').read()\"",
108
+ "node -e \"fs.readFileSync('.env')\"",
109
+ "bash -c 'cat .e''nv'",
110
+ 'cat .e\\\nnv',
111
+ "cat $'\\x2e\\x65\\x6e\\x76'",
112
+ 'env -u UNUSED cat "$FILE"',
113
+ 'timeout 1 cat "$FILE"',
114
+ 'nice -n 10 cat "$FILE"',
115
+ "eval 'cat \"$FILE\"'",
116
+ "find . -exec sh -c 'cat \"$FILE\"' \\;",
117
+ 'source "$FILE"',
118
+ 'xargs -a "$LIST" cat',
119
+ 'python3 -c \'open(os.environ["FILE"]).read()\'',
120
+ 'node -e \'fs.readFileSync(process.env.FILE)\'',
121
+ 'cat < "$FILE"',
122
+ 'cat -n "$FILE"',
107
123
  ]) {
108
124
  test(`asks natively for Bash sensitive reference: ${command}`, () => {
109
125
  const dir = tmpDir();
@@ -117,3 +133,159 @@ for (const command of [
117
133
  }
118
134
  });
119
135
  }
136
+
137
+ // --- Regression: variable indirection and command substitution (F-01) ---
138
+ // Dynamic expansion in any read operand fails closed. Single-quoted dollars are literal.
139
+ for (const command of [
140
+ 'cat $FILE',
141
+ 'cat "$FILE"',
142
+ 'cat ${FILE}',
143
+ 'cat $1',
144
+ 'cat $(echo .env)',
145
+ 'cat `echo .env`',
146
+ 'head $SECRET',
147
+ 'tail ${MY_FILE}',
148
+ 'less "$HOME/.env"',
149
+ 'cat README.md $EXTRA',
150
+ 'head README.md $UNRELATED',
151
+ 'echo ok; cat README.md $FILE',
152
+ 'cat README.md | head $FILE',
153
+ "bash -c 'cat $FILE'",
154
+ 'echo $(cat $FILE)',
155
+ 'cat README.md "$(printf safe)"',
156
+ 'cat README.md *',
157
+ 'cat README.md ~/README.md',
158
+ 'cat README.md {README.md,LICENSE}',
159
+ ]) {
160
+ test(`asks for read command with unresolved variable/substitution: ${command}`, () => {
161
+ const dir = tmpDir();
162
+ try {
163
+ const { code, stdout } = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
164
+ assert.strictEqual(code, 0);
165
+ assert.strictEqual(permissionDecision(stdout), 'ask', `should ask for ${command}`);
166
+ } finally {
167
+ fs.rmSync(dir, { recursive: true, force: true });
168
+ }
169
+ });
170
+ }
171
+
172
+ for (const command of [
173
+ 'echo $VAR',
174
+ 'echo "$HOME"',
175
+ 'git status',
176
+ 'git status $VAR',
177
+ 'cat README.md',
178
+ 'cat .env.example',
179
+ 'cat .env.sample',
180
+ 'cat .env.template',
181
+ 'cat .env.test',
182
+ "cat '$FILE'",
183
+ "cat '$HOME/README.md'",
184
+ "cat README.md '$EXTRA'",
185
+ "cat README.md '*'",
186
+ "cat README.md '~'",
187
+ "cat README.md '{README.md,LICENSE}'",
188
+ "echo '$(cat $FILE)'",
189
+ 'echo .envoy',
190
+ 'echo not.env',
191
+ 'echo .env',
192
+ "printf '%s\\n' .env",
193
+ 'git status sessions.md',
194
+ 'python3 -c "print(os.environ[\'HOME\'])"',
195
+ 'head -n "$COUNT" README.md',
196
+ 'grep .env README.md',
197
+ "sed -n '/.env/p' README.md",
198
+ ]) {
199
+ test(`allows harmless dynamic / safe static with extra var: ${command}`, () => {
200
+ const dir = tmpDir();
201
+ try {
202
+ // Create safe files so hook has cwd context, but path check is lexical
203
+ fs.writeFileSync(path.join(dir, 'README.md'), 'hi');
204
+ fs.writeFileSync(path.join(dir, '.env.example'), 'EXAMPLE=1');
205
+ const { stdout } = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
206
+ assert.strictEqual(permissionDecision(stdout), null, `should allow ${command}`);
207
+ } finally {
208
+ fs.rmSync(dir, { recursive: true, force: true });
209
+ }
210
+ });
211
+ }
212
+
213
+ test('assignment alone is harmless but dereferencing it in a read fails closed', () => {
214
+ const dir = tmpDir();
215
+ try {
216
+ const assigned = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat README.md' } }, dir);
217
+ assert.strictEqual(permissionDecision(assigned.stdout), null);
218
+ const read = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat "$FILE"' } }, dir);
219
+ assert.strictEqual(permissionDecision(read.stdout), 'ask');
220
+ } finally {
221
+ fs.rmSync(dir, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test('asks for dangling symlink aimed at protected target (regression: broken-link fail-open)', () => {
226
+ const dir = tmpDir();
227
+ try {
228
+ const env = path.join(dir, '.env');
229
+ fs.writeFileSync(env, 'SECRET=1');
230
+ const link = path.join(dir, 'innocent.txt');
231
+ fs.symlinkSync(env, link);
232
+ fs.unlinkSync(env); // now dangling
233
+ const { stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
234
+ assert.strictEqual(permissionDecision(stdout), 'ask', 'dangling link to .env must ask');
235
+ } finally {
236
+ fs.rmSync(dir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ test('allows dangling symlink to benign file and to allowed example', () => {
241
+ const dir = tmpDir();
242
+ try {
243
+ const benign = path.join(dir, 'benign.txt');
244
+ fs.writeFileSync(benign, 'hi');
245
+ const link = path.join(dir, 'dangling.txt');
246
+ fs.symlinkSync(benign, link);
247
+ fs.unlinkSync(benign);
248
+ const { stdout: s1 } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
249
+ assert.strictEqual(permissionDecision(s1), null, 'dangling benign should allow');
250
+
251
+ const example = path.join(dir, '.env.example');
252
+ fs.writeFileSync(example, 'EXAMPLE=1');
253
+ const link2 = path.join(dir, 'link-example.txt');
254
+ fs.symlinkSync(example, link2);
255
+ fs.unlinkSync(example);
256
+ const { stdout: s2 } = runHook({ tool_name: 'Read', tool_input: { file_path: link2 } }, dir);
257
+ assert.strictEqual(permissionDecision(s2), null, 'dangling link to .env.example should allow');
258
+ } finally {
259
+ fs.rmSync(dir, { recursive: true, force: true });
260
+ }
261
+ });
262
+
263
+ test('still allows real link to allowed example (regression: real link to .env.example)', () => {
264
+ const dir = tmpDir();
265
+ try {
266
+ const example = path.join(dir, '.env.example');
267
+ fs.writeFileSync(example, 'EXAMPLE=1');
268
+ const link = path.join(dir, 'real-example-link.txt');
269
+ fs.symlinkSync(example, link);
270
+ const { stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
271
+ assert.strictEqual(permissionDecision(stdout), null);
272
+ } finally {
273
+ fs.rmSync(dir, { recursive: true, force: true });
274
+ }
275
+ });
276
+
277
+ test('fails closed on malformed/looping symlink (hop limit)', () => {
278
+ const dir = tmpDir();
279
+ try {
280
+ const a = path.join(dir, 'a');
281
+ const b = path.join(dir, 'b');
282
+ fs.symlinkSync(b, a);
283
+ fs.symlinkSync(a, b);
284
+ const { stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: a } }, dir);
285
+ // Loop should fail closed to ask (treated as sensitive to be safe) or at least not allow silently
286
+ // Our implementation returns ask for looping link
287
+ assert.strictEqual(permissionDecision(stdout), 'ask');
288
+ } finally {
289
+ fs.rmSync(dir, { recursive: true, force: true });
290
+ }
291
+ });
@@ -0,0 +1,257 @@
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 PACKAGE_ROOT = path.resolve(__dirname, '../../../..');
11
+ const VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs');
12
+ const HOOKS = ['claude', 'codex'].map((runtime) => path.join(PACKAGE_ROOT, `src/${runtime}/hooks/semantic-review-authority.cjs`));
13
+
14
+ function fixture(base, name) {
15
+ const root = path.join(base, name);
16
+ const specDir = path.join(root, 'specs', 'feature');
17
+ fs.mkdirSync(specDir, { recursive: true });
18
+ fs.writeFileSync(path.join(specDir, 'requirements.md'), '# Requirements\n\n- **R1.1** The service shall return one result.\n');
19
+ fs.writeFileSync(path.join(specDir, 'design.md'), '# Design\n\n### D1 — Stable result\n\nReturn one stable result.\n');
20
+ fs.writeFileSync(path.join(specDir, 'spec.json'), `${JSON.stringify({ feature_name: 'feature', status: 'in_progress', task_registry: {}, workflow_policy: { assurance_level: 'Strict' } })}\n`);
21
+ return { root, specDir, specFile: path.join(specDir, 'spec.json') };
22
+ }
23
+
24
+ function installSemanticRuntime(target, runtime) {
25
+ const runtimeRoot = path.join(target.root, runtime === 'claude' ? '.claude' : '.codex');
26
+ const hooks = path.join(runtimeRoot, 'hooks');
27
+ const scripts = path.join(runtimeRoot, 'scripts');
28
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
29
+ fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), scripts, { recursive: true });
30
+ for (const name of ['semantic-review-authority.cjs', 'completion-authority-state.cjs']) {
31
+ fs.copyFileSync(path.join(PACKAGE_ROOT, `src/${runtime}/hooks`, name), path.join(hooks, name));
32
+ }
33
+ fs.copyFileSync(
34
+ path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
35
+ path.join(hooks, 'lib/runtime-path-safety.cjs'),
36
+ );
37
+ return {
38
+ hook: path.join(hooks, 'semantic-review-authority.cjs'),
39
+ validator: path.join(scripts, 'validate-spec-output.cjs'),
40
+ };
41
+ }
42
+
43
+ function envFor(root, home) {
44
+ return { ...process.env, HOME: home, USERPROFILE: home, PROJECT_ROOT: root };
45
+ }
46
+
47
+ function digest(target, home) {
48
+ const result = spawnSync(process.execPath, [VALIDATOR, target.specDir, '--semantic-digest'], { cwd: target.root, env: envFor(target.root, home), encoding: 'utf8' });
49
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
50
+ return result.stdout.trim();
51
+ }
52
+
53
+ function claim(target, semanticDigest, overrides = {}) {
54
+ return `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'feature', spec_file: 'specs/feature/spec.json', semantic_digest: semanticDigest, verdict: 'PASS', ...overrides })}`;
55
+ }
56
+
57
+ function runHook(hook, target, home, message, event = 'SubagentStop', payload = {}) {
58
+ return spawnSync(process.execPath, [hook], {
59
+ cwd: target.root, env: envFor(target.root, home), encoding: 'utf8',
60
+ input: JSON.stringify({ hook_event_name: event, session_id: 'host-session', agent_id: 'review-agent', agent_type: 'code_auditor', last_assistant_message: message, cwd: target.root, ...payload }),
61
+ });
62
+ }
63
+
64
+ function verify(hook, target, home, semanticDigest) {
65
+ const script = 'const a=require(process.argv[1]); console.log(JSON.stringify(a.verifyAttestation(process.argv[2],process.argv[3],process.argv[4],process.argv[5])))';
66
+ const result = spawnSync(process.execPath, ['-e', script, hook, target.root, target.specFile, 'feature', semanticDigest], { env: envFor(target.root, home), encoding: 'utf8' });
67
+ assert.equal(result.status, 0, result.stderr);
68
+ return JSON.parse(result.stdout);
69
+ }
70
+
71
+ function observationFile(home) {
72
+ const files = fs.readdirSync(home, { recursive: true }).filter((entry) => String(entry).endsWith('.json') && String(entry).includes('observation-'));
73
+ assert.equal(files.length, 1);
74
+ return path.join(home, files[0]);
75
+ }
76
+
77
+ test('Claude and Codex observe equivalent allowlisted SubagentStop claims with HMAC integrity', () => {
78
+ for (const hook of HOOKS) {
79
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-semantic-authority-'));
80
+ try {
81
+ const target = fixture(base, 'project');
82
+ const home = path.join(base, 'home');
83
+ const current = digest(target, home);
84
+ assert.equal(runHook(hook, target, home, claim(target, current), 'SessionStart').stderr, '');
85
+ assert.equal(fs.existsSync(path.join(home, '.cafekit')), false, 'non-SubagentStop event wrote authority state');
86
+ assert.equal(runHook(hook, target, home, 'Ordinary worker finished without a semantic review claim.').stderr, '');
87
+ assert.equal(fs.existsSync(path.join(home, '.cafekit')), false, 'ordinary SubagentStop wrote authority state');
88
+ const recorded = runHook(hook, target, home, claim(target, current));
89
+ assert.equal(recorded.status, 0);
90
+ assert.equal(recorded.stderr, '');
91
+ const observed = verify(hook, target, home, current);
92
+ assert.equal(observed.ok, true);
93
+ assert.equal(observed.record.kind, 'semantic-review-observation');
94
+ assert.equal(observed.record.event, 'SubagentStop');
95
+
96
+ const before = current;
97
+ const spec = JSON.parse(fs.readFileSync(target.specFile, 'utf8'));
98
+ spec.status = 'done';
99
+ spec.updated_at = '2026-08-13T12:00:00+07:00';
100
+ fs.writeFileSync(target.specFile, `${JSON.stringify(spec)}\n`);
101
+ assert.equal(digest(target, home), before);
102
+ assert.equal(verify(hook, target, home, before).ok, true);
103
+
104
+ fs.appendFileSync(path.join(target.specDir, 'requirements.md'), '\nSemantic behavior changed.\n');
105
+ const changed = digest(target, home);
106
+ assert.notEqual(changed, before);
107
+ assert.match(verify(hook, target, home, changed).reason, /stale/);
108
+ } finally { fs.rmSync(base, { recursive: true, force: true }); }
109
+ }
110
+ });
111
+
112
+ test('observation rejects wrong identity, digest, verdict, tampered MAC, copied record, and self-authored strings', () => {
113
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-semantic-adversarial-'));
114
+ try {
115
+ const home = path.join(base, 'home');
116
+ const first = fixture(base, 'first');
117
+ const second = fixture(base, 'second');
118
+ const firstDigest = digest(first, home);
119
+ const secondDigest = digest(second, home);
120
+ const hook = HOOKS[0];
121
+
122
+ for (const badClaim of [
123
+ claim(first, `sha256:${'0'.repeat(64)}`),
124
+ claim(first, firstDigest, { verdict: 'FAIL' }),
125
+ claim(first, firstDigest, { feature_name: 'copied-feature' }),
126
+ claim(first, firstDigest, { spec_file: 'specs/missing/spec.json' }),
127
+ ]) {
128
+ const result = runHook(hook, first, home, badClaim);
129
+ assert.match(result.stderr, /rejected/);
130
+ }
131
+ assert.equal(verify(hook, first, home, firstDigest).ok, false, 'self-authored claims created authority');
132
+
133
+ assert.equal(runHook(hook, first, home, claim(first, firstDigest)).stderr, '');
134
+ const firstRecordPath = observationFile(home);
135
+ const firstRecord = fs.readFileSync(firstRecordPath);
136
+ assert.equal(runHook(hook, second, home, claim(second, secondDigest)).stderr, '');
137
+ const records = fs.readdirSync(home, { recursive: true }).filter((entry) => String(entry).includes('observation-'));
138
+ assert.equal(records.length, 2);
139
+ const secondRecordPath = records.map((entry) => path.join(home, entry)).find((file) => file !== firstRecordPath);
140
+ fs.writeFileSync(secondRecordPath, firstRecord);
141
+ assert.equal(verify(hook, second, home, secondDigest).ok, false, 'copied project record was accepted');
142
+
143
+ const tampered = JSON.parse(fs.readFileSync(firstRecordPath, 'utf8'));
144
+ tampered.verdict = 'FAIL';
145
+ fs.writeFileSync(firstRecordPath, `${JSON.stringify(tampered)}\n`);
146
+ assert.match(verify(hook, first, home, firstDigest).reason, /MAC|malformed/);
147
+ } finally { fs.rmSync(base, { recursive: true, force: true }); }
148
+ });
149
+
150
+ test('SubagentStop markers and claimed capabilities cannot turn a non-reviewer into authority', () => {
151
+ for (const hook of HOOKS) {
152
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-semantic-role-'));
153
+ try {
154
+ const target = fixture(base, 'project');
155
+ const home = path.join(base, 'home');
156
+ const current = digest(target, home);
157
+ for (const payload of [
158
+ { agent_type: 'implementer' },
159
+ { agent_type: 'test_runner', capability: 'reviewer' },
160
+ { agent_type: 'worker', reviewer_capability: true },
161
+ ]) {
162
+ const result = runHook(hook, target, home, claim(target, current), 'SubagentStop', payload);
163
+ assert.equal(result.status, 0);
164
+ assert.equal(result.stderr, '');
165
+ assert.equal(verify(hook, target, home, current).ok, false);
166
+ }
167
+ assert.equal(runHook(hook, target, home, claim(target, current), 'SubagentStop', { agent_type: 'code-auditor' }).stderr, '');
168
+ assert.equal(verify(hook, target, home, current).ok, true);
169
+ } finally { fs.rmSync(base, { recursive: true, force: true }); }
170
+ }
171
+ });
172
+
173
+ test('canonicalizes and contains the validator before semantic digest spawn', () => {
174
+ for (const runtime of ['claude', 'codex']) {
175
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), `cafekit-semantic-validator-${runtime}-`));
176
+ try {
177
+ const target = fixture(base, 'project');
178
+ const installed = installSemanticRuntime(target, runtime);
179
+ const digestValue = digest(target, path.join(base, 'digest-home'));
180
+ const validHome = path.join(base, 'valid-home');
181
+ const valid = runHook(installed.hook, target, validHome, claim(target, digestValue));
182
+ assert.equal(valid.status, 0);
183
+ assert.equal(valid.stderr, '', `${runtime} contained validator should be accepted`);
184
+ assert.equal(verify(installed.hook, target, validHome, digestValue).ok, true);
185
+
186
+ const outsideValidator = path.join(base, 'outside-validator.cjs');
187
+ fs.copyFileSync(VALIDATOR, outsideValidator);
188
+ fs.rmSync(installed.validator);
189
+ fs.symlinkSync(outsideValidator, installed.validator);
190
+ const escapedHome = path.join(base, 'escaped-home');
191
+ const escaped = runHook(installed.hook, target, escapedHome, claim(target, digestValue));
192
+ assert.equal(escaped.status, 0);
193
+ assert.match(escaped.stderr, /semantic validator|escapes|rejected/i, `${runtime} validator escape must be rejected`);
194
+ assert.equal(verify(installed.hook, target, escapedHome, digestValue).ok, false);
195
+
196
+ fs.rmSync(installed.validator);
197
+ fs.copyFileSync(VALIDATOR, installed.validator);
198
+ const dependency = path.join(path.dirname(installed.validator), 'workflow-policy.cjs');
199
+ const outsideDependency = path.join(base, 'outside-workflow-policy.cjs');
200
+ fs.copyFileSync(dependency, outsideDependency);
201
+ fs.rmSync(dependency);
202
+ fs.symlinkSync(outsideDependency, dependency);
203
+ const dependencyHome = path.join(base, 'dependency-home');
204
+ const dependencyEscape = runHook(installed.hook, target, dependencyHome, claim(target, digestValue));
205
+ assert.equal(dependencyEscape.status, 0);
206
+ assert.match(dependencyEscape.stderr, /dependency|symlink|rejected/i, `${runtime} transitive dependency escape must be rejected`);
207
+ assert.equal(verify(installed.hook, target, dependencyHome, digestValue).ok, false);
208
+
209
+ fs.rmSync(dependency);
210
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs'), dependency);
211
+ const runtimeRoot = path.dirname(path.dirname(installed.hook));
212
+ const outsideRuntime = path.join(base, `outside-${runtime}`);
213
+ fs.renameSync(runtimeRoot, outsideRuntime);
214
+ fs.symlinkSync(outsideRuntime, runtimeRoot);
215
+ const rootHome = path.join(base, 'root-home');
216
+ const rootEscape = runHook(installed.hook, target, rootHome, claim(target, digestValue));
217
+ assert.equal(rootEscape.status, 0);
218
+ assert.match(rootEscape.stderr, /symlink|identity|rejected/i, `${runtime} adapter-root symlink must be rejected`);
219
+ assert.equal(verify(installed.hook, target, rootHome, digestValue).ok, false);
220
+ } finally { fs.rmSync(base, { recursive: true, force: true }); }
221
+ }
222
+ });
223
+
224
+ test('rejects scratch/spec.json and traversal feature identities', () => {
225
+ for (const hook of HOOKS) {
226
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-semantic-scratch-'));
227
+ try {
228
+ const target = fixture(base, 'project');
229
+ const home = path.join(base, 'home');
230
+ const current = digest(target, home);
231
+ const badScratch = claim(target, current, { spec_file: 'scratch/spec.json' });
232
+ const resScratch = runHook(hook, target, home, badScratch);
233
+ assert.match(resScratch.stderr, /rejected|spec identity/);
234
+ assert.equal(verify(hook, target, home, current).ok, false, 'scratch identity should not create observation');
235
+
236
+ const traversal = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: '../evil', spec_file: 'specs/../evil/spec.json', semantic_digest: current, verdict: 'PASS' })}`;
237
+ const resTraversal = runHook(hook, target, home, traversal);
238
+ assert.match(resTraversal.stderr, /rejected|malformed/);
239
+ assert.equal(verify(hook, target, home, current).ok, false);
240
+
241
+ const slash = claim(target, current, { feature_name: 'evil/traversal' });
242
+ const resSlash = runHook(hook, target, home, slash);
243
+ assert.match(resSlash.stderr, /rejected|malformed/);
244
+ } finally { fs.rmSync(base, { recursive: true, force: true }); }
245
+ }
246
+ });
247
+
248
+ test('code-auditor agent ships Strict conditional marker contract', () => {
249
+ const agentPath = path.join(PACKAGE_ROOT, 'src/claude/agents/code-auditor.md');
250
+ const content = fs.readFileSync(agentPath, 'utf8');
251
+ assert.match(content, /Strict Semantic Review Attestation/);
252
+ assert.match(content, /CAFEKIT_SEMANTIC_REVIEW_ATTESTATION/);
253
+ assert.match(content, /Emit only for `Strict`/);
254
+ assert.match(content, /specs\/<feature>\/spec\.json/);
255
+ assert.match(content, /MAC-protected host-hook observation/);
256
+ assert.doesNotMatch(content, /host-signed|proves causal/);
257
+ });