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,396 @@
1
+ """
2
+ Session coordinator module for session lifecycle management.
3
+
4
+ This module is extracted from hook_manager.py using Strangler Fig pattern.
5
+ It provides centralized session registration tracking, message caching,
6
+ and lifecycle coordination.
7
+
8
+ Classes:
9
+ SessionCoordinator: Coordinates session lifecycle operations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import threading
16
+ import time
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.storage.agents import LocalAgentRunManager
21
+ from gobby.storage.sessions import LocalSessionManager
22
+ from gobby.storage.worktrees import LocalWorktreeManager
23
+
24
+
25
+ class SessionCoordinator:
26
+ """
27
+ Coordinates session lifecycle operations.
28
+
29
+ Provides centralized tracking for:
30
+ - Session registration with daemon
31
+ - Title synthesis status
32
+ - Agent message caching between hooks
33
+ - Session lifecycle transitions (completion, cleanup)
34
+
35
+ Thread-safe for concurrent operations.
36
+
37
+ Extracted from HookManager to separate session coordination concerns.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ session_storage: LocalSessionManager | None = None,
43
+ message_processor: Any | None = None,
44
+ agent_run_manager: LocalAgentRunManager | None = None,
45
+ worktree_manager: LocalWorktreeManager | None = None,
46
+ logger: logging.Logger | None = None,
47
+ ) -> None:
48
+ """
49
+ Initialize SessionCoordinator.
50
+
51
+ Args:
52
+ session_storage: LocalSessionManager for session queries
53
+ message_processor: SessionMessageProcessor for message registration
54
+ agent_run_manager: LocalAgentRunManager for agent run completion
55
+ worktree_manager: LocalWorktreeManager for worktree release
56
+ logger: Optional logger instance
57
+ """
58
+ self._session_storage = session_storage
59
+ self._message_processor = message_processor
60
+ self._agent_run_manager = agent_run_manager
61
+ self._worktree_manager = worktree_manager
62
+ self.logger = logger or logging.getLogger(__name__)
63
+
64
+ # Session registration tracking (to avoid noisy logs)
65
+ # Tracks which sessions have been registered with daemon
66
+ self._registered_sessions: set[str] = set()
67
+ self._registered_sessions_lock = threading.Lock()
68
+
69
+ # Session title synthesis tracking
70
+ # Tracks which sessions have had titles synthesized
71
+ self._title_synthesized_sessions: set[str] = set()
72
+ self._title_synthesized_lock = threading.Lock()
73
+
74
+ # Agent message cache (session_id -> (message, timestamp))
75
+ # Used to pass agent responses from stop hook to post-tool-use hook
76
+ self._agent_message_cache: dict[str, tuple[str, float]] = {}
77
+ self._cache_lock = threading.Lock()
78
+
79
+ # Lock for session lookups to prevent race conditions (double firing)
80
+ self._lookup_lock = threading.Lock()
81
+
82
+ # ==================== REGISTRATION TRACKING ====================
83
+
84
+ def register_session(self, session_id: str) -> None:
85
+ """
86
+ Mark a session as registered with the daemon.
87
+
88
+ Args:
89
+ session_id: The session ID to register
90
+ """
91
+ with self._registered_sessions_lock:
92
+ self._registered_sessions.add(session_id)
93
+
94
+ def unregister_session(self, session_id: str) -> None:
95
+ """
96
+ Remove a session from registration tracking.
97
+
98
+ Args:
99
+ session_id: The session ID to unregister
100
+ """
101
+ with self._registered_sessions_lock:
102
+ self._registered_sessions.discard(session_id)
103
+
104
+ def is_registered(self, session_id: str) -> bool:
105
+ """
106
+ Check if a session is registered with the daemon.
107
+
108
+ Args:
109
+ session_id: The session ID to check
110
+
111
+ Returns:
112
+ True if registered, False otherwise
113
+ """
114
+ with self._registered_sessions_lock:
115
+ return session_id in self._registered_sessions
116
+
117
+ def clear_registrations(self) -> None:
118
+ """Clear all session registrations."""
119
+ with self._registered_sessions_lock:
120
+ self._registered_sessions.clear()
121
+
122
+ # ==================== TITLE SYNTHESIS TRACKING ====================
123
+
124
+ def mark_title_synthesized(self, session_id: str) -> None:
125
+ """
126
+ Mark a session as having had its title synthesized.
127
+
128
+ Args:
129
+ session_id: The session ID to mark
130
+ """
131
+ with self._title_synthesized_lock:
132
+ self._title_synthesized_sessions.add(session_id)
133
+
134
+ def is_title_synthesized(self, session_id: str) -> bool:
135
+ """
136
+ Check if a session has had its title synthesized.
137
+
138
+ Args:
139
+ session_id: The session ID to check
140
+
141
+ Returns:
142
+ True if title has been synthesized, False otherwise
143
+ """
144
+ with self._title_synthesized_lock:
145
+ return session_id in self._title_synthesized_sessions
146
+
147
+ # ==================== MESSAGE CACHING ====================
148
+
149
+ def cache_agent_message(self, session_id: str, message: str) -> None:
150
+ """
151
+ Cache an agent message for later retrieval.
152
+
153
+ Args:
154
+ session_id: The session ID
155
+ message: The message to cache
156
+ """
157
+ with self._cache_lock:
158
+ self._agent_message_cache[session_id] = (message, time.time())
159
+
160
+ def get_cached_message(
161
+ self, session_id: str, max_age_seconds: float | None = None
162
+ ) -> str | None:
163
+ """
164
+ Get a cached agent message.
165
+
166
+ Args:
167
+ session_id: The session ID
168
+ max_age_seconds: Optional maximum age in seconds. If set, returns None
169
+ for messages older than this.
170
+
171
+ Returns:
172
+ The cached message, or None if not found or expired
173
+ """
174
+ with self._cache_lock:
175
+ if session_id not in self._agent_message_cache:
176
+ return None
177
+
178
+ message, timestamp = self._agent_message_cache[session_id]
179
+
180
+ if max_age_seconds is not None:
181
+ age = time.time() - timestamp
182
+ if age > max_age_seconds:
183
+ return None
184
+
185
+ return message
186
+
187
+ def clear_cached_message(self, session_id: str) -> None:
188
+ """
189
+ Clear a cached agent message.
190
+
191
+ Args:
192
+ session_id: The session ID
193
+ """
194
+ with self._cache_lock:
195
+ self._agent_message_cache.pop(session_id, None)
196
+
197
+ # ==================== LOOKUP LOCK ====================
198
+
199
+ def get_lookup_lock(self) -> threading.Lock:
200
+ """
201
+ Get the lookup lock for preventing race conditions.
202
+
203
+ Returns:
204
+ The lookup lock
205
+ """
206
+ return self._lookup_lock
207
+
208
+ # ==================== LIFECYCLE OPERATIONS ====================
209
+
210
+ def reregister_active_sessions(self, limit: int = 1000) -> int:
211
+ """
212
+ Re-register active sessions with the message processor.
213
+
214
+ Called during initialization to restore message processing
215
+ for sessions that were active before a daemon restart.
216
+
217
+ Args:
218
+ limit: Maximum number of sessions to re-register (default 1000).
219
+ Sessions beyond this limit will not be re-registered.
220
+
221
+ Returns:
222
+ Number of sessions successfully re-registered
223
+ """
224
+ if not self._message_processor or not self._session_storage:
225
+ return 0
226
+
227
+ try:
228
+ # Query active sessions from storage
229
+ active_sessions = self._session_storage.list(status="active", limit=limit)
230
+ registered_count = 0
231
+
232
+ for session in active_sessions:
233
+ jsonl_path = getattr(session, "jsonl_path", None)
234
+ if not jsonl_path:
235
+ continue
236
+
237
+ try:
238
+ # Determine source from session (default to claude)
239
+ source = getattr(session, "source", "claude") or "claude"
240
+ self._message_processor.register_session(session.id, jsonl_path, source=source)
241
+ registered_count += 1
242
+ except Exception as e:
243
+ self.logger.warning(f"Failed to re-register session {session.id}: {e}")
244
+
245
+ if registered_count > 0:
246
+ self.logger.info(
247
+ f"Re-registered {registered_count} active sessions with message processor"
248
+ )
249
+
250
+ return registered_count
251
+
252
+ except Exception as e:
253
+ self.logger.warning(f"Failed to re-register active sessions: {e}")
254
+ return 0
255
+
256
+ def start_agent_run(self, agent_run_id: str) -> bool:
257
+ """
258
+ Mark an agent run as started when its terminal-mode session begins.
259
+
260
+ Called from handle_session_start when a pre-created session with an
261
+ agent_run_id is detected. This updates the status from 'pending' to
262
+ 'running' and sets the started_at timestamp.
263
+
264
+ Args:
265
+ agent_run_id: The agent run ID to start
266
+
267
+ Returns:
268
+ True if the run was started, False otherwise
269
+ """
270
+ if not self._agent_run_manager:
271
+ self.logger.debug("start_agent_run: No agent_run_manager, skipping")
272
+ return False
273
+
274
+ try:
275
+ agent_run = self._agent_run_manager.get(agent_run_id)
276
+ if not agent_run:
277
+ self.logger.warning(f"Agent run {agent_run_id} not found")
278
+ return False
279
+
280
+ # Only start if currently pending
281
+ if agent_run.status != "pending":
282
+ self.logger.debug(
283
+ f"Agent run {agent_run_id} not pending (status={agent_run.status}), skipping start"
284
+ )
285
+ return False
286
+
287
+ self._agent_run_manager.start(agent_run_id)
288
+ self.logger.info(f"Started agent run {agent_run_id}")
289
+ return True
290
+
291
+ except Exception as e:
292
+ self.logger.error(f"Failed to start agent run {agent_run_id}: {e}")
293
+ return False
294
+
295
+ def complete_agent_run(self, session: Any) -> None:
296
+ """
297
+ Complete an agent run when its terminal-mode session ends.
298
+
299
+ Updates the agent run status based on session outcome, removes the
300
+ agent from the in-memory running registry, and releases any worktrees
301
+ associated with the session.
302
+
303
+ Args:
304
+ session: Session object with agent_run_id
305
+ """
306
+ # Check for agent_run_id
307
+ agent_run_id = getattr(session, "agent_run_id", None)
308
+ if not agent_run_id:
309
+ return
310
+
311
+ self.logger.debug(f"Completing agent run {agent_run_id} for session {session.id}")
312
+
313
+ # Remove from in-memory running agents registry
314
+ try:
315
+ from gobby.agents.registry import get_running_agent_registry
316
+
317
+ running_registry = get_running_agent_registry()
318
+ removed = running_registry.remove(agent_run_id)
319
+ if removed:
320
+ self.logger.debug(f"Unregistered running agent {agent_run_id} from registry")
321
+ except Exception as e:
322
+ self.logger.warning(f"Failed to unregister agent from running registry: {e}")
323
+
324
+ if not self._agent_run_manager:
325
+ return
326
+
327
+ try:
328
+ agent_run = self._agent_run_manager.get(agent_run_id)
329
+ if not agent_run:
330
+ self.logger.warning(f"Agent run {agent_run_id} not found")
331
+ return
332
+
333
+ # Skip if already completed
334
+ if agent_run.status in ("success", "error", "timeout", "cancelled"):
335
+ self.logger.debug(
336
+ f"Agent run {agent_run_id} already in terminal state: {agent_run.status}"
337
+ )
338
+ return
339
+
340
+ # Use summary as result if available
341
+ result = (
342
+ getattr(session, "summary_markdown", None)
343
+ or getattr(session, "compact_markdown", None)
344
+ or ""
345
+ )
346
+
347
+ # Mark as success
348
+ self._agent_run_manager.complete(
349
+ run_id=agent_run_id,
350
+ result=result,
351
+ tool_calls_count=0,
352
+ turns_used=0,
353
+ )
354
+ self.logger.info(f"Completed agent run {agent_run_id}")
355
+
356
+ except Exception as e:
357
+ self.logger.error(f"Failed to complete agent run {agent_run_id}: {e}")
358
+
359
+ # Release any worktrees associated with this session
360
+ try:
361
+ self.release_session_worktrees(session.id)
362
+ except Exception as e:
363
+ self.logger.warning(f"Failed to release worktrees for session {session.id}: {e}")
364
+
365
+ def release_session_worktrees(self, session_id: str) -> None:
366
+ """
367
+ Release all worktrees claimed by a session.
368
+
369
+ When a session ends, any worktrees it claimed should be released
370
+ so they can be reused by other sessions.
371
+
372
+ Args:
373
+ session_id: The session ID whose worktrees to release
374
+ """
375
+ if not self._worktree_manager:
376
+ return
377
+
378
+ try:
379
+ # Find worktrees owned by this session
380
+ worktrees = self._worktree_manager.list_worktrees(agent_session_id=session_id)
381
+
382
+ for worktree in worktrees:
383
+ try:
384
+ # Release the worktree (sets agent_session_id to NULL)
385
+ self._worktree_manager.release(worktree.id)
386
+ self.logger.debug(f"Released worktree {worktree.id} from session {session_id}")
387
+ except Exception as e:
388
+ self.logger.warning(f"Failed to release worktree {worktree.id}: {e}")
389
+
390
+ if worktrees:
391
+ self.logger.info(f"Released {len(worktrees)} worktree(s) from session {session_id}")
392
+ except Exception as e:
393
+ self.logger.warning(f"Failed to list worktrees for session {session_id}: {e}")
394
+
395
+
396
+ __all__ = ["SessionCoordinator"]
@@ -0,0 +1,268 @@
1
+ """Verification runner for git hooks.
2
+
3
+ Executes configured verification commands (lint, typecheck, tests, etc.) for git hook stages.
4
+ """
5
+
6
+ import logging
7
+ import subprocess # nosec B404 - subprocess needed for verification commands
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from gobby.config.features import HooksConfig, HookStageConfig, ProjectVerificationConfig
13
+ from gobby.utils.project_context import get_hooks_config, get_verification_config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Default timeout for verification commands (5 minutes)
18
+ DEFAULT_TIMEOUT = 300
19
+
20
+
21
+ @dataclass
22
+ class VerificationResult:
23
+ """Result of running a single verification command."""
24
+
25
+ name: str
26
+ command: str
27
+ success: bool
28
+ exit_code: int | None = None
29
+ stdout: str = ""
30
+ stderr: str = ""
31
+ duration_ms: int = 0
32
+ skipped: bool = False
33
+ skip_reason: str | None = None
34
+ error: str | None = None
35
+
36
+
37
+ @dataclass
38
+ class StageResult:
39
+ """Result of running all verification commands for a hook stage."""
40
+
41
+ stage: str
42
+ success: bool
43
+ results: list[VerificationResult] = field(default_factory=list)
44
+ skipped: bool = False
45
+ skip_reason: str | None = None
46
+
47
+ @property
48
+ def failed_count(self) -> int:
49
+ """Number of failed verifications."""
50
+ return sum(1 for r in self.results if not r.success and not r.skipped)
51
+
52
+ @property
53
+ def passed_count(self) -> int:
54
+ """Number of passed verifications."""
55
+ return sum(1 for r in self.results if r.success)
56
+
57
+ @property
58
+ def skipped_count(self) -> int:
59
+ """Number of skipped verifications."""
60
+ return sum(1 for r in self.results if r.skipped)
61
+
62
+
63
+ def run_command(
64
+ name: str,
65
+ command: str,
66
+ cwd: Path,
67
+ timeout: int = DEFAULT_TIMEOUT,
68
+ ) -> VerificationResult:
69
+ """Execute a verification command and return the result.
70
+
71
+ Args:
72
+ name: Name of the verification (e.g., 'lint', 'unit_tests').
73
+ command: The command to execute.
74
+ cwd: Working directory for the command.
75
+ timeout: Maximum execution time in seconds.
76
+
77
+ Returns:
78
+ VerificationResult with command output and status.
79
+ """
80
+ start_time = time.time()
81
+
82
+ try:
83
+ result = subprocess.run(
84
+ command,
85
+ shell=True, # nosec B602 - user-configured verification commands require shell features
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=timeout,
89
+ cwd=cwd,
90
+ )
91
+
92
+ duration_ms = int((time.time() - start_time) * 1000)
93
+
94
+ return VerificationResult(
95
+ name=name,
96
+ command=command,
97
+ success=result.returncode == 0,
98
+ exit_code=result.returncode,
99
+ stdout=result.stdout,
100
+ stderr=result.stderr,
101
+ duration_ms=duration_ms,
102
+ )
103
+
104
+ except subprocess.TimeoutExpired:
105
+ duration_ms = int((time.time() - start_time) * 1000)
106
+ return VerificationResult(
107
+ name=name,
108
+ command=command,
109
+ success=False,
110
+ duration_ms=duration_ms,
111
+ error=f"Command timed out after {timeout} seconds",
112
+ )
113
+
114
+ except Exception as e:
115
+ duration_ms = int((time.time() - start_time) * 1000)
116
+ return VerificationResult(
117
+ name=name,
118
+ command=command,
119
+ success=False,
120
+ duration_ms=duration_ms,
121
+ error=str(e),
122
+ )
123
+
124
+
125
+ class VerificationRunner:
126
+ """Runs verification commands for git hooks.
127
+
128
+ Reads configuration from project.json and executes the appropriate
129
+ commands for each hook stage (pre-commit, pre-push, pre-merge).
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ verification_config: ProjectVerificationConfig | None = None,
135
+ hooks_config: HooksConfig | None = None,
136
+ cwd: Path | None = None,
137
+ ):
138
+ """Initialize VerificationRunner.
139
+
140
+ Args:
141
+ verification_config: Verification commands configuration.
142
+ hooks_config: Git hooks configuration.
143
+ cwd: Working directory (auto-detected if None).
144
+ """
145
+ self.cwd = cwd or Path.cwd()
146
+ self.verification_config = verification_config
147
+ self.hooks_config = hooks_config
148
+
149
+ @classmethod
150
+ def from_project(cls, cwd: Path | None = None) -> "VerificationRunner":
151
+ """Create a VerificationRunner from project configuration.
152
+
153
+ Args:
154
+ cwd: Working directory to search for project config.
155
+
156
+ Returns:
157
+ VerificationRunner instance with loaded configuration.
158
+ """
159
+ cwd = cwd or Path.cwd()
160
+ verification_config = get_verification_config(cwd)
161
+ hooks_config = get_hooks_config(cwd)
162
+ return cls(
163
+ verification_config=verification_config,
164
+ hooks_config=hooks_config,
165
+ cwd=cwd,
166
+ )
167
+
168
+ def run_stage(self, stage: str) -> StageResult:
169
+ """Run all verification commands for a hook stage.
170
+
171
+ Args:
172
+ stage: Hook stage name (e.g., 'pre-commit', 'pre-push', 'pre-merge').
173
+
174
+ Returns:
175
+ StageResult with all verification results.
176
+ """
177
+ # Check if hooks are configured
178
+ if not self.hooks_config:
179
+ return StageResult(
180
+ stage=stage,
181
+ success=True,
182
+ skipped=True,
183
+ skip_reason="No hooks configured in project.json",
184
+ )
185
+
186
+ # Get stage configuration
187
+ stage_config = self.hooks_config.get_stage(stage)
188
+
189
+ # Check if stage is enabled
190
+ if not stage_config.enabled:
191
+ return StageResult(
192
+ stage=stage,
193
+ success=True,
194
+ skipped=True,
195
+ skip_reason=f"Hook stage '{stage}' is disabled",
196
+ )
197
+
198
+ # Check if any commands are configured for this stage
199
+ if not stage_config.run:
200
+ return StageResult(
201
+ stage=stage,
202
+ success=True,
203
+ skipped=True,
204
+ skip_reason=f"No commands configured for '{stage}'",
205
+ )
206
+
207
+ # Check if verification config exists
208
+ if not self.verification_config:
209
+ return StageResult(
210
+ stage=stage,
211
+ success=True,
212
+ skipped=True,
213
+ skip_reason="No verification commands defined in project.json",
214
+ )
215
+
216
+ # Run each command
217
+ results: list[VerificationResult] = []
218
+ overall_success = True
219
+
220
+ for cmd_name in stage_config.run:
221
+ command = self.verification_config.get_command(cmd_name)
222
+
223
+ if not command:
224
+ # Command not defined - skip with warning
225
+ results.append(
226
+ VerificationResult(
227
+ name=cmd_name,
228
+ command="",
229
+ success=True,
230
+ skipped=True,
231
+ skip_reason=f"Command '{cmd_name}' not defined in verification config",
232
+ )
233
+ )
234
+ continue
235
+
236
+ # Run the command
237
+ result = run_command(
238
+ name=cmd_name,
239
+ command=command,
240
+ cwd=self.cwd,
241
+ timeout=stage_config.timeout,
242
+ )
243
+ results.append(result)
244
+
245
+ if not result.success:
246
+ overall_success = False
247
+ if stage_config.fail_fast:
248
+ # Stop on first failure
249
+ break
250
+
251
+ return StageResult(
252
+ stage=stage,
253
+ success=overall_success,
254
+ results=results,
255
+ )
256
+
257
+ def get_stage_config(self, stage: str) -> HookStageConfig | None:
258
+ """Get configuration for a hook stage.
259
+
260
+ Args:
261
+ stage: Hook stage name.
262
+
263
+ Returns:
264
+ HookStageConfig if configured, None otherwise.
265
+ """
266
+ if not self.hooks_config:
267
+ return None
268
+ return self.hooks_config.get_stage(stage)