From fa48a13c9ccde924cb3da17629483b61cebc1001 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Tue, 20 Jan 2026 00:49:07 -0600 Subject: [PATCH] feat(intel): add Stop hook to prune deleted files New intel-prune.js hook runs after each Claude response. Checks if indexed files still exist, removes stale entries. Fast (only fs.existsSync checks) and silent (never blocks). Co-Authored-By: Claude Opus 4.5 --- bin/install.js | 23 +++++++++++++ hooks/intel-prune.js | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 hooks/intel-prune.js diff --git a/bin/install.js b/bin/install.js index 3d299c294..5592fdb4e 100755 --- a/bin/install.js +++ b/bin/install.js @@ -456,6 +456,29 @@ function install(isGlobal) { console.log(` ${green}✓${reset} Configured intel session hook`); } + // Stop hook for pruning deleted files + const intelPruneCommand = isGlobal + ? 'node "$HOME/.claude/hooks/intel-prune.js"' + : 'node .claude/hooks/intel-prune.js'; + + if (!settings.hooks.Stop) { + settings.hooks.Stop = []; + } + + const hasIntelPruneHook = settings.hooks.Stop.some(entry => + entry.hooks && entry.hooks.some(h => h.command && h.command.includes('intel-prune')) + ); + + if (!hasIntelPruneHook) { + settings.hooks.Stop.push({ + hooks: [{ + type: 'command', + command: intelPruneCommand + }] + }); + console.log(` ${green}✓${reset} Configured intel prune hook`); + } + return { settingsPath, settings, statuslineCommand }; } diff --git a/hooks/intel-prune.js b/hooks/intel-prune.js new file mode 100644 index 000000000..aa2c58b06 --- /dev/null +++ b/hooks/intel-prune.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +/** + * Intel Prune Hook (Stop event) + * + * Removes stale entries from index.json when files no longer exist. + * Runs after each Claude response to keep intel fresh. + * + * Fast: Only does fs.existsSync checks, no file reading. + * Silent: Never blocks or errors, always exits 0. + */ + +const fs = require('fs'); +const path = require('path'); + +function pruneIndex() { + const intelDir = path.join(process.cwd(), '.planning', 'intel'); + const indexPath = path.join(intelDir, 'index.json'); + + // Only run if intel directory exists (opt-in check) + if (!fs.existsSync(intelDir)) { + return { pruned: 0, total: 0 }; + } + + // Read existing index + let index; + try { + const content = fs.readFileSync(indexPath, 'utf8'); + index = JSON.parse(content); + } catch (e) { + // No index or invalid JSON + return { pruned: 0, total: 0 }; + } + + if (!index.files || typeof index.files !== 'object') { + return { pruned: 0, total: 0 }; + } + + // Check each file and collect deleted ones + const filePaths = Object.keys(index.files); + const deleted = filePaths.filter(filePath => !fs.existsSync(filePath)); + + if (deleted.length === 0) { + return { pruned: 0, total: filePaths.length }; + } + + // Remove deleted entries + for (const filePath of deleted) { + delete index.files[filePath]; + } + index.updated = Date.now(); + + // Write updated index + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); + + // Regenerate conventions and summary after pruning + // Import detection logic from intel-index.js would be complex, + // so we just update the index. Conventions/summary stay until + // next PostToolUse or /gsd:analyze-codebase refresh. + + return { pruned: deleted.length, total: filePaths.length }; +} + +// Read JSON from stdin (standard hook pattern) +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + try { + // Stop hook receives session data, but we don't need it + // Just prune stale entries + pruneIndex(); + process.exit(0); + } catch (error) { + // Silent failure - never block Claude + process.exit(0); + } +});