From a3521161cd30a9df314b8afb7272cbb2e9cb6fc0 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Mon, 19 Jan 2026 23:44:07 -0600 Subject: [PATCH] feat(02-02): create SessionStart context injection hook - Read session metadata from stdin - Check for startup/resume source - Read index.json and conventions.json from .planning/intel/ - Silent exit if no intel files present --- hooks/intel-session.js | 110 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 hooks/intel-session.js diff --git a/hooks/intel-session.js b/hooks/intel-session.js new file mode 100644 index 000000000..4820bf440 --- /dev/null +++ b/hooks/intel-session.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// Codebase Intelligence - SessionStart Context Injection Hook +// Injects codebase summary into Claude's context at session start + +const fs = require('fs'); +const path = require('path'); + +/** + * Generate concise codebase summary for context injection + * Target: < 500 tokens to minimize context usage + */ +function generateSummary(index, conventions) { + const lines = []; + + // File count + const fileCount = Object.keys(index.files || {}).length; + if (fileCount === 0) return null; + + lines.push(`Indexed files: ${fileCount}`); + + // Naming conventions (if detected with confidence) + if (conventions.naming?.exports?.dominant) { + const n = conventions.naming.exports; + lines.push(`Export naming: ${n.dominant} (${n.percentage}%)`); + } + + // Key directories (top 5) + const dirs = Object.entries(conventions.directories || {}); + if (dirs.length > 0) { + lines.push(''); + lines.push('Key directories:'); + for (const [dir, info] of dirs.slice(0, 5)) { + lines.push(` ${dir}: ${info.purpose} (${info.files} files)`); + } + } + + // Suffix patterns (top 3) + const suffixes = Object.entries(conventions.suffixes || {}); + if (suffixes.length > 0) { + lines.push(''); + lines.push('File patterns:'); + for (const [suffix, info] of suffixes.slice(0, 3)) { + lines.push(` ${suffix}: ${info.purpose} (${info.count} files)`); + } + } + + // Key exports overview (categorize if enough data) + const allExports = []; + for (const [filePath, fileData] of Object.entries(index.files || {})) { + for (const exp of fileData.exports || []) { + if (exp !== 'default') { + allExports.push(exp); + } + } + } + + if (allExports.length > 0) { + lines.push(''); + lines.push(`Total exports: ${allExports.length}`); + // List a few example exports if available + if (allExports.length <= 10) { + lines.push(`Exports: ${allExports.join(', ')}`); + } + } + + return lines.join('\n'); +} + +// 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 { + const data = JSON.parse(input); + + // Only inject on startup or resume + if (!['startup', 'resume'].includes(data.source)) { + process.exit(0); + } + + // Check for intel files + const intelDir = path.join(process.cwd(), '.planning', 'intel'); + const indexPath = path.join(intelDir, 'index.json'); + + if (!fs.existsSync(indexPath)) { + process.exit(0); // No intel, skip silently + } + + // Read intel files + const index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); + + let conventions = { naming: {}, directories: {}, suffixes: {} }; + const conventionsPath = path.join(intelDir, 'conventions.json'); + if (fs.existsSync(conventionsPath)) { + conventions = JSON.parse(fs.readFileSync(conventionsPath, 'utf8')); + } + + // Generate and output summary + const summary = generateSummary(index, conventions); + if (summary) { + process.stdout.write(`\n${summary}\n`); + } + + process.exit(0); + } catch (error) { + // Silent failure - never block Claude + process.exit(0); + } +});