@haposoft/cafekit 0.18.1 → 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 +774 -6
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +192 -59
- 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 +1 -1
- 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-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__/privacy-paths.test.js +317 -0
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- 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 -52
- package/bin/phases/copy-payload.js +1 -3
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +4 -0
- 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 +141 -10
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -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 +9 -26
- package/src/claude/hooks/spec-state.cjs +43 -59
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +8 -1
- package/src/claude/rules/hook-protocols.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/settings/settings.json +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/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- 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-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- 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 +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- 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/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
17
|
+
const { packetAt, namedPackets, explicitFailure } = require('./lib/spec-touch.cjs');
|
|
16
18
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
17
19
|
|
|
18
20
|
function logCrash(error) {
|
|
@@ -40,44 +42,6 @@ function sessionIdentity(payload) {
|
|
|
40
42
|
return candidates.find((value) => typeof value === 'string' && value.length > 0) || null;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function canonicalPath(value) {
|
|
44
|
-
try { return fs.realpathSync(value); } catch { return null; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Real path of `value`, resolving its deepest existing ancestor (the file may not exist yet). */
|
|
48
|
-
function canonicalTarget(value) {
|
|
49
|
-
if (typeof value !== 'string' || !value) return null;
|
|
50
|
-
let current = path.resolve(value);
|
|
51
|
-
const rest = [];
|
|
52
|
-
while (!fs.existsSync(current)) {
|
|
53
|
-
const parent = path.dirname(current);
|
|
54
|
-
if (parent === current) return null;
|
|
55
|
-
rest.unshift(path.basename(current));
|
|
56
|
-
current = parent;
|
|
57
|
-
}
|
|
58
|
-
const real = canonicalPath(current);
|
|
59
|
-
return real ? path.join(real, ...rest) : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function escapeRegExp(text) {
|
|
63
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Packets the prompt names: `specs/<name>`, a `/cf:<command> <name>` token, or the bare
|
|
68
|
-
* name when it carries `-`, `_` or a digit. A plain word such as `docs` must use `specs/`,
|
|
69
|
-
* so an ordinary sentence does not touch a packet.
|
|
70
|
-
*/
|
|
71
|
-
function packetsNamedIn(text, names) {
|
|
72
|
-
if (typeof text !== 'string' || !text) return [];
|
|
73
|
-
return names.filter((name) => {
|
|
74
|
-
const n = escapeRegExp(name);
|
|
75
|
-
if (new RegExp(`(^|[^\\w-])specs/${n}(?![\\w-])`).test(text)) return true;
|
|
76
|
-
if (new RegExp(`/cf:[\\w-]+\\s+(?:specs/)?${n}(?![\\w-])`).test(text)) return true;
|
|
77
|
-
return /[-_0-9]/.test(name) && new RegExp(`(^|[^\\w/-])${n}(?![\\w-])`).test(text);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
45
|
try {
|
|
82
46
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
83
47
|
|
|
@@ -94,6 +58,7 @@ try {
|
|
|
94
58
|
let RECEIPT;
|
|
95
59
|
try {
|
|
96
60
|
POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
|
|
61
|
+
require(path.join(__dirname, '..', 'scripts', 'provenance.cjs')).enableSnapshotMemo();
|
|
97
62
|
RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
|
|
98
63
|
RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
|
|
99
64
|
if (typeof RESOLVER.resolveWorkflowCandidate !== 'function') throw new Error('shared spec resolver has no resolveWorkflowCandidate function');
|
|
@@ -103,10 +68,12 @@ try {
|
|
|
103
68
|
process.exit(0);
|
|
104
69
|
}
|
|
105
70
|
|
|
71
|
+
const baseDir = projectRoot(payload);
|
|
72
|
+
|
|
106
73
|
// Read runtime configuration if exists
|
|
107
74
|
let runtime = {};
|
|
108
75
|
try {
|
|
109
|
-
const p = runtimePath(
|
|
76
|
+
const p = runtimePath(baseDir, 'runtime.json');
|
|
110
77
|
if (fs.existsSync(p)) runtime = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
111
78
|
} catch { /* ignore */ }
|
|
112
79
|
|
|
@@ -114,20 +81,17 @@ try {
|
|
|
114
81
|
// (The Stop completion gate has its own toggle: spec.completion_gate.)
|
|
115
82
|
if (runtime.spec && runtime.spec.tollgate === false) process.exit(0);
|
|
116
83
|
|
|
117
|
-
// Same rule as spec-gate: an installed hook trusts its own location over the environment.
|
|
118
|
-
const installedRoot = path.basename(path.resolve(__dirname, '..')).startsWith('.') ? path.resolve(__dirname, '..', '..') : null;
|
|
119
|
-
const baseDir = installedRoot || process.env.PROJECT_ROOT || cwd;
|
|
120
84
|
const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
|
|
121
|
-
const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
|
|
85
|
+
const explicitPath = payload.explicitPath || payload.specPath || payload.spec_path || payload.featurePath || null;
|
|
122
86
|
|
|
123
|
-
// ── Per-session touch set (Claude
|
|
124
|
-
// Several sessions can share one checkout. On Claude the tollgate speaks only
|
|
125
|
-
// packet this session touched.
|
|
126
|
-
// `sessionId` before normalization, so
|
|
87
|
+
// ── Per-session touch set (Claude and omp) ────────────────────────────────
|
|
88
|
+
// Several sessions can share one checkout. On Claude and omp the tollgate speaks only
|
|
89
|
+
// about a packet this session touched. Codex runs its own copy, and grok sends
|
|
90
|
+
// `sessionId` before normalization, so both keep the old behaviour.
|
|
127
91
|
const claudeSession = rawPayload && typeof rawPayload.session_id === 'string' && rawPayload.session_id
|
|
128
92
|
? rawPayload.session_id
|
|
129
93
|
: null;
|
|
130
|
-
const touchFilter =
|
|
94
|
+
const touchFilter = ['.claude', '.omp'].includes(runtimeDirName()) && claudeSession !== null;
|
|
131
95
|
const touchFile = touchFilter
|
|
132
96
|
? path.join(
|
|
133
97
|
require('./lib/hook-state-dir.cjs').hookStateDir(),
|
|
@@ -155,14 +119,9 @@ try {
|
|
|
155
119
|
|
|
156
120
|
if (payload.hook_event_name === 'PostToolUse') {
|
|
157
121
|
// Record only; no resolve, no git, no tollgate cache.
|
|
158
|
-
if (touchFilter && specsRoot && /^(Edit|Write|MultiEdit)$/.test(payload.tool_name || '')) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
if (realRoot && realTarget) {
|
|
162
|
-
const rel = path.relative(realRoot, realTarget);
|
|
163
|
-
const parts = rel.split(path.sep);
|
|
164
|
-
if (rel && parts[0] !== '..' && !path.isAbsolute(rel) && parts.length > 1) addTouched([parts[0]]);
|
|
165
|
-
}
|
|
122
|
+
if (touchFilter && specsRoot && !explicitFailure(payload) && /^(Edit|Write|MultiEdit)$/.test(payload.tool_name || '')) {
|
|
123
|
+
const name = packetAt(payload.tool_input && payload.tool_input.file_path, specsRoot, cwd);
|
|
124
|
+
if (name) addTouched([name]);
|
|
166
125
|
}
|
|
167
126
|
process.exit(0);
|
|
168
127
|
}
|
|
@@ -204,12 +163,29 @@ try {
|
|
|
204
163
|
try {
|
|
205
164
|
packets = fs.readdirSync(specsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
206
165
|
} catch { /* no specs root */ }
|
|
207
|
-
const named =
|
|
166
|
+
const named = specsRoot ? namedPackets(payload.prompt || payload.user_prompt, packets, specsRoot, baseDir) : [];
|
|
208
167
|
if (typeof explicitFeature === 'string' && explicitFeature) named.push(explicitFeature);
|
|
168
|
+
const fromPath = specsRoot ? packetAt(explicitPath, specsRoot, baseDir) : null;
|
|
169
|
+
if (fromPath) named.push(fromPath);
|
|
209
170
|
if (named.length) addTouched(named);
|
|
210
171
|
}
|
|
211
172
|
|
|
212
|
-
|
|
173
|
+
let resolved = RESOLVER.resolveWorkflowCandidate({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
|
|
174
|
+
if (touchFilter && !explicitFeature && !explicitPath
|
|
175
|
+
&& ['multiple_active', 'invalid_specs'].includes(resolved?.error)) {
|
|
176
|
+
const touched = readTouched();
|
|
177
|
+
const candidates = resolved.candidates || [];
|
|
178
|
+
if (!candidates.includes('<specs>') && !candidates.some(name => touched.has(name))) {
|
|
179
|
+
const selected = [...touched].map(name => RESOLVER.resolveWorkflowCandidate({
|
|
180
|
+
projectRoot: baseDir, runtime, explicitFeature: name,
|
|
181
|
+
}));
|
|
182
|
+
resolved = selected.find(candidate => candidate?.error);
|
|
183
|
+
const active = selected.filter(candidate => candidate && !candidate.error && !candidate.allTasksDone);
|
|
184
|
+
if (!resolved) resolved = active.length > 1
|
|
185
|
+
? { error: 'multiple_active', candidates: active.map(candidate => candidate.featureName) }
|
|
186
|
+
: active[0] || null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
213
189
|
|
|
214
190
|
if (!resolved) {
|
|
215
191
|
process.exit(0); // No active spec, do nothing
|
|
@@ -299,7 +275,10 @@ try {
|
|
|
299
275
|
done: taskCounts.done || 0,
|
|
300
276
|
total: taskEntries.length,
|
|
301
277
|
});
|
|
302
|
-
const
|
|
278
|
+
const cacheName = touchFilter
|
|
279
|
+
? `tollgate-last-${require('crypto').createHash('sha256').update(claudeSession).digest('hex').slice(0, 16)}.txt`
|
|
280
|
+
: 'tollgate-last.txt';
|
|
281
|
+
const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), cacheName);
|
|
303
282
|
|
|
304
283
|
let lastKey = '';
|
|
305
284
|
if (stateKey) {
|
|
@@ -335,12 +314,17 @@ try {
|
|
|
335
314
|
}
|
|
336
315
|
lines.push('- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.');
|
|
337
316
|
lines.push(`- Workflow source: \`specs/${featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`);
|
|
317
|
+
lines.push('- Hooks revalidate receipt bytes but never grant approval.');
|
|
338
318
|
lines.push('');
|
|
339
319
|
|
|
340
320
|
console.log(lines.join('\n'));
|
|
341
321
|
process.exit(0);
|
|
342
322
|
|
|
343
323
|
} catch (e) {
|
|
324
|
+
if (e.code === 'no_git_history') {
|
|
325
|
+
console.log('Repo has no commits yet; create the first commit, then record Base/Head after it.');
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
344
328
|
logCrash(e);
|
|
345
329
|
emitControlledFailure(e.message);
|
|
346
330
|
process.exit(0);
|
|
@@ -8,14 +8,26 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Events:
|
|
10
10
|
* SessionStart → load previous state and print to context
|
|
11
|
-
* PostToolUse → refresh state after Task/TaskCreate/TaskUpdate/TodoWrite
|
|
11
|
+
* PostToolUse → refresh state after Agent/Task/TaskCreate/TaskUpdate/TodoWrite
|
|
12
12
|
* Stop → persist full session state and archive
|
|
13
13
|
* SubagentStop → append agent completion note to current state
|
|
14
14
|
*
|
|
15
|
-
* Storage: <runtime>/session-state/latest.md (+ archive/)
|
|
15
|
+
* Storage: <runtime>/session-state/[session hash/]latest.md (+ archive/)
|
|
16
16
|
* Exit: 0 always (fail-open)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
function logCrash(error) {
|
|
20
|
+
try {
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
24
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
25
|
+
fs.appendFileSync(path.join(logDir, 'hook-log.jsonl'), JSON.stringify({
|
|
26
|
+
ts: new Date().toISOString(), hook: 'state', status: 'crash', error: error.message,
|
|
27
|
+
}) + '\n');
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
try {
|
|
20
32
|
const fs = require('fs');
|
|
21
33
|
const path = require('path');
|
|
@@ -23,34 +35,73 @@ try {
|
|
|
23
35
|
const crypto = require('crypto');
|
|
24
36
|
const { execSync } = require('child_process');
|
|
25
37
|
const { parseTranscript } = require('./lib/parser.cjs');
|
|
38
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
39
|
+
const { containsSecretKeyword } = require('./lib/secret-keywords.cjs');
|
|
26
40
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
27
41
|
|
|
28
42
|
const EXPIRY_DAYS = 7;
|
|
29
43
|
const MAX_ARCHIVES = 5;
|
|
30
44
|
const MAX_AGENT_SECTIONS = 3;
|
|
31
45
|
const MAX_MODIFIED_FILES_DISPLAY = 5;
|
|
32
|
-
const TRACKED_POST_TOOL_EVENTS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
|
|
46
|
+
const TRACKED_POST_TOOL_EVENTS = new Set(['Agent', 'Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
|
|
47
|
+
const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
33
48
|
|
|
34
|
-
function
|
|
49
|
+
function withStateLock(dir, callback) {
|
|
50
|
+
const lock = path.join(dir, '.write-lock');
|
|
51
|
+
const deadline = Date.now() + 3000;
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(lock);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code !== 'EEXIST') return;
|
|
57
|
+
try {
|
|
58
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 15000) {
|
|
59
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
} catch { continue; }
|
|
63
|
+
Atomics.wait(WAIT_BUFFER, 0, 0, 20);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
try { return callback(); } finally {
|
|
67
|
+
try { fs.rmdirSync(lock); } catch { /* already released */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assistantSummary(value) {
|
|
73
|
+
if (value === null || value === undefined) return '';
|
|
74
|
+
let text;
|
|
75
|
+
if (typeof value === 'object') {
|
|
76
|
+
const preferred = value.message ?? value.output ?? value.result ?? value.content;
|
|
77
|
+
text = typeof preferred === 'string' ? preferred : JSON.stringify(value);
|
|
78
|
+
} else {
|
|
79
|
+
text = String(value);
|
|
80
|
+
}
|
|
81
|
+
if (containsSecretKeyword(text)) return '[omitted: possible secret]';
|
|
82
|
+
const clean = text.replace(/\0/g, '').trim();
|
|
83
|
+
return clean.length > 600 ? `${clean.slice(0, 600)}…` : clean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stateDir(cwd, sessionId, create = true) {
|
|
35
87
|
try {
|
|
36
|
-
|
|
88
|
+
let dir;
|
|
37
89
|
if (fs.existsSync(runtimeDir(cwd))) {
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
dir = runtimePath(cwd, 'session-state');
|
|
91
|
+
} else {
|
|
92
|
+
const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
93
|
+
dir = path.join(os.homedir(), '.claude', 'session-states', hash);
|
|
40
94
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!fs.existsSync(global)) fs.mkdirSync(global, { recursive: true });
|
|
45
|
-
return global;
|
|
95
|
+
if (sessionId) dir = path.join(dir, crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 16));
|
|
96
|
+
if (create && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
97
|
+
return dir;
|
|
46
98
|
} catch {
|
|
47
99
|
return null;
|
|
48
100
|
}
|
|
49
101
|
}
|
|
50
102
|
|
|
51
|
-
function loadLatest(
|
|
103
|
+
function loadLatest(dir) {
|
|
52
104
|
try {
|
|
53
|
-
const dir = stateDir(cwd);
|
|
54
105
|
if (!dir) return null;
|
|
55
106
|
|
|
56
107
|
const file = path.join(dir, 'latest.md');
|
|
@@ -109,7 +160,8 @@ try {
|
|
|
109
160
|
timestamp: new Date().toISOString(),
|
|
110
161
|
branch: process.env.GIT_BRANCH || '',
|
|
111
162
|
todos: [],
|
|
112
|
-
modifiedFiles: []
|
|
163
|
+
modifiedFiles: [],
|
|
164
|
+
lastAssistant: assistantSummary(stdinData.last_assistant_message),
|
|
113
165
|
};
|
|
114
166
|
|
|
115
167
|
if (stdinData.transcript_path && fs.existsSync(stdinData.transcript_path)) {
|
|
@@ -178,6 +230,7 @@ try {
|
|
|
178
230
|
"## What's Left",
|
|
179
231
|
...(pending.length ? pending.map((todo) => `- [ ] ${todo.content}`) : ['- (All tasks completed)']),
|
|
180
232
|
'',
|
|
233
|
+
...(data.lastAssistant ? ['## Latest Assistant Message', data.lastAssistant, ''] : []),
|
|
181
234
|
'## Key Files Modified',
|
|
182
235
|
...(data.modifiedFiles.length ? data.modifiedFiles.map((file) => `- ${file}`) : ['- (No file changes detected)']),
|
|
183
236
|
''
|
|
@@ -186,9 +239,10 @@ try {
|
|
|
186
239
|
|
|
187
240
|
function buildAgentSection(data) {
|
|
188
241
|
const agentType = data.agent_type || '';
|
|
189
|
-
|
|
242
|
+
const message = assistantSummary(data.last_assistant_message);
|
|
243
|
+
if (!agentType || agentType === 'unknown') return message ? `\n## Latest Assistant Message\n${message}\n` : '';
|
|
190
244
|
const time = new Date().toISOString().slice(11, 19);
|
|
191
|
-
return `\n## Agent Result: ${agentType} (${time})\n- Completed at ${time}\n`;
|
|
245
|
+
return `\n## Agent Result: ${agentType} (${time})\n- ${message || `Completed at ${time}`}\n`;
|
|
192
246
|
}
|
|
193
247
|
|
|
194
248
|
function takeRecentAgentSections(sections) {
|
|
@@ -229,11 +283,13 @@ try {
|
|
|
229
283
|
}
|
|
230
284
|
|
|
231
285
|
async function persistSnapshot(dir, data, options = {}) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
286
|
+
withStateLock(dir, () => {
|
|
287
|
+
const file = path.join(dir, 'latest.md');
|
|
288
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
289
|
+
const content = mergeAgentSections(existing, buildStateContent(data));
|
|
290
|
+
writeAtomic(file, content);
|
|
291
|
+
if (options.archive) archive(dir);
|
|
292
|
+
});
|
|
237
293
|
}
|
|
238
294
|
|
|
239
295
|
async function main() {
|
|
@@ -244,14 +300,14 @@ try {
|
|
|
244
300
|
const rawPayload = JSON.parse(stdin);
|
|
245
301
|
const data = normalizeHookPayload(rawPayload);
|
|
246
302
|
const event = data.hook_event_name || '';
|
|
247
|
-
const cwd = data
|
|
248
|
-
const
|
|
303
|
+
const cwd = projectRoot(data);
|
|
304
|
+
const claudeSession = ['.claude', '.omp'].includes(runtimeDirName())
|
|
305
|
+
&& typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
|
|
306
|
+
const dir = stateDir(cwd, claudeSession ? rawPayload.session_id : null, event !== 'SessionStart');
|
|
249
307
|
|
|
250
308
|
if (event === 'SessionStart') {
|
|
251
|
-
const previous = loadLatest(
|
|
252
|
-
// Claude
|
|
253
|
-
const claudeSession = runtimeDirName() === '.claude'
|
|
254
|
-
&& typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
|
|
309
|
+
const previous = loadLatest(dir);
|
|
310
|
+
// Claude and omp: grok (camelCase sessionId) and Codex keep printing the block.
|
|
255
311
|
if (previous && (!claudeSession || hasPriorContent(previous))) {
|
|
256
312
|
console.log('\n=== Prior Execution Context ===');
|
|
257
313
|
console.log(previous.trim());
|
|
@@ -275,13 +331,15 @@ try {
|
|
|
275
331
|
const agentSection = buildAgentSection(data);
|
|
276
332
|
if (!agentSection) process.exit(0);
|
|
277
333
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
?
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
334
|
+
const sessionData = await extractSessionData(data);
|
|
335
|
+
withStateLock(dir, () => {
|
|
336
|
+
const file = path.join(dir, 'latest.md');
|
|
337
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
338
|
+
const updated = existing
|
|
339
|
+
? appendAgentSection(existing, agentSection)
|
|
340
|
+
: `${buildStateContent(sessionData)}\n${agentSection}`;
|
|
341
|
+
writeAtomic(file, updated);
|
|
342
|
+
});
|
|
285
343
|
process.exit(0);
|
|
286
344
|
}
|
|
287
345
|
|
|
@@ -294,24 +352,11 @@ try {
|
|
|
294
352
|
process.exit(0);
|
|
295
353
|
}
|
|
296
354
|
|
|
297
|
-
main().catch(() => {
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
logCrash(error);
|
|
298
357
|
process.exit(0);
|
|
299
358
|
});
|
|
300
359
|
} catch (error) {
|
|
301
|
-
|
|
302
|
-
const fs = require('fs');
|
|
303
|
-
const path = require('path');
|
|
304
|
-
const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
305
|
-
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
306
|
-
fs.appendFileSync(
|
|
307
|
-
path.join(logDir, 'hook-log.jsonl'),
|
|
308
|
-
JSON.stringify({
|
|
309
|
-
ts: new Date().toISOString(),
|
|
310
|
-
hook: 'state',
|
|
311
|
-
status: 'crash',
|
|
312
|
-
error: error.message
|
|
313
|
-
}) + '\n'
|
|
314
|
-
);
|
|
315
|
-
} catch (_) {}
|
|
360
|
+
logCrash(error);
|
|
316
361
|
process.exit(0);
|
|
317
362
|
}
|
|
@@ -143,7 +143,8 @@
|
|
|
143
143
|
".codex\\hooks\\semantic-review-authority.cjs",
|
|
144
144
|
".claude/hooks/task-scaffold-guard.cjs",
|
|
145
145
|
".codex/hooks/task-scaffold-guard.cjs",
|
|
146
|
-
".codex\\hooks\\task-scaffold-guard.cjs"
|
|
146
|
+
".codex\\hooks\\task-scaffold-guard.cjs",
|
|
147
|
+
".claude/hooks/inspector-block.cjs"
|
|
147
148
|
]
|
|
148
149
|
},
|
|
149
150
|
"runtime": {
|
|
@@ -170,8 +171,14 @@
|
|
|
170
171
|
"hooks/lib/config.cjs",
|
|
171
172
|
"hooks/lib/hook-state-dir.cjs",
|
|
172
173
|
"hooks/lib/runtime-dir.cjs",
|
|
174
|
+
"hooks/lib/spec-touch.cjs",
|
|
175
|
+
"hooks/lib/project-root.cjs",
|
|
176
|
+
"hooks/lib/update-check.cjs",
|
|
177
|
+
"hooks/lib/rules-reservation.cjs",
|
|
173
178
|
"hooks/lib/hook-payload.cjs",
|
|
179
|
+
"hooks/lib/inspect-paths.cjs",
|
|
174
180
|
"hooks/lib/privacy-command-analysis.cjs",
|
|
181
|
+
"hooks/lib/privacy-paths.cjs",
|
|
175
182
|
"hooks/lib/secret-keywords.cjs",
|
|
176
183
|
"hooks/lib/runtime-path-safety.cjs"
|
|
177
184
|
]
|
|
@@ -24,5 +24,5 @@ Never use another command, path, encoding, or side channel to access a privacy-b
|
|
|
24
24
|
|
|
25
25
|
- Treat hook errors as blockers when they affect safety, privacy, or task state.
|
|
26
26
|
- Record the blocker in the task/spec state when relevant.
|
|
27
|
-
- If
|
|
27
|
+
- If hook output is malformed, stop and report the exact failure instead of guessing.
|
|
28
28
|
|
package/src/claude/runtime.json
CHANGED
|
@@ -27,7 +27,7 @@ function usage() {
|
|
|
27
27
|
console.log(`Usage:
|
|
28
28
|
node <runtime>/scripts/generate-skill-catalog.cjs [--skills] [--json] [--root <skills-dir>]
|
|
29
29
|
|
|
30
|
-
The default root is bound to the script runtime:
|
|
30
|
+
The default root is bound to the script runtime: the Claude runtime skills directory for Claude,
|
|
31
31
|
.agents/skills for Codex, and ../skills for the source tree.`);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -192,7 +192,7 @@ function renderMarkdown(catalog) {
|
|
|
192
192
|
const lines = ['# CafeKit Skills Catalog', '', `Skills root: \`${catalog.root}\``,
|
|
193
193
|
`Routable skills: ${catalog.skills.length}`, ''];
|
|
194
194
|
for (const skill of catalog.skills) {
|
|
195
|
-
const intent = skill.when_to_use ? ` Use: ${skill.when_to_use}` : '';
|
|
195
|
+
const intent = skill.when_to_use && !skill.description.includes(skill.when_to_use) ? ` Use: ${skill.when_to_use}` : '';
|
|
196
196
|
lines.push(`- \`${skill.name}\` (${skill.directory}, ${skill.category}): ${skill.description}${intent}`);
|
|
197
197
|
}
|
|
198
198
|
if (catalog.diagnostics.length > 0) {
|
|
@@ -84,6 +84,19 @@ function oneLine(value, label) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function gitBase(root, specsRoot) {
|
|
87
|
+
const symbolic = spawnSync('git', ['-C', root, 'symbolic-ref', '-q', 'HEAD'], { encoding: 'buffer' });
|
|
88
|
+
if (!symbolic.error && !symbolic.signal && symbolic.status === 0) {
|
|
89
|
+
const ref = oneLine(symbolic.stdout, 'git symbolic-ref HEAD');
|
|
90
|
+
const exists = spawnSync('git', ['-C', root, 'show-ref', '--verify', '-q', ref], { encoding: 'buffer' });
|
|
91
|
+
if (!exists.error && !exists.signal && exists.status === 1) {
|
|
92
|
+
fail('no_git_history', 'repo has no commits yet; create the first commit, then record Base/Head after it');
|
|
93
|
+
}
|
|
94
|
+
if (exists.error || exists.signal || exists.status !== 0) {
|
|
95
|
+
fail('git_command_failed', `git show-ref HEAD failed: ${exists.error?.message || String(exists.stderr || '').trim() || `exit ${exists.status}`}`);
|
|
96
|
+
}
|
|
97
|
+
} else if (symbolic.error || symbolic.signal || symbolic.status !== 1) {
|
|
98
|
+
fail('git_command_failed', `git symbolic-ref HEAD failed: ${symbolic.error?.message || String(symbolic.stderr || '').trim() || `exit ${symbolic.status}`}`);
|
|
99
|
+
}
|
|
87
100
|
const specsRelative = path.relative(root, specsRoot).split(path.sep).join('/');
|
|
88
101
|
const sourceHistory = decode(runGit(
|
|
89
102
|
root,
|
|
@@ -156,6 +169,7 @@ const RUNTIME_STATE_ROOTS = Object.freeze([
|
|
|
156
169
|
'.claude/hooks/.logs', '.codex/hooks/.logs', '.omp/hooks/.logs',
|
|
157
170
|
'.claude/.logs', '.codex/.logs', '.omp/.logs',
|
|
158
171
|
'.claude/runtime.json', '.codex/runtime.json', '.omp/runtime.json',
|
|
172
|
+
'.codex/session-state', '.codex/hooks/.privacy',
|
|
159
173
|
]);
|
|
160
174
|
|
|
161
175
|
function excluded(root, absolute, specsRoot) {
|
|
@@ -5,7 +5,6 @@ Validate ai-multimodal skill setup and configuration.
|
|
|
5
5
|
Checks:
|
|
6
6
|
- API key presence and format
|
|
7
7
|
- Python dependencies
|
|
8
|
-
- Centralized resolver availability
|
|
9
8
|
- Directory structure
|
|
10
9
|
"""
|
|
11
10
|
|
|
@@ -85,69 +84,16 @@ def check_dependencies():
|
|
|
85
84
|
return True
|
|
86
85
|
|
|
87
86
|
|
|
88
|
-
def check_centralized_resolver():
|
|
89
|
-
"""Check if centralized resolver is available."""
|
|
90
|
-
print_header("Checking Centralized Resolver")
|
|
91
|
-
|
|
92
|
-
claude_root = Path(__file__).parent.parent.parent.parent
|
|
93
|
-
resolver_path = claude_root / 'scripts' / 'resolve_env.py'
|
|
94
|
-
|
|
95
|
-
if resolver_path.exists():
|
|
96
|
-
print_success(f"Centralized resolver found: {resolver_path}")
|
|
97
|
-
|
|
98
|
-
# Try to import it
|
|
99
|
-
sys.path.insert(0, str(resolver_path.parent))
|
|
100
|
-
try:
|
|
101
|
-
from resolve_env import resolve_env
|
|
102
|
-
print_success("Centralized resolver can be imported")
|
|
103
|
-
return True
|
|
104
|
-
except ImportError as e:
|
|
105
|
-
print_error(f"Centralized resolver exists but cannot be imported: {e}")
|
|
106
|
-
return False
|
|
107
|
-
else:
|
|
108
|
-
print_warning(f"Centralized resolver not found: {resolver_path}")
|
|
109
|
-
print_info("Skill will use fallback resolution logic")
|
|
110
|
-
return True # Not critical, fallback works
|
|
111
|
-
|
|
112
|
-
|
|
113
87
|
def find_api_key():
|
|
114
|
-
"""Find
|
|
88
|
+
"""Find the API key in the runtime environment."""
|
|
115
89
|
print_header("Checking API Key Configuration")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
print_info("Using centralized resolver...")
|
|
124
|
-
api_key = resolve_env('GEMINI_API_KEY', skill='ai-multimodal')
|
|
125
|
-
|
|
126
|
-
if api_key:
|
|
127
|
-
print_success("API key found via centralized resolver")
|
|
128
|
-
print_info(f"Key preview: {api_key[:20]}...{api_key[-4:]}")
|
|
129
|
-
|
|
130
|
-
# Show hierarchy
|
|
131
|
-
print_info("\nTo see where the key was found, run:")
|
|
132
|
-
print_info("python ~/packages/spec/src/claude/scripts/resolve_env.py GEMINI_API_KEY --skill ai-multimodal --verbose")
|
|
133
|
-
|
|
134
|
-
return api_key
|
|
135
|
-
else:
|
|
136
|
-
print_error("API key not found in any location")
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
except ImportError:
|
|
140
|
-
print_warning("Centralized resolver not available, using fallback")
|
|
141
|
-
|
|
142
|
-
# Fallback: check environment
|
|
143
|
-
api_key = os.getenv('GEMINI_API_KEY')
|
|
144
|
-
if api_key:
|
|
145
|
-
print_success("API key found in process.env")
|
|
146
|
-
print_info(f"Key preview: {api_key[:20]}...{api_key[-4:]}")
|
|
147
|
-
return api_key
|
|
148
|
-
else:
|
|
149
|
-
print_error("API key not found")
|
|
150
|
-
return None
|
|
90
|
+
api_key = os.getenv('GEMINI_API_KEY')
|
|
91
|
+
if api_key:
|
|
92
|
+
print_success("API key found in process.env")
|
|
93
|
+
print_info(f"Key preview: {api_key[:20]}...{api_key[-4:]}")
|
|
94
|
+
return api_key
|
|
95
|
+
print_error("API key not found")
|
|
96
|
+
return None
|
|
151
97
|
|
|
152
98
|
|
|
153
99
|
def validate_api_key_format(api_key):
|
|
@@ -250,9 +196,6 @@ def provide_setup_instructions():
|
|
|
250
196
|
print("\n3. Verify setup:")
|
|
251
197
|
print(f" $ python {Path(__file__)}")
|
|
252
198
|
|
|
253
|
-
print("\n4. Debug if needed:")
|
|
254
|
-
print(f" $ python ~/packages/spec/src/claude/scripts/resolve_env.py --show-hierarchy --skill ai-multimodal")
|
|
255
|
-
print(f" $ python ~/packages/spec/src/claude/scripts/resolve_env.py GEMINI_API_KEY --skill ai-multimodal --verbose")
|
|
256
199
|
|
|
257
200
|
|
|
258
201
|
def main():
|
|
@@ -265,9 +208,6 @@ def main():
|
|
|
265
208
|
if not check_directory_structure():
|
|
266
209
|
all_passed = False
|
|
267
210
|
|
|
268
|
-
# Check centralized resolver
|
|
269
|
-
check_centralized_resolver()
|
|
270
|
-
|
|
271
211
|
# Check dependencies
|
|
272
212
|
if not check_dependencies():
|
|
273
213
|
all_passed = False
|