gobby 0.2.5__py3-none-any.whl

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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env python3
2
+ """Hook Dispatcher - Routes Claude Code hooks to HookManager.
3
+
4
+ This is a thin wrapper script that receives hook calls from Claude Code
5
+ and routes them to the appropriate handler via HookManager.
6
+
7
+ Usage:
8
+ hook_dispatcher.py --type session-start < input.json > output.json
9
+ hook_dispatcher.py --type pre-tool-use --debug < input.json > output.json
10
+
11
+ Exit Codes:
12
+ 0 - Success
13
+ 1 - General error (logged, continues)
14
+ 2 - Invalid input (argument parsing or JSON)
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ # No longer need to import HookManager - we call it via HTTP daemon instead
24
+
25
+ # Default daemon configuration
26
+ DEFAULT_DAEMON_PORT = 8765
27
+ DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
28
+
29
+
30
+ def get_daemon_url() -> str:
31
+ """Get the daemon HTTP URL from config file.
32
+
33
+ Reads daemon_port from ~/.gobby/config.yaml if it exists,
34
+ otherwise uses the default port 8765.
35
+
36
+ Returns:
37
+ Full daemon URL like http://localhost:8765
38
+ """
39
+ config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
40
+
41
+ if config_path.exists():
42
+ try:
43
+ import yaml
44
+
45
+ with open(config_path) as f:
46
+ config = yaml.safe_load(f) or {}
47
+ port = config.get("daemon_port", DEFAULT_DAEMON_PORT)
48
+ except Exception:
49
+ # If config read fails, use default
50
+ port = DEFAULT_DAEMON_PORT
51
+ else:
52
+ port = DEFAULT_DAEMON_PORT
53
+
54
+ return f"http://localhost:{port}"
55
+
56
+
57
+ def get_terminal_context() -> dict[str, str | int | None]:
58
+ """Capture terminal/process context for session correlation.
59
+
60
+ Returns:
61
+ Dict with terminal identifiers (values may be None if unavailable)
62
+ """
63
+ context: dict[str, str | int | None] = {}
64
+
65
+ # Parent process ID (shell or Claude process)
66
+ try:
67
+ context["parent_pid"] = os.getppid()
68
+ except Exception:
69
+ context["parent_pid"] = None
70
+
71
+ # TTY device name
72
+ try:
73
+ context["tty"] = os.ttyname(0)
74
+ except Exception:
75
+ context["tty"] = None
76
+
77
+ # macOS Terminal.app session ID
78
+ context["term_session_id"] = os.environ.get("TERM_SESSION_ID")
79
+
80
+ # iTerm2 session ID
81
+ context["iterm_session_id"] = os.environ.get("ITERM_SESSION_ID")
82
+
83
+ # VS Code terminal ID (if running in VS Code integrated terminal)
84
+ context["vscode_terminal_id"] = os.environ.get("VSCODE_GIT_ASKPASS_NODE")
85
+
86
+ # Tmux pane (if running in tmux)
87
+ context["tmux_pane"] = os.environ.get("TMUX_PANE")
88
+
89
+ # Kitty terminal window ID
90
+ context["kitty_window_id"] = os.environ.get("KITTY_WINDOW_ID")
91
+
92
+ # Alacritty IPC socket path (unique per instance)
93
+ context["alacritty_socket"] = os.environ.get("ALACRITTY_SOCKET")
94
+
95
+ # Generic terminal program identifier (set by many terminals)
96
+ context["term_program"] = os.environ.get("TERM_PROGRAM")
97
+
98
+ return context
99
+
100
+
101
+ def parse_arguments() -> argparse.Namespace:
102
+ """Parse command line arguments.
103
+
104
+ Returns:
105
+ Parsed arguments with type and debug flags
106
+ """
107
+ parser = argparse.ArgumentParser(description="Claude Code Hook Dispatcher")
108
+ parser.add_argument(
109
+ "--type",
110
+ required=True,
111
+ help="Hook type (e.g., session-start, pre-tool-use)",
112
+ )
113
+ parser.add_argument(
114
+ "--debug",
115
+ action="store_true",
116
+ help="Enable debug logging",
117
+ )
118
+ return parser.parse_args()
119
+
120
+
121
+ def check_daemon_running(timeout: float = 0.5) -> bool:
122
+ """Check if gobby daemon is active and responding.
123
+
124
+ Performs a quick health check to verify the HTTP server is running
125
+ before processing hooks. This prevents hook execution when the daemon
126
+ is stopped, avoiding long timeouts and confusing error messages.
127
+
128
+ Args:
129
+ timeout: Maximum time to wait for response in seconds (default: 0.5)
130
+
131
+ Returns:
132
+ True if client is running and responding, False otherwise
133
+ """
134
+ try:
135
+ import httpx
136
+
137
+ daemon_url = get_daemon_url()
138
+ response = httpx.get(
139
+ f"{daemon_url}/admin/status",
140
+ timeout=timeout,
141
+ follow_redirects=False,
142
+ )
143
+ return response.status_code == 200
144
+ except Exception:
145
+ # Any error (connection refused, timeout, etc.) means client is not running
146
+ return False
147
+
148
+
149
+ def main() -> int:
150
+ """Main dispatcher execution.
151
+
152
+ Returns:
153
+ Exit code (0=success, 1=error, 2=invalid input)
154
+ """
155
+ try:
156
+ # Parse arguments
157
+ args = parse_arguments()
158
+ except (argparse.ArgumentError, SystemExit):
159
+ # Argument parsing failed - return empty dict and exit 2
160
+ print(json.dumps({}))
161
+ return 2
162
+
163
+ hook_type = args.type
164
+ debug_mode = args.debug
165
+
166
+ # Check if gobby daemon is running before processing hooks
167
+ if not check_daemon_running():
168
+ # Critical hooks that manage session state MUST have daemon running
169
+ # Without daemon, we lose handoff context, session tracking, etc.
170
+ critical_hooks = {"session-start", "session-end", "pre-compact"}
171
+ if hook_type in critical_hooks:
172
+ # Block the hook - forces user to start daemon before critical lifecycle events
173
+ print(
174
+ f"Gobby daemon is not running. Start with 'gobby start' before continuing. "
175
+ f"({hook_type} requires daemon for session state management)",
176
+ file=sys.stderr,
177
+ )
178
+ return 2 # Exit 2 = block operation
179
+ else:
180
+ # Non-critical hooks can proceed without daemon (tool use, notifications, etc.)
181
+ print(
182
+ json.dumps(
183
+ {"status": "daemon_not_running", "message": "gobby daemon is not running"}
184
+ )
185
+ )
186
+ return 0 # Exit 0 (success) - allow operation to continue
187
+
188
+ # Setup logger for dispatcher (not HookManager)
189
+ # Only log to stderr in debug mode - otherwise logs pollute Claude's stderr reading
190
+ import logging
191
+
192
+ logger = logging.getLogger("gobby.hooks.dispatcher")
193
+ if debug_mode:
194
+ logging.basicConfig(level=logging.DEBUG)
195
+ else:
196
+ # In non-debug mode, suppress all logging to stderr
197
+ logging.basicConfig(level=logging.WARNING, handlers=[])
198
+
199
+ try:
200
+ # Read JSON input from stdin
201
+ input_data = json.load(sys.stdin)
202
+
203
+ # Inject terminal context for session-start hooks
204
+ # This captures the terminal/process info for session correlation
205
+ if hook_type == "session-start":
206
+ input_data["terminal_context"] = get_terminal_context()
207
+
208
+ # ALWAYS log what Claude Code sends us (for debugging hook data issues)
209
+ logger.info(f"[{hook_type}] Received input keys: {list(input_data.keys())}")
210
+
211
+ # Log hook-specific critical fields (based on Claude Code SDK documentation)
212
+ if hook_type == "session-start":
213
+ logger.info(
214
+ f"[session-start] session_id={input_data.get('session_id')}, "
215
+ f"source={input_data.get('source')}"
216
+ )
217
+ elif hook_type == "session-end":
218
+ logger.info(
219
+ f"[session-end] session_id={input_data.get('session_id')}, "
220
+ f"reason={input_data.get('reason')}"
221
+ )
222
+ elif hook_type == "user-prompt-submit":
223
+ prompt = input_data.get("prompt", "")
224
+ prompt_preview = prompt[:100] + "..." if len(prompt) > 100 else prompt
225
+ logger.info(
226
+ f"[user-prompt-submit] session_id={input_data.get('session_id')}, "
227
+ f"prompt={prompt_preview}"
228
+ )
229
+ elif hook_type == "pre-tool-use":
230
+ tool_input = input_data.get("tool_input", {})
231
+ # Truncate large values for readability (keep first 200 chars)
232
+ tool_input_preview = {
233
+ k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
234
+ for k, v in tool_input.items()
235
+ }
236
+ logger.info(
237
+ f"[pre-tool-use] tool_name={input_data.get('tool_name')}, "
238
+ f"tool_input={tool_input_preview}, "
239
+ f"session_id={input_data.get('session_id')}"
240
+ )
241
+ elif hook_type == "post-tool-use":
242
+ logger.info(
243
+ f"[post-tool-use] tool_name={input_data.get('tool_name')}, "
244
+ f"has_tool_response={bool(input_data.get('tool_response'))}, "
245
+ f"has_tool_input={bool(input_data.get('tool_input'))}, "
246
+ f"session_id={input_data.get('session_id')}"
247
+ )
248
+ elif hook_type == "pre-compact":
249
+ logger.info(
250
+ f"[pre-compact] session_id={input_data.get('session_id')}, "
251
+ f"trigger={input_data.get('trigger')}, "
252
+ f"has_custom_instructions={bool(input_data.get('custom_instructions'))}"
253
+ )
254
+ elif hook_type == "stop":
255
+ logger.info(
256
+ f"[stop] session_id={input_data.get('session_id')}, "
257
+ f"stop_hook_active={input_data.get('stop_hook_active')}"
258
+ )
259
+ elif hook_type == "subagent-start":
260
+ logger.info(
261
+ f"[subagent-start] session_id={input_data.get('session_id')}, "
262
+ f"agent_id={input_data.get('agent_id')}, "
263
+ f"subagent_id={input_data.get('subagent_id')}"
264
+ )
265
+ elif hook_type == "subagent-stop":
266
+ logger.info(
267
+ f"[subagent-stop] session_id={input_data.get('session_id')}, "
268
+ f"agent_id={input_data.get('agent_id')}, "
269
+ f"subagent_id={input_data.get('subagent_id')}"
270
+ )
271
+ elif hook_type == "notification":
272
+ logger.info(
273
+ f"[notification] session_id={input_data.get('session_id')}, "
274
+ f"message={input_data.get('message')}, "
275
+ f"title={input_data.get('title', 'N/A')}"
276
+ )
277
+ elif hook_type == "permission-request":
278
+ tool_input = input_data.get("tool_input", {})
279
+ tool_input_preview = {
280
+ k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
281
+ for k, v in tool_input.items()
282
+ }
283
+ logger.info(
284
+ f"[permission-request] tool_name={input_data.get('tool_name')}, "
285
+ f"tool_input={tool_input_preview}, "
286
+ f"session_id={input_data.get('session_id')}"
287
+ )
288
+
289
+ if debug_mode:
290
+ logger.debug(f"Input data: {input_data}")
291
+
292
+ except json.JSONDecodeError as e:
293
+ # Invalid JSON input - return empty dict and exit 2
294
+ if debug_mode:
295
+ logger.error(f"JSON decode error: {e}")
296
+ print(json.dumps({}))
297
+ return 2
298
+
299
+ # Call daemon HTTP endpoint instead of creating HookManager
300
+ import httpx
301
+
302
+ daemon_url = get_daemon_url()
303
+ try:
304
+ response = httpx.post(
305
+ f"{daemon_url}/hooks/execute",
306
+ json={
307
+ "hook_type": hook_type,
308
+ "input_data": input_data,
309
+ "source": "claude", # Required: identifies CLI source
310
+ },
311
+ timeout=30.0, # Generous timeout for hook processing
312
+ )
313
+
314
+ if response.status_code == 200:
315
+ # Success - daemon returns result directly (not wrapped)
316
+ result = response.json()
317
+
318
+ if debug_mode:
319
+ logger.debug(f"Output data: {result}")
320
+
321
+ # Check for block decision - return exit code 2 to signal blocking
322
+ # For blocking, output goes to STDERR (Claude reads stderr on exit 2)
323
+ if result.get("continue") is False or result.get("decision") == "block":
324
+ # Output just the reason, not the full JSON
325
+ reason = result.get("stopReason") or result.get("reason") or "Blocked by hook"
326
+ print(reason, file=sys.stderr)
327
+ return 2
328
+
329
+ # Only print output if there's something meaningful to show
330
+ # Empty dicts cause Claude Code to show "hook success: Success"
331
+ if result and result != {}:
332
+ print(json.dumps(result))
333
+
334
+ return 0
335
+ else:
336
+ # HTTP error from daemon
337
+ error_detail = response.text
338
+ logger.error(
339
+ f"Daemon returned error: status={response.status_code}, detail={error_detail}"
340
+ )
341
+ print(json.dumps({"status": "error", "message": f"Daemon error: {error_detail}"}))
342
+ return 1
343
+
344
+ except httpx.ConnectError:
345
+ # Daemon not reachable - this shouldn't happen since we checked, but handle gracefully
346
+ logger.error("Failed to connect to daemon (unreachable)")
347
+ print(json.dumps({"status": "error", "message": "Daemon unreachable"}))
348
+ return 1
349
+
350
+ except httpx.TimeoutException:
351
+ # Hook processing took too long
352
+ logger.error(f"Hook execution timeout: {hook_type}")
353
+ print(json.dumps({"status": "error", "message": "Hook execution timeout"}))
354
+ return 1
355
+
356
+ except Exception as e:
357
+ # General error - log and return 1
358
+ logger.error(f"Hook execution failed: {e}", exc_info=True)
359
+ print(json.dumps({"status": "error", "message": str(e)}))
360
+ return 1
361
+
362
+
363
+ if __name__ == "__main__":
364
+ sys.exit(main())
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """Validate .claude/settings.json configuration.
3
+
4
+ This script validates:
5
+ - JSON syntax correctness
6
+ - Hook structure and dispatcher commands
7
+ - All required hook types are configured
8
+ - Dispatcher script exists and is executable
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ def main() -> int:
17
+ """Validate settings.json configuration.
18
+
19
+ Returns:
20
+ 0 if valid, 1 if invalid
21
+ """
22
+ # Find settings.json
23
+ claude_dir = Path(__file__).parent.parent
24
+ settings_file = claude_dir / "settings.json"
25
+
26
+ if not settings_file.exists():
27
+ print(f"❌ Settings file not found: {settings_file}")
28
+ return 1
29
+
30
+ # Validate JSON syntax
31
+ try:
32
+ with open(settings_file) as f:
33
+ settings = json.load(f)
34
+ except json.JSONDecodeError as e:
35
+ print(f"❌ Invalid JSON syntax: {e}")
36
+ return 1
37
+
38
+ print("✅ JSON syntax is valid")
39
+
40
+ # Check hooks section exists
41
+ if "hooks" not in settings:
42
+ print("❌ No 'hooks' section found in settings")
43
+ return 1
44
+
45
+ hooks = settings["hooks"]
46
+ print("✅ Hooks section found")
47
+
48
+ # Required hook types
49
+ required_hooks = [
50
+ "SessionStart",
51
+ "SessionEnd",
52
+ "UserPromptSubmit",
53
+ "PreToolUse",
54
+ "PostToolUse",
55
+ "PreCompact",
56
+ "Notification",
57
+ "Stop",
58
+ "SubagentStart",
59
+ "SubagentStop",
60
+ ]
61
+
62
+ # Validate each required hook
63
+ for hook_type in required_hooks:
64
+ if hook_type not in hooks:
65
+ print(f"❌ Missing hook type: {hook_type}")
66
+ return 1
67
+
68
+ hook_configs = hooks[hook_type]
69
+ if not isinstance(hook_configs, list) or not hook_configs:
70
+ print(f"❌ Invalid hook configuration for: {hook_type}")
71
+ return 1
72
+
73
+ # Check first configuration
74
+ config = hook_configs[0]
75
+ if "hooks" not in config:
76
+ print(f"❌ No 'hooks' array in {hook_type} configuration")
77
+ return 1
78
+
79
+ # Check command uses dispatcher
80
+ command = config["hooks"][0].get("command", "")
81
+ if "hook_dispatcher.py" not in command:
82
+ print(f"⚠️ Warning: {hook_type} not using dispatcher pattern")
83
+
84
+ print(f"✅ All {len(required_hooks)} required hook types configured")
85
+
86
+ # Validate dispatcher exists
87
+ dispatcher = claude_dir / "hooks" / "hook_dispatcher.py"
88
+ if not dispatcher.exists():
89
+ print(f"❌ Dispatcher not found: {dispatcher}")
90
+ return 1
91
+
92
+ print("✅ Dispatcher script exists")
93
+
94
+ if not dispatcher.stat().st_mode & 0o111:
95
+ print("⚠️ Warning: Dispatcher is not executable")
96
+
97
+ print("\n✅ All validations passed!")
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ sys.exit(main())
@@ -0,0 +1,118 @@
1
+ {
2
+ "allowedTools": [],
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=session-start"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionEnd": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=session-end"
20
+ }
21
+ ]
22
+ }
23
+ ],
24
+ "UserPromptSubmit": [
25
+ {
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=user-prompt-submit"
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "PreToolUse": [
35
+ {
36
+ "matcher": "*",
37
+ "hooks": [
38
+ {
39
+ "type": "command",
40
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=pre-tool-use"
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "PostToolUse": [
46
+ {
47
+ "matcher": "*",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=post-tool-use"
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ "PreCompact": [
57
+ {
58
+ "hooks": [
59
+ {
60
+ "type": "command",
61
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=pre-compact"
62
+ }
63
+ ]
64
+ }
65
+ ],
66
+ "Notification": [
67
+ {
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=notification"
72
+ }
73
+ ]
74
+ }
75
+ ],
76
+ "Stop": [
77
+ {
78
+ "hooks": [
79
+ {
80
+ "type": "command",
81
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=stop"
82
+ }
83
+ ]
84
+ }
85
+ ],
86
+ "SubagentStart": [
87
+ {
88
+ "hooks": [
89
+ {
90
+ "type": "command",
91
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=subagent-start"
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "SubagentStop": [
97
+ {
98
+ "hooks": [
99
+ {
100
+ "type": "command",
101
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=subagent-stop"
102
+ }
103
+ ]
104
+ }
105
+ ],
106
+ "PermissionRequest": [
107
+ {
108
+ "matcher": "*",
109
+ "hooks": [
110
+ {
111
+ "type": "command",
112
+ "command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=permission-request"
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ }