@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.
- package/README.md +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +938 -237
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +222 -149
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +102 -1362
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -49
- package/bin/phases/copy-payload.js +11 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +17 -89
- package/src/claude/hooks/spec-state.cjs +89 -96
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +20 -8
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +32 -74
- package/src/codex/hooks/spec-state.cjs +68 -46
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/common/AGENTS.md +4 -10
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -48,6 +48,7 @@ function install(root, platforms) {
|
|
|
48
48
|
const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
|
|
49
49
|
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
50
50
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
51
|
+
return result;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function commands(settings) {
|
|
@@ -74,7 +75,7 @@ test('fresh installs ship and register none of the five legacy hooks', () => {
|
|
|
74
75
|
});
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
test('claude upgrade
|
|
78
|
+
test('claude upgrade preserves edited legacy hooks while removing their CafeKit entries', () => {
|
|
78
79
|
inTempProject((root) => {
|
|
79
80
|
install(root, ['claude']);
|
|
80
81
|
const settingsPath = path.join(root, '.claude', 'settings.json');
|
|
@@ -99,10 +100,11 @@ test('claude upgrade removes the legacy hooks and their CafeKit entries but keep
|
|
|
99
100
|
settings.hooks.SubagentStop[0].hooks.push({ type: 'command', command: cmd('semantic-review-authority.cjs') });
|
|
100
101
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
101
102
|
|
|
102
|
-
install(root, ['claude']);
|
|
103
|
-
|
|
103
|
+
const upgrade = install(root, ['claude']);
|
|
104
|
+
assert.match(upgrade.stdout + upgrade.stderr, /Preserved 5 obsolete runtime/);
|
|
105
|
+
for (const name of NAMES) assert.equal(fs.readFileSync(path.join(root, '.claude', 'hooks', name), 'utf8'), '// user edit\n', name);
|
|
104
106
|
const after = readJson(manifestPath);
|
|
105
|
-
for (const name of NAMES) assert.equal(
|
|
107
|
+
for (const name of NAMES) assert.equal(after.files[`hooks/${name}`].sha256, '0'.repeat(64), `record ${name}`);
|
|
106
108
|
const afterCommands = commands(readJson(settingsPath));
|
|
107
109
|
for (const name of REGISTERED) {
|
|
108
110
|
assert.equal(afterCommands.some((command) => command.includes(`.claude/hooks/${name}`)), false, name);
|
|
@@ -112,7 +114,7 @@ test('claude upgrade removes the legacy hooks and their CafeKit entries but keep
|
|
|
112
114
|
});
|
|
113
115
|
});
|
|
114
116
|
|
|
115
|
-
test('codex upgrade
|
|
117
|
+
test('codex upgrade preserves edited legacy hooks and prunes only absent records', () => {
|
|
116
118
|
inTempProject((root) => {
|
|
117
119
|
install(root, ['codex']);
|
|
118
120
|
const hooksPath = path.join(root, '.codex', 'hooks.json');
|
|
@@ -137,10 +139,18 @@ test('codex upgrade removes the legacy hooks, their records and their CafeKit en
|
|
|
137
139
|
add('UserPromptSubmit', FOREIGN[0]);
|
|
138
140
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2));
|
|
139
141
|
|
|
140
|
-
install(root, ['codex']);
|
|
141
|
-
|
|
142
|
+
const upgrade = install(root, ['codex']);
|
|
143
|
+
assert.match(upgrade.stdout + upgrade.stderr, /Preserved 4 obsolete runtime/);
|
|
144
|
+
for (const name of NAMES) {
|
|
145
|
+
const file = path.join(root, '.codex', 'hooks', name);
|
|
146
|
+
if (name === 'completion-authority-state.cjs') assert.equal(fs.existsSync(file), false, name);
|
|
147
|
+
else assert.equal(fs.readFileSync(file, 'utf8'), '// user edit\n', name);
|
|
148
|
+
}
|
|
142
149
|
const after = readJson(manifestPath);
|
|
143
|
-
for (const name of NAMES)
|
|
150
|
+
for (const name of NAMES) {
|
|
151
|
+
if (name === 'completion-authority-state.cjs') assert.equal(after.files[`.codex/hooks/${name}`], undefined, `record ${name}`);
|
|
152
|
+
else assert.equal(after.files[`.codex/hooks/${name}`].sha256, '0'.repeat(64), `record ${name}`);
|
|
153
|
+
}
|
|
144
154
|
const afterCommands = commands(readJson(hooksPath));
|
|
145
155
|
for (const name of REGISTERED) {
|
|
146
156
|
assert.equal(afterCommands.some((command) => command.includes(`.codex/hooks/${name}`) || command.includes(`.codex\\hooks\\${name}`)), false, name);
|
|
@@ -22,15 +22,18 @@ const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.
|
|
|
22
22
|
* The hooks an omp install runs: the Claude set the manifest ships, then `src/omp/hooks/`
|
|
23
23
|
* written over it. Composed once under a dotted folder so runtime-dir.cjs derives `.omp`.
|
|
24
24
|
*/
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
function composeOmp(root) {
|
|
26
|
+
for (const rel of MANIFEST.runtime.files.filter((f) => f.startsWith('hooks/'))) {
|
|
27
|
+
const target = path.join(root, '.omp', rel);
|
|
28
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
29
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude', rel), target);
|
|
30
|
+
}
|
|
31
|
+
fs.cpSync(path.join(PACKAGE_ROOT, 'src/omp/hooks'), path.join(root, '.omp', 'hooks'), { recursive: true });
|
|
32
|
+
fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), path.join(root, '.omp', 'scripts'), { recursive: true });
|
|
33
|
+
return path.join(root, '.omp', 'hooks');
|
|
31
34
|
}
|
|
32
|
-
fs.
|
|
33
|
-
|
|
35
|
+
const COMPOSED_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-composed-'));
|
|
36
|
+
const HOOKS = composeOmp(COMPOSED_ROOT);
|
|
34
37
|
|
|
35
38
|
let bridge;
|
|
36
39
|
test.before(async () => { bridge = await import(BRIDGE); });
|
|
@@ -49,10 +52,10 @@ function registered(eventName) {
|
|
|
49
52
|
.filter(Boolean);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
// Deliberate, named divergences
|
|
53
|
-
//
|
|
54
|
-
const CLAUDE_ONLY = {
|
|
55
|
-
const OMP_ONLY = {
|
|
55
|
+
// Deliberate, named divergences. Each must still be a real difference, so the test fails
|
|
56
|
+
// (and the entry must go) once omp catches up.
|
|
57
|
+
const CLAUDE_ONLY = {};
|
|
58
|
+
const OMP_ONLY = {};
|
|
56
59
|
|
|
57
60
|
test('the dispatch table mirrors settings.json for every event omp can deliver', () => {
|
|
58
61
|
const { DISPATCH } = bridge;
|
|
@@ -169,3 +172,54 @@ test('input injections flow back as a transform that keeps the user text intact'
|
|
|
169
172
|
assert.match(r.inject, /Secret handling/, 'secret-output-guardrail must inject on a credential prompt');
|
|
170
173
|
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
171
174
|
});
|
|
175
|
+
|
|
176
|
+
test('the omp session id is used for every event that does not carry its own', () => {
|
|
177
|
+
const ctx = { cwd: '/tmp/project', sessionManager: { getSessionId: () => 'omp-session-1' } };
|
|
178
|
+
assert.equal(bridge.shapePayload('input', { text: 'hi' }, ctx).session_id, 'omp-session-1');
|
|
179
|
+
assert.equal(bridge.shapePayload('tool_result', { toolName: 'edit', input: {} }, ctx).session_id, 'omp-session-1');
|
|
180
|
+
assert.equal(bridge.shapePayload('session_stop', { session_id: 'omp-session-1' }, ctx).session_id, 'omp-session-1',
|
|
181
|
+
'session_stop carries the same id omp hands extensions, so touch and state stay in one place');
|
|
182
|
+
assert.ok(!bridge.DISPATCH.session_start.hooks.includes('docs-sync.cjs'), 'docs-sync runs on demand, as on Claude');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
function ompPacket(root, name) {
|
|
186
|
+
const dir = path.join(root, 'specs', name);
|
|
187
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
fs.writeFileSync(path.join(dir, 'plan.md'), `# ${name}\nSpecs-Contract: process-first-ready-v1\n`);
|
|
189
|
+
fs.writeFileSync(path.join(dir, 'task-01-work.md'), ['# Task 01 — work', '', 'Status: pending', '',
|
|
190
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', ''].join('\n'));
|
|
191
|
+
return dir;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
test('an omp edit inside a packet scopes the tollgate to it', async () => {
|
|
195
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-touch-'));
|
|
196
|
+
try {
|
|
197
|
+
const hooks = composeOmp(root);
|
|
198
|
+
const alpha = ompPacket(root, 'alpha');
|
|
199
|
+
ompPacket(root, 'beta');
|
|
200
|
+
const { spawnSync } = require('node:child_process');
|
|
201
|
+
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'],
|
|
202
|
+
['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
|
|
203
|
+
assert.equal(spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }).status, 0);
|
|
204
|
+
}
|
|
205
|
+
const session = (id) => ({ cwd: root, sessionManager: { getSessionId: () => id } });
|
|
206
|
+
|
|
207
|
+
const untouched = await bridge.runEvent('input', { text: 'Continue' }, session('omp-a'), { dir: hooks });
|
|
208
|
+
assert.doesNotMatch(untouched.inject, /Multiple active specs|Spec state changed/, 'a session that touched nothing hears nothing');
|
|
209
|
+
|
|
210
|
+
// A failed edit records nothing.
|
|
211
|
+
await bridge.runEvent('tool_result', { toolName: 'edit', input: { path: path.join(alpha, 'plan.md') }, content: [], isError: true },
|
|
212
|
+
session('omp-a'), { dir: hooks });
|
|
213
|
+
const afterFailure = await bridge.runEvent('input', { text: 'Continue' }, session('omp-a'), { dir: hooks });
|
|
214
|
+
assert.doesNotMatch(afterFailure.inject, /Multiple active specs|Spec state changed/, 'a failed edit records nothing');
|
|
215
|
+
|
|
216
|
+
await bridge.runEvent('tool_result', { toolName: 'edit', input: { path: path.join(alpha, 'plan.md') }, content: [], isError: false },
|
|
217
|
+
session('omp-a'), { dir: hooks });
|
|
218
|
+
// As on Claude, a session that touched one of two active packets is told about both.
|
|
219
|
+
const touched = await bridge.runEvent('input', { text: 'Continue' }, session('omp-a'), { dir: hooks });
|
|
220
|
+
assert.match(touched.inject, /Multiple active specs detected: alpha, beta/);
|
|
221
|
+
|
|
222
|
+
const other = await bridge.runEvent('input', { text: 'Continue' }, session('omp-b'), { dir: hooks });
|
|
223
|
+
assert.doesNotMatch(other.inject, /Multiple active specs|Spec state changed/, "another omp session's touch does not leak");
|
|
224
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
225
|
+
});
|
|
@@ -133,11 +133,9 @@ function expectedOverlay(name) {
|
|
|
133
133
|
source = source.replace(from, to);
|
|
134
134
|
};
|
|
135
135
|
if (name === 'privacy-block.cjs') {
|
|
136
|
-
edit("
|
|
137
|
-
"
|
|
138
|
-
+ "
|
|
139
|
-
edit(" permissionDecision: 'ask',\n permissionDecisionReason: 'Sensitive access could not be evaluated safely.'\n",
|
|
140
|
-
" permissionDecision: 'deny',\n permissionDecisionReason: 'Sensitive access could not be evaluated safely.'\n");
|
|
136
|
+
edit(" permissionDecision: 'ask',\n",
|
|
137
|
+
" // omp has no ask state on tool_call, so an ask under Claude becomes a denial here.\n"
|
|
138
|
+
+ " permissionDecision: 'deny',\n");
|
|
141
139
|
}
|
|
142
140
|
return source;
|
|
143
141
|
}
|
|
@@ -157,3 +155,11 @@ test('the fork differs from Claude only by its overlay', () => {
|
|
|
157
155
|
assert.match(phase, /claudeHookFiles/, 'the phase must install the Claude set first');
|
|
158
156
|
assert.match(phase, /overlayFiles/, 'the phase must write the overlay over it');
|
|
159
157
|
});
|
|
158
|
+
|
|
159
|
+
test('an empty stdin is denied under omp, where Claude asks', () => {
|
|
160
|
+
const result = spawnSync(process.execPath, [path.join(INSTALLED, 'privacy-block.cjs')], { input: '', encoding: 'utf8' });
|
|
161
|
+
assert.equal(result.status, 0, result.stderr);
|
|
162
|
+
const output = JSON.parse(result.stdout).hookSpecificOutput;
|
|
163
|
+
assert.equal(output.permissionDecision, 'deny');
|
|
164
|
+
assert.match(output.permissionDecisionReason, /Sensitive access could not be evaluated safely\./);
|
|
165
|
+
});
|
|
@@ -149,8 +149,15 @@ function composeCodexHookTree(root) {
|
|
|
149
149
|
path.join(hooksDir, 'lib', 'hook-context.cjs'));
|
|
150
150
|
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/privacy-state.cjs'),
|
|
151
151
|
path.join(hooksDir, 'lib', 'privacy-state.cjs'));
|
|
152
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/rules-reservation.cjs'),
|
|
153
|
+
path.join(hooksDir, 'lib', 'rules-reservation.cjs'));
|
|
154
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'),
|
|
155
|
+
path.join(hooksDir, 'lib', 'update-check.cjs'));
|
|
152
156
|
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
153
157
|
path.join(hooksDir, 'lib', 'runtime-path-safety.cjs'));
|
|
158
|
+
for (const name of ['privacy-paths.cjs', 'privacy-command-analysis.cjs']) {
|
|
159
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib', name), path.join(hooksDir, 'lib', name));
|
|
160
|
+
}
|
|
154
161
|
return path.join(hooksDir, 'session.cjs');
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -114,13 +114,6 @@ const REQUIRED_PAYLOAD = [
|
|
|
114
114
|
'src/claude/scripts/workflow-policy.cjs',
|
|
115
115
|
'src/claude/scripts/provenance.cjs',
|
|
116
116
|
'src/claude/scripts/spec-receipt.cjs',
|
|
117
|
-
'src/claude/scripts/spec-ground.cjs',
|
|
118
|
-
'src/claude/scripts/spec-final-state.cjs',
|
|
119
|
-
'src/claude/scripts/spec-readiness.cjs',
|
|
120
|
-
'src/claude/scripts/spec-semantic-model.cjs',
|
|
121
|
-
'src/claude/scripts/validate-spec-output.cjs',
|
|
122
|
-
'src/claude/scripts/spec-authoring-validation.cjs',
|
|
123
|
-
'src/claude/scripts/spec-authoring-digest.cjs',
|
|
124
117
|
'src/claude/scripts/generate-skill-catalog.cjs',
|
|
125
118
|
'src/claude/rules/process-management.md',
|
|
126
119
|
'src/claude/rules/review-audit-self-decision.md',
|
|
@@ -1522,13 +1515,11 @@ function assertInstalledProvenance(root, platform, fixture) {
|
|
|
1522
1515
|
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
1523
1516
|
const resolver = path.join(scripts, 'spec-resolver.cjs');
|
|
1524
1517
|
const receipt = path.join(scripts, 'spec-receipt.cjs');
|
|
1525
|
-
const finalState = path.join(scripts, 'spec-final-state.cjs');
|
|
1526
|
-
const readiness = path.join(scripts, 'spec-readiness.cjs');
|
|
1527
1518
|
const gate = path.join(
|
|
1528
1519
|
root,
|
|
1529
1520
|
platform === 'claude' ? '.claude/hooks/spec-gate.cjs' : '.codex/hooks/spec-gate.cjs'
|
|
1530
1521
|
);
|
|
1531
|
-
for (const file of [helper, policy, resolver, receipt
|
|
1522
|
+
for (const file of [helper, policy, resolver, receipt]) {
|
|
1532
1523
|
assert.equal(fs.existsSync(file), true, `${platform} installed file missing: ${file}`);
|
|
1533
1524
|
}
|
|
1534
1525
|
assert.equal(fs.existsSync(gate), true, `${platform} installed gate missing: ${gate}`);
|
|
@@ -1660,26 +1651,13 @@ function assertInstalledScripts(root, platform) {
|
|
|
1660
1651
|
const scripts = path.join(root, RUNTIMES[platform].root, 'scripts');
|
|
1661
1652
|
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
1662
1653
|
const scanner = path.join(scripts, 'scan-staged-secrets.cjs');
|
|
1663
|
-
const grounder = path.join(scripts, 'spec-ground.cjs');
|
|
1664
|
-
const finalState = path.join(scripts, 'spec-final-state.cjs');
|
|
1665
|
-
const readiness = path.join(scripts, 'spec-readiness.cjs');
|
|
1666
|
-
const validator = path.join(scripts, 'validate-spec-output.cjs');
|
|
1667
1654
|
assert.ok(fs.existsSync(policy), `installed policy missing: ${policy}`);
|
|
1668
1655
|
assert.ok(fs.existsSync(scanner), `installed scanner missing: ${scanner}`);
|
|
1669
|
-
assert.ok(fs.existsSync(grounder), `installed validator dependency missing: ${grounder}`);
|
|
1670
|
-
assert.ok(fs.existsSync(finalState), `installed final-state dependency missing: ${finalState}`);
|
|
1671
|
-
assert.ok(fs.existsSync(readiness), `installed readiness finalizer missing: ${readiness}`);
|
|
1672
|
-
assert.ok(fs.existsSync(validator), `installed validator missing: ${validator}`);
|
|
1673
1656
|
|
|
1674
1657
|
const policyRun = spawnSync(process.execPath, [policy, '--json'], { cwd: root, encoding: 'utf8' });
|
|
1675
1658
|
assert.equal(policyRun.status, 0, `${policyRun.stdout}\n${policyRun.stderr}`);
|
|
1676
1659
|
assert.equal(JSON.parse(policyRun.stdout).contract, 'execution-policy');
|
|
1677
1660
|
|
|
1678
|
-
const validatorRun = spawnSync(process.execPath, [validator], { cwd: root, encoding: 'utf8' });
|
|
1679
|
-
assert.equal(validatorRun.status, 2, `${validatorRun.stdout}\n${validatorRun.stderr}`);
|
|
1680
|
-
assert.match(validatorRun.stderr, /Usage: .*validate-spec-output\.cjs/);
|
|
1681
|
-
assert.doesNotMatch(validatorRun.stderr, /MODULE_NOT_FOUND|Cannot find module/);
|
|
1682
|
-
|
|
1683
1661
|
const safe = path.join(root, 'safe.txt');
|
|
1684
1662
|
fs.writeFileSync(safe, 'mode=safe\n');
|
|
1685
1663
|
spawnSync('git', ['init', '-q'], { cwd: root });
|
|
@@ -1689,347 +1667,10 @@ function assertInstalledScripts(root, platform) {
|
|
|
1689
1667
|
assert.match(scannerRun.stdout, /No staged secrets found/);
|
|
1690
1668
|
}
|
|
1691
1669
|
|
|
1692
|
-
function installedSemanticPaths(root, platform) {
|
|
1693
|
-
const runtimeRoot = path.join(root, RUNTIMES[platform].root);
|
|
1694
|
-
const skillRoot = platform === 'codex'
|
|
1695
|
-
? path.join(root, '.agents', 'skills')
|
|
1696
|
-
: path.join(runtimeRoot, 'skills');
|
|
1697
|
-
const paths = {
|
|
1698
|
-
policy: path.join(runtimeRoot, 'scripts', 'workflow-policy.cjs'),
|
|
1699
|
-
scaffold: path.join(runtimeRoot, 'scripts', 'spec-scaffold.cjs'),
|
|
1700
|
-
validator: path.join(runtimeRoot, 'scripts', 'validate-spec-output.cjs'),
|
|
1701
|
-
authoringValidator: path.join(runtimeRoot, 'scripts', 'spec-authoring-validation.cjs'),
|
|
1702
|
-
authoringDigest: path.join(runtimeRoot, 'scripts', 'spec-authoring-digest.cjs'),
|
|
1703
|
-
grounder: path.join(runtimeRoot, 'scripts', 'spec-ground.cjs'),
|
|
1704
|
-
finalState: path.join(runtimeRoot, 'scripts', 'spec-final-state.cjs'),
|
|
1705
|
-
readiness: path.join(runtimeRoot, 'scripts', 'spec-readiness.cjs'),
|
|
1706
|
-
semanticModel: path.join(runtimeRoot, 'scripts', 'spec-semantic-model.cjs'),
|
|
1707
|
-
resolver: path.join(runtimeRoot, 'scripts', 'spec-resolver.cjs'),
|
|
1708
|
-
provenance: path.join(runtimeRoot, 'scripts', 'provenance.cjs'),
|
|
1709
|
-
stateTemplate: path.join(skillRoot, 'specs', 'templates', 'spec-state.json'),
|
|
1710
|
-
};
|
|
1711
|
-
for (const [name, target] of Object.entries(paths)) {
|
|
1712
|
-
assert.equal(fs.existsSync(target), true, `${platform} installed ${name} missing: ${target}`);
|
|
1713
|
-
assert.equal(path.relative(root, target).startsWith('..'), false, `${platform} ${name} escaped install root`);
|
|
1714
|
-
}
|
|
1715
|
-
return paths;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function runInstalled(script, args, root, expected = 0) {
|
|
1719
|
-
const result = spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
1720
|
-
assert.equal(result.status, expected, `${script}\n${result.stdout}\n${result.stderr}`);
|
|
1721
|
-
return result;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
1670
|
function writeJson(target, value) {
|
|
1725
1671
|
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
1726
1672
|
}
|
|
1727
1673
|
|
|
1728
|
-
function materializeSpecState(paths, root, feature, { strict = false } = {}) {
|
|
1729
|
-
const featureDir = path.join(root, 'specs', feature);
|
|
1730
|
-
runInstalled(paths.scaffold, [feature], root);
|
|
1731
|
-
const state = JSON.parse(fs.readFileSync(path.join(featureDir, 'spec.json'), 'utf8'));
|
|
1732
|
-
state.scope_lock.in_scope = [`${feature} behavior`];
|
|
1733
|
-
state.scope_lock.out_of_scope = ['unrelated behavior'];
|
|
1734
|
-
if (strict) {
|
|
1735
|
-
state.workflow_policy.assurance_level = 'Strict';
|
|
1736
|
-
state.workflow_policy.classified_minimum.assurance_level = 'Strict';
|
|
1737
|
-
state.workflow_policy.risks = ['auth'];
|
|
1738
|
-
}
|
|
1739
|
-
writeJson(path.join(featureDir, 'spec.json'), state);
|
|
1740
|
-
return { featureDir, specFile: path.join(featureDir, 'spec.json'), state };
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function requirementsProjection({ proof = false } = {}) {
|
|
1744
|
-
return `# Requirements
|
|
1745
|
-
|
|
1746
|
-
## Requirements
|
|
1747
|
-
|
|
1748
|
-
### Requirement 1: Installed behavior
|
|
1749
|
-
|
|
1750
|
-
- **R1.1**: The CommonJS entrypoint \`src/entry.js\` shall return an object containing only status \`enabled\` when its sole \`valid\` argument is the boolean \`true\`; it shall return an object containing only status \`rejected\` for \`false\`, a missing argument, or any non-boolean value.${proof ? '\n- **R1.2**: The installed verifier shall prove the observed entry behavior through its grounded evidence boundary.' : ''}
|
|
1751
|
-
`;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
const TYPED_ANCHOR_COLUMNS = ['ID', 'Type', 'Target', 'Role', 'Access', 'Action'];
|
|
1755
|
-
|
|
1756
|
-
function typedAnchorTable(rows) {
|
|
1757
|
-
assert.ok(Array.isArray(rows) && rows.length > 0, 'typed anchor projection requires rows');
|
|
1758
|
-
const header = `| ${TYPED_ANCHOR_COLUMNS.join(' | ')} |`;
|
|
1759
|
-
const separator = `|${TYPED_ANCHOR_COLUMNS.map(() => '---').join('|')}|`;
|
|
1760
|
-
const body = rows.map((row) => {
|
|
1761
|
-
assert.deepEqual(Object.keys(row), ['id', 'type', 'target', 'role', 'access', 'action']);
|
|
1762
|
-
assert.ok(Object.values(row).every((value) => typeof value === 'string' && value.trim() !== ''));
|
|
1763
|
-
return `| ${row.id} | ${row.type} | \`${row.target}\` | ${row.role} | ${row.access} | ${row.action} |`;
|
|
1764
|
-
});
|
|
1765
|
-
return [header, separator, ...body].join('\n');
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
function designProjection({
|
|
1769
|
-
expected = 'exit 0; boolean true returns the exact enabled object; false, undefined, null, `"true"`, 1, and an object return the exact rejected object',
|
|
1770
|
-
taskful = false,
|
|
1771
|
-
} = {}) {
|
|
1772
|
-
const anchors = typedAnchorTable([{
|
|
1773
|
-
id: 'A-D-01', type: 'file', target: taskful ? 'src/design-boundary.js' : 'src/entry.js',
|
|
1774
|
-
role: taskful ? 'existing design boundary for the entrypoint' : 'existing runtime entrypoint and contract owner',
|
|
1775
|
-
access: 'read', action: 'read',
|
|
1776
|
-
}]);
|
|
1777
|
-
const reachabilityAnchors = taskful ? 'A-R1-01-01, A-R1-02-02' : 'A-D-01';
|
|
1778
|
-
return `# Design
|
|
1779
|
-
|
|
1780
|
-
## Boundary
|
|
1781
|
-
|
|
1782
|
-
- **Owns:** The exact return contract of the CommonJS function exported by \`src/entry.js\`.
|
|
1783
|
-
- **Reads:** One \`valid\` argument; only the boolean \`true\` is valid.
|
|
1784
|
-
- **Writes/exposes:** Exactly one status field whose value is \`enabled\` or \`rejected\`.
|
|
1785
|
-
- **Outside boundary:** Unrelated runtime behavior.
|
|
1786
|
-
|
|
1787
|
-
## Typed Anchors
|
|
1788
|
-
|
|
1789
|
-
${anchors}
|
|
1790
|
-
|
|
1791
|
-
## Decisions and Invariants
|
|
1792
|
-
|
|
1793
|
-
### D1 — Installed result decision
|
|
1794
|
-
|
|
1795
|
-
- **Decision:** \`valid === true\` returns an object containing only status \`enabled\`; every other value returns an object containing only status \`rejected\`.
|
|
1796
|
-
- **Rejects ambiguity:** Truthy non-booleans such as \`"true"\` never count as valid.
|
|
1797
|
-
- **Negative path:** \`false\`, \`undefined\`, \`null\`, strings, numbers, and objects return rejected.
|
|
1798
|
-
- **Anchors:** A-D-01
|
|
1799
|
-
|
|
1800
|
-
### I1 — Rejection invariant
|
|
1801
|
-
|
|
1802
|
-
Any input other than the boolean \`true\` never returns enabled.
|
|
1803
|
-
|
|
1804
|
-
### C1 — Result contract
|
|
1805
|
-
|
|
1806
|
-
- **Owner:** A-D-01
|
|
1807
|
-
- **Consumers:** \`test/entry.test.js\` and callers of the CommonJS export.
|
|
1808
|
-
- **Shape/behavior:** The result is an object with exactly one \`status\` field set to \`enabled\` or \`rejected\`.
|
|
1809
|
-
- **Compatibility:** The strict boolean discriminator, exact object shape, and two status values remain stable.
|
|
1810
|
-
|
|
1811
|
-
## Verification Definitions
|
|
1812
|
-
|
|
1813
|
-
- **V1**: Criteria R1.1; Owner ${taskful ? 'R1-01' : 'A-D-01'}; ${taskful ? 'Proof criteria R1.2; Proof owner R1-02; Evidence anchor A-R1-02-02; ' : ''}Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected ${expected}; Negative/failure \`false\`, missing input, \`null\`, and truthy non-booleans all return the exact rejected object; Reachability/grounding entrypoint \`src/entry.js\` via ${reachabilityAnchors}.
|
|
1814
|
-
`;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
function completeSemanticReview(paths, root, fixture, counterexamples) {
|
|
1818
|
-
// C16/D13: only the installed spec-authoring-validation.cjs coordinator may
|
|
1819
|
-
// flip authoring.* to validated and write the matching receipt (I21/R3.9) —
|
|
1820
|
-
// run it over the fixture's current bytes instead of hand-writing the enum.
|
|
1821
|
-
// Lifecycle order (I15/R3.7): authoring -> coordinator -> semantic review ->
|
|
1822
|
-
// readiness, never the reverse.
|
|
1823
|
-
runInstalled(paths.scaffold, [fixture.state.feature_name, '--sync-semantic-model'], root);
|
|
1824
|
-
runInstalled(paths.authoringValidator, [fixture.featureDir], root);
|
|
1825
|
-
const state = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
|
|
1826
|
-
fixture.reviewResult = {
|
|
1827
|
-
verdict: 'PASS',
|
|
1828
|
-
findings: [],
|
|
1829
|
-
unresolved_decisions: [],
|
|
1830
|
-
graph_coverage: [
|
|
1831
|
-
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
1832
|
-
'assumption_provenance', 'compatibility_migration',
|
|
1833
|
-
].map((surface) => ({
|
|
1834
|
-
surface, covered: true,
|
|
1835
|
-
notes: 'The review covers this semantic surface against the canonical model.',
|
|
1836
|
-
})),
|
|
1837
|
-
reviewed_criteria: counterexamples.map(({ criterion }) => criterion),
|
|
1838
|
-
counterexamples,
|
|
1839
|
-
reviewer_evidence: null,
|
|
1840
|
-
};
|
|
1841
|
-
if (state.workflow_policy.assurance_level === 'Strict') {
|
|
1842
|
-
const digest = runInstalled(paths.validator, [fixture.featureDir, '--semantic-digest'], root).stdout.trim();
|
|
1843
|
-
assert.match(digest, /^sha256:[a-f0-9]{64}$/);
|
|
1844
|
-
return digest;
|
|
1845
|
-
}
|
|
1846
|
-
const reviewFile = path.join(fixture.featureDir, '.review-result.json');
|
|
1847
|
-
writeJson(reviewFile, fixture.reviewResult);
|
|
1848
|
-
runInstalled(paths.readiness, [fixture.featureDir, '--review-result', reviewFile], root);
|
|
1849
|
-
fs.unlinkSync(reviewFile);
|
|
1850
|
-
runInstalled(paths.validator, [fixture.featureDir], root);
|
|
1851
|
-
runInstalled(paths.grounder, [fixture.featureDir, '--root', root], root);
|
|
1852
|
-
return JSON.parse(fs.readFileSync(fixture.specFile, 'utf8')).validation.semantic_review.semantic_digest;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
function createTasklessFixture(paths, root, feature, options = {}) {
|
|
1856
|
-
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
1857
|
-
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
1858
|
-
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = (valid) => ({ status: valid === true ? "enabled" : "rejected" });\n');
|
|
1859
|
-
fs.writeFileSync(path.join(root, 'src', 'design-boundary.js'), 'module.exports = { entry: "src/entry.js" };\n');
|
|
1860
|
-
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), `const test = require('node:test');
|
|
1861
|
-
const assert = require('node:assert/strict');
|
|
1862
|
-
const entry = require('../src/entry.js');
|
|
1863
|
-
|
|
1864
|
-
test('entry accepts only the boolean true discriminator', () => {
|
|
1865
|
-
assert.deepEqual(entry(true), { status: 'enabled' });
|
|
1866
|
-
for (const invalid of [false, undefined, null, 'true', 1, {}]) {
|
|
1867
|
-
assert.deepEqual(entry(invalid), { status: 'rejected' });
|
|
1868
|
-
}
|
|
1869
|
-
});
|
|
1870
|
-
`);
|
|
1871
|
-
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node --test' } }));
|
|
1872
|
-
const fixture = materializeSpecState(paths, root, feature, options);
|
|
1873
|
-
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection());
|
|
1874
|
-
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection());
|
|
1875
|
-
fixture.digest = completeSemanticReview(paths, root, fixture, [{
|
|
1876
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
1877
|
-
scenario: 'The installed entry receives input without the required discriminator.',
|
|
1878
|
-
expected: 'The installed entry returns rejected and never returns enabled.',
|
|
1879
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
1880
|
-
}]);
|
|
1881
|
-
return fixture;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function taskProjection({ id, title, ownedPath, criterion, verificationRef, role, artifact = false }) {
|
|
1885
|
-
const artifactPath = `artifacts/${id}.json`;
|
|
1886
|
-
const anchors = typedAnchorTable([
|
|
1887
|
-
{ id: `A-${id}-01`, type: 'file', target: ownedPath, role: 'owner', access: 'write', action: 'modify' },
|
|
1888
|
-
...(artifact ? [{
|
|
1889
|
-
id: `A-${id}-02`, type: 'artifact', target: artifactPath,
|
|
1890
|
-
role: 'verifier', access: 'write', action: 'create',
|
|
1891
|
-
}] : []),
|
|
1892
|
-
{
|
|
1893
|
-
id: `A-${id}-${artifact ? '03' : '02'}`, type: 'command',
|
|
1894
|
-
target: 'node --test test/entry.test.js', role: 'verifier', access: 'read', action: 'read',
|
|
1895
|
-
},
|
|
1896
|
-
]);
|
|
1897
|
-
return `# Task ${id}: ${title}
|
|
1898
|
-
**Status:** pending
|
|
1899
|
-
|
|
1900
|
-
## Outcome
|
|
1901
|
-
|
|
1902
|
-
Deliver observable ${title.toLowerCase()} behavior through the installed entrypoint.
|
|
1903
|
-
|
|
1904
|
-
## Scope
|
|
1905
|
-
|
|
1906
|
-
- **In scope:** Exact behavior owned at ${ownedPath}.
|
|
1907
|
-
- **Out of scope:** Unrelated runtime behavior.
|
|
1908
|
-
|
|
1909
|
-
## Anchors and Ownership
|
|
1910
|
-
|
|
1911
|
-
${anchors}
|
|
1912
|
-
|
|
1913
|
-
## Changes
|
|
1914
|
-
|
|
1915
|
-
- [ ] Implement the exact owned behavior. _Requirements: ${criterion.slice(1)}_
|
|
1916
|
-
|
|
1917
|
-
## Acceptance
|
|
1918
|
-
|
|
1919
|
-
- **${criterion}:** The installed command returns the criterion-specific observable state.
|
|
1920
|
-
|
|
1921
|
-
## Dependencies
|
|
1922
|
-
|
|
1923
|
-
- none
|
|
1924
|
-
|
|
1925
|
-
## Verification Plan
|
|
1926
|
-
|
|
1927
|
-
- **Verification ref:** ${verificationRef}
|
|
1928
|
-
- **Task role:** ${role}
|
|
1929
|
-
- **Command:** \`node --test test/entry.test.js\`
|
|
1930
|
-
- **Expected:** Exit code 0 and the criterion-specific state is observed.${artifact ? ` The artifact \`${artifactPath}\` must exist, and SHA-256 over its current bytes must match the recorded digest.` : ''}
|
|
1931
|
-
- **Negative path:** Invalid input returns the named rejected state.
|
|
1932
|
-
- **Reachability:** \`src/entry.js\` is reached by the installed test command.
|
|
1933
|
-
`;
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
function createTaskFixture(paths, root, feature) {
|
|
1937
|
-
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
1938
|
-
const fixture = materializeSpecState(paths, root, feature);
|
|
1939
|
-
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection({ proof: true }));
|
|
1940
|
-
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection({
|
|
1941
|
-
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json` whose SHA-256 over current bytes matches the recorded digest',
|
|
1942
|
-
taskful: true,
|
|
1943
|
-
}));
|
|
1944
|
-
const taskOne = 'tasks/task-R1-01-implement.md';
|
|
1945
|
-
const taskTwo = 'tasks/task-R1-02-verify.md';
|
|
1946
|
-
fs.mkdirSync(path.join(fixture.featureDir, 'tasks'), { recursive: true });
|
|
1947
|
-
fs.writeFileSync(path.join(fixture.featureDir, taskOne), taskProjection({
|
|
1948
|
-
id: 'R1-01', title: 'Implement behavior', ownedPath: 'src/entry.js', criterion: 'R1.1',
|
|
1949
|
-
verificationRef: 'V1', role: 'subject implements R1.1',
|
|
1950
|
-
}));
|
|
1951
|
-
fs.writeFileSync(path.join(fixture.featureDir, taskTwo), taskProjection({
|
|
1952
|
-
id: 'R1-02', title: 'Verify behavior', ownedPath: 'test/entry.test.js', criterion: 'R1.2',
|
|
1953
|
-
verificationRef: 'V1', role: 'verifier verifies V1 through separately owned R1.2 proof', artifact: true,
|
|
1954
|
-
}));
|
|
1955
|
-
const state = fixture.state;
|
|
1956
|
-
state.authoring.tasks = 'draft';
|
|
1957
|
-
state.task_files = [taskOne, taskTwo];
|
|
1958
|
-
state.task_registry = Object.fromEntries([
|
|
1959
|
-
[taskOne, { id: 'R1-01', title: 'Implement behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
|
|
1960
|
-
[taskTwo, { id: 'R1-02', title: 'Verify behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
|
|
1961
|
-
]);
|
|
1962
|
-
state.coordination.boundaries = [{
|
|
1963
|
-
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
1964
|
-
write_sets: { 'R1-01': ['src/entry.js'], 'R1-02': ['test/entry.test.js', 'artifacts/R1-02.json'] },
|
|
1965
|
-
}, {
|
|
1966
|
-
id: 'B-PROOF', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
|
|
1967
|
-
verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
|
|
1968
|
-
}];
|
|
1969
|
-
writeJson(fixture.specFile, state);
|
|
1970
|
-
fixture.digest = completeSemanticReview(paths, root, fixture, [{
|
|
1971
|
-
criterion: 'R1.1', case_kind: 'failure',
|
|
1972
|
-
scenario: 'Invalid input reaches the installed implementation boundary.',
|
|
1973
|
-
expected: 'The implementation returns rejected and never reports enabled.',
|
|
1974
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
1975
|
-
}, {
|
|
1976
|
-
criterion: 'R1.2', case_kind: 'failure',
|
|
1977
|
-
scenario: 'The verification command completes without producing its proof artifact.',
|
|
1978
|
-
expected: 'The verification criterion remains unsatisfied until `artifacts/R1-02.json` exists and its SHA-256 over current bytes matches the recorded digest.',
|
|
1979
|
-
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
1980
|
-
}]);
|
|
1981
|
-
return fixture;
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
function initializeGit(root) {
|
|
1985
|
-
fs.writeFileSync(path.join(root, '.gitignore'), `${'node_' + 'modules'}/\n`);
|
|
1986
|
-
for (const args of [
|
|
1987
|
-
['init', '-q'], ['config', 'user.name', 'CafeKit Packed E2E'],
|
|
1988
|
-
['config', 'user.email', 'packed-e2e@example.invalid'],
|
|
1989
|
-
['add', '--', '.gitignore', 'src/entry.js', 'src/design-boundary.js', 'test/entry.test.js', 'package.json'],
|
|
1990
|
-
['commit', '--no-gpg-sign', '-qm', 'packed semantic fixture'],
|
|
1991
|
-
]) {
|
|
1992
|
-
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
1993
|
-
assert.equal(result.status, 0, `${args.join(' ')}\n${result.stdout}\n${result.stderr}`);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
function assertStrictRefusedAsUnsupported(paths, root, fixture) {
|
|
1998
|
-
const specFile = path.join(fixture.featureDir, 'spec.json');
|
|
1999
|
-
const before = fs.readFileSync(specFile);
|
|
2000
|
-
const reviewFile = path.join(root, 'strict-review.json');
|
|
2001
|
-
fs.writeFileSync(reviewFile, JSON.stringify(fixture.reviewResult));
|
|
2002
|
-
const result = spawnSync(process.execPath, [paths.readiness, fixture.featureDir, '--review-result', reviewFile], { cwd: root, encoding: 'utf8' });
|
|
2003
|
-
fs.rmSync(reviewFile, { force: true });
|
|
2004
|
-
assert.notEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
2005
|
-
assert.match(`${result.stdout}\n${result.stderr}`, /Strict assurance is no longer supported/);
|
|
2006
|
-
assert.equal(fs.readFileSync(specFile).equals(before), true, 'a refused Strict readiness must leave spec.json unchanged');
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
function assertStaleDigestMutations(paths, root, taskless, taskBearing) {
|
|
2010
|
-
const requirementFile = path.join(taskless.featureDir, 'requirements.md');
|
|
2011
|
-
const requirementBytes = fs.readFileSync(requirementFile);
|
|
2012
|
-
fs.appendFileSync(requirementFile, '\nThe installed Markdown meaning changed.\n');
|
|
2013
|
-
assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
2014
|
-
fs.writeFileSync(requirementFile, requirementBytes);
|
|
2015
|
-
|
|
2016
|
-
const tasklessState = JSON.parse(fs.readFileSync(taskless.specFile, 'utf8'));
|
|
2017
|
-
const originalPolicy = JSON.parse(JSON.stringify(tasklessState.workflow_policy));
|
|
2018
|
-
tasklessState.workflow_policy.planning_depth = 'Full';
|
|
2019
|
-
writeJson(taskless.specFile, tasklessState);
|
|
2020
|
-
assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
2021
|
-
tasklessState.workflow_policy = originalPolicy;
|
|
2022
|
-
writeJson(taskless.specFile, tasklessState);
|
|
2023
|
-
|
|
2024
|
-
const taskState = JSON.parse(fs.readFileSync(taskBearing.specFile, 'utf8'));
|
|
2025
|
-
const originalId = taskState.coordination.boundaries[0].id;
|
|
2026
|
-
taskState.coordination.boundaries[0].id = 'B-OWN-MUTATED';
|
|
2027
|
-
writeJson(taskBearing.specFile, taskState);
|
|
2028
|
-
assert.match(runInstalled(paths.validator, [taskBearing.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
2029
|
-
taskState.coordination.boundaries[0].id = originalId;
|
|
2030
|
-
writeJson(taskBearing.specFile, taskState);
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
1674
|
function assertTransforms(root, platform) {
|
|
2034
1675
|
const skillRoot = path.join(
|
|
2035
1676
|
root,
|
|
@@ -2745,41 +2386,6 @@ test('repository and package guides document adaptive Docs usage', () => {
|
|
|
2745
2386
|
}
|
|
2746
2387
|
});
|
|
2747
2388
|
|
|
2748
|
-
test('packed Claude and Codex installs execute semantic kernel behavior without package source', () => {
|
|
2749
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-semantic-'));
|
|
2750
|
-
const destination = path.join(root, 'pack');
|
|
2751
|
-
fs.mkdirSync(destination, { recursive: true });
|
|
2752
|
-
try {
|
|
2753
|
-
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
2754
|
-
const tarball = path.join(destination, packed.filename);
|
|
2755
|
-
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
2756
|
-
assertCleanInventory(packedInventory(tarball));
|
|
2757
|
-
|
|
2758
|
-
for (const platform of ['claude', 'codex']) {
|
|
2759
|
-
const project = path.join(root, platform);
|
|
2760
|
-
const installer = installPacked(tarball, project, runtimeClosure);
|
|
2761
|
-
runInstaller(installer, project, [platform], null);
|
|
2762
|
-
const packedSource = path.resolve(path.dirname(installer), '..', 'src');
|
|
2763
|
-
fs.rmSync(packedSource, { recursive: true, force: true });
|
|
2764
|
-
assert.equal(fs.existsSync(packedSource), false, `${platform} package source must be unavailable`);
|
|
2765
|
-
|
|
2766
|
-
const paths = installedSemanticPaths(project, platform);
|
|
2767
|
-
const taskless = createTasklessFixture(paths, project, 'compact-installed');
|
|
2768
|
-
initializeGit(project);
|
|
2769
|
-
|
|
2770
|
-
const strict = createTasklessFixture(paths, project, 'strict-installed', { strict: true });
|
|
2771
|
-
const taskBearing = createTaskFixture(paths, project, 'tasks-installed');
|
|
2772
|
-
assertStrictRefusedAsUnsupported(paths, project, strict);
|
|
2773
|
-
assertStaleDigestMutations(paths, project, taskless, taskBearing);
|
|
2774
|
-
runInstalled(paths.validator, [taskless.featureDir], project);
|
|
2775
|
-
runInstalled(paths.validator, [taskBearing.featureDir], project);
|
|
2776
|
-
runInstalled(paths.grounder, [taskBearing.featureDir, '--root', project], project);
|
|
2777
|
-
}
|
|
2778
|
-
} finally {
|
|
2779
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
2780
|
-
}
|
|
2781
|
-
});
|
|
2782
|
-
|
|
2783
2389
|
test('packed Claude and Codex installs self-contain runtime provenance and fail closed without it', () => {
|
|
2784
2390
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-provenance-package-'));
|
|
2785
2391
|
const destination = path.join(root, 'pack');
|