|
2 | 2 | /** |
3 | 3 | * Claude Code Hook: PreCompact Session Digest |
4 | 4 | * |
5 | | - * This hook is registered with Claude Code to trigger before context compact. |
6 | | - * It delegates to the unified hook runner in aios-core. |
| 5 | + * Registered as PreCompact event — fires before context compaction. |
| 6 | + * Reads JSON from stdin (Claude Code hook protocol), delegates to |
| 7 | + * the unified hook runner in aios-core. |
7 | 8 | * |
8 | | - * Installation: |
9 | | - * - Claude Code automatically discovers hooks in .claude/hooks/ |
10 | | - * - Hook naming: {event}-{name}.js (e.g., precompact-session-digest.js) |
| 9 | + * Stdin format (PreCompact): |
| 10 | + * { |
| 11 | + * "session_id": "abc123", |
| 12 | + * "transcript_path": "/path/to/session.jsonl", |
| 13 | + * "cwd": "/path/to/project", |
| 14 | + * "hook_event_name": "PreCompact", |
| 15 | + * "trigger": "auto" | "manual" |
| 16 | + * } |
11 | 17 | * |
12 | 18 | * @see .aios-core/hooks/unified/runners/precompact-runner.js |
13 | 19 | * @see Story MIS-3 - Session Digest (PreCompact Hook) |
| 20 | + * @see Story MIS-3.1 - Fix Session-Digest Hook Registration |
14 | 21 | */ |
15 | 22 |
|
16 | 23 | 'use strict'; |
17 | 24 |
|
18 | 25 | const path = require('path'); |
19 | 26 |
|
20 | | -// Resolve path to the unified hook runner |
| 27 | +// Resolve project root via __dirname (same pattern as synapse-engine.cjs) |
| 28 | +// More robust than input.cwd — doesn't depend on external input |
21 | 29 | const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); |
22 | | -const runnerPath = path.join( |
23 | | - PROJECT_ROOT, |
24 | | - '.aios-core', |
25 | | - 'hooks', |
26 | | - 'unified', |
27 | | - 'runners', |
28 | | - 'precompact-runner.js', |
29 | | -); |
30 | | - |
31 | | -// Load and execute the hook runner |
32 | | -try { |
33 | | - const { onPreCompact } = require(runnerPath); |
34 | 30 |
|
35 | | - // Export the hook handler for Claude Code |
36 | | - module.exports = async (context) => { |
37 | | - return await onPreCompact(context); |
38 | | - }; |
39 | | -} catch (error) { |
40 | | - console.error('[PreCompact Hook] Failed to load hook runner:', error.message); |
| 31 | +/** Safety timeout (ms) — defense-in-depth; Claude Code also manages hook timeout. */ |
| 32 | +const HOOK_TIMEOUT_MS = 9000; |
41 | 33 |
|
42 | | - // Graceful degradation - export no-op function |
43 | | - module.exports = async () => { |
44 | | - console.log('[PreCompact Hook] Hook runner not available, skipping'); |
| 34 | +/** |
| 35 | + * Read all data from stdin as a JSON object. |
| 36 | + * Same pattern as synapse-engine.cjs. |
| 37 | + * @returns {Promise<object>} Parsed JSON input |
| 38 | + */ |
| 39 | +function readStdin() { |
| 40 | + return new Promise((resolve, reject) => { |
| 41 | + let data = ''; |
| 42 | + process.stdin.setEncoding('utf8'); |
| 43 | + process.stdin.on('error', (e) => reject(e)); |
| 44 | + process.stdin.on('data', (chunk) => { data += chunk; }); |
| 45 | + process.stdin.on('end', () => { |
| 46 | + try { resolve(JSON.parse(data)); } |
| 47 | + catch (e) { reject(e); } |
| 48 | + }); |
| 49 | + }); |
| 50 | +} |
| 51 | + |
| 52 | +/** Main hook execution pipeline. */ |
| 53 | +async function main() { |
| 54 | + const input = await readStdin(); |
| 55 | + |
| 56 | + // Resolve path to the unified hook runner via __dirname (not input.cwd) |
| 57 | + // Same pattern as synapse-engine.cjs — robust against incorrect cwd |
| 58 | + const runnerPath = path.join( |
| 59 | + PROJECT_ROOT, |
| 60 | + '.aios-core', |
| 61 | + 'hooks', |
| 62 | + 'unified', |
| 63 | + 'runners', |
| 64 | + 'precompact-runner.js', |
| 65 | + ); |
| 66 | + |
| 67 | + // Build context object expected by onPreCompact |
| 68 | + const context = { |
| 69 | + sessionId: input.session_id, |
| 70 | + projectDir: input.cwd || PROJECT_ROOT, |
| 71 | + transcriptPath: input.transcript_path, |
| 72 | + trigger: input.trigger || 'auto', |
| 73 | + hookEventName: input.hook_event_name || 'PreCompact', |
| 74 | + permissionMode: input.permission_mode, |
| 75 | + conversation: input, |
| 76 | + provider: 'claude', |
45 | 77 | }; |
| 78 | + |
| 79 | + const { onPreCompact } = require(runnerPath); |
| 80 | + await onPreCompact(context); |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * Safely exit the process — no-op inside Jest workers to prevent worker crashes. |
| 85 | + * @param {number} code - Exit code |
| 86 | + */ |
| 87 | +function safeExit(code) { |
| 88 | + if (process.env.JEST_WORKER_ID) return; |
| 89 | + process.exit(code); |
46 | 90 | } |
| 91 | + |
| 92 | +/** Entry point runner — sets safety timeout and executes main(). */ |
| 93 | +function run() { |
| 94 | + const timer = setTimeout(() => safeExit(0), HOOK_TIMEOUT_MS); |
| 95 | + timer.unref(); |
| 96 | + main() |
| 97 | + .then(() => safeExit(0)) |
| 98 | + .catch((err) => { |
| 99 | + console.error(`[precompact-hook] ${err.message}`); |
| 100 | + safeExit(0); // Never block the compact operation |
| 101 | + }); |
| 102 | +} |
| 103 | + |
| 104 | +if (require.main === module) run(); |
| 105 | + |
| 106 | +module.exports = { readStdin, main, run, HOOK_TIMEOUT_MS }; |
0 commit comments