@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.
- package/bin/__tests__/codex-hooks.test.js +794 -21
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +1838 -112
- package/bin/__tests__/installer-safety.test.js +82 -29
- package/bin/__tests__/opencode-spec-gate.test.js +799 -0
- package/bin/__tests__/package-inventory.test.js +841 -10
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2469 -71
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +153 -202
- package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +604 -56
- package/src/claude/hooks/completion-authority-check.cjs +56 -0
- package/src/claude/hooks/completion-authority-state.cjs +527 -0
- package/src/claude/hooks/completion-authority.cjs +154 -0
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +181 -139
- package/src/claude/hooks/spec-state.cjs +112 -47
- package/src/claude/migration-manifest.json +15 -2
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/provenance.cjs +315 -0
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +501 -0
- package/src/claude/scripts/spec-scaffold.cjs +1945 -55
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3204 -262
- package/src/claude/scripts/workflow-policy.cjs +1888 -203
- package/src/claude/settings/settings.json +12 -0
- package/src/claude/skills/code-review/SKILL.md +94 -66
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +138 -283
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +123 -148
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +192 -397
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +26 -67
- package/src/claude/skills/specs/templates/task.md +30 -82
- package/src/claude/skills/sync/SKILL.md +45 -15
- package/src/claude/skills/sync/references/sync-protocols.md +34 -16
- package/src/claude/skills/test/SKILL.md +151 -167
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +63 -0
- package/src/codex/hooks/completion-authority-state.cjs +527 -0
- package/src/codex/hooks/completion-authority.cjs +106 -0
- package/src/codex/hooks/lib/hook-context.cjs +58 -4
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
- package/src/codex/hooks/lib/spec-utils.cjs +104 -27
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +151 -24
- package/src/codex/hooks/spec-state.cjs +65 -12
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +16 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
- package/src/opencode/AGENTS.md +4 -2
- package/src/opencode/plugins/spec-gate.ts +296 -122
- package/src/opencode/plugins/spec-state.ts +183 -116
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
try {
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const {
|
|
21
|
+
DYNAMIC_READ_PATH,
|
|
22
|
+
commandAccess,
|
|
23
|
+
} = require('./lib/privacy-command-analysis.cjs');
|
|
20
24
|
|
|
21
25
|
const RESTRICTED_PATTERNS = [
|
|
22
26
|
/^\.env(?:[.\[*?{]|$)/i,
|
|
@@ -33,7 +37,13 @@ try {
|
|
|
33
37
|
/\.keystore$/i,
|
|
34
38
|
/\.jks$/i,
|
|
35
39
|
/auth\.json$/i,
|
|
36
|
-
/token(s)?\.json$/i
|
|
40
|
+
/token(s)?\.json$/i,
|
|
41
|
+
/\.cafekit/i,
|
|
42
|
+
/hmac\.key/i,
|
|
43
|
+
/completion-authority/i,
|
|
44
|
+
/session-state/i,
|
|
45
|
+
/hook-log/i,
|
|
46
|
+
/sessions/i
|
|
37
47
|
];
|
|
38
48
|
|
|
39
49
|
const ALLOWED_EXEMPTIONS = [
|
|
@@ -55,6 +65,7 @@ try {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
function isSensitive(filePath) {
|
|
68
|
+
if (filePath === DYNAMIC_READ_PATH) return true;
|
|
58
69
|
const base = path.basename(filePath);
|
|
59
70
|
return RESTRICTED_PATTERNS.some((rule) => rule.test(base) || rule.test(filePath));
|
|
60
71
|
}
|
|
@@ -63,7 +74,7 @@ try {
|
|
|
63
74
|
* Resolve a symlink to its real target so a harmless-looking name that points
|
|
64
75
|
* at a sensitive file cannot slip through the basename check. Returns null
|
|
65
76
|
* when the path cannot be resolved (missing file, broken link) — fail-open to
|
|
66
|
-
* the original-path check in that case.
|
|
77
|
+
* the original-path check in that case. See isSymlinkToSensitive for dangling handling.
|
|
67
78
|
*/
|
|
68
79
|
function resolveTarget(filePath, cwd) {
|
|
69
80
|
try {
|
|
@@ -74,19 +85,58 @@ try {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
/**
|
|
89
|
+
* When realpath fails, check whether the requested path is a symlink (or chain)
|
|
90
|
+
* whose lexical target is sensitive. Dangling link to protected target -> true (fail-closed ask);
|
|
91
|
+
* dangling benign link or link to allowed example/template -> false (allow).
|
|
92
|
+
* Limit hops to avoid loops; malformed/looping -> fail closed.
|
|
93
|
+
* TOCTOU note: check and use are not atomic; target could be swapped between this check and the actual read.
|
|
94
|
+
* This is a best-effort guardrail, not an arbitrary-shell security boundary. For strong isolation use OS sandbox.
|
|
95
|
+
*/
|
|
96
|
+
function isSymlinkToSensitive(filePath, cwd) {
|
|
97
|
+
try {
|
|
98
|
+
const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
99
|
+
let current = requested;
|
|
100
|
+
let hops = 0;
|
|
101
|
+
const MAX_HOPS = 8;
|
|
102
|
+
while (hops < MAX_HOPS) {
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = fs.lstatSync(current);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (!stat.isSymbolicLink()) return false;
|
|
110
|
+
let linkTarget;
|
|
111
|
+
try {
|
|
112
|
+
linkTarget = fs.readlinkSync(current);
|
|
113
|
+
} catch {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (isSensitive(linkTarget) && !isSafe(linkTarget)) return true;
|
|
117
|
+
if (isSafe(linkTarget)) return false;
|
|
118
|
+
const next = path.isAbsolute(linkTarget) ? linkTarget : path.resolve(path.dirname(current), linkTarget);
|
|
119
|
+
if (next === current) return true;
|
|
120
|
+
try {
|
|
121
|
+
const real = fs.realpathSync(next);
|
|
122
|
+
if (isSensitive(real) && !isSafe(real)) return true;
|
|
123
|
+
if (isSafe(real)) return false;
|
|
124
|
+
return false;
|
|
125
|
+
} catch {
|
|
126
|
+
current = next;
|
|
127
|
+
hops += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
87
130
|
}
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return true;
|
|
88
134
|
}
|
|
89
|
-
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extractBashPaths(command) {
|
|
138
|
+
const access = commandAccess(command);
|
|
139
|
+
return access.dynamic ? [...access.paths, DYNAMIC_READ_PATH] : access.paths;
|
|
90
140
|
}
|
|
91
141
|
|
|
92
142
|
function extractPaths(toolName, input) {
|
|
@@ -129,7 +179,7 @@ try {
|
|
|
129
179
|
const data = JSON.parse(stdin);
|
|
130
180
|
const toolName = data.tool_name || '';
|
|
131
181
|
const toolInput = data.tool_input || {};
|
|
132
|
-
const cwd = data.cwd
|
|
182
|
+
const cwd = typeof data.cwd === 'string' && data.cwd.trim() ? data.cwd : process.cwd();
|
|
133
183
|
const runtime = readRuntime(cwd);
|
|
134
184
|
|
|
135
185
|
if (runtime.privacyBlock === false) process.exit(0);
|
|
@@ -148,6 +198,11 @@ try {
|
|
|
148
198
|
process.exit(0);
|
|
149
199
|
}
|
|
150
200
|
if (isSafe(target)) continue;
|
|
201
|
+
} else {
|
|
202
|
+
if (isSymlinkToSensitive(filePath, cwd)) {
|
|
203
|
+
askForPermission(filePath);
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
151
206
|
}
|
|
152
207
|
|
|
153
208
|
if (isSafe(filePath) || !isSensitive(filePath)) continue;
|
|
@@ -156,21 +211,13 @@ try {
|
|
|
156
211
|
}
|
|
157
212
|
|
|
158
213
|
process.exit(0);
|
|
159
|
-
} catch (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
JSON.stringify({
|
|
168
|
-
ts: new Date().toISOString(),
|
|
169
|
-
hook: 'privacy-block',
|
|
170
|
-
status: 'crash',
|
|
171
|
-
error: error.message
|
|
172
|
-
}) + '\n'
|
|
173
|
-
);
|
|
174
|
-
} catch (_) {}
|
|
214
|
+
} catch (_) {
|
|
215
|
+
process.stdout.write(JSON.stringify({
|
|
216
|
+
hookSpecificOutput: {
|
|
217
|
+
hookEventName: 'PreToolUse',
|
|
218
|
+
permissionDecision: 'ask',
|
|
219
|
+
permissionDecisionReason: 'Sensitive access could not be evaluated safely.'
|
|
220
|
+
}
|
|
221
|
+
}) + '\n');
|
|
175
222
|
process.exit(0);
|
|
176
223
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const STATE = require('./completion-authority-state.cjs');
|
|
8
|
+
const PATH_SAFETY = require('./lib/runtime-path-safety.cjs');
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const MARKER = 'CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ';
|
|
12
|
+
const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
|
|
13
|
+
const REVIEWER_ROLE_ALLOWLIST = new Set(['code-auditor', 'code_auditor']);
|
|
14
|
+
const RECORD_FIELDS = ['schema_version', 'kind', 'identity', 'semantic_digest', 'event', 'verdict', 'host_session_id', 'reviewer_agent_id', 'reviewer_agent_type', 'issued_at', 'mac'];
|
|
15
|
+
|
|
16
|
+
function plain(value) {
|
|
17
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function exactKeys(value, fields) {
|
|
21
|
+
return plain(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inside(root, target) {
|
|
25
|
+
const relative = path.relative(root, target);
|
|
26
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function projectRoot(payload = {}) {
|
|
30
|
+
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
31
|
+
if (fs.existsSync(path.join(installedRoot, '.claude', 'hooks', path.basename(__filename)))) {
|
|
32
|
+
try { return STATE.canonicalProjectRoot(installedRoot); } catch {}
|
|
33
|
+
}
|
|
34
|
+
for (const candidate of [process.env.CLAUDE_PROJECT_DIR, process.env.PROJECT_ROOT]) {
|
|
35
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
36
|
+
try { return STATE.canonicalProjectRoot(candidate); } catch { /* continue */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd.trim() ? payload.cwd : process.cwd();
|
|
40
|
+
return STATE.canonicalProjectRoot(cwd);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function identityFor(root, specFile, featureName) {
|
|
44
|
+
if (typeof featureName !== 'string' || !featureName.trim() || featureName.includes('/') || featureName.includes('\\') || featureName === '.' || featureName === '..' || featureName.includes('..')) throw new Error('feature identity is malformed');
|
|
45
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(featureName)) throw new Error('feature identity is malformed');
|
|
46
|
+
const canonicalRoot = STATE.canonicalProjectRoot(root);
|
|
47
|
+
const canonicalSpec = fs.realpathSync(path.resolve(specFile));
|
|
48
|
+
if (!inside(canonicalRoot, canonicalSpec) || path.basename(canonicalSpec) !== 'spec.json') throw new Error('spec identity escapes the project');
|
|
49
|
+
const relative = path.relative(canonicalRoot, canonicalSpec);
|
|
50
|
+
const expected = path.join('specs', featureName, 'spec.json');
|
|
51
|
+
if (relative !== expected) throw new Error(`spec identity must be ${expected}, not ${relative}`);
|
|
52
|
+
const spec = JSON.parse(fs.readFileSync(canonicalSpec, 'utf8'));
|
|
53
|
+
if (spec.feature_name !== featureName) throw new Error('feature identity does not match spec.json');
|
|
54
|
+
return { project_root: canonicalRoot, spec_file: canonicalSpec, feature_name: featureName };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordPath(root, identity) {
|
|
58
|
+
const name = STATE.digest({ namespace: 'cafekit-semantic-review-observation-v1', identity });
|
|
59
|
+
return path.join(STATE.stateDir(root), 'semantic-reviews', `observation-${name}.json`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseClaim(message) {
|
|
63
|
+
const lines = String(message || '').split(/\r?\n/).filter((line) => line.startsWith(MARKER));
|
|
64
|
+
if (lines.length === 0) return null;
|
|
65
|
+
if (lines.length !== 1) throw new Error('exactly one semantic review attestation marker is required');
|
|
66
|
+
const claim = JSON.parse(lines[0].slice(MARKER.length));
|
|
67
|
+
if (!exactKeys(claim, ['feature_name', 'spec_file', 'semantic_digest', 'verdict'])) throw new Error('semantic review claim fields are malformed');
|
|
68
|
+
if (claim.verdict !== 'PASS' || !SHA256_RE.test(claim.semantic_digest || '')) throw new Error('semantic review claim must contain literal PASS and a sha256 digest');
|
|
69
|
+
return claim;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validatorFor(root) {
|
|
73
|
+
const installedRoot = path.join(root, '.claude');
|
|
74
|
+
const installedHook = path.join(installedRoot, 'hooks', path.basename(__filename));
|
|
75
|
+
if (fs.existsSync(installedHook)) {
|
|
76
|
+
const canonicalHook = PATH_SAFETY.canonicalRegularFile(root, installedHook, 'installed semantic hook');
|
|
77
|
+
if (canonicalHook !== fs.realpathSync(__filename)) throw new Error('installed semantic hook identity is invalid');
|
|
78
|
+
const validator = path.join(installedRoot, 'scripts', 'validate-spec-output.cjs');
|
|
79
|
+
PATH_SAFETY.verifyCommonJsClosure(installedRoot, validator);
|
|
80
|
+
return PATH_SAFETY.canonicalRegularFile(installedRoot, validator, 'semantic validator');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sourceRuntime = path.resolve(__dirname, '..');
|
|
84
|
+
const sourceRoot = path.resolve(sourceRuntime, '..');
|
|
85
|
+
if (path.basename(sourceRuntime) !== 'claude' || path.basename(sourceRoot) !== 'src') {
|
|
86
|
+
throw new Error('semantic validator runtime root is not recognized');
|
|
87
|
+
}
|
|
88
|
+
const validator = path.join(sourceRuntime, 'scripts', 'validate-spec-output.cjs');
|
|
89
|
+
PATH_SAFETY.verifyCommonJsClosure(sourceRoot, validator);
|
|
90
|
+
return PATH_SAFETY.canonicalRegularFile(sourceRoot, validator, 'semantic validator');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function currentDigest(root, specFile) {
|
|
94
|
+
const validator = validatorFor(root);
|
|
95
|
+
const result = spawnSync(process.execPath, [validator, path.dirname(specFile), '--semantic-digest'], { cwd: root, encoding: 'utf8' });
|
|
96
|
+
const digest = result.stdout.trim();
|
|
97
|
+
if (result.status !== 0 || !SHA256_RE.test(digest)) throw new Error(`semantic digest could not be recomputed (${result.stderr.trim() || `exit ${result.status}`})`);
|
|
98
|
+
return digest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function recordFromSubagentStop(payload) {
|
|
102
|
+
// Honest-agent guardrail: the host hook observes this SubagentStop payload.
|
|
103
|
+
// The worker-authored claim is not authenticated input or a security boundary.
|
|
104
|
+
if (!plain(payload) || payload.hook_event_name !== 'SubagentStop') return { recorded: false, reason: 'event' };
|
|
105
|
+
for (const field of ['session_id', 'agent_id', 'agent_type', 'last_assistant_message']) {
|
|
106
|
+
if (typeof payload[field] !== 'string' || !payload[field].trim()) return { recorded: false, reason: `missing-${field}` };
|
|
107
|
+
}
|
|
108
|
+
if (!REVIEWER_ROLE_ALLOWLIST.has(payload.agent_type)) return { recorded: false, reason: 'reviewer-role' };
|
|
109
|
+
const root = projectRoot(payload);
|
|
110
|
+
const claim = parseClaim(payload.last_assistant_message);
|
|
111
|
+
if (claim === null) return { recorded: false, reason: 'no-claim' };
|
|
112
|
+
const requestedSpec = path.resolve(root, claim.spec_file);
|
|
113
|
+
const identity = identityFor(root, requestedSpec, claim.feature_name);
|
|
114
|
+
const digest = currentDigest(root, identity.spec_file);
|
|
115
|
+
if (digest !== claim.semantic_digest) throw new Error('claimed semantic digest does not match current artifacts');
|
|
116
|
+
const key = STATE.ensureKey();
|
|
117
|
+
const record = {
|
|
118
|
+
schema_version: SCHEMA_VERSION, kind: 'semantic-review-observation', identity,
|
|
119
|
+
semantic_digest: digest, event: 'SubagentStop', verdict: 'PASS',
|
|
120
|
+
host_session_id: payload.session_id, reviewer_agent_id: payload.agent_id,
|
|
121
|
+
reviewer_agent_type: payload.agent_type, issued_at: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
record.mac = STATE.recordMac(record, key);
|
|
124
|
+
STATE.atomicWrite(recordPath(root, identity), record);
|
|
125
|
+
return { recorded: true, record };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function verifyAttestation(root, specFile, featureName, semanticDigest) {
|
|
129
|
+
let identity;
|
|
130
|
+
try { identity = identityFor(root, specFile, featureName); } catch (error) { return { ok: false, reason: error.message }; }
|
|
131
|
+
const file = recordPath(root, identity);
|
|
132
|
+
let record;
|
|
133
|
+
try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { return { ok: false, reason: error.code === 'ENOENT' ? 'matching host-hook observation is missing' : 'host-hook observation is unreadable' }; }
|
|
134
|
+
const key = STATE.readKey();
|
|
135
|
+
if (key.missing) return { ok: false, reason: 'completion authority key is missing' };
|
|
136
|
+
if (!exactKeys(record, RECORD_FIELDS) || !STATE.macMatches(record, key.key)) return { ok: false, reason: 'host-hook observation is malformed or its MAC is invalid' };
|
|
137
|
+
if (record.schema_version !== SCHEMA_VERSION || record.kind !== 'semantic-review-observation' || record.event !== 'SubagentStop' || record.verdict !== 'PASS') return { ok: false, reason: 'host-hook observation event or verdict is invalid' };
|
|
138
|
+
if (STATE.stableStringify(record.identity) !== STATE.stableStringify(identity)) return { ok: false, reason: 'host-hook observation project, feature, or spec identity is invalid' };
|
|
139
|
+
if (record.semantic_digest !== semanticDigest) return { ok: false, reason: 'host-hook observation semantic digest is stale' };
|
|
140
|
+
return { ok: true, record };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
146
|
+
if (raw) recordFromSubagentStop(JSON.parse(raw));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
process.stderr.write(`CafeKit semantic review SubagentStop claim rejected: ${error.message}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { MARKER, REVIEWER_ROLE_ALLOWLIST, recordFromSubagentStop, verifyAttestation, identityFor, recordPath };
|
|
@@ -18,10 +18,10 @@ try {
|
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
|
|
20
20
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
21
|
-
function run(cmd, fallback = '') {
|
|
21
|
+
function run(cmd, fallback = '', cwd = process.cwd()) {
|
|
22
22
|
try {
|
|
23
23
|
return execSync(cmd, {
|
|
24
|
-
encoding: 'utf8', timeout: 3000,
|
|
24
|
+
cwd, encoding: 'utf8', timeout: 3000,
|
|
25
25
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
26
26
|
}).trim();
|
|
27
27
|
} catch { return fallback; }
|
|
@@ -40,6 +40,21 @@ try {
|
|
|
40
40
|
} catch { /* fail-open */ }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function projectRoot() {
|
|
44
|
+
const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
|
|
45
|
+
? process.env.CLAUDE_PROJECT_DIR.trim()
|
|
46
|
+
: '';
|
|
47
|
+
if (configured) {
|
|
48
|
+
try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
52
|
+
const installedHook = path.join(installedRoot, '.claude', 'hooks', path.basename(__filename));
|
|
53
|
+
if (fs.existsSync(installedHook)) return installedRoot;
|
|
54
|
+
|
|
55
|
+
try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
/** Read .claude/runtime.json config */
|
|
44
59
|
function readRuntime(cwd) {
|
|
45
60
|
try {
|
|
@@ -50,8 +65,7 @@ try {
|
|
|
50
65
|
|
|
51
66
|
// ── Project Detection ──────────────────────────────────────────────────────
|
|
52
67
|
|
|
53
|
-
function detectProjectType() {
|
|
54
|
-
const cwd = process.cwd();
|
|
68
|
+
function detectProjectType(cwd) {
|
|
55
69
|
if (fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml')) ||
|
|
56
70
|
fs.existsSync(path.join(cwd, 'lerna.json'))) return 'monorepo';
|
|
57
71
|
try {
|
|
@@ -62,8 +76,7 @@ try {
|
|
|
62
76
|
return 'app';
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
function detectPackageManager() {
|
|
66
|
-
const cwd = process.cwd();
|
|
79
|
+
function detectPackageManager(cwd) {
|
|
67
80
|
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
68
81
|
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
69
82
|
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun';
|
|
@@ -71,10 +84,10 @@ try {
|
|
|
71
84
|
return '';
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
function detectFramework() {
|
|
87
|
+
function detectFramework(cwd) {
|
|
75
88
|
try {
|
|
76
89
|
const pkg = JSON.parse(
|
|
77
|
-
fs.readFileSync(path.join(
|
|
90
|
+
fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')
|
|
78
91
|
);
|
|
79
92
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
80
93
|
if (deps['next']) return 'next';
|
|
@@ -185,31 +198,32 @@ try {
|
|
|
185
198
|
const payload = stdin ? JSON.parse(stdin) : {};
|
|
186
199
|
const source = payload.source || 'unknown';
|
|
187
200
|
const envFile = process.env.CLAUDE_ENV_FILE;
|
|
188
|
-
const cwd =
|
|
201
|
+
const cwd = projectRoot();
|
|
189
202
|
const runtime = readRuntime(cwd);
|
|
203
|
+
try { require('./completion-authority-state.cjs').clearState(cwd, payload.session_id); } catch { /* old installs remain fail-open */ }
|
|
190
204
|
|
|
191
205
|
// Check CafeKit update in parallel with project detection (async, fail-open).
|
|
192
206
|
const updateCheckPromise = checkCafeKitUpdate(cwd);
|
|
193
207
|
|
|
194
208
|
// Project detection
|
|
195
209
|
const projectType = runtime.project?.type !== 'auto'
|
|
196
|
-
? (runtime.project?.type || detectProjectType())
|
|
197
|
-
: detectProjectType();
|
|
210
|
+
? (runtime.project?.type || detectProjectType(cwd))
|
|
211
|
+
: detectProjectType(cwd);
|
|
198
212
|
|
|
199
213
|
const packageManager = runtime.project?.packageManager !== 'auto'
|
|
200
|
-
? (runtime.project?.packageManager || detectPackageManager())
|
|
201
|
-
: detectPackageManager();
|
|
214
|
+
? (runtime.project?.packageManager || detectPackageManager(cwd))
|
|
215
|
+
: detectPackageManager(cwd);
|
|
202
216
|
|
|
203
217
|
const framework = runtime.project?.framework !== 'auto'
|
|
204
|
-
? (runtime.project?.framework || detectFramework())
|
|
205
|
-
: detectFramework();
|
|
218
|
+
? (runtime.project?.framework || detectFramework(cwd))
|
|
219
|
+
: detectFramework(cwd);
|
|
206
220
|
|
|
207
221
|
// Static environment
|
|
208
|
-
const gitBranch = run('git branch --show-current');
|
|
209
|
-
const gitUrl = run('git remote get-url origin');
|
|
210
|
-
const gitRoot = run('git rev-parse --show-toplevel');
|
|
222
|
+
const gitBranch = run('git branch --show-current', '', cwd);
|
|
223
|
+
const gitUrl = run('git remote get-url origin', '', cwd);
|
|
224
|
+
const gitRoot = run('git rev-parse --show-toplevel', '', cwd);
|
|
211
225
|
const nodeVer = process.version;
|
|
212
|
-
const pythonVer = run('python3 --version') || run('python --version');
|
|
226
|
+
const pythonVer = run('python3 --version', '', cwd) || run('python --version', '', cwd);
|
|
213
227
|
const user = process.env.USERNAME || process.env.USER
|
|
214
228
|
|| process.env.LOGNAME || os.userInfo().username;
|
|
215
229
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|