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
gobby/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Gobby - Local-first daemon for multi-CLI session management."""
2
+
3
+ __version__ = "0.2.3"
@@ -0,0 +1,30 @@
1
+ """CLI adapters for multi-CLI session management.
2
+
3
+ This module contains adapters that translate between CLI-specific hook formats
4
+ and the unified HookEvent/HookResponse models.
5
+
6
+ Each adapter is responsible for:
7
+ 1. Translating native CLI payloads to HookEvent
8
+ 2. Translating HookResponse back to CLI-expected format
9
+ 3. Managing CLI-specific session lifecycle
10
+
11
+ Adapters:
12
+ - ClaudeCodeAdapter: For Claude Code CLI hooks (HTTP-based)
13
+ - GeminiAdapter: For Gemini CLI hooks (HTTP-based) [Phase 3]
14
+ - CodexAdapter: For Codex CLI via app-server (JSON-RPC-based) [Phase 4]
15
+ - CodexNotifyAdapter: For Codex CLI notify events (simple HTTP-based)
16
+ """
17
+
18
+ from gobby.adapters.base import BaseAdapter
19
+ from gobby.adapters.claude_code import ClaudeCodeAdapter
20
+ from gobby.adapters.codex import CodexAdapter, CodexAppServerClient, CodexNotifyAdapter
21
+ from gobby.adapters.gemini import GeminiAdapter
22
+
23
+ __all__ = [
24
+ "BaseAdapter",
25
+ "ClaudeCodeAdapter",
26
+ "CodexAdapter",
27
+ "CodexAppServerClient",
28
+ "CodexNotifyAdapter",
29
+ "GeminiAdapter",
30
+ ]
gobby/adapters/base.py ADDED
@@ -0,0 +1,93 @@
1
+ """Base adapter class for CLI hook translation.
2
+
3
+ This module defines the abstract base class that all CLI adapters must implement.
4
+ Adapters are responsible for translating between CLI-specific hook formats and
5
+ the unified HookEvent/HookResponse models.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from gobby.hooks.events import HookEvent, HookResponse, SessionSource
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.hooks.hook_manager import HookManager
15
+
16
+
17
+ class BaseAdapter(ABC):
18
+ """Base class for CLI adapters that translate native events to HookEvents.
19
+
20
+ Each CLI (Claude Code, Gemini, Codex) has its own adapter that:
21
+ 1. Knows how to parse the CLI's native hook payload format
22
+ 2. Translates payloads to unified HookEvent objects
23
+ 3. Translates HookResponse objects back to CLI-expected format
24
+
25
+ Subclasses must implement:
26
+ - source: The SessionSource enum value for this CLI
27
+ - translate_to_hook_event(): Convert native payload to HookEvent
28
+ - translate_from_hook_response(): Convert HookResponse to native format
29
+ """
30
+
31
+ source: SessionSource
32
+
33
+ @abstractmethod
34
+ def translate_to_hook_event(self, native_event: dict[str, Any]) -> HookEvent | None:
35
+ """Convert native CLI event to unified HookEvent.
36
+
37
+ Args:
38
+ native_event: The raw payload from the CLI's hook dispatcher.
39
+ Structure varies by CLI:
40
+ - Claude Code: {"hook_type": "...", "input_data": {...}}
41
+ - Gemini: {"hook_event_name": "...", "session_id": "...", ...}
42
+ - Codex: JSON-RPC params from app-server events
43
+
44
+ Returns:
45
+ A unified HookEvent that can be processed by HookManager.
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ def translate_from_hook_response(self, response: HookResponse) -> dict[str, Any]:
51
+ """Convert HookResponse to native CLI response format.
52
+
53
+ Args:
54
+ response: The unified HookResponse from HookManager.
55
+
56
+ Returns:
57
+ A dict in the format expected by the CLI's hook dispatcher:
58
+ - Claude Code: {"continue": bool, "stopReason": str | None, ...}
59
+ - Gemini: {"decision": str, "hookSpecificOutput": {...}}
60
+ - Codex: JSON-RPC response format
61
+ """
62
+ pass
63
+
64
+ def handle_native(
65
+ self, native_event: dict[str, Any], hook_manager: "HookManager"
66
+ ) -> dict[str, Any]:
67
+ """Main entry point for HTTP endpoints.
68
+
69
+ This method handles the full round-trip:
70
+ 1. Translate native event to HookEvent
71
+ 2. Process through HookManager
72
+ 3. Translate response back to native format
73
+
74
+ Note: This method is synchronous for Phase 2A-2B compatibility.
75
+ In Phase 2C+, when HookManager.handle() is async, subclasses may
76
+ override with async versions.
77
+
78
+ Subclasses may override this to add CLI-specific behavior, such as
79
+ the strangler fig pattern used by ClaudeCodeAdapter.
80
+
81
+ Args:
82
+ native_event: The raw payload from the CLI.
83
+ hook_manager: The HookManager instance to process events.
84
+
85
+ Returns:
86
+ Response dict in CLI-specific format.
87
+ """
88
+ hook_event = self.translate_to_hook_event(native_event)
89
+ if hook_event is None:
90
+ # Event ignored by adapter
91
+ return {}
92
+ hook_response = hook_manager.handle(hook_event)
93
+ return self.translate_from_hook_response(hook_response)
@@ -0,0 +1,276 @@
1
+ """Claude Code adapter for hook translation.
2
+
3
+ This adapter translates between Claude Code's native hook format and the unified
4
+ HookEvent/HookResponse models. It implements the strangler fig pattern for safe
5
+ migration from the existing HookManager.execute() method.
6
+
7
+ Claude Code Hook Types (12 total):
8
+ - session-start, session-end: Session lifecycle
9
+ - user-prompt-submit: Before user prompt validation
10
+ - pre-tool-use, post-tool-use, post-tool-use-failure: Tool lifecycle
11
+ - pre-compact: Context compaction
12
+ - stop: Agent stops
13
+ - subagent-start, subagent-stop: Subagent lifecycle
14
+ - permission-request: Permission requests (future)
15
+ - notification: System notifications
16
+ """
17
+
18
+ from datetime import UTC, datetime
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from gobby.adapters.base import BaseAdapter
22
+ from gobby.hooks.events import HookEvent, HookEventType, HookResponse, SessionSource
23
+
24
+ if TYPE_CHECKING:
25
+ from gobby.hooks.hook_manager import HookManager
26
+
27
+
28
+ class ClaudeCodeAdapter(BaseAdapter):
29
+ """Adapter for Claude Code CLI hook translation.
30
+
31
+ This adapter:
32
+ 1. Translates Claude Code's kebab-case hook payloads to unified HookEvent
33
+ 2. Translates HookResponse back to Claude Code's expected format
34
+ 3. Calls HookManager.handle() with unified HookEvent model
35
+
36
+ Phase 2C Migration Complete:
37
+ - Now using HookManager.handle(HookEvent) for all hooks
38
+ - Legacy execute() path available via set_legacy_mode(True) for rollback
39
+ """
40
+
41
+ source = SessionSource.CLAUDE
42
+
43
+ # Event type mapping: Claude Code hook names -> unified HookEventType
44
+ # Claude Code uses kebab-case hook names in the payload's "hook_type" field
45
+ EVENT_MAP: dict[str, HookEventType] = {
46
+ "session-start": HookEventType.SESSION_START,
47
+ "session-end": HookEventType.SESSION_END,
48
+ "user-prompt-submit": HookEventType.BEFORE_AGENT,
49
+ "stop": HookEventType.STOP,
50
+ "pre-tool-use": HookEventType.BEFORE_TOOL,
51
+ "post-tool-use": HookEventType.AFTER_TOOL,
52
+ "post-tool-use-failure": HookEventType.AFTER_TOOL, # Same as AFTER_TOOL with error flag
53
+ "pre-compact": HookEventType.PRE_COMPACT,
54
+ "subagent-start": HookEventType.SUBAGENT_START,
55
+ "subagent-stop": HookEventType.SUBAGENT_STOP,
56
+ "permission-request": HookEventType.PERMISSION_REQUEST,
57
+ "notification": HookEventType.NOTIFICATION,
58
+ }
59
+
60
+ def __init__(self, hook_manager: "HookManager | None" = None):
61
+ """Initialize the Claude Code adapter.
62
+
63
+ Args:
64
+ hook_manager: Reference to HookManager for strangler fig delegation.
65
+ If None, the adapter can only translate (not handle events).
66
+ """
67
+ self._hook_manager = hook_manager
68
+ # Phase 2C: Use new handle() path with unified HookEvent model
69
+ # Note: systemMessage handoff notification bug exists in both paths (see plan-multi-cli.md)
70
+ self._use_legacy = False
71
+
72
+ def translate_to_hook_event(self, native_event: dict[str, Any]) -> HookEvent:
73
+ """Convert Claude Code native event to unified HookEvent.
74
+
75
+ Claude Code payloads have the structure:
76
+ {
77
+ "hook_type": "session-start", # kebab-case hook name
78
+ "input_data": {
79
+ "session_id": "abc123", # Claude calls this session_id but it's external_id
80
+ "machine_id": "...",
81
+ "cwd": "/path/to/project",
82
+ "transcript_path": "...",
83
+ # ... other hook-specific fields
84
+ }
85
+ }
86
+
87
+ Args:
88
+ native_event: Raw payload from Claude Code's hook_dispatcher.py
89
+
90
+ Returns:
91
+ Unified HookEvent with normalized fields.
92
+ """
93
+ hook_type = native_event.get("hook_type", "")
94
+ input_data = native_event.get("input_data", {})
95
+
96
+ # Map Claude hook type to unified event type
97
+ # Fall back to NOTIFICATION for unknown types (fail-open)
98
+ event_type = self.EVENT_MAP.get(hook_type, HookEventType.NOTIFICATION)
99
+
100
+ # Extract session_id (Claude calls it session_id but it's the external_id)
101
+ session_id = input_data.get("session_id", "")
102
+
103
+ # Check for failure flag in post-tool-use-failure
104
+ is_failure = hook_type == "post-tool-use-failure"
105
+ metadata = {"is_failure": is_failure} if is_failure else {}
106
+
107
+ return HookEvent(
108
+ event_type=event_type,
109
+ session_id=session_id,
110
+ source=self.source,
111
+ timestamp=datetime.now(UTC),
112
+ machine_id=input_data.get("machine_id"),
113
+ cwd=input_data.get("cwd"),
114
+ data=input_data,
115
+ metadata=metadata,
116
+ )
117
+
118
+ # Map Claude Code hook types to hookEventName for hookSpecificOutput
119
+ HOOK_EVENT_NAME_MAP: dict[str, str] = {
120
+ "session-start": "SessionStart",
121
+ "session-end": "SessionEnd",
122
+ "user-prompt-submit": "UserPromptSubmit",
123
+ "stop": "Stop",
124
+ "pre-tool-use": "PreToolUse",
125
+ "post-tool-use": "PostToolUse",
126
+ "post-tool-use-failure": "PostToolUse",
127
+ "pre-compact": "PreCompact",
128
+ "subagent-start": "SubagentStart",
129
+ "subagent-stop": "SubagentStop",
130
+ "permission-request": "PermissionRequest",
131
+ "notification": "Notification",
132
+ }
133
+
134
+ def translate_from_hook_response(
135
+ self, response: HookResponse, hook_type: str | None = None
136
+ ) -> dict[str, Any]:
137
+ """Convert HookResponse to Claude Code's expected format.
138
+
139
+ Claude Code expects responses in this format:
140
+ {
141
+ "continue": True/False, # Whether to continue execution
142
+ "stopReason": "...", # Reason if stopped (optional)
143
+ "decision": "approve"/"block", # Tool decision
144
+ "hookSpecificOutput": { # Hook-specific data
145
+ "hookEventName": "SessionStart", # Required!
146
+ "additionalContext": "..." # Context to inject into Claude
147
+ }
148
+ }
149
+
150
+ Args:
151
+ response: Unified HookResponse from HookManager.
152
+ hook_type: Original Claude Code hook type (e.g., "session-start")
153
+ Used to set hookEventName in hookSpecificOutput.
154
+
155
+ Returns:
156
+ Dict in Claude Code's expected format.
157
+ """
158
+ # Map decision to continue flag
159
+ # Both "deny" and "block" should stop execution
160
+ should_continue = response.decision not in ("deny", "block")
161
+
162
+ result: dict[str, Any] = {
163
+ "continue": should_continue,
164
+ }
165
+
166
+ # Add stop reason if denied or blocked
167
+ if response.decision in ("deny", "block") and response.reason:
168
+ result["stopReason"] = response.reason
169
+
170
+ # Add system_message to systemMessage (for system-level messages)
171
+ # Note: response.context goes to additionalContext below (visible to model)
172
+ if response.system_message:
173
+ result["systemMessage"] = response.system_message
174
+
175
+ # Add tool decision for pre-tool-use hooks
176
+ # Claude Code schema: decision uses "approve"/"block"
177
+ # permissionDecision uses "allow"/"deny"/"ask"
178
+ if response.decision in ("deny", "block"):
179
+ result["decision"] = "block"
180
+ else:
181
+ result["decision"] = "approve"
182
+
183
+ # Add hookSpecificOutput with additionalContext for model context injection
184
+ # This includes both workflow inject_context AND session identifiers
185
+ hook_event_name = self.HOOK_EVENT_NAME_MAP.get(hook_type or "", "Unknown")
186
+ additional_context_parts: list[str] = []
187
+
188
+ # Add workflow-injected context (from inject_context action)
189
+ # This is the primary way to inject context visible to the model
190
+ if response.context:
191
+ additional_context_parts.append(response.context)
192
+
193
+ # Add session identifiers from metadata
194
+ if response.metadata:
195
+ session_id = response.metadata.get("session_id")
196
+ if session_id:
197
+ # Build context with all available identifiers
198
+ context_lines = [f"session_id: {session_id}"]
199
+ if response.metadata.get("parent_session_id"):
200
+ context_lines.append(
201
+ f"parent_session_id: {response.metadata['parent_session_id']}"
202
+ )
203
+ if response.metadata.get("machine_id"):
204
+ context_lines.append(f"machine_id: {response.metadata['machine_id']}")
205
+ if response.metadata.get("project_id"):
206
+ context_lines.append(f"project_id: {response.metadata['project_id']}")
207
+ # Add terminal context (non-null values only)
208
+ if response.metadata.get("terminal_term_program"):
209
+ context_lines.append(f"terminal: {response.metadata['terminal_term_program']}")
210
+ if response.metadata.get("terminal_tty"):
211
+ context_lines.append(f"tty: {response.metadata['terminal_tty']}")
212
+ if response.metadata.get("terminal_parent_pid"):
213
+ context_lines.append(f"parent_pid: {response.metadata['terminal_parent_pid']}")
214
+ # Add terminal-specific session IDs (only one will be present)
215
+ for key in [
216
+ "terminal_iterm_session_id",
217
+ "terminal_term_session_id",
218
+ "terminal_kitty_window_id",
219
+ "terminal_tmux_pane",
220
+ "terminal_vscode_terminal_id",
221
+ "terminal_alacritty_socket",
222
+ ]:
223
+ if response.metadata.get(key):
224
+ # Use friendlier names in output
225
+ friendly_name = key.replace("terminal_", "").replace("_", " ")
226
+ context_lines.append(f"{friendly_name}: {response.metadata[key]}")
227
+ additional_context_parts.append("\n".join(context_lines))
228
+
229
+ # Build hookSpecificOutput if we have any context to inject
230
+ if additional_context_parts:
231
+ result["hookSpecificOutput"] = {
232
+ "hookEventName": hook_event_name,
233
+ "additionalContext": "\n\n".join(additional_context_parts),
234
+ }
235
+
236
+ return result
237
+
238
+ def handle_native(
239
+ self, native_event: dict[str, Any], hook_manager: "HookManager"
240
+ ) -> dict[str, Any]:
241
+ """Main entry point for HTTP endpoint.
242
+
243
+ Strangler fig pattern:
244
+ - Phase 2A-2B: Delegates to existing execute() — validates translation only
245
+ - Phase 2C+: Calls new handle() with HookEvent
246
+
247
+ Note: This method is synchronous for Phase 2A-2B compatibility with
248
+ the existing execute() method. In Phase 2C+, it will become async
249
+ when handle() is implemented as async.
250
+
251
+ Args:
252
+ native_event: Raw payload from Claude Code's hook_dispatcher.py
253
+ hook_manager: HookManager instance for processing.
254
+
255
+ Returns:
256
+ Response dict in Claude Code's expected format.
257
+ """
258
+ # Always translate (validates our mapping is correct)
259
+ hook_event = self.translate_to_hook_event(native_event)
260
+
261
+ # Phase 2C+: Use new HookEvent-based handler
262
+ # Legacy execute() path removed as HookManager.execute is deprecated/removed.
263
+ hook_type = native_event.get("hook_type", "")
264
+ hook_response = hook_manager.handle(hook_event)
265
+ return self.translate_from_hook_response(hook_response, hook_type=hook_type)
266
+
267
+ def set_legacy_mode(self, use_legacy: bool) -> None:
268
+ """Toggle between legacy and new code paths.
269
+
270
+ This method is used during the strangler fig migration to switch
271
+ between delegating to execute() and calling handle() directly.
272
+
273
+ Args:
274
+ use_legacy: If True, use legacy execute() path. If False, use new handle() path.
275
+ """
276
+ self._use_legacy = use_legacy