()
| 48 | // ── Main ───────────────────────────────────────────────────────────────────── |
| 49 | |
| 50 | function main() { |
| 51 | let input = ''; |
| 52 | process.stdin.setEncoding('utf8'); |
| 53 | process.stdin.on('data', (chunk) => { input += chunk; }); |
| 54 | process.stdin.on('end', () => { |
| 55 | const startTime = Date.now(); |
| 56 | let event; |
| 57 | try { |
| 58 | event = JSON.parse(input); |
| 59 | } catch { |
| 60 | process.exit(0); |
| 61 | } |
| 62 | |
| 63 | const toolName = event.tool_name || ''; |
| 64 | // Wall-clock timing from Claude Code (ms, excluding permission prompts) |
| 65 | const durationMs = typeof event.duration_ms === 'number' ? event.duration_ms : null; |
| 66 | const agentId = event.agent_id || null; |
| 67 | const agentType = event.agent_type || null; |
| 68 | |
| 69 | // Only run on Edit and Write operations |
| 70 | if (toolName !== 'Edit' && toolName !== 'Write') { |
| 71 | process.exit(0); |
| 72 | } |
| 73 | |
| 74 | // Extract file path from tool input |
| 75 | const filePath = event.tool_input?.file_path || event.tool_input?.path || ''; |
| 76 | if (!filePath) { |
| 77 | process.exit(0); |
| 78 | } |
| 79 | |
| 80 | const pathCheck = health.validatePath(filePath); |
| 81 | if (!pathCheck.safe) { |
| 82 | health.securityWarning('post-edit', `Possible injection in file path — ${pathCheck.violation}. Skipping typecheck.`); |
| 83 | process.exit(0); |
| 84 | } |
| 85 | |
| 86 | const relativePath = path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/'); |
| 87 | |
| 88 | // Capture durable knowledge close to the write so a disposable clone does |
| 89 | // not depend solely on a clean SessionEnd event. Non-memory edits take only |
| 90 | // the allowlist check and never open SQLite. |
| 91 | syncRepositoryMemory(filePath, relativePath); |
| 92 | |
| 93 | // Determine which hot-path lenses apply to this file |
| 94 | const lenses = selectHotPathLenses(filePath, relativePath); |
| 95 | |
| 96 | // Run selected lenses |
| 97 | let exitCode = 0; |
| 98 | for (const lens of lenses) { |
| 99 | const lensResult = runHotLens(lens, filePath, relativePath); |
| 100 | if (lensResult > exitCode) exitCode = lensResult; |
| 101 | } |
| 102 | |
| 103 | health.logTiming('post-edit', Date.now() - startTime, { |
| 104 | file: relativePath, |
| 105 | lenses: lenses.join(','), |
| 106 | typecheck: _typecheckOutcome || (exitCode === 0 ? 'pass' : 'fail'), |
| 107 | tool_duration_ms: durationMs, |
no test coverage detected