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,736 @@
1
+ """Task orchestration tools: review (spawn_review_agent, process_completed_agents)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, Literal
7
+
8
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
9
+ from gobby.storage.tasks import TaskNotFoundError
10
+
11
+ from .utils import get_current_project_id
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.agents.runner import AgentRunner
15
+ from gobby.storage.tasks import LocalTaskManager
16
+ from gobby.storage.worktrees import LocalWorktreeManager
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def register_reviewer(
22
+ registry: InternalToolRegistry,
23
+ task_manager: LocalTaskManager,
24
+ worktree_storage: LocalWorktreeManager,
25
+ agent_runner: AgentRunner | None = None,
26
+ default_project_id: str | None = None,
27
+ ) -> None:
28
+ """Register review tools."""
29
+ from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
30
+
31
+ async def spawn_review_agent(
32
+ task_id: str,
33
+ review_provider: Literal["claude", "gemini", "codex", "antigravity"] = "claude",
34
+ review_model: str | None = "claude-opus-4-5",
35
+ terminal: str = "auto",
36
+ mode: str = "terminal",
37
+ parent_session_id: str | None = None,
38
+ project_path: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ """
41
+ Spawn a review agent for a completed task.
42
+
43
+ Used by the auto-orchestrator workflow's review step to validate
44
+ completed work before merging/cleanup.
45
+
46
+ Args:
47
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
48
+ review_provider: LLM provider for review (default: claude)
49
+ review_model: Model for review (default: claude-opus-4-5 for thorough analysis)
50
+ terminal: Terminal for terminal mode (default: auto)
51
+ mode: Execution mode (terminal, embedded, headless)
52
+ parent_session_id: Parent session ID for context (required)
53
+ project_path: Path to project directory
54
+
55
+ Returns:
56
+ Dict with:
57
+ - success: bool
58
+ - agent_id: ID of spawned review agent
59
+ - session_id: Child session ID
60
+ - error: Optional error message
61
+ """
62
+ # Validate mode and review_provider
63
+ allowed_modes = {"terminal", "embedded", "headless"}
64
+ allowed_providers = {"claude", "gemini", "codex", "antigravity"}
65
+
66
+ mode_lower = mode.lower() if mode else "terminal"
67
+ if mode_lower not in allowed_modes:
68
+ return {
69
+ "success": False,
70
+ "error": f"Invalid mode '{mode}'. Must be one of: {sorted(allowed_modes)}",
71
+ }
72
+ mode = mode_lower # Use normalized value
73
+
74
+ if review_provider not in allowed_providers:
75
+ return {
76
+ "success": False,
77
+ "error": f"Invalid review_provider '{review_provider}'. Must be one of: {sorted(allowed_providers)}",
78
+ }
79
+
80
+ # Resolve task_id reference
81
+ try:
82
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
83
+ except (TaskNotFoundError, ValueError) as e:
84
+ return {
85
+ "success": False,
86
+ "error": f"Invalid task_id: {e}",
87
+ }
88
+
89
+ if agent_runner is None:
90
+ return {
91
+ "success": False,
92
+ "error": "Agent runner not configured. Cannot spawn review agent.",
93
+ }
94
+
95
+ if parent_session_id is None:
96
+ return {
97
+ "success": False,
98
+ "error": "parent_session_id is required for spawning review agent",
99
+ }
100
+
101
+ # Resolve project ID
102
+ resolved_project_id = default_project_id
103
+ if project_path:
104
+ from pathlib import Path
105
+
106
+ from gobby.utils.project_context import get_project_context
107
+
108
+ ctx = get_project_context(Path(project_path))
109
+ if ctx:
110
+ resolved_project_id = ctx.get("id")
111
+
112
+ if not resolved_project_id:
113
+ resolved_project_id = get_current_project_id()
114
+
115
+ if not resolved_project_id:
116
+ return {
117
+ "success": False,
118
+ "error": "Could not resolve project ID",
119
+ }
120
+
121
+ # Get the task
122
+ try:
123
+ task = task_manager.get_task(resolved_task_id)
124
+ except ValueError as e:
125
+ return {
126
+ "success": False,
127
+ "error": f"Task {task_id} not found: {e}",
128
+ }
129
+ if not task:
130
+ return {
131
+ "success": False,
132
+ "error": f"Task {task_id} not found",
133
+ }
134
+
135
+ # Get worktree for the task
136
+ worktree = worktree_storage.get_by_task(resolved_task_id)
137
+ if not worktree:
138
+ return {
139
+ "success": False,
140
+ "error": f"No worktree found for task {resolved_task_id}",
141
+ }
142
+
143
+ # Build review prompt
144
+ review_prompt = _build_review_prompt(task, worktree)
145
+
146
+ # Check spawn depth
147
+ can_spawn, reason, _depth = agent_runner.can_spawn(parent_session_id)
148
+ if not can_spawn:
149
+ return {
150
+ "success": False,
151
+ "error": reason,
152
+ }
153
+
154
+ # Prepare agent run
155
+ from gobby.agents.runner import AgentConfig
156
+ from gobby.llm.executor import AgentResult
157
+ from gobby.utils.machine_id import get_machine_id
158
+
159
+ machine_id = get_machine_id()
160
+
161
+ config = AgentConfig(
162
+ prompt=review_prompt,
163
+ parent_session_id=parent_session_id,
164
+ project_id=resolved_project_id,
165
+ machine_id=machine_id,
166
+ source=review_provider,
167
+ workflow=None, # Review doesn't need a workflow
168
+ task=resolved_task_id,
169
+ session_context="summary_markdown",
170
+ mode=mode,
171
+ terminal=terminal,
172
+ worktree_id=worktree.id,
173
+ provider=review_provider,
174
+ model=review_model,
175
+ max_turns=20, # Reviews should be shorter
176
+ timeout=300.0, # 5 minutes
177
+ project_path=worktree.worktree_path,
178
+ )
179
+
180
+ prepare_result = agent_runner.prepare_run(config)
181
+ if isinstance(prepare_result, AgentResult):
182
+ return {
183
+ "success": False,
184
+ "error": prepare_result.error or "Failed to prepare review agent run",
185
+ }
186
+
187
+ context = prepare_result
188
+ if context.session is None or context.run is None:
189
+ return {
190
+ "success": False,
191
+ "error": "Internal error: context missing session or run",
192
+ }
193
+
194
+ child_session = context.session
195
+ agent_run = context.run
196
+
197
+ # Spawn the review agent
198
+ if mode == "terminal":
199
+ from gobby.agents.spawn import TerminalSpawner
200
+
201
+ spawner = TerminalSpawner()
202
+ spawn_result = spawner.spawn_agent(
203
+ cli=review_provider,
204
+ cwd=worktree.worktree_path,
205
+ session_id=child_session.id,
206
+ parent_session_id=parent_session_id,
207
+ agent_run_id=agent_run.id,
208
+ project_id=resolved_project_id,
209
+ workflow_name=None,
210
+ agent_depth=child_session.agent_depth,
211
+ max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
212
+ terminal=terminal,
213
+ prompt=review_prompt,
214
+ )
215
+
216
+ if not spawn_result.success:
217
+ return {
218
+ "success": False,
219
+ "error": spawn_result.error or "Terminal spawn failed",
220
+ }
221
+
222
+ return {
223
+ "success": True,
224
+ "task_id": resolved_task_id,
225
+ "agent_id": agent_run.id,
226
+ "session_id": child_session.id,
227
+ "worktree_id": worktree.id,
228
+ "terminal_type": spawn_result.terminal_type,
229
+ "pid": spawn_result.pid,
230
+ "provider": review_provider,
231
+ "model": review_model,
232
+ }
233
+
234
+ elif mode == "embedded":
235
+ from gobby.agents.spawn import EmbeddedSpawner
236
+
237
+ embedded_spawner = EmbeddedSpawner()
238
+ embedded_result = embedded_spawner.spawn_agent(
239
+ cli=review_provider,
240
+ cwd=worktree.worktree_path,
241
+ session_id=child_session.id,
242
+ parent_session_id=parent_session_id,
243
+ agent_run_id=agent_run.id,
244
+ project_id=resolved_project_id,
245
+ workflow_name=None,
246
+ agent_depth=child_session.agent_depth,
247
+ max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
248
+ prompt=review_prompt,
249
+ )
250
+
251
+ if not embedded_result.success:
252
+ return {
253
+ "success": False,
254
+ "error": embedded_result.error or "Embedded spawn failed",
255
+ }
256
+
257
+ return {
258
+ "success": True,
259
+ "task_id": resolved_task_id,
260
+ "agent_id": agent_run.id,
261
+ "session_id": child_session.id,
262
+ "worktree_id": worktree.id,
263
+ "provider": review_provider,
264
+ "model": review_model,
265
+ }
266
+
267
+ else: # headless
268
+ from gobby.agents.spawn import HeadlessSpawner
269
+
270
+ headless_spawner = HeadlessSpawner()
271
+ headless_result = headless_spawner.spawn_agent(
272
+ cli=review_provider,
273
+ cwd=worktree.worktree_path,
274
+ session_id=child_session.id,
275
+ parent_session_id=parent_session_id,
276
+ agent_run_id=agent_run.id,
277
+ project_id=resolved_project_id,
278
+ workflow_name=None,
279
+ agent_depth=child_session.agent_depth,
280
+ max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
281
+ prompt=review_prompt,
282
+ )
283
+
284
+ if not headless_result.success:
285
+ return {
286
+ "success": False,
287
+ "error": headless_result.error or "Headless spawn failed",
288
+ }
289
+
290
+ return {
291
+ "success": True,
292
+ "task_id": resolved_task_id,
293
+ "agent_id": agent_run.id,
294
+ "session_id": child_session.id,
295
+ "worktree_id": worktree.id,
296
+ "pid": headless_result.pid,
297
+ "provider": review_provider,
298
+ "model": review_model,
299
+ }
300
+
301
+ registry.register(
302
+ name="spawn_review_agent",
303
+ description=(
304
+ "Spawn a review agent for a completed task. "
305
+ "Used by auto-orchestrator workflow for code review. "
306
+ "Uses review_provider/review_model for thorough analysis."
307
+ ),
308
+ input_schema={
309
+ "type": "object",
310
+ "properties": {
311
+ "task_id": {
312
+ "type": "string",
313
+ "description": "Task reference: #N, N (seq_num), path (1.2.3), or UUID",
314
+ },
315
+ "review_provider": {
316
+ "type": "string",
317
+ "description": "LLM provider for review (claude, gemini, codex, antigravity)",
318
+ "default": "claude",
319
+ },
320
+ "review_model": {
321
+ "type": "string",
322
+ "description": "Model for review (default: claude-opus-4-5 for thorough analysis)",
323
+ "default": "claude-opus-4-5",
324
+ },
325
+ "terminal": {
326
+ "type": "string",
327
+ "description": "Terminal for terminal mode (auto, ghostty, iterm2, etc.)",
328
+ "default": "auto",
329
+ },
330
+ "mode": {
331
+ "type": "string",
332
+ "description": "Execution mode (terminal, embedded, headless)",
333
+ "default": "terminal",
334
+ },
335
+ "parent_session_id": {
336
+ "type": "string",
337
+ "description": "Parent session ID for context (required)",
338
+ },
339
+ "project_path": {
340
+ "type": ["string", "null"],
341
+ "description": "Path to project directory",
342
+ "default": None,
343
+ },
344
+ },
345
+ "required": ["task_id", "parent_session_id"],
346
+ },
347
+ func=spawn_review_agent,
348
+ )
349
+
350
+ async def process_completed_agents(
351
+ parent_session_id: str,
352
+ spawn_reviews: bool = True,
353
+ review_provider: Literal["claude", "gemini", "codex", "antigravity"] | None = None,
354
+ review_model: str | None = None,
355
+ terminal: str = "auto",
356
+ mode: str = "terminal",
357
+ project_path: str | None = None,
358
+ ) -> dict[str, Any]:
359
+ """
360
+ Process completed agents and route them to review or cleanup.
361
+
362
+ Takes agents from completed_agents list and either:
363
+ - Spawns review agents for validation (if spawn_reviews=True)
364
+ - Moves directly to reviewed_agents list (if already validated)
365
+
366
+ For failed agents, optionally retries or escalates.
367
+
368
+ Args:
369
+ parent_session_id: Parent session ID (orchestrator session)
370
+ spawn_reviews: Whether to spawn review agents for completed tasks
371
+ review_provider: LLM provider for reviews (uses workflow variable if not set)
372
+ review_model: Model for reviews (uses workflow variable if not set)
373
+ terminal: Terminal for terminal mode
374
+ mode: Execution mode for review agents
375
+ project_path: Path to project directory
376
+
377
+ Returns:
378
+ Dict with:
379
+ - reviews_spawned: List of review agents spawned
380
+ - ready_for_cleanup: List of agents ready for worktree cleanup
381
+ - retries_scheduled: List of failed agents scheduled for retry
382
+ - escalated: List of agents escalated for manual intervention
383
+ """
384
+ if agent_runner is None:
385
+ return {
386
+ "success": False,
387
+ "error": "Agent runner not configured",
388
+ }
389
+
390
+ # Get workflow state
391
+ from gobby.workflows.state_manager import WorkflowStateManager
392
+
393
+ state_manager = WorkflowStateManager(task_manager.db)
394
+ state = state_manager.get_state(parent_session_id)
395
+ if not state:
396
+ return {
397
+ "success": True,
398
+ "reviews_spawned": [],
399
+ "ready_for_cleanup": [],
400
+ "retries_scheduled": [],
401
+ "escalated": [],
402
+ "message": "No workflow state found",
403
+ }
404
+
405
+ workflow_vars = state.variables
406
+
407
+ # Defensive type coercion - ensure lists of dicts, handle None/wrong types
408
+ def _safe_list_of_dicts(val: Any) -> list[dict[str, Any]]:
409
+ """Coerce value to list of dicts, filtering out non-dict entries."""
410
+ if not val:
411
+ return []
412
+ if not isinstance(val, list):
413
+ return []
414
+ return [x for x in val if isinstance(x, dict)]
415
+
416
+ completed_agents = _safe_list_of_dicts(workflow_vars.get("completed_agents"))
417
+ failed_agents = _safe_list_of_dicts(workflow_vars.get("failed_agents"))
418
+ # Create a fresh list for newly reviewed agents to avoid aliasing the stored list
419
+ newly_reviewed: list[dict[str, Any]] = []
420
+ # Shallow copy to avoid aliasing
421
+ review_agents_spawned = list(
422
+ _safe_list_of_dicts(workflow_vars.get("review_agents_spawned"))
423
+ )
424
+
425
+ # Resolve review provider from workflow vars or parameters
426
+ effective_review_provider = (
427
+ review_provider or workflow_vars.get("review_provider") or "claude"
428
+ )
429
+ effective_review_model = (
430
+ review_model or workflow_vars.get("review_model") or "claude-opus-4-5"
431
+ )
432
+
433
+ reviews_spawned: list[dict[str, Any]] = []
434
+ ready_for_cleanup: list[dict[str, Any]] = []
435
+ retries_scheduled: list[dict[str, Any]] = []
436
+ escalated: list[dict[str, Any]] = []
437
+
438
+ # Process completed agents
439
+ still_pending_review: list[dict[str, Any]] = []
440
+
441
+ for agent_info in completed_agents:
442
+ task_id = agent_info.get("task_id")
443
+ if not task_id:
444
+ # Invalid agent info
445
+ escalated.append(
446
+ {
447
+ **agent_info,
448
+ "escalation_reason": "Missing task_id",
449
+ }
450
+ )
451
+ continue
452
+
453
+ # Check task validation status
454
+ try:
455
+ task = task_manager.get_task(task_id)
456
+ except ValueError as e:
457
+ escalated.append(
458
+ {
459
+ **agent_info,
460
+ "escalation_reason": f"Task lookup failed: {e}",
461
+ }
462
+ )
463
+ continue
464
+ if not task:
465
+ escalated.append(
466
+ {
467
+ **agent_info,
468
+ "escalation_reason": "Task not found",
469
+ }
470
+ )
471
+ continue
472
+
473
+ # Check if task is already validated (passed validation)
474
+ if task.validation_status == "valid":
475
+ # Ready for cleanup
476
+ ready_for_cleanup.append(
477
+ {
478
+ **agent_info,
479
+ "validation_status": "valid",
480
+ }
481
+ )
482
+ newly_reviewed.append(agent_info)
483
+ continue
484
+
485
+ # Check if task validation failed - may need retry
486
+ if task.validation_status == "invalid":
487
+ # Check failure count
488
+ fail_count = task.validation_fail_count or 0
489
+ max_retries = 3
490
+
491
+ if fail_count >= max_retries:
492
+ # Escalate - too many failures
493
+ escalated.append(
494
+ {
495
+ **agent_info,
496
+ "escalation_reason": f"Validation failed {fail_count} times",
497
+ "validation_feedback": task.validation_feedback,
498
+ }
499
+ )
500
+ else:
501
+ # Retry - reopen task and add back to queue
502
+ try:
503
+ task_manager.reopen_task(task_id, reason="Validation failed, retrying")
504
+ retries_scheduled.append(
505
+ {
506
+ **agent_info,
507
+ "retry_count": fail_count + 1,
508
+ }
509
+ )
510
+ except Exception as e:
511
+ escalated.append(
512
+ {
513
+ **agent_info,
514
+ "escalation_reason": f"Failed to reopen task: {e}",
515
+ }
516
+ )
517
+ continue
518
+
519
+ # Task needs review - spawn review agent if enabled
520
+ if spawn_reviews:
521
+ # Check if review agent already spawned for this task
522
+ already_spawned = any(ra.get("task_id") == task_id for ra in review_agents_spawned)
523
+ if already_spawned:
524
+ # Keep in pending review list
525
+ still_pending_review.append(agent_info)
526
+ continue
527
+
528
+ # Spawn review agent
529
+ review_result = await spawn_review_agent(
530
+ task_id=task_id,
531
+ review_provider=effective_review_provider,
532
+ review_model=effective_review_model,
533
+ terminal=terminal,
534
+ mode=mode,
535
+ parent_session_id=parent_session_id,
536
+ project_path=project_path,
537
+ )
538
+
539
+ if review_result.get("success"):
540
+ reviews_spawned.append(
541
+ {
542
+ "task_id": task_id,
543
+ "agent_id": review_result.get("agent_id"),
544
+ "session_id": review_result.get("session_id"),
545
+ "worktree_id": review_result.get("worktree_id"),
546
+ }
547
+ )
548
+ review_agents_spawned.append(
549
+ {
550
+ "task_id": task_id,
551
+ "agent_id": review_result.get("agent_id"),
552
+ }
553
+ )
554
+ # Keep agent in completed list until review completes
555
+ still_pending_review.append(agent_info)
556
+ else:
557
+ # Review spawn failed - escalate
558
+ escalated.append(
559
+ {
560
+ **agent_info,
561
+ "escalation_reason": f"Review spawn failed: {review_result.get('error')}",
562
+ }
563
+ )
564
+ else:
565
+ # Not spawning reviews - move to ready_for_cleanup
566
+ ready_for_cleanup.append(
567
+ {
568
+ **agent_info,
569
+ "skipped_review": True,
570
+ }
571
+ )
572
+ newly_reviewed.append(agent_info)
573
+
574
+ # Process failed agents
575
+ still_failed: list[dict[str, Any]] = []
576
+
577
+ for agent_info in failed_agents:
578
+ task_id = agent_info.get("task_id")
579
+ failure_reason = agent_info.get("failure_reason") or "Unknown"
580
+
581
+ # Check if this is a retriable failure
582
+ if "crashed" in failure_reason.lower() or "exited" in failure_reason.lower():
583
+ # Potentially retriable - reopen task
584
+ if task_id:
585
+ retry_task: Any = None
586
+ try:
587
+ retry_task = task_manager.get_task(task_id)
588
+ except ValueError:
589
+ # Task was deleted concurrently - skip
590
+ pass
591
+ if retry_task and retry_task.status == "in_progress":
592
+ # Reopen for retry
593
+ try:
594
+ task_manager.update_task(task_id, status="open")
595
+ retries_scheduled.append(
596
+ {
597
+ **agent_info,
598
+ "retry_reason": "Agent crashed, reopened task",
599
+ }
600
+ )
601
+ continue
602
+ except Exception as e:
603
+ # Task update failed - keep in still_failed for next cycle
604
+ still_failed.append(
605
+ {
606
+ **agent_info,
607
+ "pending_retry": True,
608
+ "retry_error": str(e),
609
+ }
610
+ )
611
+ continue
612
+
613
+ # Non-retriable - escalate
614
+ escalated.append(
615
+ {
616
+ **agent_info,
617
+ "escalation_reason": failure_reason,
618
+ }
619
+ )
620
+
621
+ # Update workflow state
622
+ try:
623
+ state = state_manager.get_state(parent_session_id)
624
+ if state:
625
+ # Update completed_agents to only include pending review
626
+ state.variables["completed_agents"] = still_pending_review
627
+ # Update reviewed_agents - copy existing to avoid aliasing, then extend
628
+ existing_reviewed = list(state.variables.get("reviewed_agents", []))
629
+ existing_reviewed.extend(newly_reviewed)
630
+ state.variables["reviewed_agents"] = existing_reviewed
631
+ # Update review_agents_spawned
632
+ state.variables["review_agents_spawned"] = review_agents_spawned
633
+ # Update failed_agents
634
+ state.variables["failed_agents"] = still_failed
635
+ # Track escalated agents
636
+ existing_escalated = list(state.variables.get("escalated_agents", []))
637
+ existing_escalated.extend(escalated)
638
+ state.variables["escalated_agents"] = existing_escalated
639
+
640
+ state_manager.save_state(state)
641
+ except Exception as e:
642
+ logger.warning(f"Failed to update workflow state during processing: {e}")
643
+
644
+ return {
645
+ "success": True,
646
+ "reviews_spawned": reviews_spawned,
647
+ "ready_for_cleanup": ready_for_cleanup,
648
+ "retries_scheduled": retries_scheduled,
649
+ "escalated": escalated,
650
+ "summary": {
651
+ "reviews_spawned": len(reviews_spawned),
652
+ "ready_for_cleanup": len(ready_for_cleanup),
653
+ "retries_scheduled": len(retries_scheduled),
654
+ "escalated": len(escalated),
655
+ "pending_review": len(still_pending_review),
656
+ },
657
+ }
658
+
659
+ registry.register(
660
+ name="process_completed_agents",
661
+ description=(
662
+ "Process completed agents and route to review or cleanup. "
663
+ "Spawns review agents for validation, handles retries for failures, "
664
+ "escalates unrecoverable errors. Used by auto-orchestrator review step."
665
+ ),
666
+ input_schema={
667
+ "type": "object",
668
+ "properties": {
669
+ "parent_session_id": {
670
+ "type": "string",
671
+ "description": "Parent session ID (orchestrator session)",
672
+ },
673
+ "spawn_reviews": {
674
+ "type": "boolean",
675
+ "description": "Whether to spawn review agents for completed tasks",
676
+ "default": True,
677
+ },
678
+ "review_provider": {
679
+ "type": ["string", "null"],
680
+ "description": "LLM provider for reviews (uses workflow variable if not set)",
681
+ "default": None,
682
+ },
683
+ "review_model": {
684
+ "type": ["string", "null"],
685
+ "description": "Model for reviews (uses workflow variable if not set)",
686
+ "default": None,
687
+ },
688
+ "terminal": {
689
+ "type": "string",
690
+ "description": "Terminal for terminal mode",
691
+ "default": "auto",
692
+ },
693
+ "mode": {
694
+ "type": "string",
695
+ "description": "Execution mode for review agents",
696
+ "default": "terminal",
697
+ },
698
+ "project_path": {
699
+ "type": ["string", "null"],
700
+ "description": "Path to project directory",
701
+ "default": None,
702
+ },
703
+ },
704
+ "required": ["parent_session_id"],
705
+ },
706
+ func=process_completed_agents,
707
+ )
708
+
709
+
710
+ def _build_review_prompt(task: Any, worktree: Any) -> str:
711
+ """Build a review prompt for a completed task."""
712
+ prompt_parts = [
713
+ "# Code Review Request",
714
+ f"\n## Task: {task.title}",
715
+ f"Task ID: {task.id}",
716
+ f"Branch: {worktree.branch_name}",
717
+ ]
718
+
719
+ if task.description:
720
+ prompt_parts.append(f"\n## Task Description\n{task.description}")
721
+
722
+ if task.validation_criteria:
723
+ prompt_parts.append(f"\n## Validation Criteria\n{task.validation_criteria}")
724
+
725
+ prompt_parts.append(
726
+ "\n## Review Instructions\n"
727
+ "1. Review the code changes on this branch\n"
728
+ "2. Check that the implementation matches the task description\n"
729
+ "3. Verify tests exist and pass (if applicable)\n"
730
+ "4. Check for code quality, security issues, and best practices\n"
731
+ "5. Use validate_task() to mark as valid/invalid with feedback\n"
732
+ "6. If valid, the task can proceed to merge\n"
733
+ "7. If invalid, provide clear feedback for the implementer"
734
+ )
735
+
736
+ return "\n".join(prompt_parts)