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/hooks/events.py ADDED
@@ -0,0 +1,218 @@
1
+ """Unified hook event models for multi-CLI session management.
2
+
3
+ This module defines the unified internal representation for hook events across
4
+ all supported CLIs (Claude Code, Gemini CLI, Codex CLI). Adapters translate
5
+ between CLI-specific formats and these unified types.
6
+
7
+ Design Decision: This file coexists with hook_types.py. The existing HookType enum
8
+ in hook_types.py uses Claude-specific kebab-case names (session-start, pre-tool-use)
9
+ and Pydantic models for input validation. The HookEventType enum here is the unified
10
+ internal representation. Adapters translate between them.
11
+ """
12
+
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+ from enum import Enum
16
+ from typing import Any, Literal
17
+
18
+
19
+ class HookEventType(str, Enum):
20
+ """Unified hook event types across all CLI sources.
21
+
22
+ These map to CLI-specific hook names via adapters:
23
+ - Claude Code: kebab-case (session-start, pre-tool-use)
24
+ - Gemini CLI: PascalCase (SessionStart, BeforeTool)
25
+ - Codex CLI: JSON-RPC methods (thread/started, item/completed)
26
+ """
27
+
28
+ # Session lifecycle
29
+ SESSION_START = "session_start"
30
+ SESSION_END = "session_end"
31
+
32
+ # Agent/turn lifecycle
33
+ BEFORE_AGENT = "before_agent"
34
+ AFTER_AGENT = "after_agent"
35
+ STOP = "stop" # Agent is about to stop/exit (Claude Code only)
36
+
37
+ # Tool lifecycle
38
+ BEFORE_TOOL = "before_tool"
39
+ AFTER_TOOL = "after_tool"
40
+ BEFORE_TOOL_SELECTION = "before_tool_selection" # Gemini only
41
+
42
+ # Model lifecycle (Gemini only)
43
+ BEFORE_MODEL = "before_model"
44
+ AFTER_MODEL = "after_model"
45
+
46
+ # Context management
47
+ PRE_COMPACT = "pre_compact" # Claude: PreCompact, Gemini: PreCompress
48
+
49
+ # Subagent lifecycle (Claude Code only)
50
+ SUBAGENT_START = "subagent_start"
51
+ SUBAGENT_STOP = "subagent_stop"
52
+
53
+ # Permissions & notifications
54
+ PERMISSION_REQUEST = "permission_request" # Claude Code only
55
+ NOTIFICATION = "notification"
56
+
57
+
58
+ class SessionSource(str, Enum):
59
+ """Identifies which CLI originated the session."""
60
+
61
+ CLAUDE = "claude" # Claude Code CLI
62
+ GEMINI = "gemini"
63
+ CODEX = "codex"
64
+ CLAUDE_SDK = "claude_sdk"
65
+ ANTIGRAVITY = "antigravity" # Antigravity IDE (uses Claude Code format)
66
+
67
+
68
+ @dataclass
69
+ class HookEvent:
70
+ """Unified hook event from any CLI source.
71
+
72
+ This dataclass represents a normalized hook event that can originate from
73
+ any supported CLI. Adapters are responsible for translating CLI-specific
74
+ payloads into this format.
75
+
76
+ Attributes:
77
+ event_type: The type of hook event (from HookEventType enum).
78
+ session_id: External session identifier (external_id for Claude, thread_id for Codex).
79
+ source: Which CLI originated this event.
80
+ timestamp: When the event occurred.
81
+ data: Event-specific payload in native format (adapter passes through).
82
+
83
+ machine_id: Unique identifier for the machine (populated by adapter or manager).
84
+ cwd: Current working directory for the session.
85
+
86
+ user_id: Platform user ID (populated by HookManager after session lookup).
87
+ project_id: Platform project ID (populated by HookManager).
88
+ workflow_id: Future: ID of workflow evaluating this event.
89
+ metadata: Extensible key-value store for adapter-specific data.
90
+ """
91
+
92
+ # Core required fields
93
+ event_type: HookEventType
94
+ session_id: str # external_id / thread_id (external ID)
95
+ source: SessionSource
96
+ timestamp: datetime
97
+ data: dict[str, Any] # Event-specific payload (native format)
98
+
99
+ # Context (populated by adapter or manager)
100
+ machine_id: str | None = None
101
+ cwd: str | None = None
102
+
103
+ # Future extensibility
104
+ user_id: str | None = None
105
+ project_id: str | None = None
106
+ task_id: str | None = None
107
+ workflow_id: str | None = None
108
+ metadata: dict[str, Any] = field(default_factory=dict)
109
+
110
+
111
+ @dataclass
112
+ class HookResponse:
113
+ """Unified response returned to CLI.
114
+
115
+ This dataclass represents the response that will be translated back to
116
+ CLI-specific format by the adapter.
117
+
118
+ Attributes:
119
+ decision: Whether to allow, deny, or ask the user about the action.
120
+ context: Text to inject into the agent's context (AI-only).
121
+ system_message: User-visible message to display (e.g., handoff notification).
122
+ reason: Explanation for the decision (useful for denials).
123
+
124
+ modify_args: Future: Dict of argument modifications for the action.
125
+ trigger_action: Future: Action to trigger in the CLI.
126
+ metadata: Extensible key-value store for adapter-specific data.
127
+ """
128
+
129
+ decision: Literal["allow", "deny", "ask", "block", "modify"] = "allow"
130
+ context: str | None = None # Inject into agent context (AI-only)
131
+ system_message: str | None = None # User-visible message (e.g., handoff notification)
132
+ reason: str | None = None # Explanation for decision
133
+
134
+ # Future extensibility
135
+ modify_args: dict[str, Any] | None = None
136
+ trigger_action: str | None = None
137
+ metadata: dict[str, Any] = field(default_factory=dict)
138
+
139
+
140
+ # Event type mapping table for documentation (see plan-multi-cli.md section 1.2)
141
+ # This is informational - actual mappings are in adapters
142
+ EVENT_TYPE_CLI_SUPPORT: dict[HookEventType, dict[str, str | None]] = {
143
+ HookEventType.SESSION_START: {
144
+ "claude": "SessionStart",
145
+ "gemini": "SessionStart",
146
+ "codex": "thread/started",
147
+ },
148
+ HookEventType.SESSION_END: {
149
+ "claude": "SessionEnd",
150
+ "gemini": "SessionEnd",
151
+ "codex": "thread/archive",
152
+ },
153
+ HookEventType.BEFORE_AGENT: {
154
+ "claude": "UserPromptSubmit",
155
+ "gemini": "BeforeAgent",
156
+ "codex": "turn/started",
157
+ },
158
+ HookEventType.AFTER_AGENT: {
159
+ "claude": "Stop",
160
+ "gemini": "AfterAgent",
161
+ "codex": "turn/completed",
162
+ },
163
+ HookEventType.STOP: {
164
+ "claude": "Stop",
165
+ "gemini": None,
166
+ "codex": None,
167
+ },
168
+ HookEventType.BEFORE_TOOL: {
169
+ "claude": "PreToolUse",
170
+ "gemini": "BeforeTool",
171
+ "codex": "requestApproval",
172
+ },
173
+ HookEventType.AFTER_TOOL: {
174
+ "claude": "PostToolUse",
175
+ "gemini": "AfterTool",
176
+ "codex": "item/completed",
177
+ },
178
+ HookEventType.BEFORE_TOOL_SELECTION: {
179
+ "claude": None,
180
+ "gemini": "BeforeToolSelection",
181
+ "codex": None,
182
+ },
183
+ HookEventType.BEFORE_MODEL: {
184
+ "claude": None,
185
+ "gemini": "BeforeModel",
186
+ "codex": None,
187
+ },
188
+ HookEventType.AFTER_MODEL: {
189
+ "claude": None,
190
+ "gemini": "AfterModel",
191
+ "codex": None,
192
+ },
193
+ HookEventType.PRE_COMPACT: {
194
+ "claude": "PreCompact",
195
+ "gemini": "PreCompress",
196
+ "codex": None,
197
+ },
198
+ HookEventType.SUBAGENT_START: {
199
+ "claude": "SubagentStart",
200
+ "gemini": None,
201
+ "codex": None,
202
+ },
203
+ HookEventType.SUBAGENT_STOP: {
204
+ "claude": "SubagentStop",
205
+ "gemini": None,
206
+ "codex": None,
207
+ },
208
+ HookEventType.PERMISSION_REQUEST: {
209
+ "claude": "PermissionRequest",
210
+ "gemini": None,
211
+ "codex": None,
212
+ },
213
+ HookEventType.NOTIFICATION: {
214
+ "claude": "Notification",
215
+ "gemini": "Notification",
216
+ "codex": None,
217
+ },
218
+ }
gobby/hooks/git.py ADDED
@@ -0,0 +1,169 @@
1
+ """Git-related hooks for merge operations.
2
+
3
+ Provides hooks for pre-merge and post-merge events that can be used
4
+ to integrate merge resolution with other systems.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from collections.abc import Callable
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Type aliases for hook callbacks
15
+ PreMergeHook = Callable[[str, str, str], bool] # (worktree_id, source, target) -> allow
16
+ PostMergeHook = Callable[[str, bool], None] # (resolution_id, success) -> None
17
+
18
+
19
+ class MergeHookManager:
20
+ """
21
+ Manager for merge-related hooks.
22
+
23
+ Allows registration of pre-merge and post-merge hooks that can be
24
+ used to integrate merge resolution with other systems (task status,
25
+ notifications, etc.).
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize the hook manager."""
30
+ self._pre_merge_hooks: list[PreMergeHook] = []
31
+ self._post_merge_hooks: list[PostMergeHook] = []
32
+
33
+ def register_pre_merge(self, hook: PreMergeHook) -> None:
34
+ """
35
+ Register a pre-merge hook.
36
+
37
+ Pre-merge hooks are called before a merge operation starts.
38
+ They receive the worktree ID, source branch, and target branch.
39
+ If any hook returns False, the merge is blocked.
40
+
41
+ Args:
42
+ hook: Callback function (worktree_id, source_branch, target_branch) -> bool
43
+ """
44
+ self._pre_merge_hooks.append(hook)
45
+ logger.debug(f"Registered pre-merge hook: {hook.__name__}")
46
+
47
+ def register_post_merge(self, hook: PostMergeHook) -> None:
48
+ """
49
+ Register a post-merge hook.
50
+
51
+ Post-merge hooks are called after a merge operation completes.
52
+ They receive the resolution ID and success status.
53
+
54
+ Args:
55
+ hook: Callback function (resolution_id, success) -> None
56
+ """
57
+ self._post_merge_hooks.append(hook)
58
+ logger.debug(f"Registered post-merge hook: {hook.__name__}")
59
+
60
+ def unregister_pre_merge(self, hook: PreMergeHook) -> bool:
61
+ """
62
+ Unregister a pre-merge hook.
63
+
64
+ Args:
65
+ hook: Hook to unregister
66
+
67
+ Returns:
68
+ True if hook was found and removed
69
+ """
70
+ try:
71
+ self._pre_merge_hooks.remove(hook)
72
+ return True
73
+ except ValueError:
74
+ return False
75
+
76
+ def unregister_post_merge(self, hook: PostMergeHook) -> bool:
77
+ """
78
+ Unregister a post-merge hook.
79
+
80
+ Args:
81
+ hook: Hook to unregister
82
+
83
+ Returns:
84
+ True if hook was found and removed
85
+ """
86
+ try:
87
+ self._post_merge_hooks.remove(hook)
88
+ return True
89
+ except ValueError:
90
+ return False
91
+
92
+ def run_pre_merge_hooks(
93
+ self, worktree_id: str, source_branch: str, target_branch: str
94
+ ) -> tuple[bool, str | None]:
95
+ """
96
+ Run all pre-merge hooks.
97
+
98
+ Args:
99
+ worktree_id: ID of the worktree
100
+ source_branch: Branch being merged
101
+ target_branch: Target branch
102
+
103
+ Returns:
104
+ Tuple of (allowed, blocking_reason)
105
+ If any hook returns False, returns (False, reason)
106
+ """
107
+ for hook in self._pre_merge_hooks:
108
+ try:
109
+ result = hook(worktree_id, source_branch, target_branch)
110
+ if not result:
111
+ reason = f"Merge blocked by pre-merge hook: {hook.__name__}"
112
+ logger.warning(reason)
113
+ return (False, reason)
114
+ except Exception as e:
115
+ reason = f"Pre-merge hook {hook.__name__} raised exception: {e}"
116
+ logger.error(reason)
117
+ # Continue with other hooks on exception
118
+ continue
119
+
120
+ return (True, None)
121
+
122
+ def run_post_merge_hooks(self, resolution_id: str, success: bool) -> None:
123
+ """
124
+ Run all post-merge hooks.
125
+
126
+ Args:
127
+ resolution_id: ID of the merge resolution
128
+ success: Whether the merge was successful
129
+ """
130
+ for hook in self._post_merge_hooks:
131
+ try:
132
+ hook(resolution_id, success)
133
+ except Exception as e:
134
+ logger.error(f"Post-merge hook {hook.__name__} raised exception: {e}")
135
+ # Continue with other hooks on exception
136
+ continue
137
+
138
+ @property
139
+ def pre_merge_hook_count(self) -> int:
140
+ """Get number of registered pre-merge hooks."""
141
+ return len(self._pre_merge_hooks)
142
+
143
+ @property
144
+ def post_merge_hook_count(self) -> int:
145
+ """Get number of registered post-merge hooks."""
146
+ return len(self._post_merge_hooks)
147
+
148
+
149
+ # Singleton instance for global hook management
150
+ _default_manager: MergeHookManager | None = None
151
+
152
+
153
+ def get_merge_hook_manager() -> MergeHookManager:
154
+ """
155
+ Get the default MergeHookManager instance.
156
+
157
+ Returns:
158
+ The global MergeHookManager singleton
159
+ """
160
+ global _default_manager
161
+ if _default_manager is None:
162
+ _default_manager = MergeHookManager()
163
+ return _default_manager
164
+
165
+
166
+ def reset_merge_hook_manager() -> None:
167
+ """Reset the default MergeHookManager (for testing)."""
168
+ global _default_manager
169
+ _default_manager = None
@@ -0,0 +1,171 @@
1
+ """
2
+ Health monitor module for daemon health check monitoring.
3
+
4
+ This module is extracted from hook_manager.py using Strangler Fig pattern.
5
+ It provides background health check monitoring for the Gobby daemon.
6
+
7
+ Classes:
8
+ HealthMonitor: Background daemon health check monitoring.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import threading
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.utils.daemon_client import DaemonClient
19
+
20
+
21
+ class HealthMonitor:
22
+ """
23
+ Background daemon health check monitoring.
24
+
25
+ Periodically checks daemon health via DaemonClient and caches the result
26
+ for fast access without HTTP calls. Thread-safe.
27
+
28
+ Extracted from HookManager to separate health monitoring concerns.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ daemon_client: DaemonClient,
34
+ health_check_interval: float = 10.0,
35
+ logger: logging.Logger | None = None,
36
+ ) -> None:
37
+ """
38
+ Initialize HealthMonitor.
39
+
40
+ Args:
41
+ daemon_client: DaemonClient for health checks
42
+ health_check_interval: Interval between checks in seconds (must be >= 0)
43
+ logger: Optional logger instance
44
+
45
+ Raises:
46
+ ValueError: If health_check_interval is negative
47
+ """
48
+ if health_check_interval < 0:
49
+ raise ValueError("health_check_interval must be non-negative")
50
+
51
+ self._daemon_client = daemon_client
52
+ self._health_check_interval = health_check_interval
53
+ self.logger = logger or logging.getLogger(__name__)
54
+
55
+ # Cached health status
56
+ self._cached_daemon_is_ready: bool = False
57
+ self._cached_daemon_message: str | None = None
58
+ self._cached_daemon_status: str = "not_running"
59
+ self._cached_daemon_error: str | None = None
60
+
61
+ # Threading state
62
+ self._health_check_timer: threading.Timer | None = None
63
+ self._health_check_lock = threading.Lock()
64
+ self._is_shutdown: bool = False
65
+
66
+ def start(self) -> None:
67
+ """
68
+ Start background health check monitoring.
69
+
70
+ Idempotent - safe to call multiple times.
71
+ """
72
+ with self._health_check_lock:
73
+ if self._health_check_timer is not None:
74
+ return # Already running
75
+ if self._is_shutdown:
76
+ return # Already shutdown
77
+
78
+ def health_check_loop() -> None:
79
+ """Background health check loop."""
80
+ try:
81
+ # Update daemon status cache
82
+ # check_status() returns tuple: (is_ready, message, status, error)
83
+ is_ready, message, status, error = self._daemon_client.check_status()
84
+ with self._health_check_lock:
85
+ self._cached_daemon_is_ready = is_ready
86
+ self._cached_daemon_message = message
87
+ self._cached_daemon_status = status
88
+ self._cached_daemon_error = error
89
+ except Exception as e:
90
+ # Daemon not responding is expected when stopped, log at debug level
91
+ self.logger.debug(f"Health check failed: {e}", exc_info=True)
92
+ with self._health_check_lock:
93
+ self._cached_daemon_is_ready = False
94
+ self._cached_daemon_status = "not_running"
95
+ self._cached_daemon_error = str(e)
96
+ finally:
97
+ # Schedule next check only if not shutting down
98
+ with self._health_check_lock:
99
+ if not self._is_shutdown:
100
+ self._health_check_timer = threading.Timer(
101
+ self._health_check_interval,
102
+ health_check_loop,
103
+ )
104
+ self._health_check_timer.daemon = True
105
+ self._health_check_timer.start()
106
+
107
+ # Start first check immediately
108
+ self._health_check_timer = threading.Timer(0, health_check_loop)
109
+ self._health_check_timer.daemon = True
110
+ self._health_check_timer.start()
111
+
112
+ def stop(self) -> None:
113
+ """
114
+ Stop background health check monitoring.
115
+
116
+ Cancels any pending timer and prevents new timers from being scheduled.
117
+ Safe to call multiple times.
118
+ """
119
+ with self._health_check_lock:
120
+ self._is_shutdown = True
121
+ if self._health_check_timer is not None:
122
+ self._health_check_timer.cancel()
123
+ self._health_check_timer = None
124
+
125
+ def get_cached_status(self) -> tuple[bool, str | None, str, str | None]:
126
+ """
127
+ Get cached daemon status without making HTTP call.
128
+
129
+ Returns:
130
+ Tuple of (is_ready, message, status, error) where:
131
+ - is_ready: True if daemon is healthy
132
+ - message: Human-readable status message
133
+ - status: One of: "ready", "not_running", "cannot_access"
134
+ - error: Error details if status != "ready"
135
+ """
136
+ with self._health_check_lock:
137
+ return (
138
+ self._cached_daemon_is_ready,
139
+ self._cached_daemon_message,
140
+ self._cached_daemon_status,
141
+ self._cached_daemon_error,
142
+ )
143
+
144
+ def check_now(self) -> bool:
145
+ """
146
+ Perform immediate health check (not cached).
147
+
148
+ Makes a fresh HTTP call to check daemon status and updates the cache.
149
+ Used for retry logic when cached status indicates daemon is unavailable.
150
+
151
+ Returns:
152
+ True if daemon is healthy, False otherwise
153
+ """
154
+ try:
155
+ is_ready, message, status, error = self._daemon_client.check_status()
156
+ with self._health_check_lock:
157
+ self._cached_daemon_is_ready = is_ready
158
+ self._cached_daemon_message = message
159
+ self._cached_daemon_status = status
160
+ self._cached_daemon_error = error
161
+ return is_ready
162
+ except Exception as e:
163
+ self.logger.debug(f"Immediate health check failed: {e}")
164
+ with self._health_check_lock:
165
+ self._cached_daemon_is_ready = False
166
+ self._cached_daemon_status = "not_running"
167
+ self._cached_daemon_error = str(e)
168
+ return False
169
+
170
+
171
+ __all__ = ["HealthMonitor"]