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,723 @@
1
+ """
2
+ Event handlers module for hook event processing.
3
+
4
+ This module is extracted from hook_manager.py using Strangler Fig pattern.
5
+ It provides centralized event handler registration and dispatch.
6
+
7
+ Classes:
8
+ EventHandlers: Manages event handler registration and dispatch.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from collections.abc import Callable
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from gobby.hooks.events import HookEvent, HookEventType, HookResponse
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.hooks.session_coordinator import SessionCoordinator
21
+ from gobby.sessions.manager import SessionManager
22
+ from gobby.sessions.summary import SummaryFileGenerator
23
+ from gobby.storage.session_messages import LocalSessionMessageManager
24
+ from gobby.storage.session_tasks import SessionTaskManager
25
+ from gobby.storage.sessions import LocalSessionManager
26
+ from gobby.storage.tasks import LocalTaskManager
27
+ from gobby.workflows.hooks import WorkflowHookHandler
28
+
29
+
30
+ class EventHandlers:
31
+ """
32
+ Manages event handler registration and dispatch.
33
+
34
+ Provides handler methods for all HookEventType values and a registration
35
+ mechanism for looking up handlers by event type.
36
+
37
+ Extracted from HookManager to separate event handling concerns.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ session_manager: SessionManager | None = None,
43
+ workflow_handler: WorkflowHookHandler | None = None,
44
+ session_storage: LocalSessionManager | None = None,
45
+ session_task_manager: SessionTaskManager | None = None,
46
+ message_processor: Any | None = None,
47
+ summary_file_generator: SummaryFileGenerator | None = None,
48
+ task_manager: LocalTaskManager | None = None,
49
+ session_coordinator: SessionCoordinator | None = None,
50
+ message_manager: LocalSessionMessageManager | None = None,
51
+ get_machine_id: Callable[[], str] | None = None,
52
+ resolve_project_id: Callable[[str | None, str | None], str] | None = None,
53
+ logger: logging.Logger | None = None,
54
+ ) -> None:
55
+ """
56
+ Initialize EventHandlers.
57
+
58
+ Args:
59
+ session_manager: SessionManager for session operations
60
+ workflow_handler: WorkflowHookHandler for lifecycle workflows
61
+ session_storage: LocalSessionManager for session storage
62
+ session_task_manager: SessionTaskManager for session-task links
63
+ message_processor: SessionMessageProcessor for message handling
64
+ summary_file_generator: SummaryFileGenerator for summaries
65
+ task_manager: LocalTaskManager for task operations
66
+ session_coordinator: SessionCoordinator for session tracking
67
+ message_manager: LocalSessionMessageManager for messages
68
+ get_machine_id: Function to get machine ID
69
+ resolve_project_id: Function to resolve project ID from cwd
70
+ logger: Optional logger instance
71
+ """
72
+ self._session_manager = session_manager
73
+ self._workflow_handler = workflow_handler
74
+ self._session_storage = session_storage
75
+ self._session_task_manager = session_task_manager
76
+ self._message_processor = message_processor
77
+ self._summary_file_generator = summary_file_generator
78
+ self._task_manager = task_manager
79
+ self._session_coordinator = session_coordinator
80
+ self._message_manager = message_manager
81
+ self._get_machine_id = get_machine_id or (lambda: "unknown-machine")
82
+ self._resolve_project_id = resolve_project_id or (lambda p, c: p or "")
83
+ self.logger = logger or logging.getLogger(__name__)
84
+
85
+ # Build handler map
86
+ self._handler_map: dict[HookEventType, Callable[[HookEvent], HookResponse]] = {
87
+ HookEventType.SESSION_START: self.handle_session_start,
88
+ HookEventType.SESSION_END: self.handle_session_end,
89
+ HookEventType.BEFORE_AGENT: self.handle_before_agent,
90
+ HookEventType.AFTER_AGENT: self.handle_after_agent,
91
+ HookEventType.BEFORE_TOOL: self.handle_before_tool,
92
+ HookEventType.AFTER_TOOL: self.handle_after_tool,
93
+ HookEventType.PRE_COMPACT: self.handle_pre_compact,
94
+ HookEventType.SUBAGENT_START: self.handle_subagent_start,
95
+ HookEventType.SUBAGENT_STOP: self.handle_subagent_stop,
96
+ HookEventType.NOTIFICATION: self.handle_notification,
97
+ HookEventType.BEFORE_TOOL_SELECTION: self.handle_before_tool_selection,
98
+ HookEventType.BEFORE_MODEL: self.handle_before_model,
99
+ HookEventType.AFTER_MODEL: self.handle_after_model,
100
+ HookEventType.PERMISSION_REQUEST: self.handle_permission_request,
101
+ HookEventType.STOP: self.handle_stop,
102
+ }
103
+
104
+ def get_handler(
105
+ self, event_type: HookEventType | str
106
+ ) -> Callable[[HookEvent], HookResponse] | None:
107
+ """
108
+ Get handler for an event type.
109
+
110
+ Args:
111
+ event_type: The event type to get handler for
112
+
113
+ Returns:
114
+ Handler callable or None if not found
115
+ """
116
+ if isinstance(event_type, str):
117
+ try:
118
+ event_type = HookEventType(event_type)
119
+ except ValueError:
120
+ return None
121
+ return self._handler_map.get(event_type)
122
+
123
+ def get_handler_map(self) -> dict[HookEventType, Callable[[HookEvent], HookResponse]]:
124
+ """
125
+ Get a copy of the handler map.
126
+
127
+ Returns:
128
+ Copy of handler map (modifications don't affect internal state)
129
+ """
130
+ return dict(self._handler_map)
131
+
132
+ # ==================== SESSION HANDLERS ====================
133
+
134
+ def handle_session_start(self, event: HookEvent) -> HookResponse:
135
+ """
136
+ Handle SESSION_START event.
137
+
138
+ Register session and execute session-handoff workflow.
139
+ """
140
+ external_id = event.session_id
141
+ input_data = event.data
142
+ transcript_path = input_data.get("transcript_path")
143
+ cli_source = event.source.value
144
+ cwd = input_data.get("cwd")
145
+ session_source = input_data.get("source", "startup")
146
+
147
+ # Resolve project_id (auto-creates if needed)
148
+ project_id = self._resolve_project_id(input_data.get("project_id"), cwd)
149
+ # Always use Gobby's machine_id for cross-CLI consistency
150
+ machine_id = self._get_machine_id()
151
+
152
+ self.logger.debug(
153
+ f"SESSION_START: cli={cli_source}, project={project_id}, source={session_source}"
154
+ )
155
+
156
+ # Step 0: Check if this is a pre-created session (terminal mode agent)
157
+ # When we spawn an agent in terminal mode, we pass --session-id <internal_id>
158
+ # to Claude, so external_id here might actually be our internal session ID
159
+ existing_session = None
160
+ if self._session_storage:
161
+ try:
162
+ # Try to find by internal ID first (terminal mode case)
163
+ existing_session = self._session_storage.get(external_id)
164
+ if existing_session:
165
+ self.logger.info(
166
+ f"Found pre-created session {external_id}, updating instead of creating"
167
+ )
168
+ # Update the session with actual runtime info
169
+ self._session_storage.update(
170
+ session_id=existing_session.id,
171
+ jsonl_path=transcript_path,
172
+ status="active",
173
+ )
174
+ # Return early with the pre-created session's context
175
+ session_id: str | None = existing_session.id
176
+ parent_session_id = existing_session.parent_session_id
177
+
178
+ # Track registered session
179
+ if transcript_path and self._session_coordinator:
180
+ try:
181
+ self._session_coordinator.register_session(external_id)
182
+ except Exception as e:
183
+ self.logger.error(f"Failed to setup session tracking: {e}")
184
+
185
+ # Start the agent run if this is a terminal-mode agent session
186
+ if existing_session.agent_run_id and self._session_coordinator:
187
+ try:
188
+ self._session_coordinator.start_agent_run(existing_session.agent_run_id)
189
+ except Exception as e:
190
+ self.logger.warning(f"Failed to start agent run: {e}")
191
+
192
+ # Update event metadata
193
+ event.metadata["_platform_session_id"] = session_id
194
+
195
+ # Register with Message Processor
196
+ if self._message_processor and transcript_path:
197
+ try:
198
+ self._message_processor.register_session(
199
+ session_id, transcript_path, source=cli_source
200
+ )
201
+ except Exception as e:
202
+ self.logger.warning(f"Failed to register with message processor: {e}")
203
+
204
+ # Execute lifecycle workflows
205
+ context_parts = [""]
206
+ wf_response = HookResponse(decision="allow", context="")
207
+ if self._workflow_handler:
208
+ try:
209
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
210
+ if wf_response.context:
211
+ context_parts.append(wf_response.context)
212
+ except Exception as e:
213
+ self.logger.warning(f"Workflow error: {e}")
214
+
215
+ # Build system message (terminal display only)
216
+ system_message = "\nSession enhanced by gobby."
217
+ if parent_session_id:
218
+ context_parts.append(f"Parent session: {parent_session_id}")
219
+ if wf_response.system_message:
220
+ system_message += f"\n\n{wf_response.system_message}"
221
+
222
+ return HookResponse(
223
+ decision="allow",
224
+ context="\n".join(context_parts) if context_parts else None,
225
+ system_message=system_message,
226
+ metadata={
227
+ "session_id": session_id,
228
+ "parent_session_id": parent_session_id,
229
+ "machine_id": machine_id,
230
+ "project_id": existing_session.project_id,
231
+ "external_id": external_id,
232
+ "task_id": event.task_id,
233
+ "is_pre_created": True,
234
+ },
235
+ )
236
+ except Exception as e:
237
+ self.logger.debug(f"No pre-created session found: {e}")
238
+
239
+ # Step 1: Find parent session if this is a handoff (source='clear' only)
240
+ parent_session_id = None
241
+ if session_source == "clear" and self._session_storage:
242
+ try:
243
+ parent = self._session_storage.find_parent(
244
+ machine_id=machine_id,
245
+ project_id=project_id,
246
+ source=cli_source,
247
+ status="handoff_ready",
248
+ )
249
+ if parent:
250
+ parent_session_id = parent.id
251
+ self.logger.debug(f"Found parent session: {parent_session_id}")
252
+ except Exception as e:
253
+ self.logger.warning(f"Error finding parent session: {e}")
254
+
255
+ # Step 2: Register new session with parent if found
256
+ # Extract terminal context (injected by hook_dispatcher for terminal correlation)
257
+ terminal_context = input_data.get("terminal_context")
258
+ session_id = None
259
+ if self._session_manager:
260
+ session_id = self._session_manager.register_session(
261
+ external_id=external_id,
262
+ machine_id=machine_id,
263
+ project_id=project_id,
264
+ parent_session_id=parent_session_id,
265
+ jsonl_path=transcript_path,
266
+ source=cli_source,
267
+ project_path=cwd,
268
+ terminal_context=terminal_context,
269
+ )
270
+
271
+ # Step 2b: Mark parent session as expired after successful handoff
272
+ if parent_session_id and self._session_manager:
273
+ try:
274
+ self._session_manager.mark_session_expired(parent_session_id)
275
+ self.logger.debug(f"Marked parent session {parent_session_id} as expired")
276
+ except Exception as e:
277
+ self.logger.warning(f"Failed to mark parent session as expired: {e}")
278
+
279
+ # Step 3: Track registered session
280
+ if transcript_path and self._session_coordinator:
281
+ try:
282
+ self._session_coordinator.register_session(external_id)
283
+ except Exception as e:
284
+ self.logger.error(f"Failed to setup session tracking: {e}", exc_info=True)
285
+
286
+ # Step 4: Update event metadata with the newly registered session_id
287
+ event.metadata["_platform_session_id"] = session_id
288
+ if parent_session_id:
289
+ event.metadata["_parent_session_id"] = parent_session_id
290
+
291
+ # Step 5: Register with Message Processor
292
+ if self._message_processor and transcript_path and session_id:
293
+ try:
294
+ self._message_processor.register_session(
295
+ session_id, transcript_path, source=cli_source
296
+ )
297
+ except Exception as e:
298
+ self.logger.warning(f"Failed to register session with message processor: {e}")
299
+
300
+ # Step 6: Execute lifecycle workflows
301
+ context_parts = [""]
302
+ wf_response = HookResponse(decision="allow", context="")
303
+ if self._workflow_handler:
304
+ try:
305
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
306
+ if wf_response.context:
307
+ context_parts.append(wf_response.context)
308
+ except Exception as e:
309
+ self.logger.warning(f"Workflow error: {e}")
310
+
311
+ if parent_session_id:
312
+ context_parts.append(f"Parent session: {parent_session_id}")
313
+
314
+ # Build system message (terminal display only)
315
+ system_message = "\nSession enhanced by gobby."
316
+ if wf_response.system_message:
317
+ system_message += f"\n\n{wf_response.system_message}"
318
+
319
+ # Inject active task context if available
320
+ if event.task_id:
321
+ task_title = event.metadata.get("_task_title", "Unknown Task")
322
+ context_parts.append("\n## Active Task Context\n")
323
+ context_parts.append(f"You are working on task: {task_title} ({event.task_id})")
324
+
325
+ # Build metadata with terminal context (filter out nulls)
326
+ metadata: dict[str, Any] = {
327
+ "session_id": session_id,
328
+ "parent_session_id": parent_session_id,
329
+ "machine_id": machine_id,
330
+ "project_id": project_id,
331
+ "external_id": external_id,
332
+ "task_id": event.task_id,
333
+ }
334
+ if terminal_context:
335
+ # Only include non-null terminal values
336
+ for key, value in terminal_context.items():
337
+ if value is not None:
338
+ metadata[f"terminal_{key}"] = value
339
+
340
+ return HookResponse(
341
+ decision="allow",
342
+ context="\n".join(context_parts) if context_parts else None,
343
+ system_message=system_message,
344
+ metadata=metadata,
345
+ )
346
+
347
+ def handle_session_end(self, event: HookEvent) -> HookResponse:
348
+ """Handle SESSION_END event."""
349
+ from gobby.tasks.commits import auto_link_commits
350
+
351
+ external_id = event.session_id
352
+ session_id = event.metadata.get("_platform_session_id")
353
+
354
+ if session_id:
355
+ self.logger.debug(f"SESSION_END: session {session_id}")
356
+ else:
357
+ self.logger.warning(f"SESSION_END: session_id not found for external_id={external_id}")
358
+
359
+ # If not in mapping, query database
360
+ if not session_id and external_id and self._session_manager:
361
+ self.logger.debug(f"external_id {external_id} not in mapping, querying database")
362
+ # Resolve context for lookup
363
+ machine_id = self._get_machine_id()
364
+ cwd = event.data.get("cwd")
365
+ project_id = self._resolve_project_id(event.data.get("project_id"), cwd)
366
+ # Lookup with full composite key
367
+ session_id = self._session_manager.lookup_session_id(
368
+ external_id,
369
+ source=event.source.value,
370
+ machine_id=machine_id,
371
+ project_id=project_id,
372
+ )
373
+
374
+ # Ensure session_id is available in event metadata for workflow actions
375
+ if session_id and not event.metadata.get("_platform_session_id"):
376
+ event.metadata["_platform_session_id"] = session_id
377
+
378
+ # Execute lifecycle workflow triggers
379
+ if self._workflow_handler:
380
+ try:
381
+ self._workflow_handler.handle_all_lifecycles(event)
382
+ except Exception as e:
383
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
384
+
385
+ # Auto-link commits made during this session to tasks
386
+ if session_id and self._session_storage and self._task_manager:
387
+ try:
388
+ session = self._session_storage.get(session_id)
389
+ if session:
390
+ cwd = event.data.get("cwd")
391
+ link_result = auto_link_commits(
392
+ task_manager=self._task_manager,
393
+ since=session.created_at,
394
+ cwd=cwd,
395
+ )
396
+ if link_result.total_linked > 0:
397
+ self.logger.info(
398
+ f"Auto-linked {link_result.total_linked} commits to tasks: "
399
+ f"{list(link_result.linked_tasks.keys())}"
400
+ )
401
+ except Exception as e:
402
+ self.logger.warning(f"Failed to auto-link session commits: {e}")
403
+
404
+ # Complete agent run if this is a terminal-mode agent session
405
+ if session_id and self._session_storage and self._session_coordinator:
406
+ try:
407
+ session = self._session_storage.get(session_id)
408
+ if session and session.agent_run_id:
409
+ self._session_coordinator.complete_agent_run(session)
410
+ except Exception as e:
411
+ self.logger.warning(f"Failed to complete agent run: {e}")
412
+
413
+ # Generate independent session summary file
414
+ if self._summary_file_generator:
415
+ try:
416
+ summary_input = {
417
+ "session_id": external_id,
418
+ "transcript_path": event.data.get("transcript_path"),
419
+ }
420
+ self._summary_file_generator.generate_session_summary(
421
+ session_id=session_id or external_id,
422
+ input_data=summary_input,
423
+ )
424
+ except Exception as e:
425
+ self.logger.error(f"Failed to generate failover summary: {e}")
426
+
427
+ # Unregister from message processor
428
+ if self._message_processor and (session_id or external_id):
429
+ try:
430
+ target_id = session_id or external_id
431
+ self._message_processor.unregister_session(target_id)
432
+ except Exception as e:
433
+ self.logger.warning(f"Failed to unregister session from message processor: {e}")
434
+
435
+ return HookResponse(decision="allow")
436
+
437
+ # ==================== AGENT HANDLERS ====================
438
+
439
+ def handle_before_agent(self, event: HookEvent) -> HookResponse:
440
+ """Handle BEFORE_AGENT event (user prompt submit)."""
441
+ input_data = event.data
442
+ prompt = input_data.get("prompt", "")
443
+ transcript_path = input_data.get("transcript_path")
444
+ session_id = event.metadata.get("_platform_session_id")
445
+
446
+ context_parts = []
447
+
448
+ if session_id:
449
+ self.logger.debug(f"BEFORE_AGENT: session {session_id}")
450
+ self.logger.debug(f" Prompt: {prompt[:100]}...")
451
+
452
+ # Update status to active (unless /clear or /exit)
453
+ prompt_lower = prompt.strip().lower()
454
+ if prompt_lower not in ("/clear", "/exit") and self._session_manager:
455
+ try:
456
+ self._session_manager.update_session_status(session_id, "active")
457
+ if self._session_storage:
458
+ self._session_storage.reset_transcript_processed(session_id)
459
+ except Exception as e:
460
+ self.logger.warning(f"Failed to update session status: {e}")
461
+
462
+ # Handle /clear command - lifecycle workflows handle handoff
463
+ if prompt_lower in ("/clear", "/exit") and transcript_path:
464
+ self.logger.debug(f"Detected {prompt_lower} - lifecycle workflows handle handoff")
465
+
466
+ # Execute lifecycle workflow triggers
467
+ if self._workflow_handler:
468
+ try:
469
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
470
+ if wf_response.context:
471
+ context_parts.append(wf_response.context)
472
+ if wf_response.decision != "allow":
473
+ return wf_response
474
+ except Exception as e:
475
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
476
+
477
+ return HookResponse(
478
+ decision="allow",
479
+ context="\n\n".join(context_parts) if context_parts else None,
480
+ )
481
+
482
+ def handle_after_agent(self, event: HookEvent) -> HookResponse:
483
+ """Handle AFTER_AGENT event."""
484
+ session_id = event.metadata.get("_platform_session_id")
485
+ cli_source = event.source.value
486
+
487
+ if session_id:
488
+ self.logger.debug(f"AFTER_AGENT: session {session_id}, cli={cli_source}")
489
+ if self._session_manager:
490
+ try:
491
+ self._session_manager.update_session_status(session_id, "paused")
492
+ except Exception as e:
493
+ self.logger.warning(f"Failed to update session status: {e}")
494
+ else:
495
+ self.logger.debug(f"AFTER_AGENT: cli={cli_source}")
496
+
497
+ # Execute lifecycle workflow triggers
498
+ if self._workflow_handler:
499
+ try:
500
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
501
+ if wf_response.decision != "allow":
502
+ return wf_response
503
+ if wf_response.context:
504
+ return wf_response
505
+ except Exception as e:
506
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
507
+
508
+ return HookResponse(decision="allow")
509
+
510
+ # ==================== TOOL HANDLERS ====================
511
+
512
+ def handle_before_tool(self, event: HookEvent) -> HookResponse:
513
+ """Handle BEFORE_TOOL event."""
514
+ input_data = event.data
515
+ tool_name = input_data.get("tool_name", "unknown")
516
+ session_id = event.metadata.get("_platform_session_id")
517
+
518
+ if session_id:
519
+ self.logger.debug(f"BEFORE_TOOL: {tool_name}, session {session_id}")
520
+ else:
521
+ self.logger.debug(f"BEFORE_TOOL: {tool_name}")
522
+
523
+ context_parts = []
524
+
525
+ # Execute lifecycle workflow triggers
526
+ if self._workflow_handler:
527
+ try:
528
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
529
+ if wf_response.context:
530
+ context_parts.append(wf_response.context)
531
+ if wf_response.decision != "allow":
532
+ return wf_response
533
+ except Exception as e:
534
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
535
+
536
+ return HookResponse(
537
+ decision="allow",
538
+ context="\n\n".join(context_parts) if context_parts else None,
539
+ )
540
+
541
+ def handle_after_tool(self, event: HookEvent) -> HookResponse:
542
+ """Handle AFTER_TOOL event."""
543
+ input_data = event.data
544
+ tool_name = input_data.get("tool_name", "unknown")
545
+ session_id = event.metadata.get("_platform_session_id")
546
+ is_failure = event.metadata.get("is_failure", False)
547
+
548
+ status = "FAIL" if is_failure else "OK"
549
+ if session_id:
550
+ self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
551
+ else:
552
+ self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
553
+
554
+ context_parts = []
555
+
556
+ # Execute lifecycle workflow triggers
557
+ if self._workflow_handler:
558
+ try:
559
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
560
+ if wf_response.context:
561
+ context_parts.append(wf_response.context)
562
+ if wf_response.decision != "allow":
563
+ return wf_response
564
+ except Exception as e:
565
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
566
+
567
+ return HookResponse(
568
+ decision="allow",
569
+ context="\n\n".join(context_parts) if context_parts else None,
570
+ )
571
+
572
+ # ==================== STOP HANDLER ====================
573
+
574
+ def handle_stop(self, event: HookEvent) -> HookResponse:
575
+ """Handle STOP event (Claude Code only)."""
576
+ session_id = event.metadata.get("_platform_session_id")
577
+ cli_source = event.source.value
578
+
579
+ self.logger.debug(f"STOP: session {session_id}, cli={cli_source}")
580
+
581
+ # Execute lifecycle workflow triggers for on_stop
582
+ if self._workflow_handler:
583
+ try:
584
+ wf_response = self._workflow_handler.handle_all_lifecycles(event)
585
+ if wf_response.decision != "allow":
586
+ return wf_response
587
+ if wf_response.context:
588
+ return wf_response
589
+ except Exception as e:
590
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
591
+
592
+ return HookResponse(decision="allow")
593
+
594
+ # ==================== COMPACT HANDLER ====================
595
+
596
+ def handle_pre_compact(self, event: HookEvent) -> HookResponse:
597
+ """Handle PRE_COMPACT event."""
598
+ trigger = event.data.get("trigger", "auto")
599
+ session_id = event.metadata.get("_platform_session_id")
600
+
601
+ if session_id:
602
+ self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id}")
603
+ # Mark session as handoff_ready so it can be found as parent after compact
604
+ if self._session_manager:
605
+ self._session_manager.update_session_status(session_id, "handoff_ready")
606
+ else:
607
+ self.logger.debug(f"PRE_COMPACT ({trigger})")
608
+
609
+ # Execute lifecycle workflows
610
+ if self._workflow_handler:
611
+ try:
612
+ return self._workflow_handler.handle_all_lifecycles(event)
613
+ except Exception as e:
614
+ self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
615
+
616
+ return HookResponse(decision="allow")
617
+
618
+ # ==================== SUBAGENT HANDLERS ====================
619
+
620
+ def handle_subagent_start(self, event: HookEvent) -> HookResponse:
621
+ """Handle SUBAGENT_START event."""
622
+ input_data = event.data
623
+ session_id = event.metadata.get("_platform_session_id")
624
+ agent_id = input_data.get("agent_id")
625
+ subagent_id = input_data.get("subagent_id")
626
+
627
+ log_msg = f"SUBAGENT_START: session {session_id}" if session_id else "SUBAGENT_START"
628
+ if agent_id:
629
+ log_msg += f", agent_id={agent_id}"
630
+ if subagent_id:
631
+ log_msg += f", subagent_id={subagent_id}"
632
+ self.logger.debug(log_msg)
633
+
634
+ return HookResponse(decision="allow")
635
+
636
+ def handle_subagent_stop(self, event: HookEvent) -> HookResponse:
637
+ """Handle SUBAGENT_STOP event."""
638
+ session_id = event.metadata.get("_platform_session_id")
639
+
640
+ if session_id:
641
+ self.logger.debug(f"SUBAGENT_STOP: session {session_id}")
642
+ else:
643
+ self.logger.debug("SUBAGENT_STOP")
644
+
645
+ return HookResponse(decision="allow")
646
+
647
+ # ==================== NOTIFICATION HANDLER ====================
648
+
649
+ def handle_notification(self, event: HookEvent) -> HookResponse:
650
+ """Handle NOTIFICATION event."""
651
+ input_data = event.data
652
+ notification_type = (
653
+ input_data.get("notification_type")
654
+ or input_data.get("notificationType")
655
+ or input_data.get("type")
656
+ or "general"
657
+ )
658
+ session_id = event.metadata.get("_platform_session_id")
659
+
660
+ if session_id:
661
+ self.logger.debug(f"NOTIFICATION ({notification_type}): session {session_id}")
662
+ if self._session_manager:
663
+ try:
664
+ self._session_manager.update_session_status(session_id, "paused")
665
+ except Exception as e:
666
+ self.logger.warning(f"Failed to update session status: {e}")
667
+ else:
668
+ self.logger.debug(f"NOTIFICATION ({notification_type})")
669
+
670
+ return HookResponse(decision="allow")
671
+
672
+ # ==================== PERMISSION HANDLER ====================
673
+
674
+ def handle_permission_request(self, event: HookEvent) -> HookResponse:
675
+ """Handle PERMISSION_REQUEST event (Claude Code only)."""
676
+ input_data = event.data
677
+ session_id = event.metadata.get("_platform_session_id")
678
+ permission_type = input_data.get("permission_type", "unknown")
679
+
680
+ if session_id:
681
+ self.logger.debug(f"PERMISSION_REQUEST ({permission_type}): session {session_id}")
682
+ else:
683
+ self.logger.debug(f"PERMISSION_REQUEST ({permission_type})")
684
+
685
+ return HookResponse(decision="allow")
686
+
687
+ # ==================== GEMINI-ONLY HANDLERS ====================
688
+
689
+ def handle_before_tool_selection(self, event: HookEvent) -> HookResponse:
690
+ """Handle BEFORE_TOOL_SELECTION event (Gemini only)."""
691
+ session_id = event.metadata.get("_platform_session_id")
692
+
693
+ if session_id:
694
+ self.logger.debug(f"BEFORE_TOOL_SELECTION: session {session_id}")
695
+ else:
696
+ self.logger.debug("BEFORE_TOOL_SELECTION")
697
+
698
+ return HookResponse(decision="allow")
699
+
700
+ def handle_before_model(self, event: HookEvent) -> HookResponse:
701
+ """Handle BEFORE_MODEL event (Gemini only)."""
702
+ session_id = event.metadata.get("_platform_session_id")
703
+
704
+ if session_id:
705
+ self.logger.debug(f"BEFORE_MODEL: session {session_id}")
706
+ else:
707
+ self.logger.debug("BEFORE_MODEL")
708
+
709
+ return HookResponse(decision="allow")
710
+
711
+ def handle_after_model(self, event: HookEvent) -> HookResponse:
712
+ """Handle AFTER_MODEL event (Gemini only)."""
713
+ session_id = event.metadata.get("_platform_session_id")
714
+
715
+ if session_id:
716
+ self.logger.debug(f"AFTER_MODEL: session {session_id}")
717
+ else:
718
+ self.logger.debug("AFTER_MODEL")
719
+
720
+ return HookResponse(decision="allow")
721
+
722
+
723
+ __all__ = ["EventHandlers"]