laminark 2.21.6

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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/README.md +182 -0
  3. package/package.json +63 -0
  4. package/plugin/.claude-plugin/plugin.json +13 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/dist/analysis/worker.d.ts +1 -0
  7. package/plugin/dist/analysis/worker.js +233 -0
  8. package/plugin/dist/analysis/worker.js.map +1 -0
  9. package/plugin/dist/config-t8LZeB-u.mjs +90 -0
  10. package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
  11. package/plugin/dist/hooks/handler.d.ts +284 -0
  12. package/plugin/dist/hooks/handler.d.ts.map +1 -0
  13. package/plugin/dist/hooks/handler.js +2125 -0
  14. package/plugin/dist/hooks/handler.js.map +1 -0
  15. package/plugin/dist/index.d.ts +445 -0
  16. package/plugin/dist/index.d.ts.map +1 -0
  17. package/plugin/dist/index.js +5831 -0
  18. package/plugin/dist/index.js.map +1 -0
  19. package/plugin/dist/observations-Ch0nc47i.d.mts +170 -0
  20. package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
  21. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs +2655 -0
  22. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
  23. package/plugin/hooks/hooks.json +78 -0
  24. package/plugin/scripts/README.md +47 -0
  25. package/plugin/scripts/bump-version.sh +44 -0
  26. package/plugin/scripts/ensure-deps.sh +12 -0
  27. package/plugin/scripts/install.sh +63 -0
  28. package/plugin/scripts/local-install.sh +103 -0
  29. package/plugin/scripts/setup-tmpdir.sh +65 -0
  30. package/plugin/scripts/uninstall.sh +95 -0
  31. package/plugin/scripts/update.sh +88 -0
  32. package/plugin/scripts/verify-install.sh +43 -0
  33. package/plugin/ui/activity.js +185 -0
  34. package/plugin/ui/app.js +1642 -0
  35. package/plugin/ui/graph.js +2333 -0
  36. package/plugin/ui/help.js +228 -0
  37. package/plugin/ui/index.html +492 -0
  38. package/plugin/ui/settings.js +650 -0
  39. package/plugin/ui/styles.css +2910 -0
  40. package/plugin/ui/timeline.js +652 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","names":["truncate","truncate"],"sources":["../../../src/hooks/self-referential.ts","../../../src/hooks/capture.ts","../../../src/curation/summarizer.ts","../../../src/context/injection.ts","../../../src/hooks/config-scanner.ts","../../../src/routing/intent-patterns.ts","../../../src/hooks/session-lifecycle.ts","../../../src/hooks/privacy-filter.ts","../../../src/hooks/admission-filter.ts","../../../src/paths/path-recall.ts","../../../src/hooks/pre-tool-context.ts","../../../src/routing/types.ts","../../../src/routing/heuristic-fallback.ts","../../../src/routing/conversation-router.ts","../../../src/hooks/handler.ts"],"sourcesContent":["/**\n * Self-referential tool detection for Laminark.\n *\n * Laminark's MCP tools appear with different prefixes depending on\n * how Claude Code discovers the server:\n *\n * - Project-scoped (.mcp.json): `mcp__laminark__<tool>`\n * - Global plugin (~/.claude/plugins/): `mcp__plugin_laminark_laminark__<tool>`\n *\n * Both prefixes must be detected to prevent Laminark from capturing\n * its own tool calls as observations, which would create a feedback loop.\n */\n\n/**\n * All known prefixes for Laminark's own MCP tools.\n * Order: project-scoped first (most common), plugin-scoped second.\n */\nexport const LAMINARK_PREFIXES = [\n 'mcp__laminark__',\n 'mcp__plugin_laminark_laminark__',\n] as const;\n\n/**\n * Returns true if the given tool name belongs to Laminark.\n *\n * Checks against all known Laminark MCP prefixes to detect self-referential\n * tool calls regardless of installation method (project-scoped or global plugin).\n */\nexport function isLaminarksOwnTool(toolName: string): boolean {\n return LAMINARK_PREFIXES.some((prefix) => toolName.startsWith(prefix));\n}\n","import type { ObservationRepository } from '../storage/observations.js';\nimport { isLaminarksOwnTool } from './self-referential.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Payload shape for PostToolUse / PostToolUseFailure hook events.\n * Matches the official Claude Code hook JSON format.\n */\nexport interface PostToolUsePayload {\n session_id: string;\n cwd: string;\n hook_event_name: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response?: Record<string, unknown>;\n tool_use_id?: string;\n}\n\n/**\n * Truncates a string to maxLength, appending '...' if truncated.\n */\nexport function truncate(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength) + '...';\n}\n\n/**\n * Extracts a semantic observation summary from a PostToolUse payload.\n * Returns null if no meaningful observation can be derived.\n *\n * Summaries are human-readable, not raw tool output. Each tool type\n * gets a format optimized for later search and recall.\n */\nexport function extractObservation(payload: PostToolUsePayload): string | null {\n const { tool_name, tool_input, tool_response } = payload;\n\n switch (tool_name) {\n case 'Write':\n // High signal: file creation. Include path and first ~200 chars of content.\n return `[Write] Created ${tool_input.file_path}\\n${truncate(String(tool_input.content ?? ''), 200)}`;\n\n case 'Edit':\n // High signal: code change. Include path, old->new summary.\n return `[Edit] Modified ${tool_input.file_path}: replaced \"${truncate(String(tool_input.old_string ?? ''), 80)}\" with \"${truncate(String(tool_input.new_string ?? ''), 80)}\"`;\n\n case 'Bash': {\n // Medium signal: command execution. Include command and first ~200 chars of response.\n const cmd = truncate(String(tool_input.command ?? ''), 100);\n const output = truncate(JSON.stringify(tool_response ?? ''), 200);\n return `[Bash] $ ${cmd}\\n${output}`;\n }\n\n case 'Read':\n case 'Glob':\n case 'Grep':\n // Routed to research buffer by handler.ts -- should not reach here.\n // Return null to skip if reached via legacy code path.\n return null;\n\n case 'WebFetch': {\n const url = String(tool_input.url ?? '');\n const prompt = truncate(String(tool_input.prompt ?? ''), 100);\n const response = truncate(JSON.stringify(tool_response ?? ''), 300);\n return `[WebFetch] ${url}\\nPrompt: ${prompt}\\n${response}`;\n }\n\n case 'WebSearch': {\n const query = String(tool_input.query ?? '');\n const response = truncate(JSON.stringify(tool_response ?? ''), 300);\n return `[WebSearch] \"${query}\"\\n${response}`;\n }\n\n default:\n // MCP tools and others -- capture tool name + input summary.\n return `[${tool_name}] ${truncate(JSON.stringify(tool_input), 200)}`;\n }\n}\n\n/**\n * Processes a PostToolUse or PostToolUseFailure event.\n *\n * Validates the input, skips self-referential Laminark tools (both prefix variants),\n * extracts a semantic observation summary, and persists it to the database.\n */\nexport function processPostToolUse(\n input: Record<string, unknown>,\n obsRepo: ObservationRepository,\n): void {\n const toolName = input.tool_name as string | undefined;\n\n if (!toolName) {\n debug('hook', 'PostToolUse missing tool_name, skipping');\n return;\n }\n\n // Skip self-referential capture (Laminark observing its own operations)\n if (isLaminarksOwnTool(toolName)) {\n debug('hook', 'Skipping self-referential tool', { tool: toolName });\n return;\n }\n\n const payload: PostToolUsePayload = {\n session_id: input.session_id as string,\n cwd: input.cwd as string,\n hook_event_name: input.hook_event_name as string,\n tool_name: toolName,\n tool_input: (input.tool_input as Record<string, unknown>) ?? {},\n tool_response: input.tool_response as Record<string, unknown> | undefined,\n tool_use_id: input.tool_use_id as string | undefined,\n };\n\n const summary = extractObservation(payload);\n\n if (summary === null) {\n debug('hook', 'No observation extracted', { tool: toolName });\n return;\n }\n\n obsRepo.create({\n content: summary,\n source: 'hook:' + toolName,\n sessionId: payload.session_id ?? null,\n });\n\n debug('hook', 'Captured observation', { tool: toolName, length: summary.length });\n}\n","import type { Observation, ObservationKind } from '../shared/types.js';\nimport { ObservationRepository } from '../storage/observations.js';\nimport { SessionRepository } from '../storage/sessions.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Summary result returned after generating a session summary.\n */\nexport interface SessionSummary {\n sessionId: string;\n summary: string;\n observationCount: number;\n generatedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Kind-aware extraction helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Groups observations by their kind field.\n */\nfunction groupByKind(observations: Observation[]): Record<ObservationKind, Observation[]> {\n const groups: Record<ObservationKind, Observation[]> = {\n change: [],\n reference: [],\n finding: [],\n decision: [],\n verification: [],\n };\n\n for (const obs of observations) {\n const kind = obs.kind ?? 'finding';\n if (groups[kind]) {\n groups[kind].push(obs);\n } else {\n groups.finding.push(obs);\n }\n }\n\n return groups;\n}\n\n/**\n * Extracts a snippet from observation content (first line, max 120 chars).\n */\nfunction snippet(content: string, maxLen: number = 120): string {\n const firstLine = content.split('\\n')[0].trim();\n if (firstLine.length <= maxLen) return firstLine;\n return firstLine.slice(0, maxLen - 3) + '...';\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Compresses an array of session observations into a structured text summary.\n *\n * Kind-aware: groups observations by their `kind` field instead of heuristic\n * keyword matching. Produces structured sections:\n * - Changes (kind='change'): file modifications\n * - Decisions (kind='decision'): choices made\n * - Verifications (kind='verification'): test/build results\n * - References (kind='reference'): external resources consulted\n * - Findings (kind='finding'): manual saves and insights\n *\n * Target output: under 500 tokens (~2000 characters).\n * If the raw extraction exceeds this budget, sections are trimmed by priority:\n * references first, then findings, then verifications, then changes.\n */\nexport function compressObservations(observations: Observation[]): string {\n if (observations.length === 0) {\n return '';\n }\n\n const groups = groupByKind(observations);\n\n // Build the summary with section headers\n const sections: string[] = [];\n\n sections.push('## Session Summary');\n\n // Timestamps from first and last observation\n const sorted = [...observations].sort(\n (a, b) => a.createdAt.localeCompare(b.createdAt),\n );\n const startedAt = sorted[0].createdAt;\n const endedAt = sorted[sorted.length - 1].createdAt;\n\n sections.push(`**Duration:** ${startedAt} to ${endedAt}`);\n sections.push(`**Observations:** ${observations.length}`);\n\n // Changes section (most important)\n if (groups.change.length > 0) {\n sections.push('');\n sections.push('### Changes');\n for (const obs of groups.change.slice(0, 10)) {\n sections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n // Decisions section\n if (groups.decision.length > 0) {\n sections.push('');\n sections.push('### Decisions');\n for (const obs of groups.decision.slice(0, 5)) {\n sections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n // Verifications section\n if (groups.verification.length > 0) {\n sections.push('');\n sections.push('### Verifications');\n for (const obs of groups.verification.slice(0, 5)) {\n sections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n // References section\n if (groups.reference.length > 0) {\n sections.push('');\n sections.push('### References');\n for (const obs of groups.reference.slice(0, 3)) {\n sections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n // Findings section\n if (groups.finding.length > 0) {\n sections.push('');\n sections.push('### Findings');\n for (const obs of groups.finding.slice(0, 5)) {\n sections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n let result = sections.join('\\n');\n\n // Enforce ~2000 char budget (approx 500 tokens at ~4 chars/token)\n if (result.length > 2000) {\n // Progressively trim: references first, then findings, then verifications\n const trimSections: string[] = [];\n trimSections.push('## Session Summary');\n trimSections.push(`**Duration:** ${startedAt} to ${endedAt}`);\n trimSections.push(`**Observations:** ${observations.length}`);\n\n if (groups.change.length > 0) {\n trimSections.push('');\n trimSections.push('### Changes');\n for (const obs of groups.change.slice(0, 5)) {\n trimSections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n if (groups.decision.length > 0) {\n trimSections.push('');\n trimSections.push('### Decisions');\n for (const obs of groups.decision.slice(0, 3)) {\n trimSections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n if (groups.verification.length > 0) {\n trimSections.push('');\n trimSections.push('### Verifications');\n for (const obs of groups.verification.slice(0, 3)) {\n trimSections.push(`- ${snippet(obs.content)}`);\n }\n }\n\n result = trimSections.join('\\n');\n }\n\n return result;\n}\n\n/**\n * Generates a session summary by reading all observations for the given session,\n * compressing them into a concise summary, and storing it back on the session row.\n *\n * Returns null if the session has zero observations (graceful no-op).\n *\n * @param sessionId - The session ID to summarize\n * @param obsRepo - Repository for reading observations\n * @param sessionRepo - Repository for updating the session summary\n * @returns SessionSummary or null if no observations\n */\nexport function generateSessionSummary(\n sessionId: string,\n obsRepo: ObservationRepository,\n sessionRepo: SessionRepository,\n): SessionSummary | null {\n debug('curation', 'Generating session summary', { sessionId });\n\n // Fetch all non-deleted observations for this session, ordered by createdAt\n const observations = obsRepo.list({\n sessionId,\n limit: 1000, // generous limit -- compress handles any count\n });\n\n if (observations.length === 0) {\n debug('curation', 'No observations for session, skipping summary', {\n sessionId,\n });\n return null;\n }\n\n const summary = compressObservations(observations);\n const generatedAt = new Date().toISOString();\n\n // Store the summary on the session row\n sessionRepo.updateSessionSummary(sessionId, summary);\n\n debug('curation', 'Session summary generated', {\n sessionId,\n observationCount: observations.length,\n summaryLength: summary.length,\n });\n\n return {\n sessionId,\n summary,\n observationCount: observations.length,\n generatedAt,\n };\n}\n","import type BetterSqlite3 from 'better-sqlite3';\n\nimport type { Observation, ObservationKind, Session } from '../shared/types.js';\nimport type { ObservationRow } from '../shared/types.js';\nimport { rowToObservation } from '../shared/types.js';\nimport type { ToolRegistryRepository } from '../storage/tool-registry.js';\nimport type { ToolRegistryRow, ToolUsageStats } from '../shared/tool-types.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Maximum character budget for injected context (~2000 tokens at ~3 chars/token).\n * If the assembled context exceeds this, observations are truncated.\n */\nconst MAX_CONTEXT_CHARS = 6000;\n\n/**\n * Maximum number of characters to show per observation in the index.\n */\nconst OBSERVATION_CONTENT_LIMIT = 120;\n\n/**\n * Maximum number of tools to show in the context section.\n * Keeps the tool section compact to preserve budget for observations.\n */\nconst MAX_TOOLS_IN_CONTEXT = 10;\n\n/**\n * Maximum character budget for the \"## Available Tools\" section.\n * Prevents tool listings from consuming too much of the 6000-char overall budget.\n */\nconst TOOL_SECTION_BUDGET = 500;\n\n/**\n * Welcome message for first-ever session (no prior sessions or observations).\n */\nconst WELCOME_MESSAGE = `[Laminark] First session detected. Memory system is active and capturing observations.\nUse /laminark:remember to save important context. Use /laminark:recall to search memories.`;\n\n/**\n * Formats an ISO 8601 timestamp into a human-readable relative time string.\n *\n * @param isoDate - ISO 8601 timestamp string\n * @returns Relative time string (e.g., \"2 hours ago\", \"yesterday\", \"3 days ago\")\n */\nexport function formatRelativeTime(isoDate: string): string {\n const now = Date.now();\n const then = new Date(isoDate).getTime();\n const diffMs = now - then;\n\n if (diffMs < 0) {\n return 'just now';\n }\n\n const seconds = Math.floor(diffMs / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n\n if (minutes < 1) return 'just now';\n if (minutes === 1) return '1 minute ago';\n if (minutes < 60) return `${minutes} minutes ago`;\n if (hours === 1) return '1 hour ago';\n if (hours < 24) return `${hours} hours ago`;\n if (days === 1) return 'yesterday';\n if (days < 7) return `${days} days ago`;\n if (weeks === 1) return '1 week ago';\n return `${weeks} weeks ago`;\n}\n\n/**\n * Truncates a string to `maxLen` characters, appending \"...\" if truncated.\n */\nfunction truncate(text: string, maxLen: number): string {\n // Normalize whitespace (collapse newlines/tabs to spaces)\n const normalized = text.replace(/\\s+/g, ' ').trim();\n if (normalized.length <= maxLen) return normalized;\n return normalized.slice(0, maxLen) + '...';\n}\n\n/**\n * Queries recent observations filtered by kind with a time window.\n */\nfunction getRecentByKind(\n db: BetterSqlite3.Database,\n projectHash: string,\n kind: ObservationKind,\n limit: number,\n sinceDays: number,\n): Observation[] {\n const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();\n const rows = db\n .prepare(\n `SELECT * FROM observations\n WHERE project_hash = ? AND kind = ? AND deleted_at IS NULL\n AND created_at >= ?\n ORDER BY created_at DESC, rowid DESC\n LIMIT ?`,\n )\n .all(projectHash, kind, since, limit) as ObservationRow[];\n return rows.map(rowToObservation);\n}\n\n/**\n * Formats the context using structured kind-aware sections.\n *\n * Produces a compact index suitable for Claude's context window:\n * - Last session summary\n * - Recent changes (with provenance context)\n * - Active decisions\n * - Reference docs\n * - Findings\n */\nexport function formatContextIndex(\n lastSession: Session | null,\n sections: {\n changes: Observation[];\n decisions: Observation[];\n findings: Observation[];\n references: Observation[];\n },\n): string {\n const hasContent = lastSession?.summary ||\n sections.changes.length > 0 ||\n sections.decisions.length > 0 ||\n sections.findings.length > 0 ||\n sections.references.length > 0;\n\n if (!hasContent) {\n return WELCOME_MESSAGE;\n }\n\n const lines: string[] = ['[Laminark - Session Context]', ''];\n\n if (lastSession && lastSession.summary) {\n lines.push('## Previous Session');\n lines.push(lastSession.summary);\n lines.push('');\n }\n\n if (sections.changes.length > 0) {\n lines.push('## Recent Changes');\n for (const obs of sections.changes) {\n const content = truncate(obs.content, OBSERVATION_CONTENT_LIMIT);\n const relTime = formatRelativeTime(obs.createdAt);\n lines.push(`- ${content} (${relTime})`);\n }\n lines.push('');\n }\n\n if (sections.decisions.length > 0) {\n lines.push('## Active Decisions');\n for (const obs of sections.decisions) {\n const content = truncate(obs.content, OBSERVATION_CONTENT_LIMIT);\n lines.push(`- ${content}`);\n }\n lines.push('');\n }\n\n if (sections.references.length > 0) {\n lines.push('## Reference Docs');\n for (const obs of sections.references) {\n const content = truncate(obs.content, OBSERVATION_CONTENT_LIMIT);\n lines.push(`- ${content}`);\n }\n lines.push('');\n }\n\n if (sections.findings.length > 0) {\n lines.push('## Recent Findings');\n for (const obs of sections.findings) {\n const shortId = obs.id.slice(0, 8);\n const content = truncate(obs.content, OBSERVATION_CONTENT_LIMIT);\n lines.push(`- [${shortId}] ${content}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Queries recent high-value observations for context injection.\n * Kind-aware: prioritizes changes, decisions, and findings.\n *\n * @param db - better-sqlite3 database connection\n * @param projectHash - Project scope identifier\n * @param limit - Maximum observations to return (default 5)\n * @returns Array of high-value observations\n */\nexport function getHighValueObservations(\n db: BetterSqlite3.Database,\n projectHash: string,\n limit: number = 5,\n): Observation[] {\n debug('context', 'Querying high-value observations', { projectHash, limit });\n\n const rows = db\n .prepare(\n `SELECT * FROM observations\n WHERE project_hash = ? AND deleted_at IS NULL\n AND classification IS NOT NULL AND classification != 'noise'\n ORDER BY\n CASE\n WHEN source = 'mcp:save_memory' THEN 0\n WHEN source = 'slash:remember' THEN 0\n WHEN kind = 'change' THEN 1\n WHEN kind = 'decision' THEN 1\n ELSE 2\n END ASC,\n created_at DESC,\n rowid DESC\n LIMIT ?`,\n )\n .all(projectHash, limit) as ObservationRow[];\n\n debug('context', 'High-value observations retrieved', { count: rows.length });\n\n return rows.map(rowToObservation);\n}\n\n/**\n * Gets the most recent completed session with a non-null summary.\n */\nfunction getLastCompletedSession(\n db: BetterSqlite3.Database,\n projectHash: string,\n): Session | null {\n const row = db\n .prepare(\n `SELECT * FROM sessions\n WHERE project_hash = ? AND summary IS NOT NULL AND ended_at IS NOT NULL\n ORDER BY ended_at DESC, rowid DESC\n LIMIT 1`,\n )\n .get(projectHash) as\n | {\n id: string;\n project_hash: string;\n started_at: string;\n ended_at: string | null;\n summary: string | null;\n }\n | undefined;\n\n if (!row) return null;\n\n return {\n id: row.id,\n projectHash: row.project_hash,\n startedAt: row.started_at,\n endedAt: row.ended_at,\n summary: row.summary,\n };\n}\n\n/**\n * Ranks tools by relevance using a weighted combination of recent usage\n * frequency and recency. Tools with no recent usage score 0.\n *\n * Formula: score = eventCount / totalEvents (frequency share among peers)\n *\n * Uses event-count-based window (last N events) instead of time-based decay.\n * This is immune to usage gaps — if you don't use the app for a week,\n * your usage patterns are preserved because the window slides by event\n * count, not calendar time.\n *\n * MCP server entries aggregate usage stats from their individual tool events\n * to ensure accurate scoring.\n */\nfunction rankToolsByRelevance(\n tools: ToolRegistryRow[],\n usageStats: ToolUsageStats[],\n): ToolRegistryRow[] {\n if (usageStats.length === 0) return tools; // No event data: keep existing order\n\n // Build direct lookup: tool_name -> stats\n const statsMap = new Map<string, ToolUsageStats>();\n for (const stat of usageStats) {\n statsMap.set(stat.tool_name, stat);\n }\n\n // Aggregate usage stats by MCP server prefix for server-level rows\n const serverStats = new Map<string, { usage_count: number }>();\n for (const stat of usageStats) {\n const match = stat.tool_name.match(/^mcp__([^_]+(?:_[^_]+)*)__/);\n if (match) {\n const serverName = match[1];\n const existing = serverStats.get(serverName);\n if (existing) {\n existing.usage_count += stat.usage_count;\n } else {\n serverStats.set(serverName, { usage_count: stat.usage_count });\n }\n }\n }\n\n // Total events across all tools (floor of 1 prevents division by zero)\n const totalEvents = Math.max(1,\n [...statsMap.values()].reduce((sum, s) => sum + s.usage_count, 0),\n );\n\n const scored = tools.map(row => {\n // Look up stats directly by tool name first\n let count: number | undefined = statsMap.get(row.name)?.usage_count;\n\n // For MCP server rows, fall back to aggregated server stats\n if (count === undefined && row.tool_type === 'mcp_server' && row.server_name) {\n count = serverStats.get(row.server_name)?.usage_count;\n }\n\n if (count === undefined) {\n return { row, score: 0 };\n }\n\n let score = count / totalEvents;\n\n // STAL-02/03: Deprioritize stale and demoted tools\n if (row.status === 'stale' || row.status === 'demoted') {\n score *= 0.25; // Reduce to 25% of normal score\n }\n\n // STAL-02: Age-based deprioritization for tools not seen in 30+ days\n const lastUsed = row.last_used_at || row.discovered_at;\n const lastSeen = new Date(Math.max(\n new Date(lastUsed).getTime(),\n new Date(row.updated_at).getTime(),\n ));\n const daysSinceLastSeen = (Date.now() - lastSeen.getTime()) / (1000 * 60 * 60 * 24);\n if (daysSinceLastSeen > 30) {\n score *= 0.5; // Additional 50% penalty for age (stacks with status penalty)\n }\n\n return { row, score };\n });\n\n // Sort by score descending; ties broken by lifetime usage_count descending\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.row.usage_count - a.row.usage_count;\n });\n\n return scored.map(s => s.row);\n}\n\n/**\n * Formats available tools as a compact section for session context.\n *\n * Deduplicates MCP servers vs individual MCP tools (prefers server entries).\n * Excludes built-in tools (Claude already knows Read, Write, Edit, Bash, etc.).\n * Enforces a 500-character sub-budget via incremental line checking.\n */\nfunction formatToolSection(tools: ToolRegistryRow[]): string {\n if (tools.length === 0) return '';\n\n // Deduplicate: prefer mcp_server entries over individual mcp_tool entries\n const seenServers = new Set<string>();\n const deduped: ToolRegistryRow[] = [];\n\n // First pass: collect server-level entries\n for (const tool of tools) {\n if (tool.tool_type === 'mcp_server') {\n seenServers.add(tool.server_name ?? tool.name);\n deduped.push(tool);\n }\n }\n // Second pass: add non-server entries, skipping individual tools from listed servers\n for (const tool of tools) {\n if (tool.tool_type !== 'mcp_server') {\n if (tool.tool_type === 'mcp_tool' && tool.server_name && seenServers.has(tool.server_name)) {\n continue;\n }\n deduped.push(tool);\n }\n }\n\n // Exclude built-in tools -- Claude already knows about them\n const displayable = deduped.filter(t => t.tool_type !== 'builtin');\n\n if (displayable.length === 0) return '';\n\n const lines: string[] = ['## Available Tools'];\n\n for (const tool of displayable) {\n const scopeTag = tool.scope === 'project' ? 'project' : 'global';\n const usageStr = tool.usage_count > 0 ? `, ${tool.usage_count}x` : '';\n\n let candidateLine: string;\n if (tool.tool_type === 'mcp_server') {\n candidateLine = `- MCP: ${tool.server_name ?? tool.name} (${scopeTag}${usageStr})`;\n } else if (tool.tool_type === 'slash_command') {\n candidateLine = `- ${tool.name} (${scopeTag}${usageStr})`;\n } else if (tool.tool_type === 'skill') {\n const desc = tool.description ? ` - ${tool.description}` : '';\n candidateLine = `- skill: ${tool.name} (${scopeTag})${desc}`;\n } else if (tool.tool_type === 'plugin') {\n candidateLine = `- plugin: ${tool.name} (${scopeTag})`;\n } else {\n candidateLine = `- ${tool.name} (${scopeTag}${usageStr})`;\n }\n\n // Incremental budget check: stop if adding this line exceeds 500 chars\n if ([...lines, candidateLine].join('\\n').length > TOOL_SECTION_BUDGET) break;\n lines.push(candidateLine);\n }\n\n // Overflow indicator: show how many tools were dropped\n const added = lines.length - 1; // subtract header line\n if (displayable.length > added && added > 0) {\n const overflow = `(${displayable.length - added} more available)`;\n if ((lines.join('\\n') + '\\n' + overflow).length <= TOOL_SECTION_BUDGET) {\n lines.push(overflow);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Assembles the complete context string for SessionStart injection.\n *\n * Kind-aware: queries changes (last 24h), decisions (last 7d),\n * findings (last 7d), and references (last 3d) separately,\n * then assembles them into structured sections.\n *\n * Token budget: Total output stays under 2000 tokens (~6000 characters).\n */\nexport function assembleSessionContext(\n db: BetterSqlite3.Database,\n projectHash: string,\n toolRegistry?: ToolRegistryRepository,\n): string {\n debug('context', 'Assembling session context', { projectHash });\n\n const lastSession = getLastCompletedSession(db, projectHash);\n\n // Kind-aware queries with different time windows and limits\n const changes = getRecentByKind(db, projectHash, 'change', 10, 1);\n const decisions = getRecentByKind(db, projectHash, 'decision', 5, 7);\n const findings = getRecentByKind(db, projectHash, 'finding', 5, 7);\n const references = getRecentByKind(db, projectHash, 'reference', 3, 3);\n\n // SCOP-02: Query scope-filtered tools for this session\n let toolSection = '';\n if (toolRegistry) {\n try {\n const availableTools = toolRegistry.getAvailableForSession(projectHash);\n const usageStats = toolRegistry.getRecentUsage(projectHash, 200);\n const ranked = rankToolsByRelevance(availableTools, usageStats);\n toolSection = formatToolSection(ranked);\n } catch {\n // Tool registry is supplementary -- never block context assembly\n }\n }\n\n let context = formatContextIndex(lastSession, { changes, decisions, findings, references });\n\n // Append tool section after observations (lower priority for budget)\n if (toolSection) {\n context = context + '\\n\\n' + toolSection;\n }\n\n // Enforce token budget: progressively trim sections\n if (context.length > MAX_CONTEXT_CHARS) {\n debug('context', 'Context exceeds budget, trimming', {\n length: context.length,\n budget: MAX_CONTEXT_CHARS,\n });\n\n // Drop tool section first (lowest priority)\n if (toolSection) {\n context = formatContextIndex(lastSession, { changes, decisions, findings, references });\n toolSection = ''; // Mark as dropped so we don't re-add\n }\n }\n\n if (context.length > MAX_CONTEXT_CHARS) {\n // Trim in priority order: references first, then findings, then changes\n let trimmedRefs = references.slice();\n let trimmedFindings = findings.slice();\n let trimmedChanges = changes.slice();\n\n while (context.length > MAX_CONTEXT_CHARS && trimmedRefs.length > 0) {\n trimmedRefs = trimmedRefs.slice(0, -1);\n context = formatContextIndex(lastSession, {\n changes: trimmedChanges, decisions, findings: trimmedFindings, references: trimmedRefs,\n });\n }\n if (context.length > MAX_CONTEXT_CHARS) {\n while (context.length > MAX_CONTEXT_CHARS && trimmedFindings.length > 0) {\n trimmedFindings = trimmedFindings.slice(0, -1);\n context = formatContextIndex(lastSession, {\n changes: trimmedChanges, decisions, findings: trimmedFindings, references: trimmedRefs,\n });\n }\n }\n if (context.length > MAX_CONTEXT_CHARS) {\n while (context.length > MAX_CONTEXT_CHARS && trimmedChanges.length > 0) {\n trimmedChanges = trimmedChanges.slice(0, -1);\n context = formatContextIndex(lastSession, {\n changes: trimmedChanges, decisions, findings: trimmedFindings, references: trimmedRefs,\n });\n }\n }\n }\n\n debug('context', 'Session context assembled', { length: context.length });\n\n return context;\n}\n","import { readFileSync, readdirSync, existsSync } from 'node:fs';\nimport { join, basename } from 'node:path';\nimport { homedir } from 'node:os';\n\nimport { debug } from '../shared/debug.js';\nimport type { DiscoveredTool, ToolScope } from '../shared/tool-types.js';\n\n/**\n * Extracts a description from YAML frontmatter in a Markdown file.\n * Reads only the first 500 bytes for performance.\n */\nfunction extractDescription(filePath: string): string | null {\n try {\n const fd = readFileSync(filePath, { encoding: 'utf-8', flag: 'r' });\n const head = fd.slice(0, 500);\n const fmMatch = head.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!fmMatch) return null;\n const descMatch = fmMatch[1].match(/description:\\s*(.+)/);\n return descMatch ? descMatch[1].trim() : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Scans an .mcp.json file for MCP server entries.\n * Each server key becomes a wildcard tool entry (individual tool names are not in config).\n */\nfunction scanMcpJson(\n filePath: string,\n scope: ToolScope,\n projectHash: string | null,\n tools: DiscoveredTool[],\n): void {\n try {\n if (!existsSync(filePath)) return;\n\n const raw = readFileSync(filePath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n const mcpServers = config.mcpServers as Record<string, unknown> | undefined;\n\n if (!mcpServers || typeof mcpServers !== 'object') return;\n\n for (const serverName of Object.keys(mcpServers)) {\n tools.push({\n name: `mcp__${serverName}__*`,\n toolType: 'mcp_server',\n scope,\n source: `config:${filePath}`,\n projectHash,\n description: null,\n serverName,\n });\n }\n } catch (err) {\n debug('scanner', 'Failed to scan MCP config', { filePath, error: String(err) });\n }\n}\n\n/**\n * Scans ~/.claude.json for MCP servers (top-level and per-project).\n */\nfunction scanClaudeJson(filePath: string, tools: DiscoveredTool[]): void {\n try {\n if (!existsSync(filePath)) return;\n\n const raw = readFileSync(filePath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n\n // Top-level mcpServers (user-scope global servers)\n const topServers = config.mcpServers as Record<string, unknown> | undefined;\n if (topServers && typeof topServers === 'object') {\n for (const serverName of Object.keys(topServers)) {\n tools.push({\n name: `mcp__${serverName}__*`,\n toolType: 'mcp_server',\n scope: 'global',\n source: 'config:~/.claude.json',\n projectHash: null,\n description: null,\n serverName,\n });\n }\n }\n\n // Per-project mcpServers (projects.*.mcpServers)\n const projects = config.projects as Record<string, Record<string, unknown>> | undefined;\n if (projects && typeof projects === 'object') {\n for (const projectEntry of Object.values(projects)) {\n const projServers = projectEntry.mcpServers as Record<string, unknown> | undefined;\n if (projServers && typeof projServers === 'object') {\n for (const serverName of Object.keys(projServers)) {\n tools.push({\n name: `mcp__${serverName}__*`,\n toolType: 'mcp_server',\n scope: 'global',\n source: 'config:~/.claude.json',\n projectHash: null,\n description: null,\n serverName,\n });\n }\n }\n }\n }\n } catch (err) {\n debug('scanner', 'Failed to scan claude.json', { filePath, error: String(err) });\n }\n}\n\n/**\n * Scans a commands directory for slash command .md files.\n * Supports one level of subdirectory nesting for namespaced commands.\n */\nfunction scanCommands(\n dirPath: string,\n scope: ToolScope,\n projectHash: string | null,\n tools: DiscoveredTool[],\n): void {\n try {\n if (!existsSync(dirPath)) return;\n\n const entries = readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isFile() && entry.name.endsWith('.md')) {\n const cmdName = `/${basename(entry.name, '.md')}`;\n const description = extractDescription(join(dirPath, entry.name));\n tools.push({\n name: cmdName,\n toolType: 'slash_command',\n scope,\n source: `config:${dirPath}`,\n projectHash,\n description,\n serverName: null,\n });\n } else if (entry.isDirectory()) {\n // One level deep: namespaced commands\n const subDir = join(dirPath, entry.name);\n try {\n const subEntries = readdirSync(subDir, { withFileTypes: true });\n for (const subEntry of subEntries) {\n if (subEntry.isFile() && subEntry.name.endsWith('.md')) {\n const cmdName = `/${entry.name}:${basename(subEntry.name, '.md')}`;\n const description = extractDescription(join(subDir, subEntry.name));\n tools.push({\n name: cmdName,\n toolType: 'slash_command',\n scope,\n source: `config:${dirPath}`,\n projectHash,\n description,\n serverName: null,\n });\n }\n }\n } catch {\n // Subdirectory unreadable -- skip\n }\n }\n }\n } catch (err) {\n debug('scanner', 'Failed to scan commands directory', { dirPath, error: String(err) });\n }\n}\n\n/**\n * Scans a skills directory for skill subdirectories containing SKILL.md.\n */\nfunction scanSkills(\n dirPath: string,\n scope: ToolScope,\n projectHash: string | null,\n tools: DiscoveredTool[],\n): void {\n try {\n if (!existsSync(dirPath)) return;\n\n const entries = readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const skillMdPath = join(dirPath, entry.name, 'SKILL.md');\n if (existsSync(skillMdPath)) {\n const description = extractDescription(skillMdPath);\n tools.push({\n name: entry.name,\n toolType: 'skill',\n scope,\n source: `config:${dirPath}`,\n projectHash,\n description,\n serverName: null,\n });\n }\n }\n }\n } catch (err) {\n debug('scanner', 'Failed to scan skills directory', { dirPath, error: String(err) });\n }\n}\n\n/**\n * Scans installed_plugins.json for installed Claude plugins.\n * Version 2 format: { version: 2, plugins: { \"name@marketplace\": [{ scope, installPath, version }] } }\n */\nfunction scanInstalledPlugins(filePath: string, tools: DiscoveredTool[]): void {\n try {\n if (!existsSync(filePath)) return;\n\n const raw = readFileSync(filePath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n\n const plugins = config.plugins as Record<string, unknown[]> | undefined;\n if (!plugins || typeof plugins !== 'object') return;\n\n for (const [key, installations] of Object.entries(plugins)) {\n const pluginName = key.split('@')[0];\n if (!Array.isArray(installations)) continue;\n\n for (const install of installations) {\n const inst = install as Record<string, unknown>;\n const instScope: ToolScope = inst.scope === 'user' ? 'global' : 'project';\n\n tools.push({\n name: pluginName,\n toolType: 'plugin',\n scope: instScope,\n source: 'config:installed_plugins.json',\n projectHash: null,\n description: null,\n serverName: null,\n });\n\n // If the installation has an installPath, scan its .mcp.json for MCP servers\n if (typeof inst.installPath === 'string') {\n scanMcpJson(\n join(inst.installPath, '.mcp.json'),\n 'plugin',\n null,\n tools,\n );\n }\n }\n }\n } catch (err) {\n debug('scanner', 'Failed to scan installed plugins', { filePath, error: String(err) });\n }\n}\n\n/**\n * Scans all Claude Code config surfaces for tool discovery.\n * Called during SessionStart to proactively populate the tool registry.\n *\n * All filesystem operations are synchronous (SessionStart hook is synchronous).\n * Every scanner is wrapped in try/catch -- malformed configs never crash the hook.\n *\n * Config surfaces scanned:\n * DISC-01: .mcp.json (project) + ~/.claude.json (global)\n * DISC-02: .claude/commands (project) + ~/.claude/commands (global)\n * DISC-03: .claude/skills (project) + ~/.claude/skills (global)\n * DISC-04: installed_plugins.json (global plugins)\n */\nexport function scanConfigForTools(cwd: string, projectHash: string): DiscoveredTool[] {\n const tools: DiscoveredTool[] = [];\n const home = homedir();\n\n // DISC-01: MCP server configs\n scanMcpJson(join(cwd, '.mcp.json'), 'project', projectHash, tools);\n scanClaudeJson(join(home, '.claude.json'), tools);\n\n // DISC-02: Slash commands\n scanCommands(join(cwd, '.claude', 'commands'), 'project', projectHash, tools);\n scanCommands(join(home, '.claude', 'commands'), 'global', null, tools);\n\n // DISC-03: Skills\n scanSkills(join(cwd, '.claude', 'skills'), 'project', projectHash, tools);\n scanSkills(join(home, '.claude', 'skills'), 'global', null, tools);\n\n // DISC-04: Installed plugins\n scanInstalledPlugins(join(home, '.claude', 'plugins', 'installed_plugins.json'), tools);\n\n return tools;\n}\n","import type BetterSqlite3 from 'better-sqlite3';\n\nimport type { ToolPattern, RoutingSuggestion } from './types.js';\nimport { inferToolType } from '../hooks/tool-name-parser.js';\nimport { isLaminarksOwnTool } from '../hooks/self-referential.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Extracts tool sequence patterns from historical tool_usage_events.\n *\n * Scans all successful tool usage events for the project, groups them by session,\n * and identifies recurring sliding-window patterns where a specific sequence of\n * preceding tool calls led to a target tool activation.\n *\n * Runs at SessionStart and stores results in the routing_patterns table for\n * cheap PostToolUse lookup.\n *\n * @param db - Database connection\n * @param projectHash - Project identifier\n * @param windowSize - Number of preceding tools to consider (default 5)\n * @returns Extracted patterns sorted by frequency descending\n */\nexport function extractPatterns(\n db: BetterSqlite3.Database,\n projectHash: string,\n windowSize: number = 5,\n): ToolPattern[] {\n // Query successful tool usage events for this project, ordered by session then time\n const events = db.prepare(`\n SELECT tool_name, session_id\n FROM tool_usage_events\n WHERE project_hash = ? AND success = 1\n ORDER BY session_id, created_at\n `).all(projectHash) as Array<{ tool_name: string; session_id: string }>;\n\n // Group events by session into arrays of tool_name strings\n const sessions = new Map<string, string[]>();\n for (const evt of events) {\n if (!sessions.has(evt.session_id)) {\n sessions.set(evt.session_id, []);\n }\n sessions.get(evt.session_id)!.push(evt.tool_name);\n }\n\n // Extract sliding-window patterns\n const patternCounts = new Map<string, { target: string; preceding: string[]; count: number }>();\n\n for (const [, toolSequence] of sessions) {\n for (let i = windowSize; i < toolSequence.length; i++) {\n const target = toolSequence[i];\n const preceding = toolSequence.slice(i - windowSize, i);\n\n // Skip built-in tools as targets (we never suggest those)\n if (inferToolType(target) === 'builtin') continue;\n // Skip Laminark's own tools as targets\n if (isLaminarksOwnTool(target)) continue;\n\n const key = `${target}:${preceding.join(',')}`;\n const existing = patternCounts.get(key);\n if (existing) {\n existing.count++;\n } else {\n patternCounts.set(key, { target, preceding, count: 1 });\n }\n }\n }\n\n // Filter to patterns with frequency >= 2 (appeared at least twice) and sort by frequency\n return Array.from(patternCounts.values())\n .filter(p => p.count >= 2)\n .map(p => ({\n targetTool: p.target,\n precedingTools: p.preceding,\n frequency: p.count,\n }))\n .sort((a, b) => b.frequency - a.frequency);\n}\n\n/**\n * Stores pre-computed routing patterns in the routing_patterns table.\n *\n * Creates the table inline (CREATE TABLE IF NOT EXISTS), deletes old patterns\n * for the project, and inserts new ones in a transaction.\n *\n * @param db - Database connection\n * @param projectHash - Project identifier\n * @param patterns - Pre-computed patterns from extractPatterns()\n */\nexport function storePrecomputedPatterns(\n db: BetterSqlite3.Database,\n projectHash: string,\n patterns: ToolPattern[],\n): void {\n // Create table inline (no migration -- transient data refreshed each SessionStart)\n db.exec(`\n CREATE TABLE IF NOT EXISTS routing_patterns (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_hash TEXT NOT NULL,\n target_tool TEXT NOT NULL,\n preceding_tools TEXT NOT NULL,\n frequency INTEGER NOT NULL,\n computed_at TEXT NOT NULL DEFAULT (datetime('now'))\n )\n `);\n db.exec(`\n CREATE INDEX IF NOT EXISTS idx_routing_patterns_project ON routing_patterns(project_hash)\n `);\n\n // Transaction: delete old patterns for this project, insert new ones\n const deleteStmt = db.prepare('DELETE FROM routing_patterns WHERE project_hash = ?');\n const insertStmt = db.prepare(\n 'INSERT INTO routing_patterns (project_hash, target_tool, preceding_tools, frequency) VALUES (?, ?, ?, ?)',\n );\n\n const upsertAll = db.transaction(() => {\n deleteStmt.run(projectHash);\n for (const pattern of patterns) {\n insertStmt.run(\n projectHash,\n pattern.targetTool,\n JSON.stringify(pattern.precedingTools),\n pattern.frequency,\n );\n }\n });\n\n upsertAll();\n debug('routing', 'Stored pre-computed patterns', { projectHash, count: patterns.length });\n}\n\n/**\n * Evaluates the current session's recent tool sequence against pre-computed patterns.\n *\n * Queries the current session's recent tool names, compares against stored patterns,\n * and returns the best match if it exceeds the confidence threshold and the target\n * tool is in the suggestable set.\n *\n * @param db - Database connection\n * @param sessionId - Current session identifier\n * @param projectHash - Project identifier\n * @param suggestableToolNames - Set of tool names available for suggestion (availability gate)\n * @param confidenceThreshold - Minimum confidence to return a suggestion\n * @returns Best matching suggestion, or null if none qualifies\n */\nexport function evaluateLearnedPatterns(\n db: BetterSqlite3.Database,\n sessionId: string,\n projectHash: string,\n suggestableToolNames: Set<string>,\n confidenceThreshold: number,\n): RoutingSuggestion | null {\n // Get the current session's recent tool names (last 10 events, newest first)\n const recentEvents = db.prepare(`\n SELECT tool_name FROM tool_usage_events\n WHERE session_id = ? AND project_hash = ?\n ORDER BY created_at DESC\n LIMIT 10\n `).all(sessionId, projectHash) as Array<{ tool_name: string }>;\n\n // Reverse to chronological order for sequence comparison\n const currentTools = recentEvents.map(e => e.tool_name).reverse();\n\n if (currentTools.length === 0) return null;\n\n // Query pre-computed patterns for this project\n const storedPatterns = db.prepare(`\n SELECT target_tool, preceding_tools, frequency\n FROM routing_patterns\n WHERE project_hash = ?\n ORDER BY frequency DESC\n `).all(projectHash) as Array<{ target_tool: string; preceding_tools: string; frequency: number }>;\n\n if (storedPatterns.length === 0) return null;\n\n let bestMatch: { targetTool: string; confidence: number; frequency: number } | null = null;\n\n for (const row of storedPatterns) {\n // Availability gate: only consider patterns whose target is in the suggestable set\n if (!suggestableToolNames.has(row.target_tool)) continue;\n\n const patternTools: string[] = JSON.parse(row.preceding_tools);\n const overlap = computeSequenceOverlap(currentTools, patternTools);\n\n if (overlap > (bestMatch?.confidence ?? 0)) {\n bestMatch = {\n targetTool: row.target_tool,\n confidence: overlap,\n frequency: row.frequency,\n };\n }\n }\n\n if (!bestMatch || bestMatch.confidence < confidenceThreshold) return null;\n\n return {\n toolName: bestMatch.targetTool,\n toolDescription: null,\n confidence: bestMatch.confidence,\n tier: 'learned',\n reason: `Tool sequence pattern match (seen ${bestMatch.frequency}x in similar contexts)`,\n };\n}\n\n/**\n * Computes Jaccard-like overlap between the current session's recent tool set\n * and a pattern's preceding tools set.\n *\n * Takes the last N tools from the current sequence (where N = pattern length),\n * converts both to sets, and counts how many pattern tools appear in the current set.\n *\n * @param currentTools - Current session's recent tool names (chronological order)\n * @param patternTools - Pattern's preceding tools\n * @returns Overlap score from 0.0 to 1.0\n */\nexport function computeSequenceOverlap(\n currentTools: string[],\n patternTools: string[],\n): number {\n if (patternTools.length === 0) return 0;\n\n const current = new Set(currentTools.slice(-patternTools.length));\n const pattern = new Set(patternTools);\n\n let matches = 0;\n for (const tool of pattern) {\n if (current.has(tool)) matches++;\n }\n\n return matches / pattern.size;\n}\n","import type BetterSqlite3 from 'better-sqlite3';\n\nimport type { ObservationRepository } from '../storage/observations.js';\nimport type { SessionRepository } from '../storage/sessions.js';\nimport type { ToolRegistryRepository } from '../storage/tool-registry.js';\nimport type { DiscoveredTool } from '../shared/tool-types.js';\nimport { generateSessionSummary } from '../curation/summarizer.js';\nimport { assembleSessionContext } from '../context/injection.js';\nimport { scanConfigForTools } from './config-scanner.js';\nimport { extractPatterns, storePrecomputedPatterns } from '../routing/intent-patterns.js';\nimport type { PathRepository } from '../paths/path-repository.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * STAL-01: Detects tools that have been removed from config since last scan.\n *\n * Compares currently scanned config tools against the registry and marks\n * missing config-sourced tools as stale. Also cascades to individual MCP tools\n * from removed MCP servers.\n */\nfunction detectRemovedTools(\n toolRegistry: ToolRegistryRepository,\n scannedTools: DiscoveredTool[],\n projectHash: string,\n): void {\n // 1. Get all config-sourced tools currently marked active for this project (+ globals)\n const registeredConfigTools = toolRegistry.getConfigSourcedTools(projectHash);\n\n // 2. Build a Set of scanned tool names for O(1) lookup\n const scannedNames = new Set(scannedTools.map(t => t.name));\n\n // 3. Mark tools missing from scan as stale\n // IMPORTANT: For wildcard MCP server entries (mcp__X__*), also mark\n // individual tools from that server (mcp__X__specific_tool).\n // Extract server name from wildcard entries that disappeared.\n const removedServers = new Set<string>();\n\n for (const registered of registeredConfigTools) {\n if (!scannedNames.has(registered.name)) {\n toolRegistry.markStale(registered.name, registered.project_hash);\n\n // Track removed MCP server names for individual tool cleanup\n if (registered.tool_type === 'mcp_server' && registered.server_name) {\n removedServers.add(registered.server_name);\n }\n }\n }\n\n // 4. Mark individual tools from removed MCP servers as stale\n // These are organically-discovered tools (source = 'hook:PostToolUse')\n // whose parent server was removed from config\n if (removedServers.size > 0) {\n for (const registered of toolRegistry.getAvailableForSession(projectHash)) {\n if (\n registered.server_name &&\n removedServers.has(registered.server_name) &&\n registered.tool_type === 'mcp_tool'\n ) {\n toolRegistry.markStale(registered.name, registered.project_hash);\n }\n }\n }\n\n // 5. Restore tools that reappeared in the scan\n // The upsert in the scan loop already sets status='active' (per Plan 01),\n // so we only need to handle tools that were previously stale\n // and are now scanned again. The upsert handles this automatically.\n}\n\n/**\n * Handles a SessionStart hook event.\n *\n * Creates a new session record in the database, then assembles context\n * from prior sessions and observations for injection into Claude's\n * context window.\n *\n * This hook is SYNCHRONOUS -- stdout is injected into Claude's context.\n * Must complete within 2 seconds (performance budget for sync hooks).\n * Expected execution: <100ms (session create + 2-3 SELECT queries).\n *\n * @returns Context string to write to stdout, or null if no context available\n */\nexport function handleSessionStart(\n input: Record<string, unknown>,\n sessionRepo: SessionRepository,\n db: BetterSqlite3.Database,\n projectHash: string,\n toolRegistry?: ToolRegistryRepository,\n pathRepo?: PathRepository,\n): string | null {\n const sessionId = input.session_id as string | undefined;\n\n if (!sessionId) {\n debug('session', 'SessionStart missing session_id, skipping');\n return null;\n }\n\n sessionRepo.create(sessionId);\n debug('session', 'Session started', { sessionId });\n\n // Update project_metadata with the real project directory from the hook input.\n // The MCP server's process.cwd() returns the plugin install path, not the user's project.\n const cwd = input.cwd as string;\n if (cwd) {\n try {\n db.prepare(`\n INSERT INTO project_metadata (project_hash, project_path, last_seen_at)\n VALUES (?, ?, datetime('now'))\n ON CONFLICT(project_hash) DO UPDATE SET\n project_path = excluded.project_path,\n last_seen_at = excluded.last_seen_at\n `).run(projectHash, cwd);\n } catch {\n // Table may not exist yet\n }\n }\n\n // DISC-01 through DISC-04: Scan config files for available tools\n if (toolRegistry) {\n const cwd = input.cwd as string;\n try {\n const scanStart = Date.now();\n const tools = scanConfigForTools(cwd, projectHash);\n for (const tool of tools) {\n toolRegistry.upsert(tool);\n }\n\n // STAL-01: Detect tools removed from config\n try {\n detectRemovedTools(toolRegistry, tools, projectHash);\n debug('session', 'Staleness detection completed');\n } catch {\n debug('session', 'Staleness detection failed (non-fatal)');\n }\n\n const scanElapsed = Date.now() - scanStart;\n debug('session', 'Config scan completed', { toolsFound: tools.length, elapsed: scanElapsed });\n if (scanElapsed > 200) {\n debug('session', 'Config scan slow (>200ms budget)', { elapsed: scanElapsed });\n }\n } catch {\n // Tool registry is supplementary -- never block session start\n debug('session', 'Config scan failed (non-fatal)');\n }\n }\n\n // ROUT-01: Pre-compute routing patterns for this session\n if (toolRegistry) {\n try {\n const precomputeStart = Date.now();\n const patterns = extractPatterns(db, projectHash, 5);\n storePrecomputedPatterns(db, projectHash, patterns);\n const precomputeElapsed = Date.now() - precomputeStart;\n debug('session', 'Routing patterns pre-computed', { patternCount: patterns.length, elapsed: precomputeElapsed });\n if (precomputeElapsed > 50) {\n debug('session', 'Pattern pre-computation slow (>50ms)', { elapsed: precomputeElapsed });\n }\n } catch {\n // Routing is supplementary -- never block session start\n debug('session', 'Pattern pre-computation failed (non-fatal)');\n }\n }\n\n // Assemble context from prior sessions and observations\n const startTime = Date.now();\n let context = assembleSessionContext(db, projectHash, toolRegistry);\n const elapsed = Date.now() - startTime;\n\n if (elapsed > 500) {\n debug('session', 'Context assembly slow', { elapsed, sessionId });\n }\n\n debug('session', 'Context assembled for injection', {\n sessionId,\n contextLength: context.length,\n elapsed,\n });\n\n // PATH-06: Check for active debug paths from prior sessions\n if (pathRepo) {\n try {\n const activePath = pathRepo.findRecentActivePath();\n if (activePath) {\n const ageMs = Date.now() - new Date(activePath.started_at).getTime();\n if (ageMs > 24 * 60 * 60 * 1000) {\n // Auto-abandon stale paths (>24h old)\n pathRepo.abandonPath(activePath.id);\n debug('session', 'Auto-abandoned stale debug path', { pathId: activePath.id, ageMs });\n } else {\n // Surface the active path for continuation\n const waypoints = pathRepo.getWaypoints(activePath.id);\n const lastWaypoint = waypoints[waypoints.length - 1];\n const pathContext = `\\n[Laminark] Active debug path carried over from prior session:\\n` +\n ` Issue: ${activePath.trigger_summary}\\n` +\n ` Waypoints: ${waypoints.length}\\n` +\n ` Last activity: ${lastWaypoint?.summary?.slice(0, 100) ?? 'none'}\\n` +\n ` Use path_show to see full path, or path_resolve to close it.\\n`;\n context = context + pathContext;\n }\n }\n } catch {\n debug('session', 'Cross-session path check failed (non-fatal)');\n }\n }\n\n // DISC-05: Prompt Claude to report its available tools for registry population.\n // The report_available_tools MCP tool accepts {tools: [{name, description}]}.\n // This runs every session (upsert is idempotent) to keep the registry fresh.\n const toolReportPrompt = toolRegistry\n ? '\\nCall report_available_tools with all your tools (built-in and MCP) so Laminark can index them for discovery.'\n : '';\n\n return context + toolReportPrompt;\n}\n\n/**\n * Handles a SessionEnd hook event.\n *\n * Closes the session record by setting ended_at timestamp.\n */\nexport function handleSessionEnd(\n input: Record<string, unknown>,\n sessionRepo: SessionRepository,\n): void {\n const sessionId = input.session_id as string | undefined;\n\n if (!sessionId) {\n debug('session', 'SessionEnd missing session_id, skipping');\n return;\n }\n\n sessionRepo.end(sessionId);\n\n debug('session', 'Session ended', { sessionId });\n}\n\n/**\n * Handles a Stop hook event.\n *\n * Triggers session summary generation by compressing all observations\n * from the session into a concise summary stored on the session row.\n *\n * Stop fires after SessionEnd, so the session is already closed.\n * Summary generation is heuristic (no LLM call) and typically completes\n * in under 10ms even with many observations.\n *\n * If the session has zero observations, this is a graceful no-op.\n */\nexport function handleStop(\n input: Record<string, unknown>,\n obsRepo: ObservationRepository,\n sessionRepo: SessionRepository,\n): void {\n const sessionId = input.session_id as string | undefined;\n\n if (!sessionId) {\n debug('session', 'Stop missing session_id, skipping');\n return;\n }\n\n debug('session', 'Stop event received, generating summary', { sessionId });\n\n const result = generateSessionSummary(sessionId, obsRepo, sessionRepo);\n\n if (result) {\n debug('session', 'Session summary generated', {\n sessionId,\n observationCount: result.observationCount,\n summaryLength: result.summary.length,\n });\n } else {\n debug('session', 'No observations to summarize', { sessionId });\n }\n}\n","import { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, basename } from 'node:path';\nimport { debug } from '../shared/debug.js';\n\n// =============================================================================\n// Types\n// =============================================================================\n\ninterface PrivacyPattern {\n name: string;\n regex: RegExp;\n replacement: string;\n category: string;\n}\n\ninterface UserPrivacyConfig {\n additionalPatterns?: Array<{\n regex: string;\n replacement: string;\n }>;\n excludedFiles?: string[];\n}\n\n// =============================================================================\n// Default Patterns\n// =============================================================================\n\n/**\n * Built-in privacy patterns that are always active.\n *\n * Order matters: more specific patterns should come before more general ones.\n * For example, api_key patterns before env_variable to avoid double-matching.\n */\nconst DEFAULT_PRIVACY_PATTERNS: PrivacyPattern[] = [\n {\n name: 'private_key',\n regex: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,\n replacement: '[REDACTED:private_key]',\n category: 'private_key',\n },\n {\n name: 'jwt_token',\n regex: /eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g,\n replacement: '[REDACTED:jwt]',\n category: 'jwt',\n },\n {\n name: 'connection_string',\n regex: /(postgresql|mongodb|mysql|redis):\\/\\/[^\\s]+/g,\n replacement: '$1://[REDACTED:connection_string]',\n category: 'connection_string',\n },\n {\n name: 'api_key_openai',\n regex: /sk-[a-zA-Z0-9]{20,}/g,\n replacement: '[REDACTED:api_key]',\n category: 'api_key',\n },\n {\n name: 'api_key_github',\n regex: /ghp_[a-zA-Z0-9]{36,}/g,\n replacement: '[REDACTED:api_key]',\n category: 'api_key',\n },\n {\n name: 'aws_access_key',\n regex: /AKIA[A-Z0-9]{12,}/g,\n replacement: '[REDACTED:api_key]',\n category: 'api_key',\n },\n {\n name: 'env_variable',\n // Match KEY=value where value is 8+ chars and NOT already redacted\n regex: /\\b([A-Z][A-Z0-9_]{2,})=([\"']?)(?!\\[REDACTED:)([^\\s\"']{8,})\\2/g,\n replacement: '$1=[REDACTED:env]',\n category: 'env',\n },\n];\n\n/**\n * Default file patterns that trigger full exclusion (return null).\n */\nconst DEFAULT_EXCLUDED_FILE_PATTERNS: RegExp[] = [\n /\\.env(\\.|$)/, // .env, .env.local, .env.production\n /credentials/i, // credentials.json, etc.\n /secrets/i, // secrets.yaml, etc.\n /\\.pem$/, // SSL certificates\n /\\.key$/, // Private keys\n /id_rsa/, // SSH keys\n];\n\n// =============================================================================\n// Pattern Loading\n// =============================================================================\n\n/**\n * Cached patterns (loaded once per process).\n * null = not yet loaded.\n */\nlet _cachedPatterns: PrivacyPattern[] | null = null;\nlet _cachedExcludedFiles: RegExp[] | null = null;\n\n/**\n * Loads user privacy patterns from ~/.laminark/config.json.\n * Merges with defaults. Caches result.\n *\n * If the config file doesn't exist or is invalid, returns defaults only.\n */\nfunction loadPatterns(): PrivacyPattern[] {\n if (_cachedPatterns !== null) {\n return _cachedPatterns;\n }\n\n const patterns = [...DEFAULT_PRIVACY_PATTERNS];\n\n try {\n const configPath = join(homedir(), '.laminark', 'config.json');\n const raw = readFileSync(configPath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n const privacy = config.privacy as UserPrivacyConfig | undefined;\n\n if (privacy?.additionalPatterns) {\n for (const p of privacy.additionalPatterns) {\n patterns.push({\n name: `user_${p.regex}`,\n regex: new RegExp(p.regex, 'g'),\n replacement: p.replacement,\n category: 'user',\n });\n }\n debug('privacy', 'Loaded user privacy patterns', {\n count: privacy.additionalPatterns.length,\n });\n }\n } catch {\n // Config file doesn't exist or is invalid -- use defaults only\n }\n\n _cachedPatterns = patterns;\n return patterns;\n}\n\n/**\n * Loads excluded file patterns (default + user-configured).\n */\nfunction loadExcludedFiles(): RegExp[] {\n if (_cachedExcludedFiles !== null) {\n return _cachedExcludedFiles;\n }\n\n const patterns = [...DEFAULT_EXCLUDED_FILE_PATTERNS];\n\n try {\n const configPath = join(homedir(), '.laminark', 'config.json');\n const raw = readFileSync(configPath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n const privacy = config.privacy as UserPrivacyConfig | undefined;\n\n if (privacy?.excludedFiles) {\n for (const pattern of privacy.excludedFiles) {\n patterns.push(new RegExp(pattern));\n }\n }\n } catch {\n // Config file doesn't exist or is invalid -- use defaults only\n }\n\n _cachedExcludedFiles = patterns;\n return patterns;\n}\n\n/**\n * Reset cached patterns. Used for testing when HOME changes.\n * @internal\n */\nexport function _resetPatternCache(): void {\n _cachedPatterns = null;\n _cachedExcludedFiles = null;\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Checks whether a file path matches any excluded file pattern.\n *\n * Excluded files should have their observations fully dropped (return null\n * from redactSensitiveContent) rather than just redacted.\n *\n * @param filePath - The file path to check (can be absolute or relative)\n * @returns true if the file should be excluded from observation storage\n */\nexport function isExcludedFile(filePath: string): boolean {\n const name = basename(filePath);\n const patterns = loadExcludedFiles();\n\n for (const pattern of patterns) {\n if (pattern.test(name) || pattern.test(filePath)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Redacts sensitive content before storage.\n *\n * - If filePath is provided and matches an excluded file pattern, returns null\n * (the entire observation should be dropped)\n * - Otherwise, applies all privacy patterns (default + user-configured)\n * sequentially to the text\n * - Returns the redacted text, or the original if no patterns matched\n *\n * @param text - The observation text to redact\n * @param filePath - Optional file path that triggered the observation\n * @returns Redacted text, or null if the file should be fully excluded\n */\nexport function redactSensitiveContent(\n text: string,\n filePath?: string,\n): string | null {\n // Check file exclusion first\n if (filePath && isExcludedFile(filePath)) {\n debug('privacy', 'File excluded from observation', { filePath });\n return null;\n }\n\n const patterns = loadPatterns();\n let result = text;\n const matchedPatterns: string[] = [];\n\n for (const pattern of patterns) {\n // Reset regex lastIndex (global flag means stateful)\n pattern.regex.lastIndex = 0;\n if (pattern.regex.test(result)) {\n matchedPatterns.push(pattern.name);\n // Reset lastIndex again before replace\n pattern.regex.lastIndex = 0;\n result = result.replace(pattern.regex, pattern.replacement);\n }\n }\n\n if (matchedPatterns.length > 0) {\n debug('privacy', 'Content redacted', { patterns: matchedPatterns });\n }\n\n return result;\n}\n","import { isLaminarksOwnTool } from './self-referential.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Tools that are always admitted regardless of content.\n *\n * Write and Edit observations are high-signal by definition --\n * they represent intentional code changes. Content pattern matching\n * must NEVER reject these tools (see research pitfall #3).\n *\n * WebFetch and WebSearch are reference material -- always valuable.\n */\nconst HIGH_SIGNAL_TOOLS = new Set(['Write', 'Edit', 'WebFetch', 'WebSearch']);\n\n// =============================================================================\n// Bash Command Filtering\n// =============================================================================\n\n/**\n * Navigation/exploration Bash commands that produce noise observations.\n * Matched against the start of the command string (after trimming).\n */\nconst NAVIGATION_BASH_PREFIXES = [\n 'ls', 'cd ', 'pwd', 'cat ', 'head ', 'tail ', 'echo ',\n 'wc ', 'which ', 'find ', 'tree', 'file ',\n];\n\n/**\n * Git read-only commands that are navigation (not mutations).\n */\nconst NAVIGATION_GIT_PATTERNS = [\n /^git\\s+status\\b/,\n /^git\\s+log\\b/,\n /^git\\s+diff\\b(?!.*--)/,\n /^git\\s+branch\\b(?!\\s+-[dDmM])/,\n /^git\\s+show\\b/,\n /^git\\s+remote\\b/,\n /^git\\s+stash\\s+list\\b/,\n];\n\n/**\n * Commands that are always meaningful and should be admitted.\n */\nconst MEANINGFUL_BASH_PATTERNS = [\n // Test runners\n /^npm\\s+test\\b/, /^npx\\s+vitest\\b/, /^npx\\s+jest\\b/, /^vitest\\b/, /^jest\\b/,\n /^pytest\\b/, /^cargo\\s+test\\b/, /^go\\s+test\\b/, /^make\\s+test\\b/,\n // Build commands\n /^npm\\s+run\\s+build\\b/, /^npx\\s+tsc\\b/, /^cargo\\s+build\\b/, /^make\\b/,\n /^go\\s+build\\b/, /^gradle\\b/, /^mvn\\b/,\n // Git mutations\n /^git\\s+commit\\b/, /^git\\s+push\\b/, /^git\\s+merge\\b/, /^git\\s+rebase\\b/,\n /^git\\s+cherry-pick\\b/, /^git\\s+reset\\b/, /^git\\s+revert\\b/,\n /^git\\s+checkout\\s+-b\\b/, /^git\\s+switch\\s+-c\\b/,\n /^git\\s+stash\\s+(?:push|pop|apply|drop)\\b/,\n // Containers and infra\n /^docker\\b/, /^kubectl\\b/, /^terraform\\b/, /^helm\\b/,\n // Package management (mutations)\n /^npm\\s+install\\b/, /^npm\\s+i\\b/, /^yarn\\s+add\\b/, /^pnpm\\s+add\\b/,\n /^pip\\s+install\\b/, /^cargo\\s+add\\b/,\n];\n\n/**\n * Determines if a Bash command is meaningful enough to capture.\n *\n * Navigation commands (ls, cd, pwd, cat, git status, git log, etc.) are\n * filtered out. Test runners, build commands, git mutations, and container\n * commands are always admitted. Unknown commands default to admit.\n */\nexport function isMeaningfulBashCommand(command: string): boolean {\n const trimmed = command.trim();\n if (!trimmed) return false;\n\n // Always admit meaningful commands\n for (const pattern of MEANINGFUL_BASH_PATTERNS) {\n if (pattern.test(trimmed)) return true;\n }\n\n // Reject navigation commands\n for (const prefix of NAVIGATION_BASH_PREFIXES) {\n if (trimmed.startsWith(prefix) || trimmed === prefix.trim()) return false;\n }\n\n // Reject git read-only commands\n for (const pattern of NAVIGATION_GIT_PATTERNS) {\n if (pattern.test(trimmed)) return false;\n }\n\n // Default: admit unknown commands (they might be significant)\n return true;\n}\n\n/**\n * Maximum content length before requiring decision/error indicators.\n * Content over this threshold with no meaningful indicators is likely\n * a raw file dump or verbose command output.\n */\nconst MAX_CONTENT_LENGTH = 5000;\n\n/**\n * Patterns that indicate meaningful content even in long output.\n * If content exceeds MAX_CONTENT_LENGTH, it must contain at least\n * one of these to be admitted.\n */\nconst DECISION_OR_ERROR_INDICATORS = [\n /\\berror\\b/i,\n /\\bfailed\\b/i,\n /\\bexception\\b/i,\n /\\bbug\\b/i,\n /\\bdecided\\b/i,\n /\\bchose\\b/i,\n /\\bbecause\\b/i,\n /\\binstead of\\b/i,\n];\n\n/**\n * Decides whether an observation is worth storing in the database.\n *\n * This is the primary quality gate for the observation pipeline.\n * It prevents the database from filling with noise (build output,\n * linter spam, package install logs).\n *\n * Critical rule: Write and Edit tools are NEVER rejected based on\n * content patterns alone. Tool type is the primary signal.\n *\n * @param toolName - The name of the tool that produced the observation\n * @param content - The observation content to evaluate\n * @returns true if the observation should be stored, false to reject\n */\nexport function shouldAdmit(toolName: string, content: string): boolean {\n // Reject Laminark self-referential MCP tools\n if (isLaminarksOwnTool(toolName)) {\n debug('hook', 'Observation rejected', { tool: toolName, reason: 'self-referential' });\n return false;\n }\n\n // Empty/whitespace content is always rejected, even for high-signal tools\n if (!content || content.trim().length === 0) {\n debug('hook', 'Observation rejected', { tool: toolName, reason: 'empty' });\n return false;\n }\n\n // High-signal tools are always admitted (Write, Edit)\n if (HIGH_SIGNAL_TOOLS.has(toolName)) {\n return true;\n }\n\n // Noise pattern detection is now handled post-storage by the HaikuProcessor.\n // Observations are stored first, then classified by Haiku, and noise is soft-deleted.\n // Only cheap structural filters remain here as pre-storage gates.\n\n // Long content without decision/error indicators is likely noise\n if (content.length > MAX_CONTENT_LENGTH) {\n const hasIndicator = DECISION_OR_ERROR_INDICATORS.some((pattern) =>\n pattern.test(content),\n );\n if (!hasIndicator) {\n debug('hook', 'Observation rejected', {\n tool: toolName,\n reason: 'long_content_no_indicators',\n length: content.length,\n });\n return false;\n }\n }\n\n return true;\n}\n","/**\n * Path recall — finds relevant past resolved debug paths based on text similarity.\n *\n * Used by the PreToolUse hook to surface \"you've seen this before\" context\n * when new debugging starts on similar issues.\n *\n * Implements INTEL-03: proactive path recall via Jaccard similarity matching.\n */\n\nimport { jaccardSimilarity } from '../shared/similarity.js';\nimport type { PathRepository } from './path-repository.js';\nimport type { DebugPath } from './types.js';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface PathRecallResult {\n path: DebugPath;\n similarity: number;\n kissSummary: string | null;\n}\n\n// =============================================================================\n// Core Functions\n// =============================================================================\n\n/**\n * Finds past resolved debug paths similar to the current context text.\n *\n * Computes Jaccard similarity against both trigger_summary and resolution_summary\n * of recent resolved paths, taking the max score. Filters to paths scoring >= 0.25\n * and returns the top `limit` results sorted by similarity descending.\n */\nexport function findSimilarPaths(\n pathRepo: PathRepository,\n currentContext: string,\n limit: number = 3,\n): PathRecallResult[] {\n // Get recent paths and filter to resolved ones\n const recentPaths = pathRepo.listPaths(50);\n const resolvedPaths = recentPaths.filter(p => p.status === 'resolved');\n\n if (resolvedPaths.length === 0) return [];\n\n // Score each resolved path by similarity to current context\n const scored: PathRecallResult[] = [];\n\n for (const path of resolvedPaths) {\n const triggerScore = jaccardSimilarity(currentContext, path.trigger_summary);\n const resolutionScore = jaccardSimilarity(currentContext, path.resolution_summary ?? '');\n const similarity = Math.max(triggerScore, resolutionScore);\n\n if (similarity >= 0.25) {\n // Parse kiss_summary from JSON string\n let kissSummary: string | null = null;\n if (path.kiss_summary) {\n try {\n const parsed = JSON.parse(path.kiss_summary);\n // KissSummary has a next_time field — use that as the actionable summary\n kissSummary = parsed.next_time ?? parsed.root_cause ?? null;\n } catch {\n kissSummary = null;\n }\n }\n\n scored.push({ path, similarity, kissSummary });\n }\n }\n\n // Sort by similarity descending, return top N\n scored.sort((a, b) => b.similarity - a.similarity);\n return scored.slice(0, limit);\n}\n\n/**\n * Formats path recall results into a compact string for context injection.\n *\n * Returns empty string if no results. Caps total output to 600 chars.\n */\nexport function formatPathRecall(results: PathRecallResult[]): string {\n if (results.length === 0) return '';\n\n const lines: string[] = ['[Laminark] Similar past debug paths found:'];\n\n for (const r of results) {\n const trigger = r.path.trigger_summary.slice(0, 80);\n lines.push(`- ${trigger} (similarity: ${r.similarity.toFixed(2)})`);\n lines.push(` KISS: ${r.kissSummary ?? 'No summary available'}`);\n }\n\n const output = lines.join('\\n');\n\n if (output.length > 600) {\n return output.slice(0, 597) + '...';\n }\n\n return output;\n}\n","/**\n * PreToolUse hook: proactively surfaces relevant memories and graph entities\n * before Claude executes a tool.\n *\n * This is a synchronous hook -- stdout is injected into Claude's context window.\n * Must be fast (<100ms target, 2s timeout).\n */\n\nimport type BetterSqlite3 from 'better-sqlite3';\nimport { basename } from 'node:path';\n\nimport { isLaminarksOwnTool } from './self-referential.js';\nimport { SearchEngine } from '../storage/search.js';\nimport { getNodeByNameAndType, traverseFrom } from '../graph/schema.js';\nimport { findSimilarPaths, formatPathRecall } from '../paths/path-recall.js';\nimport type { PathRepository } from '../paths/path-repository.js';\nimport { debug } from '../shared/debug.js';\n\n/** Tools where we skip context injection entirely. */\nconst SKIP_TOOLS = new Set(['Glob', 'Task', 'NotebookEdit', 'EnterPlanMode', 'ExitPlanMode', 'AskUserQuestion', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList']);\n\n/** Bash commands that are navigation/noise -- not worth searching for. */\nconst NOISE_BASH_RE = /^\\s*(cd|ls|pwd|echo|cat|head|tail|mkdir|rm|cp|mv|npm\\s+(run|start|test|install)|yarn|pnpm|git\\s+(status|log|diff|add|branch)|exit|clear)\\b/;\n\n/**\n * Extracts a search query from tool input based on tool type.\n * Returns null if the tool should be skipped or has no meaningful target.\n */\nfunction extractSearchQuery(toolName: string, toolInput: Record<string, unknown>): string | null {\n switch (toolName) {\n case 'Write':\n case 'Edit':\n case 'Read': {\n const filePath = toolInput.file_path as string | undefined;\n if (!filePath) return null;\n // Use stem (without extension) for FTS5 search since dots get stripped\n const base = basename(filePath);\n const stem = base.replace(/\\.[^.]+$/, '');\n return stem.length >= 2 ? stem : base;\n }\n case 'Bash': {\n const command = (toolInput.command as string | undefined) ?? '';\n if (NOISE_BASH_RE.test(command)) return null;\n // Extract key terms: strip common prefixes and take first meaningful words\n const cleaned = command\n .replace(/^\\s*(sudo|bash|sh|env)\\s+/, '')\n .replace(/[|><&;]+.*$/, '') // stop at pipes/redirects\n .trim();\n if (!cleaned || cleaned.length < 3) return null;\n // Take first 3 words as search terms\n const words = cleaned.split(/\\s+/).slice(0, 3).join(' ');\n return words.length >= 3 ? words : null;\n }\n case 'Grep': {\n const pattern = toolInput.pattern as string | undefined;\n return pattern && pattern.length >= 2 ? pattern : null;\n }\n case 'WebFetch': {\n const url = toolInput.url as string | undefined;\n if (!url) return null;\n try {\n return new URL(url).hostname;\n } catch {\n return null;\n }\n }\n case 'WebSearch': {\n return (toolInput.query as string | undefined) ?? null;\n }\n default:\n return null;\n }\n}\n\n/**\n * Formats age of an observation as a human-readable string.\n */\nfunction formatAge(createdAt: string): string {\n const ageMs = Date.now() - new Date(createdAt).getTime();\n const hours = Math.floor(ageMs / 3_600_000);\n if (hours < 1) return 'just now';\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return '1d ago';\n return `${days}d ago`;\n}\n\n/**\n * Truncates text to a max length, adding ellipsis if needed.\n */\nfunction truncate(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max - 3) + '...';\n}\n\n/**\n * Main PreToolUse handler. Searches observations and graph for context\n * relevant to the tool about to execute.\n *\n * Returns a formatted context string to inject via stdout, or null if\n * no relevant context was found.\n */\nexport function handlePreToolUse(\n input: Record<string, unknown>,\n db: BetterSqlite3.Database,\n projectHash: string,\n pathRepo?: PathRepository,\n): string | null {\n const toolName = input.tool_name as string | undefined;\n if (!toolName) return null;\n\n // Skip Laminark's own tools\n if (isLaminarksOwnTool(toolName)) return null;\n\n // Skip tools with no meaningful target\n if (SKIP_TOOLS.has(toolName)) return null;\n\n const toolInput = (input.tool_input as Record<string, unknown>) ?? {};\n const query = extractSearchQuery(toolName, toolInput);\n if (!query) return null;\n\n debug('hook', 'PreToolUse searching', { tool: toolName, query });\n\n const lines: string[] = [];\n\n // 1. FTS5 search for relevant observations\n try {\n const search = new SearchEngine(db, projectHash);\n const results = search.searchKeyword(query, { limit: 3 });\n for (const result of results) {\n const snippet = result.snippet\n ? result.snippet.replace(/<\\/?mark>/g, '')\n : truncate(result.observation.content, 120);\n const age = formatAge(result.observation.created_at);\n lines.push(`- ${truncate(snippet, 120)} (${result.observation.source}, ${age})`);\n }\n } catch {\n debug('hook', 'PreToolUse FTS5 search failed');\n }\n\n // 2. Graph lookup for file entities\n try {\n if (toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') {\n const filePath = toolInput.file_path as string | undefined;\n if (filePath) {\n const node = getNodeByNameAndType(db, filePath, 'File');\n if (node) {\n const connected = traverseFrom(db, node.id, { depth: 1, direction: 'both' });\n if (connected.length > 0) {\n const names = connected\n .slice(0, 5)\n .map(r => `${r.node.name} (${r.node.type})`)\n .join(', ');\n lines.push(`Related: ${names}`);\n }\n }\n }\n }\n } catch {\n debug('hook', 'PreToolUse graph lookup failed');\n }\n\n // 3. Path recall for debugging context\n if (pathRepo) {\n try {\n const toolOutput = (toolInput.content as string) ?? (toolInput.command as string) ?? query ?? '';\n if (toolOutput.length > 20) {\n const similar = findSimilarPaths(pathRepo, toolOutput, 2);\n const recall = formatPathRecall(similar);\n if (recall) {\n lines.push(recall);\n }\n }\n } catch {\n debug('hook', 'PreToolUse path recall failed');\n }\n }\n\n if (lines.length === 0) return null;\n\n // Format as a compact context block — use basename for display (not the search stem)\n let target = query;\n if ((toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') && toolInput.file_path) {\n target = basename(toolInput.file_path as string);\n }\n const output = `[Laminark] Context for ${target}:\\n${lines.join('\\n')}\\n`;\n\n // Cap total output to ~500 chars\n if (output.length > 500) {\n return output.slice(0, 497) + '...\\n';\n }\n\n return output;\n}\n","/**\n * A routing suggestion produced by either the heuristic or learned tier.\n * Delivered to the user via NotificationStore as a tool recommendation.\n */\nexport interface RoutingSuggestion {\n /** The suggested tool name (e.g., \"mcp__playwright__browser_screenshot\"). */\n toolName: string;\n /** Optional description for display in the notification. */\n toolDescription: string | null;\n /** Confidence score from 0.0 to 1.0. */\n confidence: number;\n /** Which routing tier produced this suggestion. */\n tier: 'heuristic' | 'learned';\n /** Human-readable explanation for why this tool was suggested. */\n reason: string;\n}\n\n/**\n * Configuration for the conversation routing system.\n * Controls thresholds, rate limits, and pattern matching parameters.\n */\nexport interface RoutingConfig {\n /** Minimum confidence score to emit a suggestion (0.0-1.0). */\n confidenceThreshold: number;\n /** Maximum number of suggestions per session (rate limit). */\n maxSuggestionsPerSession: number;\n /** Minimum tool_usage_events before learned patterns activate. */\n minEventsForLearned: number;\n /** Minimum tool calls between suggestions (cooldown). */\n suggestionCooldown: number;\n /** Minimum tool calls before the first suggestion in a session. */\n minCallsBeforeFirstSuggestion: number;\n /** Sliding window size for learned pattern extraction. */\n patternWindowSize: number;\n}\n\n/**\n * Per-session routing state persisted in SQLite (mirrors routing_state table).\n * Tracks suggestion counts and cooldown counters across handler invocations.\n */\nexport interface RoutingState {\n /** The current session identifier. */\n sessionId: string;\n /** Hash of the project directory. */\n projectHash: string;\n /** Number of suggestions emitted this session. */\n suggestionsMade: number;\n /** ISO timestamp of the last suggestion, or null if none yet. */\n lastSuggestionAt: string | null;\n /** Number of tool calls since the last suggestion was emitted. */\n toolCallsSinceSuggestion: number;\n}\n\n/**\n * A learned tool usage pattern extracted from historical tool_usage_events.\n * Represents a recurring sequence of tool calls that precedes a target tool.\n */\nexport interface ToolPattern {\n /** The tool that was eventually used after the preceding sequence. */\n targetTool: string;\n /** Tools used in the N calls before the target tool. */\n precedingTools: string[];\n /** How many times this exact pattern occurred in history. */\n frequency: number;\n}\n\n/**\n * Default routing configuration values.\n * Threshold and rate limits tuned to avoid over-suggestion (Clippy problem).\n */\nexport const DEFAULT_ROUTING_CONFIG: RoutingConfig = {\n confidenceThreshold: 0.6,\n maxSuggestionsPerSession: 2,\n minEventsForLearned: 20,\n suggestionCooldown: 5,\n minCallsBeforeFirstSuggestion: 3,\n patternWindowSize: 5,\n};\n","import type { ToolRegistryRow } from '../shared/tool-types.js';\nimport type { RoutingSuggestion } from './types.js';\n\n/**\n * Stop words filtered from keyword extraction.\n * Common English function words that carry no discriminative signal for tool matching.\n */\nconst STOP_WORDS = new Set([\n 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',\n 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',\n 'would', 'could', 'should', 'may', 'might', 'can', 'shall',\n 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from',\n 'as', 'into', 'through', 'and', 'but', 'or', 'nor', 'not',\n 'so', 'yet', 'this', 'that', 'these', 'those', 'it', 'its',\n]);\n\n/**\n * Tokenizes text into lowercase keywords for matching.\n *\n * Replaces non-alphanumeric characters (except hyphens and underscores) with spaces,\n * splits on whitespace, filters words shorter than 3 characters and stop words,\n * and returns unique keywords.\n */\nexport function extractKeywords(text: string): string[] {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s\\-_]/g, ' ')\n .split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w));\n\n return [...new Set(words)];\n}\n\n/**\n * Extracts keywords from a tool's description, server name, and parsed name.\n *\n * - Description text is tokenized via extractKeywords\n * - Server name is added as a keyword (lowercase)\n * - Slash commands are parsed by splitting on `:`, `-`, `_`\n * - Skills are parsed by splitting on `-` and `_`\n *\n * Returns a deduplicated array of keywords.\n */\nexport function extractToolKeywords(tool: ToolRegistryRow): string[] {\n const sources: string[] = [];\n\n // Description keywords (highest value)\n if (tool.description) {\n sources.push(...extractKeywords(tool.description));\n }\n\n // Server name as keyword (e.g., \"playwright\", \"github\")\n if (tool.server_name) {\n sources.push(tool.server_name.toLowerCase());\n }\n\n // Parse slash command path (e.g., \"/gsd:plan-phase\" -> [\"gsd\", \"plan\", \"phase\"])\n if (tool.tool_type === 'slash_command') {\n const parts = tool.name\n .replace(/^\\//, '')\n .split(/[:\\-_]/)\n .filter(p => p.length > 0);\n sources.push(...parts.map(p => p.toLowerCase()));\n }\n\n // Skill name keywords (e.g., \"debug-memory\" -> [\"debug\", \"memory\"])\n if (tool.tool_type === 'skill') {\n const parts = tool.name\n .split(/[\\-_]/)\n .filter(p => p.length > 0);\n sources.push(...parts.map(p => p.toLowerCase()));\n }\n\n return [...new Set(sources)];\n}\n\n/**\n * Evaluates heuristic keyword matching between recent observations and available tools.\n *\n * This is the cold-start routing tier (ROUT-04). It works with zero accumulated usage\n * history by matching keywords from recent session observations against tool descriptions\n * and names.\n *\n * Returns the highest-confidence match above the threshold, or null if no match qualifies.\n *\n * @param recentObservations - Recent observation content strings from the current session\n * @param suggestableTools - Scope-filtered, non-builtin, non-Laminark tools\n * @param confidenceThreshold - Minimum score to return a suggestion (0.0-1.0)\n */\nexport function evaluateHeuristic(\n recentObservations: string[],\n suggestableTools: ToolRegistryRow[],\n confidenceThreshold: number,\n): RoutingSuggestion | null {\n // Too early to judge intent from fewer than 2 observations\n if (recentObservations.length < 2) return null;\n\n // Build context keyword set from all recent observations\n const contextKeywords = new Set(\n recentObservations.flatMap(obs => extractKeywords(obs)),\n );\n\n if (contextKeywords.size === 0) return null;\n\n let bestMatch: { tool: ToolRegistryRow; score: number } | null = null;\n\n for (const tool of suggestableTools) {\n const toolKeywords = extractToolKeywords(tool);\n if (toolKeywords.length === 0) continue;\n\n const matchCount = toolKeywords.filter(kw => contextKeywords.has(kw)).length;\n const score = matchCount / toolKeywords.length;\n\n if (score > (bestMatch?.score ?? 0)) {\n bestMatch = { tool, score };\n }\n }\n\n if (!bestMatch || bestMatch.score < confidenceThreshold) return null;\n\n return {\n toolName: bestMatch.tool.name,\n toolDescription: bestMatch.tool.description,\n confidence: bestMatch.score,\n tier: 'heuristic',\n reason: 'Keywords match between current work and tool description',\n };\n}\n","import type BetterSqlite3 from 'better-sqlite3';\n\nimport type { RoutingConfig, RoutingSuggestion } from './types.js';\nimport { DEFAULT_ROUTING_CONFIG } from './types.js';\nimport { evaluateLearnedPatterns } from './intent-patterns.js';\nimport { evaluateHeuristic } from './heuristic-fallback.js';\nimport { inferToolType } from '../hooks/tool-name-parser.js';\nimport { isLaminarksOwnTool } from '../hooks/self-referential.js';\nimport { NotificationStore } from '../storage/notifications.js';\nimport type { ToolRegistryRepository } from '../storage/tool-registry.js';\nimport type { ToolRegistryRow } from '../shared/tool-types.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * ConversationRouter orchestrates tool suggestion routing.\n *\n * Combines two tiers of suggestion:\n * - Learned patterns: historical tool sequence matching (ROUT-01)\n * - Heuristic fallback: keyword-based cold-start matching (ROUT-04)\n *\n * Suggestions are gated by confidence threshold (ROUT-03) and rate limits,\n * then delivered via NotificationStore (ROUT-02).\n *\n * Instantiated per-evaluation in the PostToolUse handler. No long-lived state --\n * state persists across invocations via the routing_state SQLite table.\n */\nexport class ConversationRouter {\n private readonly db: BetterSqlite3.Database;\n private readonly projectHash: string;\n private readonly config: RoutingConfig;\n\n constructor(\n db: BetterSqlite3.Database,\n projectHash: string,\n config?: Partial<RoutingConfig>,\n ) {\n this.db = db;\n this.projectHash = projectHash;\n this.config = { ...DEFAULT_ROUTING_CONFIG, ...config };\n\n // Create routing_state table inline (transient, no migration)\n db.exec(`\n CREATE TABLE IF NOT EXISTS routing_state (\n session_id TEXT NOT NULL,\n project_hash TEXT NOT NULL,\n suggestions_made INTEGER NOT NULL DEFAULT 0,\n last_suggestion_at TEXT,\n tool_calls_since_suggestion INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (session_id, project_hash)\n )\n `);\n }\n\n /**\n * Evaluates whether a tool suggestion should be surfaced for the current context.\n *\n * Called from PostToolUse handler after observation storage.\n * Runs AFTER the self-referential filter -- never evaluates Laminark's own tools.\n *\n * The entire method is wrapped in try/catch -- routing is supplementary\n * and must NEVER block or fail the core handler pipeline.\n *\n * @param sessionId - Current session identifier\n * @param toolName - The tool just used\n * @param toolRegistry - Tool registry for availability checking\n */\n evaluate(sessionId: string, toolName: string, toolRegistry: ToolRegistryRepository): void {\n try {\n this._evaluate(sessionId, toolName, toolRegistry);\n } catch (err) {\n // Routing is supplementary -- never block core pipeline\n debug('routing', 'Routing evaluation failed (non-fatal)', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private _evaluate(sessionId: string, toolName: string, toolRegistry: ToolRegistryRepository): void {\n // 1. Skip built-in tools\n if (inferToolType(toolName) === 'builtin') return;\n\n // 2. Skip Laminark's own tools\n if (isLaminarksOwnTool(toolName)) return;\n\n // 3. Load or create routing state for this session\n const state = this.getOrCreateState(sessionId);\n\n // 4. Increment tool_calls_since_suggestion and update state\n state.toolCallsSinceSuggestion++;\n this.updateState(sessionId, state);\n\n // 5. Check rate limits\n if (state.suggestionsMade >= this.config.maxSuggestionsPerSession) {\n debug('routing', 'Rate limited: max suggestions reached', { sessionId, made: state.suggestionsMade });\n return;\n }\n if (state.toolCallsSinceSuggestion < this.config.suggestionCooldown) {\n debug('routing', 'Rate limited: cooldown active', {\n sessionId, callsSince: state.toolCallsSinceSuggestion, cooldown: this.config.suggestionCooldown,\n });\n return;\n }\n\n // Check total tool calls this session (must have enough before first suggestion)\n const totalCalls = this.getTotalCallsForSession(sessionId);\n if (totalCalls < this.config.minCallsBeforeFirstSuggestion) {\n debug('routing', 'Too early: not enough tool calls', { sessionId, totalCalls });\n return;\n }\n\n // 6. Get available tools (scope-filtered)\n const availableTools = toolRegistry.getAvailableForSession(this.projectHash);\n\n // 7. Filter to suggestable: exclude built-in, Laminark, and stale/demoted tools\n const suggestableTools = availableTools.filter(\n (t: ToolRegistryRow) =>\n t.tool_type !== 'builtin' &&\n !isLaminarksOwnTool(t.name) &&\n t.status === 'active', // STAL: Only suggest tools in good standing\n );\n\n // 8. If no suggestable tools: return\n if (suggestableTools.length === 0) return;\n\n const suggestableNames = new Set(suggestableTools.map((t: ToolRegistryRow) => t.name));\n\n // 9. Try learned patterns first (if enough historical data)\n let suggestion: RoutingSuggestion | null = null;\n\n const eventCount = this.countRecentEvents();\n if (eventCount >= this.config.minEventsForLearned) {\n suggestion = evaluateLearnedPatterns(\n this.db,\n sessionId,\n this.projectHash,\n suggestableNames,\n this.config.confidenceThreshold,\n );\n }\n\n // 10. Fall back to heuristic if no learned suggestion\n if (!suggestion) {\n const recentObservations = this.getRecentObservations(sessionId);\n suggestion = evaluateHeuristic(recentObservations, suggestableTools, this.config.confidenceThreshold);\n }\n\n // 11. If no suggestion from either tier: return\n if (!suggestion) return;\n\n // 12. Confidence gate (belt-and-suspenders -- tiers already check, but guard here too)\n if (suggestion.confidence < this.config.confidenceThreshold) return;\n\n // 13. Deliver via NotificationStore\n const notifStore = new NotificationStore(this.db);\n const description = suggestion.toolDescription ? ` -- ${suggestion.toolDescription}` : '';\n const usageHint = suggestion.tier === 'learned' ? ` (${suggestion.reason})` : '';\n const message = `Tool suggestion: ${suggestion.toolName}${description}${usageHint}`;\n notifStore.add(this.projectHash, message);\n\n debug('routing', 'Suggestion delivered', {\n tool: suggestion.toolName,\n tier: suggestion.tier,\n confidence: suggestion.confidence,\n });\n\n // 14. Update routing state: increment suggestions_made, reset cooldown\n state.suggestionsMade++;\n state.lastSuggestionAt = new Date().toISOString();\n state.toolCallsSinceSuggestion = 0;\n this.updateState(sessionId, state);\n }\n\n /**\n * Gets or creates routing state for a session.\n */\n private getOrCreateState(sessionId: string): {\n suggestionsMade: number;\n lastSuggestionAt: string | null;\n toolCallsSinceSuggestion: number;\n } {\n const row = this.db.prepare(`\n SELECT suggestions_made, last_suggestion_at, tool_calls_since_suggestion\n FROM routing_state\n WHERE session_id = ? AND project_hash = ?\n `).get(sessionId, this.projectHash) as {\n suggestions_made: number;\n last_suggestion_at: string | null;\n tool_calls_since_suggestion: number;\n } | undefined;\n\n if (row) {\n return {\n suggestionsMade: row.suggestions_made,\n lastSuggestionAt: row.last_suggestion_at,\n toolCallsSinceSuggestion: row.tool_calls_since_suggestion,\n };\n }\n\n // Create new state row\n this.db.prepare(`\n INSERT INTO routing_state (session_id, project_hash, suggestions_made, tool_calls_since_suggestion)\n VALUES (?, ?, 0, 0)\n `).run(sessionId, this.projectHash);\n\n return {\n suggestionsMade: 0,\n lastSuggestionAt: null,\n toolCallsSinceSuggestion: 0,\n };\n }\n\n /**\n * Updates routing state in the database.\n */\n private updateState(\n sessionId: string,\n state: {\n suggestionsMade: number;\n lastSuggestionAt: string | null;\n toolCallsSinceSuggestion: number;\n },\n ): void {\n this.db.prepare(`\n UPDATE routing_state\n SET suggestions_made = ?, last_suggestion_at = ?, tool_calls_since_suggestion = ?\n WHERE session_id = ? AND project_hash = ?\n `).run(\n state.suggestionsMade,\n state.lastSuggestionAt,\n state.toolCallsSinceSuggestion,\n sessionId,\n this.projectHash,\n );\n }\n\n /**\n * Returns total tool calls for the current session (from routing_state).\n */\n private getTotalCallsForSession(sessionId: string): number {\n const row = this.db.prepare(`\n SELECT COUNT(*) as count FROM tool_usage_events\n WHERE session_id = ? AND project_hash = ?\n `).get(sessionId, this.projectHash) as { count: number };\n return row.count;\n }\n\n /**\n * Counts total tool_usage_events for this project (for learned pattern threshold).\n */\n private countRecentEvents(): number {\n const row = this.db.prepare(`\n SELECT COUNT(*) as count FROM tool_usage_events WHERE project_hash = ?\n `).get(this.projectHash) as { count: number };\n return row.count;\n }\n\n /**\n * Gets recent observation content strings for heuristic matching.\n */\n private getRecentObservations(sessionId: string): string[] {\n const rows = this.db.prepare(`\n SELECT content FROM observations\n WHERE project_hash = ? AND session_id = ? AND deleted_at IS NULL\n ORDER BY created_at DESC\n LIMIT 5\n `).all(this.projectHash, sessionId) as Array<{ content: string }>;\n return rows.map(r => r.content);\n }\n}\n","import { openDatabase } from '../storage/database.js';\nimport { getDatabaseConfig, getProjectHash } from '../shared/config.js';\nimport { ObservationRepository } from '../storage/observations.js';\nimport { ResearchBufferRepository } from '../storage/research-buffer.js';\nimport { SessionRepository } from '../storage/sessions.js';\nimport { extractObservation } from './capture.js';\nimport { handleSessionStart, handleSessionEnd, handleStop } from './session-lifecycle.js';\nimport { redactSensitiveContent, isExcludedFile } from './privacy-filter.js';\nimport { shouldAdmit, isMeaningfulBashCommand } from './admission-filter.js';\nimport { SaveGuard } from './save-guard.js';\nimport { isLaminarksOwnTool } from './self-referential.js';\nimport { handlePreToolUse } from './pre-tool-context.js';\nimport { ToolRegistryRepository } from '../storage/tool-registry.js';\nimport { ConversationRouter } from '../routing/conversation-router.js';\nimport { inferToolType, inferScope, extractServerName } from './tool-name-parser.js';\nimport { PathRepository } from '../paths/path-repository.js';\nimport { initPathSchema } from '../paths/schema.js';\nimport { debug } from '../shared/debug.js';\n\n/**\n * Hook handler entry point.\n *\n * This file is the CLI entry point for all Claude Code hook events.\n * It reads stdin JSON, opens a direct SQLite connection (no HTTP intermediary),\n * dispatches to the appropriate handler based on hook_event_name, and exits 0.\n *\n * CRITICAL CONSTRAINTS:\n * - Only SessionStart and PreToolUse write to stdout (synchronous hooks -- stdout is injected into Claude's context window)\n * - All other hooks NEVER write to stdout (stdout output is interpreted by Claude Code)\n * - ALWAYS exits 0 (non-zero exit codes surface as errors to Claude)\n * - Opens its own database connection (WAL mode handles concurrent access with MCP server)\n * - Imports only storage modules -- NO @modelcontextprotocol/sdk (cold start overhead)\n *\n * Filter pipeline (PostToolUse/PostToolUseFailure):\n * 0. Organic tool discovery (DISC-05: records ALL tools including Laminark's own)\n * 1. Self-referential filter (dual-prefix: mcp__laminark__ and mcp__plugin_laminark_laminark__)\n * 2. Extract observation text from payload\n * 3. Privacy filter: exclude sensitive files, redact secrets\n * 4. Admission filter: reject noise content\n * 5. Store to database\n */\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n return Buffer.concat(chunks).toString('utf-8');\n}\n\n/**\n * Tools that are routed to the research buffer instead of creating observations.\n * These are high-volume exploration tools whose individual calls are noise,\n * but whose targets provide useful provenance context for subsequent changes.\n */\nconst RESEARCH_TOOLS = new Set(['Read', 'Glob', 'Grep']);\n\n/**\n * Processes a PostToolUse or PostToolUseFailure event through the full\n * filter pipeline: route research tools -> extract -> privacy -> admission -> store.\n *\n * Exported for unit testing of the pipeline logic.\n */\nexport function processPostToolUseFiltered(\n input: Record<string, unknown>,\n obsRepo: ObservationRepository,\n researchBuffer?: ResearchBufferRepository,\n toolRegistry?: ToolRegistryRepository,\n projectHash?: string,\n db?: import('better-sqlite3').Database,\n): void {\n const toolName = input.tool_name as string | undefined;\n const hookEventName = input.hook_event_name as string | undefined;\n\n if (!toolName) {\n debug('hook', 'PostToolUse missing tool_name, skipping');\n return;\n }\n\n // 0. DISC-05: Organic tool discovery -- record every tool we see\n // This runs BEFORE the self-referential filter so Laminark's own tools are registered\n if (toolRegistry) {\n try {\n const sessionId = input.session_id as string | undefined;\n const isFailure = hookEventName === 'PostToolUseFailure';\n toolRegistry.recordOrCreate(toolName, {\n toolType: inferToolType(toolName),\n scope: inferScope(toolName),\n source: 'hook:PostToolUse',\n projectHash: projectHash ?? null,\n description: null,\n serverName: extractServerName(toolName),\n }, sessionId ?? null, !isFailure);\n\n // STAL-03: Failure-driven demotion / success restoration\n if (isFailure) {\n // Check sliding window: 3+ failures in last 5 events triggers demotion\n const recentEvents = toolRegistry.getRecentEventsForTool(\n toolName, projectHash ?? '', 5,\n );\n const failures = recentEvents.filter(e => e.success === 0).length;\n if (failures >= 3) {\n toolRegistry.markDemoted(toolName, projectHash ?? null);\n debug('hook', 'Tool demoted due to failures', { tool: toolName, failures });\n }\n } else {\n // Successful use restores demoted tools immediately\n toolRegistry.markActive(toolName, projectHash ?? null);\n }\n } catch {\n // Non-fatal: registry is supplementary to core memory function\n }\n }\n\n // 1. Skip self-referential capture (Laminark observing its own operations)\n if (isLaminarksOwnTool(toolName)) {\n debug('hook', 'Skipping self-referential tool', { tool: toolName });\n return;\n }\n\n // 2. Extract file path from tool_input (for file exclusion check)\n const toolInput = (input.tool_input as Record<string, unknown>) ?? {};\n const filePath = toolInput.file_path as string | undefined;\n\n // 3. Privacy filter: check file exclusion first\n if (filePath && isExcludedFile(filePath)) {\n debug('hook', 'Observation excluded (sensitive file)', { tool: toolName, filePath });\n return;\n }\n\n // 3.5. Route exploration tools to research buffer (not full observations)\n if (RESEARCH_TOOLS.has(toolName) && researchBuffer) {\n const target = String(toolInput.file_path ?? toolInput.pattern ?? '');\n researchBuffer.add({\n sessionId: (input.session_id as string) ?? null,\n toolName,\n target,\n });\n return;\n }\n\n // 3.6. Filter navigation Bash commands (only for success events)\n if (toolName === 'Bash' && hookEventName !== 'PostToolUseFailure') {\n const command = String(toolInput.command ?? '');\n if (!isMeaningfulBashCommand(command)) {\n debug('hook', 'Bash command filtered as navigation', { command: command.slice(0, 60) });\n return;\n }\n }\n\n // 4. Extract observation text from payload\n const payload = {\n session_id: input.session_id as string,\n cwd: input.cwd as string,\n hook_event_name: input.hook_event_name as string,\n tool_name: toolName,\n tool_input: toolInput,\n tool_response: input.tool_response as Record<string, unknown> | undefined,\n tool_use_id: input.tool_use_id as string | undefined,\n };\n\n const summary = extractObservation(payload);\n\n if (summary === null) {\n debug('hook', 'No observation extracted', { tool: toolName });\n return;\n }\n\n // 5. Privacy filter: redact sensitive content\n let redacted = redactSensitiveContent(summary, filePath);\n\n if (redacted === null) {\n debug('hook', 'Observation excluded by privacy filter', { tool: toolName });\n return;\n }\n\n // 5.5. Attach research context to Write/Edit observations\n if ((toolName === 'Write' || toolName === 'Edit') && researchBuffer && payload.session_id) {\n const research = researchBuffer.getRecent(payload.session_id, 5);\n if (research.length > 0) {\n const lines = research.map(r => ` - [${r.toolName}] ${r.target}`).join('\\n');\n redacted += `\\nResearch context:\\n${lines}`;\n }\n }\n\n // 6. Admission filter: reject noise\n if (!shouldAdmit(toolName, redacted)) {\n debug('hook', 'Observation rejected by admission filter', { tool: toolName });\n return;\n }\n\n // 6.5. Save guard: duplicate detection\n const guard = new SaveGuard(obsRepo);\n const decision = guard.evaluateSync(redacted, 'hook:' + toolName);\n if (!decision.save) {\n debug('hook', 'Observation rejected by save guard', {\n tool: toolName, reason: decision.reason, duplicateOf: decision.duplicateOf,\n });\n return;\n }\n\n // 7. Determine observation kind from tool type\n let kind = 'finding';\n if (toolName === 'Write' || toolName === 'Edit') {\n kind = 'change';\n } else if (toolName === 'WebFetch' || toolName === 'WebSearch') {\n kind = 'reference';\n } else if (toolName === 'Bash') {\n const command = String(toolInput.command ?? '');\n if (/^git\\s+(commit|push|merge|rebase|cherry-pick)\\b/.test(command.trim())) {\n kind = 'change';\n } else {\n kind = 'verification';\n }\n }\n\n // 8. Store the filtered, redacted observation\n obsRepo.create({\n content: redacted,\n source: 'hook:' + toolName,\n kind,\n sessionId: payload.session_id ?? null,\n });\n\n debug('hook', 'Captured observation', { tool: toolName, kind, length: redacted.length });\n\n // 9. ROUT-01/02/03/04: Routing evaluation (suggest tools based on conversation context)\n // Runs AFTER observation storage and self-referential filter\n if (db && toolRegistry && projectHash) {\n try {\n const sessionId = input.session_id as string | undefined;\n if (sessionId) {\n const router = new ConversationRouter(db, projectHash);\n router.evaluate(sessionId, toolName, toolRegistry);\n }\n } catch {\n // Routing is supplementary -- never block core pipeline\n }\n }\n}\n\nasync function main(): Promise<void> {\n const raw = await readStdin();\n const input = JSON.parse(raw) as Record<string, unknown>;\n\n const eventName = input.hook_event_name as string;\n const cwd = input.cwd as string;\n\n if (!eventName || !cwd) {\n debug('hook', 'Missing hook_event_name or cwd in input');\n return;\n }\n\n const projectHash = getProjectHash(cwd);\n\n debug('hook', 'Processing hook event', { eventName, projectHash });\n\n // Open database -- cheap with WAL mode (~2ms)\n const laminarkDb = openDatabase(getDatabaseConfig());\n\n try {\n const obsRepo = new ObservationRepository(laminarkDb.db, projectHash);\n const sessionRepo = new SessionRepository(laminarkDb.db, projectHash);\n let researchBuffer: ResearchBufferRepository | undefined;\n try {\n researchBuffer = new ResearchBufferRepository(laminarkDb.db, projectHash);\n } catch {\n // research_buffer table may not exist yet before migration 13\n }\n let toolRegistry: ToolRegistryRepository | undefined;\n try {\n toolRegistry = new ToolRegistryRepository(laminarkDb.db);\n } catch {\n // tool_registry table may not exist yet before migration 16\n }\n let pathRepo: PathRepository | undefined;\n try {\n initPathSchema(laminarkDb.db);\n pathRepo = new PathRepository(laminarkDb.db, projectHash);\n } catch {\n // debug_paths table may not exist yet\n }\n\n switch (eventName) {\n case 'PreToolUse': {\n const preContext = handlePreToolUse(input, laminarkDb.db, projectHash, pathRepo);\n if (preContext) process.stdout.write(preContext);\n break;\n }\n case 'PostToolUse':\n case 'PostToolUseFailure':\n processPostToolUseFiltered(input, obsRepo, researchBuffer, toolRegistry, projectHash, laminarkDb.db);\n break;\n case 'SessionStart': {\n const context = handleSessionStart(input, sessionRepo, laminarkDb.db, projectHash, toolRegistry, pathRepo);\n // SessionStart is synchronous -- stdout is injected into Claude's context window\n if (context) {\n process.stdout.write(context);\n }\n break;\n }\n case 'SessionEnd':\n handleSessionEnd(input, sessionRepo);\n break;\n case 'Stop':\n handleStop(input, obsRepo, sessionRepo);\n break;\n default:\n debug('hook', 'Unknown hook event', { eventName });\n break;\n }\n } finally {\n laminarkDb.close();\n }\n}\n\n// Wrap in .catch() -- hooks must NEVER fail. Always exit 0.\nmain().catch((err: Error) => {\n debug('hook', 'Hook handler error', { error: err.message });\n});\n"],"mappings":"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"}