all-hands-cli 0.1.0

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 (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,1442 @@
1
+ /**
2
+ * Context Hooks - TLDR-powered context injection
3
+ *
4
+ * PreToolUse and PostToolUse hooks that use TLDR daemon for
5
+ * token-efficient code analysis and context injection.
6
+ *
7
+ * All hooks gracefully degrade if TLDR is not installed.
8
+ */
9
+
10
+ import type { Command } from 'commander';
11
+ import { spawnSync } from 'child_process';
12
+ import { existsSync, statSync } from 'fs';
13
+ import {
14
+ HookInput,
15
+ HookCategory,
16
+ RegisterFn,
17
+ allowTool,
18
+ outputContext,
19
+ preToolContext,
20
+ injectContext,
21
+ getProjectDir,
22
+ SearchContext,
23
+ saveSearchContext,
24
+ loadSearchContext,
25
+ denyTool,
26
+ registerCategory,
27
+ registerCategoryForDaemon,
28
+ } from './shared.js';
29
+ import { logHookSuccess } from '../lib/trace-store.js';
30
+ import { sendNotification } from '../lib/notification.js';
31
+ import {
32
+ isTldrInstalled,
33
+ isTldrDaemonRunning,
34
+ contextDaemon,
35
+ cfgDaemon,
36
+ dfgDaemon,
37
+ archDaemon,
38
+ extractDaemon,
39
+ searchDaemon,
40
+ diagnosticsDaemon,
41
+ notifyFileChanged,
42
+ impactDaemon,
43
+ } from '../lib/tldr.js';
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // Hook Names
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ const HOOK_TLDR_INJECT = 'context tldr-inject';
50
+ const HOOK_EDIT_INJECT = 'context edit-inject';
51
+ const HOOK_ARCH_INJECT = 'context arch-inject';
52
+ const HOOK_SIGNATURE = 'context signature';
53
+ const HOOK_DIAGNOSTICS = 'context diagnostics';
54
+ const HOOK_IMPORT_VALIDATE = 'context import-validate';
55
+ const HOOK_EDIT_NOTIFY = 'context edit-notify';
56
+ const HOOK_READ_ENFORCER = 'context read-enforcer';
57
+ const HOOK_SEARCH_ROUTER = 'context search-router';
58
+ const HOOK_TRANSCRIPT_SAFEGUARD_PRE = 'context transcript-safeguard-pre';
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // Intent Detection
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ type AnalysisIntent =
65
+ | 'debug' // debug/investigate → Call Graph + CFG
66
+ | 'dataflow' // where does X come from → DFG
67
+ | 'slice' // what affects line Z → PDG/slice
68
+ | 'structure' // show structure → AST only
69
+ | 'arch' // plan/design/refactor → Architecture layers
70
+ | 'default'; // Default → Call Graph
71
+
72
+ /**
73
+ * Detect analysis intent from prompt content.
74
+ */
75
+ function detectIntent(prompt: string): AnalysisIntent {
76
+ const lower = prompt.toLowerCase();
77
+
78
+ // Debug/investigate patterns
79
+ if (
80
+ lower.includes('debug') ||
81
+ lower.includes('investigate') ||
82
+ lower.includes('trace') ||
83
+ lower.includes('why does') ||
84
+ lower.includes('how does')
85
+ ) {
86
+ return 'debug';
87
+ }
88
+
89
+ // Data flow patterns
90
+ if (
91
+ lower.includes('where does') ||
92
+ lower.includes('come from') ||
93
+ lower.includes('origin of') ||
94
+ lower.includes('source of') ||
95
+ lower.includes('data flow')
96
+ ) {
97
+ return 'dataflow';
98
+ }
99
+
100
+ // Slice/affect patterns
101
+ if (
102
+ lower.includes('what affects') ||
103
+ lower.includes('depends on') ||
104
+ lower.includes('impact of') ||
105
+ lower.includes('slice')
106
+ ) {
107
+ return 'slice';
108
+ }
109
+
110
+ // Structure patterns
111
+ if (
112
+ lower.includes('show structure') ||
113
+ lower.includes('list functions') ||
114
+ lower.includes('list classes') ||
115
+ lower.includes('symbols in')
116
+ ) {
117
+ return 'structure';
118
+ }
119
+
120
+ // Architecture patterns
121
+ if (
122
+ lower.includes('plan') ||
123
+ lower.includes('design') ||
124
+ lower.includes('refactor') ||
125
+ lower.includes('architecture') ||
126
+ lower.includes('overview')
127
+ ) {
128
+ return 'arch';
129
+ }
130
+
131
+ return 'default';
132
+ }
133
+
134
+ /**
135
+ * Extract function/symbol references from prompt.
136
+ */
137
+ function extractReferences(prompt: string): string[] {
138
+ const refs: string[] = [];
139
+
140
+ // Match function-like references: func(), Class.method(), etc.
141
+ const funcPattern = /\b([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\(/g;
142
+ let match;
143
+ while ((match = funcPattern.exec(prompt)) !== null) {
144
+ refs.push(match[1]);
145
+ }
146
+
147
+ // Match backtick references: `functionName`
148
+ const backtickPattern = /`([A-Za-z_][A-Za-z0-9_.]*)`/g;
149
+ while ((match = backtickPattern.exec(prompt)) !== null) {
150
+ refs.push(match[1]);
151
+ }
152
+
153
+ return Array.from(new Set(refs)); // Deduplicate
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // PreToolUse: tldr-context-inject (Task)
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Inject TLDR context for Task tool based on intent.
162
+ *
163
+ * Routes to different analysis layers:
164
+ * - debug → Call Graph + CFG
165
+ * - dataflow → DFG
166
+ * - arch → Architecture layers
167
+ * - default → Call Graph
168
+ */
169
+ function tldrContextInject(input: HookInput): void {
170
+ const projectDir = getProjectDir();
171
+
172
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
173
+ allowTool(HOOK_TLDR_INJECT);
174
+ }
175
+
176
+ const prompt = (input.tool_input?.prompt as string) || '';
177
+ if (!prompt) {
178
+ allowTool(HOOK_TLDR_INJECT);
179
+ }
180
+
181
+ const intent = detectIntent(prompt);
182
+ const refs = extractReferences(prompt);
183
+ const contextParts: string[] = [];
184
+
185
+ switch (intent) {
186
+ case 'debug': {
187
+ // Call graph + CFG for referenced functions
188
+ for (const ref of refs.slice(0, 3)) {
189
+ const ctx = contextDaemon(ref, projectDir);
190
+ if (ctx) {
191
+ contextParts.push(`## Call Graph: ${ref}`);
192
+ contextParts.push(`Callers: ${ctx.callers.join(', ') || 'none'}`);
193
+ contextParts.push(`Callees: ${ctx.callees.join(', ') || 'none'}`);
194
+
195
+ // Try to get CFG if we can identify the file
196
+ const searchResults = searchDaemon(ref, projectDir);
197
+ if (searchResults.length > 0) {
198
+ const cfg = cfgDaemon(searchResults[0].file, ref, projectDir);
199
+ if (cfg) {
200
+ contextParts.push(`\n### Control Flow (${ref})`);
201
+ contextParts.push(`Nodes: ${cfg.nodes.length}, Edges: ${cfg.edges.length}`);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ break;
207
+ }
208
+
209
+ case 'dataflow': {
210
+ // DFG for referenced functions
211
+ for (const ref of refs.slice(0, 3)) {
212
+ const searchResults = searchDaemon(ref, projectDir);
213
+ if (searchResults.length > 0) {
214
+ const dfg = dfgDaemon(searchResults[0].file, ref, projectDir);
215
+ if (dfg) {
216
+ contextParts.push(`## Data Flow: ${ref}`);
217
+ for (const v of dfg.variables.slice(0, 5)) {
218
+ contextParts.push(`- ${v.name}: defined at ${v.definitions.join(',')}, used at ${v.uses.join(',')}`);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ break;
224
+ }
225
+
226
+ case 'arch': {
227
+ const arch = archDaemon(projectDir);
228
+ if (arch) {
229
+ contextParts.push('## Architecture Layers');
230
+ for (const layer of arch.layers) {
231
+ contextParts.push(`\n### ${layer.name}`);
232
+ if (layer.description) {
233
+ contextParts.push(layer.description);
234
+ }
235
+ contextParts.push(`Files: ${layer.files.slice(0, 10).join(', ')}${layer.files.length > 10 ? '...' : ''}`);
236
+ }
237
+ }
238
+ break;
239
+ }
240
+
241
+ case 'structure': {
242
+ // Extract symbols from referenced files
243
+ for (const ref of refs.slice(0, 3)) {
244
+ const searchResults = searchDaemon(ref, projectDir);
245
+ if (searchResults.length > 0) {
246
+ const extract = extractDaemon(searchResults[0].file, projectDir);
247
+ if (extract) {
248
+ contextParts.push(`## Structure: ${searchResults[0].file}`);
249
+ // Show classes first
250
+ for (const cls of (extract.classes || []).slice(0, 5)) {
251
+ contextParts.push(`- class ${cls.name} (line ${cls.line_number})`);
252
+ }
253
+ // Then functions
254
+ for (const fn of extract.functions.slice(0, 10)) {
255
+ contextParts.push(`- function ${fn.name} (line ${fn.line_number})`);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ break;
261
+ }
262
+
263
+ default: {
264
+ // Default: Call graph for referenced functions
265
+ for (const ref of refs.slice(0, 3)) {
266
+ const ctx = contextDaemon(ref, projectDir);
267
+ if (ctx) {
268
+ contextParts.push(`## ${ref}`);
269
+ contextParts.push(`Callers: ${ctx.callers.join(', ') || 'none'}`);
270
+ contextParts.push(`Callees: ${ctx.callees.join(', ') || 'none'}`);
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ if (contextParts.length > 0) {
277
+ // Inject context into the Task prompt using updatedInput (like Continuous-Claude-v3)
278
+ injectContext(
279
+ input.tool_input as Record<string, unknown>,
280
+ `# TLDR Analysis (${intent})\n\n${contextParts.join('\n')}`,
281
+ 'prompt',
282
+ HOOK_TLDR_INJECT
283
+ );
284
+ }
285
+
286
+ allowTool(HOOK_TLDR_INJECT);
287
+ }
288
+
289
+ // ─────────────────────────────────────────────────────────────────────────────
290
+ // PreToolUse: edit-context-inject (Edit)
291
+ // ─────────────────────────────────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Inject file structure context before edits.
295
+ */
296
+ function editContextInject(input: HookInput): void {
297
+ const projectDir = getProjectDir();
298
+
299
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
300
+ allowTool(HOOK_EDIT_INJECT);
301
+ }
302
+
303
+ const filePath = (input.tool_input?.file_path as string) || '';
304
+ if (!filePath) {
305
+ allowTool(HOOK_EDIT_INJECT);
306
+ }
307
+
308
+ const extract = extractDaemon(filePath, projectDir);
309
+ if (!extract || extract.functions.length === 0) {
310
+ allowTool(HOOK_EDIT_INJECT);
311
+ }
312
+
313
+ const contextParts: string[] = ['## File Structure', ''];
314
+
315
+ // Classes come from separate array in extract
316
+ const classes = extract!.classes || [];
317
+ const functions = extract!.functions || [];
318
+
319
+ if (classes.length > 0) {
320
+ contextParts.push('### Classes');
321
+ for (const c of classes) {
322
+ contextParts.push(`- ${c.name} (line ${c.line_number})`);
323
+ }
324
+ }
325
+
326
+ if (functions.length > 0) {
327
+ contextParts.push('\n### Functions');
328
+ for (const f of functions.slice(0, 15)) {
329
+ const sig = f.signature ? `: ${f.signature}` : '';
330
+ contextParts.push(`- ${f.name}${sig} (line ${f.line_number})`);
331
+ }
332
+ if (functions.length > 15) {
333
+ contextParts.push(`... and ${functions.length - 15} more`);
334
+ }
335
+ }
336
+
337
+ if (extract!.imports.length > 0) {
338
+ contextParts.push('\n### Imports');
339
+ contextParts.push(extract!.imports.slice(0, 10).map(i => i.module).join(', '));
340
+ }
341
+
342
+ preToolContext(contextParts.join('\n'), HOOK_EDIT_INJECT);
343
+ }
344
+
345
+ // ─────────────────────────────────────────────────────────────────────────────
346
+ // PreToolUse: arch-context-inject (Task for planning)
347
+ // ─────────────────────────────────────────────────────────────────────────────
348
+
349
+ /**
350
+ * Inject architecture layers for planning tasks.
351
+ */
352
+ function archContextInject(input: HookInput): void {
353
+ const projectDir = getProjectDir();
354
+
355
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
356
+ allowTool(HOOK_ARCH_INJECT);
357
+ }
358
+
359
+ const prompt = (input.tool_input?.prompt as string) || '';
360
+
361
+ // Only inject for planning-related prompts
362
+ const lower = prompt.toLowerCase();
363
+ if (
364
+ !lower.includes('plan') &&
365
+ !lower.includes('design') &&
366
+ !lower.includes('architect') &&
367
+ !lower.includes('overview') &&
368
+ !lower.includes('structure')
369
+ ) {
370
+ allowTool(HOOK_ARCH_INJECT);
371
+ }
372
+
373
+ const arch = archDaemon(projectDir);
374
+ if (!arch) {
375
+ allowTool(HOOK_ARCH_INJECT);
376
+ }
377
+
378
+ const contextParts: string[] = ['## Project Architecture', ''];
379
+
380
+ for (const layer of arch!.layers) {
381
+ contextParts.push(`### ${layer.name}`);
382
+ if (layer.description) {
383
+ contextParts.push(layer.description);
384
+ }
385
+ contextParts.push(`Files: ${layer.files.slice(0, 8).join(', ')}${layer.files.length > 8 ? '...' : ''}`);
386
+ contextParts.push('');
387
+ }
388
+
389
+ if (arch!.dependencies.length > 0) {
390
+ contextParts.push('### Layer Dependencies');
391
+ for (const dep of arch!.dependencies.slice(0, 10)) {
392
+ contextParts.push(`- ${dep.from} -> ${dep.to}`);
393
+ }
394
+ }
395
+
396
+ preToolContext(contextParts.join('\n'), HOOK_ARCH_INJECT);
397
+ }
398
+
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+ // PreToolUse: signature-helper (Edit)
401
+ // ─────────────────────────────────────────────────────────────────────────────
402
+
403
+ // Keywords and builtins to skip - no point searching for these
404
+ const SIGNATURE_SKIP_NAMES = new Set([
405
+ // Python keywords/builtins
406
+ 'if', 'for', 'while', 'with', 'except', 'match', 'case', 'assert',
407
+ 'print', 'len', 'str', 'int', 'list', 'dict', 'set', 'tuple', 'bool', 'float',
408
+ 'range', 'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed',
409
+ 'type', 'isinstance', 'hasattr', 'getattr', 'setattr', 'super', 'object',
410
+ 'open', 'input', 'any', 'all', 'min', 'max', 'sum', 'abs', 'round', 'repr',
411
+ // JS/TS keywords/builtins
412
+ 'require', 'import', 'export', 'return', 'const', 'let', 'var',
413
+ 'function', 'async', 'await', 'new', 'this', 'class', 'extends', 'typeof',
414
+ 'console', 'log', 'warn', 'error', 'info', 'debug',
415
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'Map', 'Set',
416
+ 'JSON', 'parse', 'stringify', 'Math', 'Date', 'Error', 'RegExp',
417
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
418
+ 'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'concat', 'join',
419
+ 'split', 'trim', 'replace', 'match', 'test', 'exec', 'find', 'includes',
420
+ 'forEach', 'filter', 'reduce', 'some', 'every', 'keys', 'values', 'entries',
421
+ ]);
422
+
423
+ /**
424
+ * Extract function calls from code, filtering out keywords/builtins.
425
+ */
426
+ function extractFunctionCalls(code: string): string[] {
427
+ const callRe = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
428
+ const calls = new Set<string>();
429
+ let match;
430
+ while ((match = callRe.exec(code)) !== null) {
431
+ const name = match[1];
432
+ if (!SIGNATURE_SKIP_NAMES.has(name) && !SIGNATURE_SKIP_NAMES.has(name.toLowerCase())) {
433
+ calls.add(name);
434
+ }
435
+ }
436
+ return Array.from(calls);
437
+ }
438
+
439
+ /**
440
+ * Get the search pattern for a function based on file language.
441
+ */
442
+ function getSearchPattern(funcName: string, filePath: string): string {
443
+ const ext = filePath.split('.').pop()?.toLowerCase() || '';
444
+ if (ext === 'py') {
445
+ return `def ${funcName}`;
446
+ }
447
+ // For JS/TS, function declarations are more common
448
+ return `function ${funcName}`;
449
+ }
450
+
451
+ /**
452
+ * Inject function signatures for called functions in edit context.
453
+ *
454
+ * Optimized approach:
455
+ * - Skip upfront daemon checks (let calls fail gracefully)
456
+ * - Filter out builtins/keywords before any I/O
457
+ * - Use single search pattern based on file type
458
+ * - Early exit for tiny edits
459
+ */
460
+ function signatureHelper(input: HookInput): void {
461
+ const projectDir = getProjectDir();
462
+ const filePath = (input.tool_input?.file_path as string) || '';
463
+ const newString = (input.tool_input?.new_string as string) || '';
464
+
465
+ // Early exit for tiny edits (not worth the overhead)
466
+ if (!newString || newString.length < 15) {
467
+ allowTool(HOOK_SIGNATURE);
468
+ }
469
+
470
+ // Extract function calls, filtering out builtins
471
+ const calls = extractFunctionCalls(newString);
472
+ if (calls.length === 0) {
473
+ allowTool(HOOK_SIGNATURE);
474
+ }
475
+
476
+ const signatures: string[] = [];
477
+
478
+ // Limit to 3 calls for performance (was 5)
479
+ for (const call of calls.slice(0, 3)) {
480
+ // Use single search pattern based on file type
481
+ const pattern = getSearchPattern(call, filePath);
482
+ const searchResults = searchDaemon(pattern, projectDir);
483
+
484
+ if (searchResults.length === 0) continue;
485
+
486
+ const firstResult = searchResults[0];
487
+ const foundFile = firstResult.file.startsWith('/')
488
+ ? firstResult.file
489
+ : `${projectDir}/${firstResult.file}`;
490
+
491
+ // Extract symbols from the file to get signature
492
+ const extracted = extractDaemon(foundFile, projectDir);
493
+ if (!extracted) continue;
494
+
495
+ // Look for the function in extracted symbols
496
+ for (const func of extracted.functions || []) {
497
+ if (func.name === call || func.name === `async ${call}`) {
498
+ if (func.signature) {
499
+ signatures.push(`${call}: ${func.signature}`);
500
+ break;
501
+ }
502
+ }
503
+ }
504
+ }
505
+
506
+ if (signatures.length > 0) {
507
+ logHookSuccess(HOOK_SIGNATURE, { action: 'signature', count: signatures.length });
508
+ const output = {
509
+ hookSpecificOutput: {
510
+ hookEventName: 'PreToolUse',
511
+ additionalContext: `[Signatures from TLDR]\n${signatures.join('\n')}`,
512
+ },
513
+ };
514
+ console.log(JSON.stringify(output));
515
+ process.exit(0);
516
+ }
517
+
518
+ allowTool(HOOK_SIGNATURE);
519
+ }
520
+
521
+ // ─────────────────────────────────────────────────────────────────────────────
522
+ // PostToolUse: post-edit-diagnostics (Edit/Write)
523
+ // ─────────────────────────────────────────────────────────────────────────────
524
+
525
+ /**
526
+ * Run pyright+ruff diagnostics via TLDR daemon after edits.
527
+ */
528
+ function postEditDiagnostics(input: HookInput): void {
529
+ const projectDir = getProjectDir();
530
+
531
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
532
+ allowTool(HOOK_DIAGNOSTICS);
533
+ }
534
+
535
+ const filePath = (input.tool_input?.file_path as string) || '';
536
+ if (!filePath) {
537
+ allowTool(HOOK_DIAGNOSTICS);
538
+ }
539
+
540
+ // Only for Python files
541
+ if (!filePath.endsWith('.py')) {
542
+ allowTool(HOOK_DIAGNOSTICS);
543
+ }
544
+
545
+ const diag = diagnosticsDaemon(filePath, projectDir);
546
+ if (!diag || diag.errors.length === 0) {
547
+ allowTool(HOOK_DIAGNOSTICS);
548
+ }
549
+
550
+ const errors = diag!.errors.filter((e) => e.severity === 'error');
551
+ const warnings = diag!.errors.filter((e) => e.severity === 'warning');
552
+
553
+ if (errors.length === 0) {
554
+ allowTool(HOOK_DIAGNOSTICS);
555
+ }
556
+
557
+ const contextParts: string[] = ['## TLDR Diagnostics'];
558
+
559
+ if (errors.length > 0) {
560
+ contextParts.push('\n### Errors');
561
+ for (const e of errors.slice(0, 5)) {
562
+ contextParts.push(`- ${filePath}:${e.line}:${e.column} [${e.source}] ${e.message}`);
563
+ }
564
+ }
565
+
566
+ if (warnings.length > 0 && warnings.length <= 3) {
567
+ contextParts.push('\n### Warnings');
568
+ for (const w of warnings.slice(0, 3)) {
569
+ contextParts.push(`- ${filePath}:${w.line} [${w.source}] ${w.message}`);
570
+ }
571
+ }
572
+
573
+ contextParts.push('\nPlease fix these issues.');
574
+
575
+ outputContext(contextParts.join('\n'), HOOK_DIAGNOSTICS);
576
+ }
577
+
578
+ // ─────────────────────────────────────────────────────────────────────────────
579
+ // PostToolUse: import-validator (Write/Edit)
580
+ // ─────────────────────────────────────────────────────────────────────────────
581
+
582
+ /**
583
+ * Validate import paths against known symbols.
584
+ */
585
+ function importValidator(input: HookInput): void {
586
+ const projectDir = getProjectDir();
587
+
588
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
589
+ allowTool(HOOK_IMPORT_VALIDATE);
590
+ }
591
+
592
+ const filePath = (input.tool_input?.file_path as string) || '';
593
+ if (!filePath) {
594
+ allowTool(HOOK_IMPORT_VALIDATE);
595
+ }
596
+
597
+ // Only for Python files
598
+ if (!filePath.endsWith('.py')) {
599
+ allowTool(HOOK_IMPORT_VALIDATE);
600
+ }
601
+
602
+ const extract = extractDaemon(filePath, projectDir);
603
+ if (!extract || extract.imports.length === 0) {
604
+ allowTool(HOOK_IMPORT_VALIDATE);
605
+ }
606
+
607
+ const invalidImports: string[] = [];
608
+
609
+ // Python standard library modules to skip
610
+ const STDLIB_MODULES = new Set([
611
+ 'os', 'sys', 'typing', 'pathlib', 'json', 're', 'io', 'abc', 'collections',
612
+ 'itertools', 'functools', 'dataclasses', 'enum', 'datetime', 'time', 'math',
613
+ 'random', 'copy', 'logging', 'warnings', 'contextlib', 'inspect', 'types',
614
+ 'subprocess', 'shutil', 'tempfile', 'glob', 'fnmatch', 'hashlib', 'hmac',
615
+ 'secrets', 'struct', 'codecs', 'unicodedata', 'string', 'textwrap', 'difflib',
616
+ 'unittest', 'pytest', 'asyncio', 'concurrent', 'threading', 'multiprocessing',
617
+ 'socket', 'ssl', 'http', 'urllib', 'email', 'html', 'xml', 'base64', 'binascii',
618
+ 'pickle', 'shelve', 'sqlite3', 'csv', 'configparser', 'argparse', 'getopt',
619
+ 'pprint', 'reprlib', 'traceback', 'gc', 'weakref', 'array', 'bisect', 'heapq',
620
+ 'operator', 'decimal', 'fractions', 'statistics', 'cmath', 'numbers',
621
+ ]);
622
+
623
+ for (const imp of extract!.imports) {
624
+ const moduleName = imp.module;
625
+
626
+ // Skip standard library and common packages
627
+ // Module name is the root module (e.g., "os" from "os.path")
628
+ const rootModule = moduleName.split('.')[0];
629
+ if (STDLIB_MODULES.has(rootModule)) {
630
+ continue;
631
+ }
632
+
633
+ // Skip common third-party packages
634
+ const commonPackages = ['numpy', 'pandas', 'requests', 'flask', 'django', 'pytest', 'pydantic'];
635
+ if (commonPackages.includes(rootModule)) {
636
+ continue;
637
+ }
638
+
639
+ // Check if it's a local relative import (indicated by is_from with relative path)
640
+ if (imp.is_from && moduleName.startsWith('.')) {
641
+ // Try to find the module in the project
642
+ const searchPattern = moduleName.replace(/^\.+/, '');
643
+ if (searchPattern) {
644
+ const results = searchDaemon(searchPattern, projectDir);
645
+ if (results.length === 0) {
646
+ invalidImports.push(moduleName);
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ if (invalidImports.length > 0) {
653
+ outputContext(
654
+ `## Import Warnings\n\nThese imports may be invalid:\n${invalidImports.map((i) => `- ${i}`).join('\n')}`,
655
+ HOOK_IMPORT_VALIDATE
656
+ );
657
+ }
658
+
659
+ allowTool(HOOK_IMPORT_VALIDATE);
660
+ }
661
+
662
+ // ─────────────────────────────────────────────────────────────────────────────
663
+ // PostToolUse: edit-notify (Edit/Write)
664
+ // ─────────────────────────────────────────────────────────────────────────────
665
+
666
+ /**
667
+ * Notify TLDR daemon of file changes for incremental indexing.
668
+ */
669
+ async function editNotify(input: HookInput): Promise<void> {
670
+ const projectDir = getProjectDir();
671
+
672
+ if (!isTldrInstalled()) {
673
+ return allowTool(HOOK_EDIT_NOTIFY);
674
+ }
675
+
676
+ // Start daemon if not running (ensures dirty tracking works)
677
+ if (!isTldrDaemonRunning(projectDir)) {
678
+ try {
679
+ const { execSync } = await import('child_process');
680
+ execSync(`tldr daemon start --project "${projectDir}"`, {
681
+ stdio: 'ignore',
682
+ timeout: 5000,
683
+ });
684
+ // Give daemon a moment to start accepting connections
685
+ await new Promise((resolve) => setTimeout(resolve, 500));
686
+ } catch {
687
+ // Best effort - continue even if daemon start fails
688
+ }
689
+ }
690
+
691
+ const filePath = (input.tool_input?.file_path as string) || '';
692
+ if (!filePath) {
693
+ return allowTool(HOOK_EDIT_NOTIFY);
694
+ }
695
+
696
+ // Notify TLDR and capture response for observability
697
+ const response = await notifyFileChanged(projectDir, filePath);
698
+
699
+ // Log with dirty file tracking info from TLDR response
700
+ logHookSuccess(HOOK_EDIT_NOTIFY, {
701
+ action: 'notify',
702
+ file: filePath,
703
+ dirty_count: response?.dirty_count ?? null,
704
+ threshold: response?.threshold ?? null,
705
+ reindex_triggered: response?.reindex_triggered ?? false,
706
+ });
707
+
708
+ // Send system notification when reindex is triggered
709
+ if (response?.reindex_triggered) {
710
+ sendNotification({
711
+ title: 'TLDR Reindexing',
712
+ message: `Auto-reindex triggered (${response.dirty_count}/${response.threshold} dirty files)`,
713
+ type: 'banner',
714
+ });
715
+ }
716
+
717
+ allowTool();
718
+ }
719
+
720
+ // ─────────────────────────────────────────────────────────────────────────────
721
+ // PreToolUse: tldr-read-enforcer (Read)
722
+ // ─────────────────────────────────────────────────────────────────────────────
723
+
724
+ /** Code file extensions that benefit from TLDR analysis */
725
+ const CODE_EXTENSIONS = [
726
+ '.py', '.pyx', '.pyi', // Python
727
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', // JavaScript/TypeScript
728
+ '.go', // Go
729
+ '.rs', // Rust
730
+ '.java', '.kt', '.scala', // JVM
731
+ '.c', '.cpp', '.cc', '.h', '.hpp', // C/C++
732
+ '.rb', // Ruby
733
+ '.php', // PHP
734
+ ];
735
+
736
+ /** Minimum line count to trigger TLDR enforcement */
737
+ const MIN_LINES_FOR_TLDR = 100;
738
+
739
+ /**
740
+ * Count lines in a file (approximate, for quick check).
741
+ */
742
+ function countFileLines(filePath: string): number {
743
+ try {
744
+ const stats = statSync(filePath);
745
+ // Rough estimate: ~40 chars per line average for code
746
+ return Math.ceil(stats.size / 40);
747
+ } catch {
748
+ return 0;
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Check if file is a code file based on extension.
754
+ */
755
+ function isCodeFile(filePath: string): boolean {
756
+ const lower = filePath.toLowerCase();
757
+ return CODE_EXTENSIONS.some((ext) => lower.endsWith(ext));
758
+ }
759
+
760
+ /**
761
+ * TLDR Read Enforcer - intercepts Read on large code files.
762
+ *
763
+ * For large code files (>100 lines), returns TLDR AST/symbol context
764
+ * instead of raw file content, saving ~95% tokens.
765
+ *
766
+ * Bypass conditions:
767
+ * - File has offset/limit params (explicit partial read)
768
+ * - File is not a code file
769
+ * - File is small (<100 lines)
770
+ * - TLDR not installed/running
771
+ * - Recent search context suggests specific location
772
+ */
773
+ function tldrReadEnforcer(input: HookInput): void {
774
+ const projectDir = getProjectDir();
775
+ const sessionId = input.session_id || 'default';
776
+
777
+ // Always allow if TLDR not available
778
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
779
+ return allowTool(HOOK_READ_ENFORCER);
780
+ }
781
+
782
+ const filePath = (input.tool_input?.file_path as string) || '';
783
+ if (!filePath) {
784
+ return allowTool(HOOK_READ_ENFORCER);
785
+ }
786
+
787
+ // Bypass: explicit offset/limit means user wants specific lines
788
+ const offset = input.tool_input?.offset as number | undefined;
789
+ const limit = input.tool_input?.limit as number | undefined;
790
+ if (offset !== undefined || limit !== undefined) {
791
+ return allowTool(HOOK_READ_ENFORCER);
792
+ }
793
+
794
+ // Bypass: not a code file
795
+ if (!isCodeFile(filePath)) {
796
+ return allowTool(HOOK_READ_ENFORCER);
797
+ }
798
+
799
+ // Bypass: file doesn't exist
800
+ if (!existsSync(filePath)) {
801
+ return allowTool(HOOK_READ_ENFORCER);
802
+ }
803
+
804
+ // Bypass: small file
805
+ const lineCount = countFileLines(filePath);
806
+ if (lineCount < MIN_LINES_FOR_TLDR) {
807
+ return allowTool(HOOK_READ_ENFORCER);
808
+ }
809
+
810
+ // Check search context - if recent search targeted specific location, allow read
811
+ const searchCtx = loadSearchContext(sessionId);
812
+ if (searchCtx && searchCtx.definitionLocation) {
813
+ return allowTool(HOOK_READ_ENFORCER);
814
+ }
815
+
816
+ // Get TLDR extract for the file
817
+ const extract = extractDaemon(filePath, projectDir);
818
+ if (!extract || extract.functions.length === 0) {
819
+ return allowTool(HOOK_READ_ENFORCER);
820
+ }
821
+
822
+ // Build token-efficient summary instead of raw file
823
+ const parts: string[] = [
824
+ `## File: ${filePath}`,
825
+ `**Note**: This file has ~${lineCount} lines. Showing TLDR summary for token efficiency.`,
826
+ `To read specific lines, use \`offset\` and \`limit\` parameters.`,
827
+ '',
828
+ ];
829
+
830
+ // Imports
831
+ if (extract!.imports.length > 0) {
832
+ parts.push('### Imports');
833
+ parts.push('```');
834
+ for (const imp of extract!.imports.slice(0, 20)) {
835
+ parts.push(imp.module);
836
+ }
837
+ if (extract!.imports.length > 20) {
838
+ parts.push(`... and ${extract!.imports.length - 20} more imports`);
839
+ }
840
+ parts.push('```');
841
+ parts.push('');
842
+ }
843
+
844
+ // Classes (from separate array)
845
+ const classes = extract!.classes || [];
846
+ if (classes.length > 0) {
847
+ parts.push('### Classes');
848
+ for (const cls of classes) {
849
+ const doc = cls.docstring ? ` - ${cls.docstring.slice(0, 60)}...` : '';
850
+ parts.push(`- **${cls.name}** (line ${cls.line_number})${doc}`);
851
+ }
852
+ parts.push('');
853
+ }
854
+
855
+ // Functions
856
+ const functions = extract!.functions;
857
+ if (functions.length > 0) {
858
+ parts.push('### Functions');
859
+ for (const fn of functions.slice(0, 25)) {
860
+ const sig = fn.signature ? `\`${fn.signature}\`` : '';
861
+ parts.push(`- **${fn.name}** (line ${fn.line_number}) ${sig}`);
862
+ }
863
+ if (functions.length > 25) {
864
+ parts.push(`... and ${functions.length - 25} more functions`);
865
+ }
866
+ parts.push('');
867
+ }
868
+
869
+ parts.push('---');
870
+ parts.push('To read specific sections, use: `Read(file_path, offset=LINE, limit=COUNT)`');
871
+
872
+ // Put full TLDR summary in permissionDecisionReason so it's reliably shown
873
+ // (systemMessage isn't consistently displayed for denied tools)
874
+ denyTool(parts.join('\n'), HOOK_READ_ENFORCER);
875
+ }
876
+
877
+ // ─────────────────────────────────────────────────────────────────────────────
878
+ // PreToolUse: smart-search-router (Grep)
879
+ // ─────────────────────────────────────────────────────────────────────────────
880
+
881
+ type QueryType = 'structural' | 'semantic' | 'literal';
882
+ type TargetType = 'function' | 'class' | 'variable' | 'import' | 'decorator' | 'unknown';
883
+
884
+ /**
885
+ * Classify a search pattern to determine optimal routing.
886
+ */
887
+ function classifySearchPattern(pattern: string): { queryType: QueryType; targetType: TargetType } {
888
+ // Structural patterns (AST-level)
889
+ const structuralPatterns = [
890
+ { regex: /^(def|function|func)\s+\w+/, targetType: 'function' as TargetType },
891
+ { regex: /^(class|struct|interface)\s+\w+/, targetType: 'class' as TargetType },
892
+ { regex: /^(import|from|require|use)\s+/, targetType: 'import' as TargetType },
893
+ { regex: /^@\w+/, targetType: 'decorator' as TargetType },
894
+ { regex: /^\w+\s*=\s*/, targetType: 'variable' as TargetType },
895
+ ];
896
+
897
+ for (const { regex, targetType } of structuralPatterns) {
898
+ if (regex.test(pattern)) {
899
+ return { queryType: 'structural', targetType };
900
+ }
901
+ }
902
+
903
+ // Semantic patterns (natural language / concept search)
904
+ const semanticIndicators = [
905
+ 'where', 'how', 'what', 'find', 'search',
906
+ 'related', 'similar', 'like', 'about',
907
+ ];
908
+ const lowerPattern = pattern.toLowerCase();
909
+ if (semanticIndicators.some((ind) => lowerPattern.includes(ind))) {
910
+ return { queryType: 'semantic', targetType: 'unknown' };
911
+ }
912
+
913
+ // Check if it looks like an identifier (potential symbol search)
914
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(pattern)) {
915
+ // Single identifier - could be function/class name
916
+ if (pattern[0] === pattern[0].toUpperCase()) {
917
+ return { queryType: 'structural', targetType: 'class' };
918
+ }
919
+ return { queryType: 'structural', targetType: 'function' };
920
+ }
921
+
922
+ // Default to literal search
923
+ return { queryType: 'literal', targetType: 'unknown' };
924
+ }
925
+
926
+ /**
927
+ * Suggest TLDR layers based on query type and target.
928
+ */
929
+ function suggestLayers(queryType: QueryType, targetType: TargetType): string[] {
930
+ switch (queryType) {
931
+ case 'structural':
932
+ switch (targetType) {
933
+ case 'function':
934
+ return ['L1-AST', 'L2-CallGraph'];
935
+ case 'class':
936
+ return ['L1-AST'];
937
+ case 'import':
938
+ return ['L1-AST'];
939
+ case 'decorator':
940
+ return ['L1-AST'];
941
+ case 'variable':
942
+ return ['L1-AST', 'L4-DFG'];
943
+ default:
944
+ return ['L1-AST'];
945
+ }
946
+ case 'semantic':
947
+ return ['semantic-search'];
948
+ case 'literal':
949
+ default:
950
+ return ['ripgrep'];
951
+ }
952
+ }
953
+
954
+ // ─────────────────────────────────────────────────────────────────────────────
955
+ // AST-grep and Semantic Search Helpers
956
+ // ─────────────────────────────────────────────────────────────────────────────
957
+
958
+ interface AstGrepMatch {
959
+ file: string;
960
+ line: number;
961
+ column: number;
962
+ text: string;
963
+ lines: string;
964
+ language: string;
965
+ }
966
+
967
+ interface SemanticMatch {
968
+ name: string;
969
+ qualified_name: string;
970
+ file: string;
971
+ line: number;
972
+ unit_type: string;
973
+ signature: string;
974
+ score: number;
975
+ }
976
+
977
+ /**
978
+ * Try running ast-grep with the given pattern and path.
979
+ * Returns matches if successful, empty array if pattern is invalid or no matches.
980
+ */
981
+ function tryAstGrep(pattern: string, searchPath: string, projectDir: string): AstGrepMatch[] {
982
+ try {
983
+ // Resolve relative paths against project directory
984
+ let absolutePath: string;
985
+ if (searchPath.startsWith('/')) {
986
+ absolutePath = searchPath;
987
+ } else if (searchPath === '.') {
988
+ absolutePath = projectDir;
989
+ } else {
990
+ absolutePath = `${projectDir}/${searchPath}`;
991
+ }
992
+ // Clean up path (remove double slashes, trailing slashes)
993
+ absolutePath = absolutePath.replace(/\/+/g, '/').replace(/\/$/, '');
994
+
995
+ const result = spawnSync(
996
+ 'ast-grep',
997
+ ['run', '--pattern', pattern, '--json', '--no-ignore', 'hidden', absolutePath],
998
+ {
999
+ encoding: 'utf-8',
1000
+ timeout: 10000,
1001
+ maxBuffer: 1024 * 1024,
1002
+ cwd: projectDir,
1003
+ }
1004
+ );
1005
+
1006
+ if (result.status !== 0 || !result.stdout) {
1007
+ return [];
1008
+ }
1009
+
1010
+ const parsed = JSON.parse(result.stdout);
1011
+ if (!Array.isArray(parsed)) {
1012
+ return [];
1013
+ }
1014
+
1015
+ return parsed.map((match: Record<string, unknown>) => ({
1016
+ file: match.file as string,
1017
+ line: (match.range as { start: { line: number } })?.start?.line ?? 0,
1018
+ column: (match.range as { start: { column: number } })?.start?.column ?? 0,
1019
+ text: match.text as string,
1020
+ lines: match.lines as string,
1021
+ language: match.language as string,
1022
+ }));
1023
+ } catch {
1024
+ return [];
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Try running tldr semantic search with the given query.
1030
+ * Returns matches if successful, empty array if unavailable or no matches.
1031
+ */
1032
+ function trySemanticSearch(query: string, projectDir: string, limit: number = 10): SemanticMatch[] {
1033
+ try {
1034
+ const result = spawnSync(
1035
+ 'tldr',
1036
+ ['semantic', 'search', query, '--path', projectDir, '--k', String(limit)],
1037
+ {
1038
+ encoding: 'utf-8',
1039
+ timeout: 15000,
1040
+ maxBuffer: 1024 * 1024,
1041
+ }
1042
+ );
1043
+
1044
+ if (result.status !== 0 || !result.stdout) {
1045
+ return [];
1046
+ }
1047
+
1048
+ const parsed = JSON.parse(result.stdout);
1049
+ if (!Array.isArray(parsed)) {
1050
+ return [];
1051
+ }
1052
+
1053
+ return parsed as SemanticMatch[];
1054
+ } catch {
1055
+ return [];
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Format ast-grep matches for display.
1061
+ */
1062
+ function formatAstGrepResults(matches: AstGrepMatch[], pattern: string): string {
1063
+ const lines = matches.slice(0, 15).map((m) => {
1064
+ const lineNum = m.line + 1; // ast-grep uses 0-indexed lines
1065
+ return ` ${m.file}:${lineNum} - ${m.lines.trim().substring(0, 100)}`;
1066
+ });
1067
+
1068
+ return `🎯 **AST-grep Results** for \`${pattern}\`:
1069
+
1070
+ ${lines.join('\n')}
1071
+
1072
+ To get more context on a result:
1073
+ \`\`\`bash
1074
+ tldr context <function_name> --project .
1075
+ \`\`\`
1076
+
1077
+ If results are wrong, try manually:
1078
+ \`\`\`bash
1079
+ ast-grep run --pattern "${pattern}" <path>
1080
+ \`\`\``;
1081
+ }
1082
+
1083
+ /**
1084
+ * Format semantic search matches for display.
1085
+ */
1086
+ function formatSemanticResults(matches: SemanticMatch[], query: string): string {
1087
+ const lines = matches.slice(0, 10).map((m) => {
1088
+ const score = (m.score * 100).toFixed(0);
1089
+ return ` ${m.file}:${m.line} - ${m.name} (${m.unit_type}, ${score}% match)`;
1090
+ });
1091
+
1092
+ return `🧠 **Semantic Search Results** for \`${query}\`:
1093
+
1094
+ ${lines.join('\n')}
1095
+
1096
+ To get more context on a result:
1097
+ \`\`\`bash
1098
+ tldr context <function_name> --project .
1099
+ \`\`\`
1100
+
1101
+ If results are wrong, try manually:
1102
+ \`\`\`bash
1103
+ tldr semantic search "${query}" --path .
1104
+ \`\`\``;
1105
+ }
1106
+
1107
+ /**
1108
+ * Smart Search Router - intercepts Grep and executes token-efficient search.
1109
+ *
1110
+ * Strategy:
1111
+ * 1. Try ast-grep first (works for code patterns, fast)
1112
+ * 2. If no results, try tldr semantic search (conceptual matches)
1113
+ * 3. Return actual results, not suggestions
1114
+ *
1115
+ * This runs the tools and returns results directly instead of lecturing
1116
+ * the agent about what commands to run.
1117
+ */
1118
+ function smartSearchRouter(input: HookInput): void {
1119
+ const projectDir = getProjectDir();
1120
+ const sessionId = input.session_id || 'default';
1121
+
1122
+ const pattern = (input.tool_input?.pattern as string) || '';
1123
+ if (!pattern) {
1124
+ allowTool(HOOK_SEARCH_ROUTER);
1125
+ return;
1126
+ }
1127
+
1128
+ // Get the search path from input, default to project dir
1129
+ const searchPath = (input.tool_input?.path as string) || projectDir;
1130
+
1131
+ // Classify the query for context saving
1132
+ const { queryType, targetType } = classifySearchPattern(pattern);
1133
+ const suggestedLayers = suggestLayers(queryType, targetType);
1134
+
1135
+ // Extract target name from pattern
1136
+ const nameMatch = pattern.match(/(?:def|function|func|class|struct|interface)\s+(\w+)/);
1137
+ const target = nameMatch ? nameMatch[1] : pattern.match(/^(\w+)/)?.[1] || pattern;
1138
+
1139
+ // Build search context for downstream hooks
1140
+ const searchContext: SearchContext = {
1141
+ timestamp: Date.now(),
1142
+ queryType,
1143
+ pattern,
1144
+ target,
1145
+ targetType,
1146
+ suggestedLayers,
1147
+ };
1148
+
1149
+ // Save context for downstream hooks (read enforcer will use this)
1150
+ saveSearchContext(sessionId, searchContext);
1151
+
1152
+ // Step 1: Try ast-grep first (good for code patterns)
1153
+ const astGrepMatches = tryAstGrep(pattern, searchPath, projectDir);
1154
+ if (astGrepMatches.length > 0) {
1155
+ const resultMsg = formatAstGrepResults(astGrepMatches, pattern);
1156
+ denyTool(resultMsg, HOOK_SEARCH_ROUTER);
1157
+ return;
1158
+ }
1159
+
1160
+ // Step 2: Try semantic search (good for natural language / concepts)
1161
+ const semanticMatches = trySemanticSearch(pattern, projectDir);
1162
+ if (semanticMatches.length > 0) {
1163
+ const resultMsg = formatSemanticResults(semanticMatches, pattern);
1164
+ denyTool(resultMsg, HOOK_SEARCH_ROUTER);
1165
+ return;
1166
+ }
1167
+
1168
+ // Step 3: Both failed - give manual fallback commands
1169
+ const fallbackMsg = `No results found for \`${pattern}\`.
1170
+
1171
+ Try manually:
1172
+ \`\`\`bash
1173
+ # AST-grep (precise pattern match):
1174
+ ast-grep run --pattern "${pattern}" ${searchPath}
1175
+
1176
+ # Semantic search (conceptual match):
1177
+ tldr semantic search "${pattern}" --path ${projectDir}
1178
+
1179
+ # Literal grep fallback:
1180
+ rg "${pattern}" ${searchPath}
1181
+ \`\`\``;
1182
+
1183
+ denyTool(fallbackMsg, HOOK_SEARCH_ROUTER);
1184
+ }
1185
+
1186
+ // ─────────────────────────────────────────────────────────────────────────────
1187
+ // UserPromptSubmit: impact-refactor
1188
+ // ─────────────────────────────────────────────────────────────────────────────
1189
+
1190
+ /** Keywords that trigger impact analysis */
1191
+ const REFACTOR_KEYWORDS = [
1192
+ /\brefactor\b/i,
1193
+ /\brename\b/i,
1194
+ /\bchange\b.*\bfunction\b/i,
1195
+ /\bmodify\b.*\b(?:function|method|class)\b/i,
1196
+ /\bupdate\b.*\bsignature\b/i,
1197
+ /\bmove\b.*\bfunction\b/i,
1198
+ /\bdelete\b.*\b(?:function|method)\b/i,
1199
+ /\bremove\b.*\b(?:function|method)\b/i,
1200
+ /\bextract\b.*\b(?:function|method)\b/i,
1201
+ /\binline\b.*\b(?:function|method)\b/i,
1202
+ ];
1203
+
1204
+ /** Extract function/method names from prompt */
1205
+ const IMPACT_FUNCTION_PATTERNS = [
1206
+ /(?:refactor|rename|change|modify|update|move|delete|remove)\s+(?:the\s+)?(?:function\s+)?[`"']?(\w+)[`"']?/gi,
1207
+ /[`"'](\w+)[`"']\s+(?:function|method)/gi,
1208
+ /(?:function|method|def|fn)\s+[`"']?(\w+)[`"']?/gi,
1209
+ ];
1210
+
1211
+ const IMPACT_EXCLUDE_WORDS = new Set([
1212
+ 'the', 'this', 'that', 'function', 'method', 'class', 'file',
1213
+ 'to', 'from', 'into', 'a', 'an', 'and', 'or', 'for', 'with',
1214
+ ]);
1215
+
1216
+ function shouldTriggerImpact(prompt: string): boolean {
1217
+ return REFACTOR_KEYWORDS.some((pattern) => pattern.test(prompt));
1218
+ }
1219
+
1220
+ function extractImpactFunctionNames(prompt: string): string[] {
1221
+ const candidates: Set<string> = new Set();
1222
+
1223
+ for (const pattern of IMPACT_FUNCTION_PATTERNS) {
1224
+ let match;
1225
+ pattern.lastIndex = 0;
1226
+ while ((match = pattern.exec(prompt)) !== null) {
1227
+ const name = match[1];
1228
+ if (name && name.length > 2 && !IMPACT_EXCLUDE_WORDS.has(name.toLowerCase())) {
1229
+ candidates.add(name);
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ // Also look for snake_case and camelCase identifiers
1235
+ const identifierPattern = /\b([a-z][a-z0-9_]*[a-z0-9])\b/gi;
1236
+ let match;
1237
+ while ((match = identifierPattern.exec(prompt)) !== null) {
1238
+ const name = match[1];
1239
+ if (name.length > 4 && !IMPACT_EXCLUDE_WORDS.has(name.toLowerCase())) {
1240
+ if (name.includes('_') || /[a-z][A-Z]/.test(name)) {
1241
+ candidates.add(name);
1242
+ }
1243
+ }
1244
+ }
1245
+
1246
+ return Array.from(candidates);
1247
+ }
1248
+
1249
+ /**
1250
+ * Impact analysis for refactoring (UserPromptSubmit).
1251
+ * When user mentions refactor/rename + function name, shows callers.
1252
+ */
1253
+ function impactRefactor(input: HookInput): void {
1254
+ const prompt = (input.tool_input?.prompt as string) || (input.tool_input?.message as string) || '';
1255
+
1256
+ if (!shouldTriggerImpact(prompt)) {
1257
+ process.exit(0);
1258
+ }
1259
+
1260
+ const functions = extractImpactFunctionNames(prompt);
1261
+ if (functions.length === 0) {
1262
+ process.exit(0);
1263
+ }
1264
+
1265
+ const projectDir = getProjectDir();
1266
+
1267
+ if (!isTldrInstalled() || !isTldrDaemonRunning(projectDir)) {
1268
+ process.exit(0);
1269
+ }
1270
+
1271
+ const results: string[] = [];
1272
+
1273
+ for (const funcName of functions.slice(0, 3)) {
1274
+ const impact = impactDaemon(funcName, projectDir);
1275
+
1276
+ if (!impact) {
1277
+ continue;
1278
+ }
1279
+
1280
+ const callers = impact.callers;
1281
+ let callerText: string;
1282
+
1283
+ if (callers.length === 0) {
1284
+ callerText = 'No callers found (entry point or unused)';
1285
+ } else {
1286
+ callerText = callers
1287
+ .slice(0, 15)
1288
+ .map((c) => ` - ${c.function || 'unknown'} in ${c.file}:${c.line}`)
1289
+ .join('\n');
1290
+ if (callers.length > 15) {
1291
+ callerText += `\n ... and ${callers.length - 15} more`;
1292
+ }
1293
+ }
1294
+
1295
+ results.push(`**Impact: ${funcName}**\nCallers:\n${callerText}`);
1296
+ }
1297
+
1298
+ if (results.length > 0) {
1299
+ console.log(`\n## REFACTORING IMPACT ANALYSIS\n\n${results.join('\n\n')}\n\nConsider all callers before making changes.\n`);
1300
+ }
1301
+
1302
+ process.exit(0);
1303
+ }
1304
+
1305
+ // ─────────────────────────────────────────────────────────────────────────────
1306
+ // PreToolUse: transcript-safeguard-pre (TaskOutput)
1307
+ // ─────────────────────────────────────────────────────────────────────────────
1308
+
1309
+
1310
+ /**
1311
+ * PreToolUse transcript safeguard — blocks all TaskOutput calls.
1312
+ *
1313
+ * Background tasks broadcast a completion notification with their result
1314
+ * when they finish. Calling TaskOutput is redundant and risks dumping
1315
+ * a massive raw transcript into context. Always deny.
1316
+ */
1317
+ function transcriptSafeguardPre(input: HookInput): void {
1318
+ if (input.tool_name !== 'TaskOutput') {
1319
+ return allowTool(HOOK_TRANSCRIPT_SAFEGUARD_PRE);
1320
+ }
1321
+
1322
+ denyTool(
1323
+ 'TaskOutput is not needed — background task completion notifications already include agent results. ' +
1324
+ 'Continue executing your current flow.',
1325
+ HOOK_TRANSCRIPT_SAFEGUARD_PRE,
1326
+ );
1327
+ }
1328
+
1329
+ // ─────────────────────────────────────────────────────────────────────────────
1330
+ // Hook Category Definition
1331
+ // ─────────────────────────────────────────────────────────────────────────────
1332
+
1333
+ /** Context hooks category */
1334
+ export const category: HookCategory = {
1335
+ name: 'context',
1336
+ description: 'TLDR context injection hooks',
1337
+ hooks: [
1338
+ // PreToolUse hooks
1339
+ {
1340
+ name: 'tldr-inject',
1341
+ description: 'Inject TLDR context for Task (PreToolUse:Task)',
1342
+ handler: tldrContextInject,
1343
+ errorFallback: { type: 'allowTool' },
1344
+ logPayload: (input) => ({ tool: input.tool_name }),
1345
+ },
1346
+ {
1347
+ name: 'edit-inject',
1348
+ description: 'Inject file structure before edits (PreToolUse:Edit)',
1349
+ handler: editContextInject,
1350
+ errorFallback: { type: 'allowTool' },
1351
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1352
+ },
1353
+ {
1354
+ name: 'arch-inject',
1355
+ description: 'Inject architecture layers for planning (PreToolUse:Task)',
1356
+ handler: archContextInject,
1357
+ errorFallback: { type: 'allowTool' },
1358
+ logPayload: (input) => ({ tool: input.tool_name }),
1359
+ },
1360
+ {
1361
+ name: 'signature',
1362
+ description: 'Inject function signatures for Edit (PreToolUse:Edit)',
1363
+ handler: signatureHelper,
1364
+ errorFallback: { type: 'allowTool' },
1365
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1366
+ },
1367
+ {
1368
+ name: 'read-enforcer',
1369
+ description: 'Enforce TLDR for large code files (PreToolUse:Read)',
1370
+ handler: tldrReadEnforcer,
1371
+ errorFallback: { type: 'allowTool' },
1372
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1373
+ },
1374
+ {
1375
+ name: 'search-router',
1376
+ description: 'Route searches to optimal tool (PreToolUse:Grep)',
1377
+ handler: smartSearchRouter,
1378
+ errorFallback: { type: 'allowTool' },
1379
+ logPayload: (input) => ({ tool: input.tool_name, pattern: input.tool_input?.pattern }),
1380
+ },
1381
+ {
1382
+ name: 'transcript-safeguard-pre',
1383
+ description: 'Intercept oversized TaskOutput before execution (PreToolUse:TaskOutput)',
1384
+ handler: transcriptSafeguardPre,
1385
+ errorFallback: { type: 'allowTool' },
1386
+ logPayload: (input) => ({ tool: input.tool_name, taskId: input.tool_input?.task_id }),
1387
+ },
1388
+ // PostToolUse hooks
1389
+ {
1390
+ name: 'diagnostics',
1391
+ description: 'Run TLDR diagnostics after edits (PostToolUse:Edit|Write)',
1392
+ handler: postEditDiagnostics,
1393
+ errorFallback: { type: 'allowTool' },
1394
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1395
+ },
1396
+ {
1397
+ name: 'import-validate',
1398
+ description: 'Validate imports after edits (PostToolUse:Edit|Write)',
1399
+ handler: importValidator,
1400
+ errorFallback: { type: 'allowTool' },
1401
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1402
+ },
1403
+ {
1404
+ name: 'edit-notify',
1405
+ description: 'Notify TLDR daemon of file changes (PostToolUse:Edit|Write)',
1406
+ handler: editNotify,
1407
+ errorFallback: { type: 'allowTool' },
1408
+ logPayload: (input) => ({ tool: input.tool_name, file: input.tool_input?.file_path }),
1409
+ },
1410
+ // UserPromptSubmit hooks
1411
+ {
1412
+ name: 'impact-refactor',
1413
+ description: 'Show impact analysis for refactoring (UserPromptSubmit)',
1414
+ handler: impactRefactor,
1415
+ errorFallback: { type: 'silent' },
1416
+ logPayload: (input) => ({ prompt: (input.tool_input?.prompt as string)?.slice(0, 50) }),
1417
+ },
1418
+ ],
1419
+ };
1420
+
1421
+ // ─────────────────────────────────────────────────────────────────────────────
1422
+ // Command Registration
1423
+ // ─────────────────────────────────────────────────────────────────────────────
1424
+
1425
+ /**
1426
+ * Register context hook subcommands.
1427
+ */
1428
+ export function register(parent: Command): void {
1429
+ registerCategory(parent, category);
1430
+ }
1431
+
1432
+ // ─────────────────────────────────────────────────────────────────────────────
1433
+ // Daemon Handler Registration
1434
+ // ─────────────────────────────────────────────────────────────────────────────
1435
+
1436
+ /**
1437
+ * Register handlers for daemon mode.
1438
+ * The daemon intercepts stdout and process.exit(), so handlers run unchanged.
1439
+ */
1440
+ export function registerDaemonHandlers(register: RegisterFn): void {
1441
+ registerCategoryForDaemon(category, register);
1442
+ }