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,23 @@
1
+ """Orchestration tool modules.
2
+
3
+ Contains decomposed orchestration functionality:
4
+ - orchestrate: Core orchestration tools (orchestrate_ready_tasks)
5
+ - monitor: Status monitoring tools (get_orchestration_status, poll_agent_status)
6
+ - review: Review workflow tools (spawn_review_agent, process_completed_agents)
7
+ - cleanup: Cleanup tools (cleanup_reviewed_worktrees, cleanup_stale_worktrees)
8
+ - utils: Shared utilities
9
+ """
10
+
11
+ from gobby.mcp_proxy.tools.orchestration.cleanup import register_cleanup
12
+ from gobby.mcp_proxy.tools.orchestration.monitor import register_monitor
13
+ from gobby.mcp_proxy.tools.orchestration.orchestrate import register_orchestrator
14
+ from gobby.mcp_proxy.tools.orchestration.review import register_reviewer
15
+ from gobby.mcp_proxy.tools.orchestration.utils import get_current_project_id
16
+
17
+ __all__ = [
18
+ "register_cleanup",
19
+ "register_monitor",
20
+ "register_orchestrator",
21
+ "register_reviewer",
22
+ "get_current_project_id",
23
+ ]
@@ -0,0 +1,619 @@
1
+ """Task orchestration tools: cleanup (cleanup_reviewed_worktrees, cleanup_stale_worktrees)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import UTC, datetime, timedelta
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
10
+
11
+ from .utils import get_current_project_id
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.storage.tasks import LocalTaskManager
15
+ from gobby.storage.worktrees import LocalWorktreeManager
16
+ from gobby.worktrees.git import WorktreeGitManager
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def register_cleanup(
22
+ registry: InternalToolRegistry,
23
+ task_manager: LocalTaskManager,
24
+ worktree_storage: LocalWorktreeManager,
25
+ git_manager: WorktreeGitManager | None = None,
26
+ default_project_id: str | None = None,
27
+ ) -> None:
28
+ """Register cleanup tools."""
29
+
30
+ async def cleanup_reviewed_worktrees(
31
+ parent_session_id: str,
32
+ merge_to_base: bool = True,
33
+ delete_worktrees: bool = True,
34
+ delete_branches: bool = False,
35
+ force: bool = False,
36
+ project_path: str | None = None,
37
+ ) -> dict[str, Any]:
38
+ """
39
+ Clean up worktrees for reviewed agents.
40
+
41
+ After successful review, this tool:
42
+ 1. Merges worktree branch to its base branch (if merge_to_base=True)
43
+ 2. Marks worktree as merged in database
44
+ 3. Deletes the git worktree (if delete_worktrees=True)
45
+ 4. Optionally deletes the branch (if delete_branches=True)
46
+ 5. Updates workflow state by clearing reviewed_agents
47
+
48
+ The base branch is whatever branch the worktree was created from
49
+ (stored in worktree.base_branch), allowing the orchestrator to work
50
+ on any branch (dev, main, feature branches, etc.).
51
+
52
+ Used by the auto-orchestrator workflow's cleanup step.
53
+
54
+ Args:
55
+ parent_session_id: Parent session ID (orchestrator session)
56
+ merge_to_base: Whether to merge branch to base before cleanup
57
+ delete_worktrees: Whether to delete git worktrees
58
+ delete_branches: Whether to delete branches after cleanup
59
+ force: Force deletion even if worktree is dirty
60
+ project_path: Path to project directory
61
+
62
+ Returns:
63
+ Dict with:
64
+ - merged: List of successfully merged branches
65
+ - deleted: List of deleted worktrees
66
+ - failed: List of failed operations with reasons
67
+ - summary: Counts
68
+ """
69
+ if git_manager is None:
70
+ return {
71
+ "success": False,
72
+ "error": "Git manager not configured. Cannot cleanup worktrees.",
73
+ }
74
+
75
+ # Get workflow state
76
+ from gobby.workflows.state_manager import WorkflowStateManager
77
+
78
+ state_manager = WorkflowStateManager(task_manager.db)
79
+ state = state_manager.get_state(parent_session_id)
80
+ if not state:
81
+ return {
82
+ "success": True,
83
+ "merged": [],
84
+ "deleted": [],
85
+ "failed": [],
86
+ "summary": {"merged": 0, "deleted": 0, "failed": 0},
87
+ "message": "No workflow state found",
88
+ }
89
+
90
+ workflow_vars = state.variables
91
+ reviewed_agents = workflow_vars.get("reviewed_agents", [])
92
+
93
+ if not reviewed_agents:
94
+ return {
95
+ "success": True,
96
+ "merged": [],
97
+ "deleted": [],
98
+ "failed": [],
99
+ "summary": {"merged": 0, "deleted": 0, "failed": 0},
100
+ "message": "No reviewed agents to cleanup",
101
+ }
102
+
103
+ merged: list[dict[str, Any]] = []
104
+ deleted: list[dict[str, Any]] = []
105
+ failed: list[dict[str, Any]] = []
106
+ cleaned_agents: list[dict[str, Any]] = []
107
+
108
+ for agent_info in reviewed_agents:
109
+ worktree_id = agent_info.get("worktree_id")
110
+ task_id = agent_info.get("task_id")
111
+ branch_name = agent_info.get("branch_name")
112
+
113
+ if not worktree_id:
114
+ failed.append(
115
+ {
116
+ **agent_info,
117
+ "failure_reason": "Missing worktree_id",
118
+ }
119
+ )
120
+ continue
121
+
122
+ # Get worktree from storage
123
+ worktree = worktree_storage.get(worktree_id)
124
+ if not worktree:
125
+ # Worktree already deleted, consider it cleaned
126
+ cleaned_agents.append(agent_info)
127
+ continue
128
+
129
+ branch = branch_name or worktree.branch_name
130
+ worktree_path = worktree.worktree_path
131
+
132
+ try:
133
+ # Track if at least one successful operation occurred
134
+ operation_succeeded = False
135
+
136
+ # Step 1: Merge branch to base (if enabled)
137
+ merge_succeeded = False
138
+ if merge_to_base:
139
+ # Validate required fields for merge
140
+ if not branch:
141
+ failed.append(
142
+ {
143
+ **agent_info,
144
+ "failure_reason": "Missing branch name for merge operation",
145
+ }
146
+ )
147
+ continue
148
+ if not worktree.base_branch:
149
+ failed.append(
150
+ {
151
+ **agent_info,
152
+ "failure_reason": "Missing base_branch for merge operation",
153
+ }
154
+ )
155
+ continue
156
+
157
+ merge_result = _merge_branch_to_base(
158
+ git_manager=git_manager,
159
+ branch_name=branch,
160
+ base_branch=worktree.base_branch,
161
+ )
162
+
163
+ if merge_result["success"]:
164
+ merge_succeeded = True
165
+ operation_succeeded = True
166
+ merged.append(
167
+ {
168
+ "worktree_id": worktree_id,
169
+ "task_id": task_id,
170
+ "branch_name": branch,
171
+ "merge_commit": merge_result.get("merge_commit"),
172
+ }
173
+ )
174
+ else:
175
+ # Merge failed - cannot proceed with cleanup
176
+ failed.append(
177
+ {
178
+ **agent_info,
179
+ "failure_reason": f"Merge failed: {merge_result.get('error')}",
180
+ "merge_error": merge_result.get("error"),
181
+ }
182
+ )
183
+ continue
184
+
185
+ # Step 2: Mark worktree as merged (only if merge actually occurred)
186
+ if merge_succeeded:
187
+ worktree_storage.mark_merged(worktree_id)
188
+
189
+ # Step 3: Delete git worktree (if enabled)
190
+ if delete_worktrees:
191
+ # Validate required fields for deletion
192
+ if not worktree_path:
193
+ failed.append(
194
+ {
195
+ **agent_info,
196
+ "failure_reason": "Missing worktree_path for delete operation",
197
+ }
198
+ )
199
+ continue
200
+
201
+ delete_result = git_manager.delete_worktree(
202
+ worktree_path=worktree_path,
203
+ force=force,
204
+ delete_branch=delete_branches,
205
+ )
206
+
207
+ if delete_result.success:
208
+ operation_succeeded = True
209
+ deleted.append(
210
+ {
211
+ "worktree_id": worktree_id,
212
+ "task_id": task_id,
213
+ "worktree_path": worktree_path,
214
+ "branch_deleted": delete_branches,
215
+ }
216
+ )
217
+
218
+ # Also delete the database record
219
+ worktree_storage.delete(worktree_id)
220
+ else:
221
+ # Worktree deletion failed - report actual merge status
222
+ failed.append(
223
+ {
224
+ **agent_info,
225
+ "failure_reason": f"Worktree deletion failed: {delete_result.message}",
226
+ "worktree_status": "merged"
227
+ if merge_succeeded
228
+ else agent_info.get("worktree_status", "unmerged"),
229
+ }
230
+ )
231
+ continue
232
+
233
+ # Only mark as cleaned if at least one operation succeeded
234
+ if operation_succeeded:
235
+ cleaned_agents.append(agent_info)
236
+
237
+ except Exception as e:
238
+ logger.exception(f"Error cleaning up worktree {worktree_id}")
239
+ failed.append(
240
+ {
241
+ **agent_info,
242
+ "failure_reason": str(e),
243
+ }
244
+ )
245
+
246
+ # Update workflow state
247
+ try:
248
+ state = state_manager.get_state(parent_session_id)
249
+ if state:
250
+ # Remove successfully cleaned agents from reviewed_agents
251
+ # Compare by worktree_id to avoid dict identity issues
252
+ cleaned_worktree_ids = {a.get("worktree_id") for a in cleaned_agents}
253
+ remaining_reviewed = [
254
+ a for a in reviewed_agents if a.get("worktree_id") not in cleaned_worktree_ids
255
+ ]
256
+ state.variables["reviewed_agents"] = remaining_reviewed
257
+
258
+ # Track cleanup history
259
+ cleanup_history = state.variables.get("cleanup_history", [])
260
+ cleanup_history.append(
261
+ {
262
+ "merged_count": len(merged),
263
+ "deleted_count": len(deleted),
264
+ "failed_count": len(failed),
265
+ "timestamp": datetime.now(UTC).isoformat(),
266
+ }
267
+ )
268
+ state.variables["cleanup_history"] = cleanup_history
269
+
270
+ state_manager.save_state(state)
271
+ except Exception as e:
272
+ logger.warning(f"Failed to update workflow state after cleanup: {e}")
273
+
274
+ return {
275
+ "success": True,
276
+ "merged": merged,
277
+ "deleted": deleted,
278
+ "failed": failed,
279
+ "summary": {
280
+ "merged": len(merged),
281
+ "deleted": len(deleted),
282
+ "failed": len(failed),
283
+ },
284
+ "remaining_reviewed": len(reviewed_agents) - len(cleaned_agents),
285
+ }
286
+
287
+ async def cleanup_stale_worktrees(
288
+ project_path: str | None = None,
289
+ older_than_hours: int = 24,
290
+ force: bool = False,
291
+ ) -> dict[str, Any]:
292
+ """
293
+ Clean up stale worktrees that have been inactive.
294
+
295
+ Finds worktrees marked as stale or with no active agent session
296
+ that are older than the specified threshold, and cleans them up.
297
+
298
+ Args:
299
+ project_path: Path to project directory
300
+ older_than_hours: Only cleanup worktrees older than this (hours)
301
+ force: Force deletion even if dirty
302
+
303
+ Returns:
304
+ Dict with cleanup results
305
+ """
306
+ if git_manager is None:
307
+ return {
308
+ "success": False,
309
+ "error": "Git manager not configured",
310
+ }
311
+
312
+ # Validate older_than_hours
313
+ try:
314
+ older_than_hours = int(older_than_hours)
315
+ except (TypeError, ValueError):
316
+ return {
317
+ "success": False,
318
+ "error": "older_than_hours must be an integer",
319
+ }
320
+ if older_than_hours < 0:
321
+ return {
322
+ "success": False,
323
+ "error": "older_than_hours must be non-negative",
324
+ }
325
+
326
+ # Resolve project ID
327
+ resolved_project_id = default_project_id
328
+ if project_path:
329
+ from pathlib import Path
330
+
331
+ from gobby.utils.project_context import get_project_context
332
+
333
+ ctx = get_project_context(Path(project_path))
334
+ if ctx:
335
+ resolved_project_id = ctx.get("id")
336
+
337
+ if not resolved_project_id:
338
+ resolved_project_id = get_current_project_id()
339
+
340
+ if not resolved_project_id:
341
+ return {
342
+ "success": False,
343
+ "error": "Could not resolve project ID",
344
+ }
345
+
346
+ from gobby.storage.worktrees import WorktreeStatus as WTStatus
347
+
348
+ # Get all worktrees to check for stale or abandoned candidates
349
+ all_worktrees = worktree_storage.list_worktrees(
350
+ project_id=resolved_project_id,
351
+ limit=100,
352
+ )
353
+
354
+ cutoff = datetime.now(UTC) - timedelta(hours=older_than_hours)
355
+ candidates = []
356
+
357
+ for wt in all_worktrees:
358
+ # Skip merged or abandoned
359
+ if wt.status in [WTStatus.MERGED.value, WTStatus.ABANDONED.value]:
360
+ continue
361
+
362
+ # Add stale worktrees
363
+ if wt.status == WTStatus.STALE.value:
364
+ candidates.append(wt)
365
+ continue
366
+
367
+ # Add active worktrees with no active session that are old enough
368
+ if wt.agent_session_id is None:
369
+ try:
370
+ created = datetime.fromisoformat(wt.created_at.replace("Z", "+00:00"))
371
+ if created < cutoff:
372
+ candidates.append(wt)
373
+ except (ValueError, AttributeError):
374
+ pass
375
+
376
+ deleted: list[dict[str, Any]] = []
377
+ failed: list[dict[str, Any]] = []
378
+
379
+ for wt in candidates:
380
+ try:
381
+ # Mark as stale first if not already
382
+ if wt.status != WTStatus.STALE.value:
383
+ worktree_storage.mark_stale(wt.id)
384
+
385
+ # Delete the git worktree
386
+ delete_result = git_manager.delete_worktree(
387
+ worktree_path=wt.worktree_path,
388
+ force=force,
389
+ delete_branch=False, # Keep branches for stale cleanup
390
+ )
391
+
392
+ if delete_result.success:
393
+ # Mark as abandoned and delete record
394
+ worktree_storage.mark_abandoned(wt.id)
395
+ worktree_storage.delete(wt.id)
396
+ deleted.append(
397
+ {
398
+ "worktree_id": wt.id,
399
+ "branch_name": wt.branch_name,
400
+ "worktree_path": wt.worktree_path,
401
+ }
402
+ )
403
+ else:
404
+ failed.append(
405
+ {
406
+ "worktree_id": wt.id,
407
+ "branch_name": wt.branch_name,
408
+ "failure_reason": delete_result.message,
409
+ }
410
+ )
411
+
412
+ except Exception as e:
413
+ logger.exception(f"Error cleaning up stale worktree {wt.id}")
414
+ failed.append(
415
+ {
416
+ "worktree_id": wt.id,
417
+ "branch_name": wt.branch_name,
418
+ "failure_reason": str(e),
419
+ }
420
+ )
421
+
422
+ return {
423
+ "success": True,
424
+ "deleted": deleted,
425
+ "failed": failed,
426
+ "summary": {
427
+ "candidates": len(candidates),
428
+ "deleted": len(deleted),
429
+ "failed": len(failed),
430
+ },
431
+ }
432
+
433
+ registry.register(
434
+ name="cleanup_reviewed_worktrees",
435
+ description=(
436
+ "Clean up worktrees for reviewed agents. "
437
+ "Merges branches to base branch (from worktree.base_branch), marks as merged, deletes worktrees. "
438
+ "Used by auto-orchestrator cleanup step."
439
+ ),
440
+ input_schema={
441
+ "type": "object",
442
+ "properties": {
443
+ "parent_session_id": {
444
+ "type": "string",
445
+ "description": "Parent session ID (orchestrator session)",
446
+ },
447
+ "merge_to_base": {
448
+ "type": "boolean",
449
+ "description": "Whether to merge branch to base before cleanup",
450
+ "default": True,
451
+ },
452
+ "delete_worktrees": {
453
+ "type": "boolean",
454
+ "description": "Whether to delete git worktrees",
455
+ "default": True,
456
+ },
457
+ "delete_branches": {
458
+ "type": "boolean",
459
+ "description": "Whether to delete branches after cleanup",
460
+ "default": False,
461
+ },
462
+ "force": {
463
+ "type": "boolean",
464
+ "description": "Force deletion even if worktree is dirty",
465
+ "default": False,
466
+ },
467
+ "project_path": {
468
+ "type": "string",
469
+ "description": "Path to project directory (optional)",
470
+ },
471
+ },
472
+ "required": ["parent_session_id"],
473
+ },
474
+ func=cleanup_reviewed_worktrees,
475
+ )
476
+
477
+ registry.register(
478
+ name="cleanup_stale_worktrees",
479
+ description=(
480
+ "Clean up stale worktrees that have been inactive. "
481
+ "Deletes worktrees with no active agent older than threshold."
482
+ ),
483
+ input_schema={
484
+ "type": "object",
485
+ "properties": {
486
+ "project_path": {
487
+ "type": "string",
488
+ "description": "Path to project directory (optional)",
489
+ },
490
+ "older_than_hours": {
491
+ "type": "integer",
492
+ "description": "Only cleanup worktrees older than this (hours)",
493
+ "default": 24,
494
+ },
495
+ "force": {
496
+ "type": "boolean",
497
+ "description": "Force deletion even if dirty",
498
+ "default": False,
499
+ },
500
+ },
501
+ "required": [],
502
+ },
503
+ func=cleanup_stale_worktrees,
504
+ )
505
+
506
+
507
+ def _merge_branch_to_base(
508
+ git_manager: WorktreeGitManager,
509
+ branch_name: str,
510
+ base_branch: str = "main",
511
+ ) -> dict[str, Any]:
512
+ """
513
+ Merge a branch back to its base branch.
514
+
515
+ The base_branch is typically the branch the worktree was created from
516
+ (e.g., dev, main, or a feature branch). This allows the orchestrator
517
+ to run on any branch and merge completed work back.
518
+
519
+ Args:
520
+ git_manager: Git manager instance
521
+ branch_name: Branch to merge (the worktree branch)
522
+ base_branch: Target branch to merge into (from worktree.base_branch)
523
+
524
+ Returns:
525
+ Dict with success status, merge_commit, and error details
526
+ """
527
+ try:
528
+ # Fetch latest from remote
529
+ fetch_result = git_manager._run_git(
530
+ ["fetch", "origin", base_branch],
531
+ timeout=60,
532
+ )
533
+ if fetch_result.returncode != 0:
534
+ return {
535
+ "success": False,
536
+ "error": f"Failed to fetch: {fetch_result.stderr}",
537
+ }
538
+
539
+ # Checkout the base branch
540
+ checkout_result = git_manager._run_git(
541
+ ["checkout", base_branch],
542
+ timeout=30,
543
+ )
544
+ if checkout_result.returncode != 0:
545
+ return {
546
+ "success": False,
547
+ "error": f"Failed to checkout {base_branch}: {checkout_result.stderr}",
548
+ }
549
+
550
+ # Pull latest
551
+ pull_result = git_manager._run_git(
552
+ ["pull", "origin", base_branch],
553
+ timeout=60,
554
+ )
555
+ if pull_result.returncode != 0:
556
+ return {
557
+ "success": False,
558
+ "error": f"Failed to pull: {pull_result.stderr}",
559
+ }
560
+
561
+ # Merge the branch
562
+ merge_result = git_manager._run_git(
563
+ ["merge", branch_name, "--no-ff", "-m", f"Merge branch '{branch_name}'"],
564
+ timeout=120,
565
+ )
566
+
567
+ if merge_result.returncode != 0:
568
+ # Check for conflicts
569
+ has_conflicts = "CONFLICT" in merge_result.stdout or "CONFLICT" in merge_result.stderr
570
+
571
+ # Always try to abort/reset the repo to ensure clean state
572
+ abort_result = git_manager._run_git(["merge", "--abort"], timeout=10)
573
+ if abort_result.returncode != 0:
574
+ # Abort failed, force reset to clean state
575
+ git_manager._run_git(["reset", "--hard", "HEAD"], timeout=10)
576
+ git_manager._run_git(["clean", "-fd"], timeout=10)
577
+
578
+ if has_conflicts:
579
+ return {
580
+ "success": False,
581
+ "error": "Merge conflict detected",
582
+ "conflicts": True,
583
+ }
584
+ return {
585
+ "success": False,
586
+ "error": merge_result.stderr or merge_result.stdout,
587
+ }
588
+
589
+ # Get the merge commit SHA
590
+ log_result = git_manager._run_git(
591
+ ["rev-parse", "HEAD"],
592
+ timeout=10,
593
+ )
594
+ merge_commit = log_result.stdout.strip() if log_result.returncode == 0 else None
595
+
596
+ # Push the merge to remote
597
+ push_result = git_manager._run_git(
598
+ ["push", "origin", base_branch],
599
+ timeout=60,
600
+ )
601
+ if push_result.returncode != 0:
602
+ return {
603
+ "success": False,
604
+ "error": f"Merge succeeded but push failed: {push_result.stderr}",
605
+ "merge_commit": merge_commit,
606
+ "push_failed": True,
607
+ }
608
+
609
+ return {
610
+ "success": True,
611
+ "merge_commit": merge_commit,
612
+ "message": f"Successfully merged {branch_name} to {base_branch}",
613
+ }
614
+
615
+ except Exception as e:
616
+ return {
617
+ "success": False,
618
+ "error": str(e),
619
+ }