gobby 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/agents.py ADDED
@@ -0,0 +1,529 @@
1
+ """
2
+ Agent management CLI commands.
3
+
4
+ Commands for managing subagent runs:
5
+ - start: Start a new agent
6
+ - list: List agent runs for a session
7
+ - show: Show details for an agent run
8
+ - status: Check status of a running agent
9
+ - stop: Stop a running agent (marks cancelled in DB, does not kill process)
10
+ - kill: Kill a running agent process (SIGTERM/SIGKILL)
11
+ """
12
+
13
+ import json
14
+
15
+ import click
16
+ import httpx
17
+
18
+ from gobby.cli.utils import resolve_session_id
19
+ from gobby.storage.agents import LocalAgentRunManager
20
+ from gobby.storage.database import LocalDatabase
21
+
22
+
23
+ def get_agent_run_manager() -> LocalAgentRunManager:
24
+ """Get initialized agent run manager."""
25
+ db = LocalDatabase()
26
+ return LocalAgentRunManager(db)
27
+
28
+
29
+ def resolve_agent_run_id(run_ref: str) -> str:
30
+ """
31
+ Resolve agent run reference (exact or prefix) to full ID.
32
+
33
+ Args:
34
+ run_ref: Agent run ID or prefix
35
+
36
+ Returns:
37
+ Full UUID string
38
+
39
+ Raises:
40
+ click.ClickException: If run not found or ambiguous
41
+ """
42
+ manager = get_agent_run_manager()
43
+
44
+ # Try exact match first
45
+ # Optimization: check 36 chars?
46
+ if len(run_ref) == 36 and manager.get(run_ref):
47
+ return run_ref
48
+
49
+ # Try prefix match
50
+ db = LocalDatabase()
51
+ rows = db.fetchall(
52
+ "SELECT id FROM agent_runs WHERE id LIKE ? LIMIT 5",
53
+ (f"{run_ref}%",),
54
+ )
55
+
56
+ if not rows:
57
+ raise click.ClickException(f"Agent run not found: {run_ref}")
58
+
59
+ if len(rows) > 1:
60
+ click.echo(f"Ambiguous agent run reference '{run_ref}' matches:", err=True)
61
+ for row in rows:
62
+ click.echo(f" {row['id']}", err=True)
63
+ raise click.ClickException(f"Ambiguous agent run reference: {run_ref}")
64
+
65
+ return str(rows[0]["id"])
66
+
67
+
68
+ def get_daemon_url() -> str:
69
+ """Get daemon URL from config."""
70
+ from gobby.config.app import load_config
71
+
72
+ config = load_config()
73
+ return f"http://localhost:{config.daemon_port}"
74
+
75
+
76
+ @click.group()
77
+ def agents() -> None:
78
+ """Manage subagent runs."""
79
+ pass
80
+
81
+
82
+ @agents.command("start")
83
+ @click.argument("prompt")
84
+ @click.option("--session", "-s", "parent_session_id", required=True, help="Parent session ID")
85
+ @click.option("--workflow", "-w", help="Workflow name to execute")
86
+ @click.option("--task", "-t", help="Task ID or 'next' for auto-select")
87
+ @click.option(
88
+ "--mode",
89
+ "-m",
90
+ type=click.Choice(["in_process", "terminal", "embedded", "headless"]),
91
+ default="terminal",
92
+ help="Execution mode (default: terminal)",
93
+ )
94
+ @click.option(
95
+ "--terminal",
96
+ type=click.Choice(["auto", "ghostty", "iterm", "kitty", "wezterm", "terminal"]),
97
+ default="auto",
98
+ help="Terminal for terminal/embedded modes",
99
+ )
100
+ @click.option("--provider", "-p", default="claude", help="LLM provider (claude, gemini, etc.)")
101
+ @click.option("--model", help="Model override")
102
+ @click.option("--timeout", default=120.0, help="Execution timeout in seconds")
103
+ @click.option("--max-turns", default=10, help="Maximum turns")
104
+ @click.option(
105
+ "--context",
106
+ "-c",
107
+ "session_context",
108
+ default="summary_markdown",
109
+ help="Context source (summary_markdown, compact_markdown, transcript:<n>, file:<path>)",
110
+ )
111
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
112
+ def start_agent_cmd(
113
+ prompt: str,
114
+ parent_session_id: str,
115
+ workflow: str | None,
116
+ task: str | None,
117
+ mode: str,
118
+ terminal: str,
119
+ provider: str,
120
+ model: str | None,
121
+ timeout: float,
122
+ max_turns: int,
123
+ session_context: str,
124
+ json_format: bool,
125
+ ) -> None:
126
+ """Start a new agent with the given prompt.
127
+
128
+ Examples:
129
+
130
+ gobby agents start "Implement feature X" --session sess-abc123
131
+
132
+ gobby agents start "Fix the bug" -s sess-abc123 --mode terminal
133
+
134
+ gobby agents start "Run tests" -s sess-abc123 --mode headless
135
+ """
136
+ daemon_url = get_daemon_url()
137
+
138
+ # Resolve session ID
139
+ try:
140
+ parent_session_id = resolve_session_id(parent_session_id)
141
+ except click.ClickException as e:
142
+ raise SystemExit(1) from e
143
+
144
+ # Build arguments for the MCP tool call
145
+ arguments = {
146
+ "prompt": prompt,
147
+ "parent_session_id": parent_session_id,
148
+ "mode": mode,
149
+ "terminal": terminal,
150
+ "provider": provider,
151
+ "timeout": timeout,
152
+ "max_turns": max_turns,
153
+ "session_context": session_context,
154
+ }
155
+
156
+ if workflow:
157
+ arguments["workflow"] = workflow
158
+ if task:
159
+ arguments["task"] = task
160
+ if model:
161
+ arguments["model"] = model
162
+
163
+ # Call the daemon's MCP tool endpoint
164
+ try:
165
+ response = httpx.post(
166
+ f"{daemon_url}/mcp/gobby-agents/tools/start_agent",
167
+ json=arguments,
168
+ timeout=30.0,
169
+ )
170
+ response.raise_for_status()
171
+ result = response.json()
172
+ except httpx.ConnectError:
173
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
174
+ click.echo("Start with: gobby start", err=True)
175
+ return
176
+ except httpx.HTTPStatusError as e:
177
+ click.echo(f"Error: Daemon returned {e.response.status_code}", err=True)
178
+ click.echo(e.response.text, err=True)
179
+ return
180
+ except Exception as e:
181
+ click.echo(f"Error: {e}", err=True)
182
+ return
183
+
184
+ if json_format:
185
+ click.echo(json.dumps(result, indent=2, default=str))
186
+ return
187
+
188
+ # Check result
189
+ if result.get("success"):
190
+ run_id = result.get("run_id", "unknown")
191
+ child_session_id = result.get("child_session_id", "unknown")
192
+ status = result.get("status", "unknown")
193
+
194
+ click.echo(f"Started agent run: {run_id}")
195
+ click.echo(f" Child session: {child_session_id}")
196
+ click.echo(f" Status: {status}")
197
+
198
+ if result.get("message"):
199
+ click.echo(f" {result['message']}")
200
+
201
+ if mode == "in_process" and result.get("output"):
202
+ click.echo(f"\nOutput:\n{result['output']}")
203
+ else:
204
+ error = result.get("error", "Unknown error")
205
+ click.echo(f"Failed to start agent: {error}", err=True)
206
+
207
+
208
+ @agents.command("list")
209
+ @click.option("--session", "-s", "session_id", help="Filter by parent session ID")
210
+ @click.option(
211
+ "--status",
212
+ type=click.Choice(["pending", "running", "success", "error", "timeout", "cancelled"]),
213
+ help="Filter by status",
214
+ )
215
+ @click.option("--limit", "-n", default=20, help="Max runs to show")
216
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
217
+ def list_agents(
218
+ session_id: str | None,
219
+ status: str | None,
220
+ limit: int,
221
+ json_format: bool,
222
+ ) -> None:
223
+ """List agent runs."""
224
+ manager = get_agent_run_manager()
225
+
226
+ if session_id:
227
+ try:
228
+ session_id = resolve_session_id(session_id)
229
+ except click.ClickException as e:
230
+ raise SystemExit(1) from e
231
+ runs = manager.list_by_session(session_id, status=status, limit=limit) # type: ignore
232
+ elif status == "running":
233
+ runs = manager.list_running(limit=limit)
234
+ else:
235
+ # List recent runs across all sessions
236
+ # Note: This requires querying without session filter
237
+ db = LocalDatabase()
238
+ query = "SELECT * FROM agent_runs"
239
+ params: list[str | int] = []
240
+
241
+ if status:
242
+ query += " WHERE status = ?"
243
+ params.append(status)
244
+
245
+ query += " ORDER BY created_at DESC LIMIT ?"
246
+ params.append(limit)
247
+
248
+ rows = db.fetchall(query, tuple(params))
249
+ from gobby.storage.agents import AgentRun
250
+
251
+ runs = [AgentRun.from_row(row) for row in rows]
252
+
253
+ if json_format:
254
+ click.echo(json.dumps([r.to_dict() for r in runs], indent=2, default=str))
255
+ return
256
+
257
+ if not runs:
258
+ click.echo("No agent runs found.")
259
+ return
260
+
261
+ click.echo(f"Found {len(runs)} agent run(s):\n")
262
+ for run in runs:
263
+ status_icon = {
264
+ "pending": "○",
265
+ "running": "◐",
266
+ "success": "✓",
267
+ "error": "✗",
268
+ "timeout": "⏱",
269
+ "cancelled": "⊘",
270
+ }.get(run.status, "?")
271
+
272
+ # Truncate prompt
273
+ prompt = run.prompt[:40] + "..." if len(run.prompt) > 40 else run.prompt
274
+ prompt = prompt.replace("\n", " ")
275
+
276
+ click.echo(f"{status_icon} {run.id[:12]} {run.status:<10} {run.provider:<8} {prompt}")
277
+
278
+
279
+ @agents.command("show")
280
+ @click.argument("run_ref")
281
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
282
+ def show_agent(run_ref: str, json_format: bool) -> None:
283
+ """Show details for an agent run (UUID or prefix)."""
284
+ run_id = resolve_agent_run_id(run_ref)
285
+ manager = get_agent_run_manager()
286
+ run = manager.get(run_id)
287
+
288
+ if not run:
289
+ # Should not happen if resolve succeeded, but safe check
290
+ click.echo(f"Agent run not found: {run_id}", err=True)
291
+ return
292
+
293
+ if json_format:
294
+ click.echo(json.dumps(run.to_dict(), indent=2, default=str))
295
+ return
296
+
297
+ click.echo(f"Agent Run: {run.id}")
298
+ click.echo(f"Status: {run.status}")
299
+ click.echo(f"Provider: {run.provider}")
300
+ if run.model:
301
+ click.echo(f"Model: {run.model}")
302
+ click.echo(f"Parent Session: {run.parent_session_id}")
303
+ if run.child_session_id:
304
+ click.echo(f"Child Session: {run.child_session_id}")
305
+ if run.workflow_name:
306
+ click.echo(f"Workflow: {run.workflow_name}")
307
+
308
+ click.echo(f"\nPrompt:\n{run.prompt[:500]}")
309
+ if len(run.prompt) > 500:
310
+ click.echo("...")
311
+
312
+ if run.result:
313
+ click.echo(f"\nResult:\n{run.result[:500]}")
314
+ if len(run.result) > 500:
315
+ click.echo("...")
316
+
317
+ if run.error:
318
+ click.echo(f"\nError: {run.error}")
319
+
320
+ click.echo(f"\nTurns Used: {run.turns_used}")
321
+ click.echo(f"Tool Calls: {run.tool_calls_count}")
322
+ click.echo(f"Created: {run.created_at}")
323
+ if run.started_at:
324
+ click.echo(f"Started: {run.started_at}")
325
+ if run.completed_at:
326
+ click.echo(f"Completed: {run.completed_at}")
327
+
328
+
329
+ @agents.command("status")
330
+ @click.argument("run_ref")
331
+ def agent_status(run_ref: str) -> None:
332
+ """Check status of an agent run (UUID or prefix)."""
333
+ run_id = resolve_agent_run_id(run_ref)
334
+ manager = get_agent_run_manager()
335
+ run = manager.get(run_id)
336
+
337
+ if not run:
338
+ click.echo(f"Agent run not found: {run_id}", err=True)
339
+ return
340
+
341
+ status_icon = {
342
+ "pending": "○",
343
+ "running": "◐",
344
+ "success": "✓",
345
+ "error": "✗",
346
+ "timeout": "⏱",
347
+ "cancelled": "⊘",
348
+ }.get(run.status, "?")
349
+
350
+ click.echo(f"{status_icon} {run.id}: {run.status}")
351
+
352
+ if run.status == "running" and run.started_at:
353
+ click.echo(f" Running since: {run.started_at}")
354
+ click.echo(f" Turns used: {run.turns_used}")
355
+ elif run.status in ("success", "error", "timeout", "cancelled"):
356
+ if run.completed_at:
357
+ click.echo(f" Completed: {run.completed_at}")
358
+ if run.error:
359
+ click.echo(f" Error: {run.error}")
360
+
361
+
362
+ @agents.command("stop")
363
+ @click.argument("run_ref")
364
+ @click.confirmation_option(prompt="Are you sure you want to stop this agent run?")
365
+ def stop_agent(run_ref: str) -> None:
366
+ """Stop a running agent (marks as cancelled, does not kill process)."""
367
+ run_id = resolve_agent_run_id(run_ref)
368
+ manager = get_agent_run_manager()
369
+ run = manager.get(run_id)
370
+
371
+ if not run:
372
+ click.echo(f"Agent run not found: {run_id}", err=True)
373
+ return
374
+
375
+ if run.status not in ("pending", "running"):
376
+ click.echo(f"Cannot stop agent in status: {run.status}", err=True)
377
+ return
378
+
379
+ manager.cancel(run.id)
380
+ click.echo(f"Stopped agent run: {run.id}")
381
+
382
+
383
+ @agents.command("kill")
384
+ @click.argument("run_ref")
385
+ @click.option("--force", "-f", is_flag=True, help="Use SIGKILL immediately")
386
+ @click.option("--stop", "-s", is_flag=True, help="Also end workflow (prevents restart)")
387
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
388
+ def kill_agent(run_ref: str, force: bool, stop: bool, yes: bool) -> None:
389
+ """Kill a running agent process.
390
+
391
+ Sends SIGTERM (or SIGKILL with -f) to terminate the agent process.
392
+ Without --stop: workflow may restart the agent in a new terminal.
393
+ With --stop: also ends the workflow (prevents restart).
394
+
395
+ \b
396
+ Examples:
397
+ gobby agents kill abc123 -y # Kill with SIGTERM
398
+ gobby agents kill abc123 -f -y # Force kill with SIGKILL
399
+ gobby agents kill abc123 -s -y # Kill and end workflow
400
+ gobby agents kill abc123 -fs -y # Force kill and end workflow
401
+ """
402
+ from gobby.utils.daemon_client import DaemonClient
403
+
404
+ run_id = resolve_agent_run_id(run_ref)
405
+
406
+ if not yes:
407
+ msg = "Force kill agent" if force else "Kill agent"
408
+ if stop:
409
+ msg += " and end workflow for"
410
+ if not click.confirm(f"{msg} {run_id[:12]}?"):
411
+ return
412
+
413
+ # Call daemon MCP tool
414
+ client = DaemonClient()
415
+ try:
416
+ result = client.call_mcp_tool(
417
+ server_name="gobby-agents",
418
+ tool_name="kill_agent",
419
+ arguments={
420
+ "run_id": run_id,
421
+ "force": force,
422
+ "stop": stop,
423
+ },
424
+ )
425
+ except Exception as e:
426
+ click.echo(f"Error: {e}", err=True)
427
+ return
428
+
429
+ if result.get("success"):
430
+ msg = result.get("message", f"Killed agent {run_id}")
431
+ click.echo(msg)
432
+ if result.get("found_via") == "pgrep":
433
+ click.echo(f" (found via pgrep, PID {result.get('pid')})")
434
+ if result.get("already_dead"):
435
+ click.echo(" (process was already terminated)")
436
+ if result.get("workflow_stopped"):
437
+ click.echo(" (workflow ended)")
438
+ else:
439
+ click.echo(f"Failed: {result.get('error')}", err=True)
440
+
441
+
442
+ @agents.command("stats")
443
+ @click.option("--session", "-s", "session_id", help="Filter by parent session ID")
444
+ def agent_stats(session_id: str | None) -> None:
445
+ """Show agent run statistics."""
446
+ db = LocalDatabase()
447
+
448
+ if session_id:
449
+ try:
450
+ session_id = resolve_session_id(session_id)
451
+ except click.ClickException as e:
452
+ raise SystemExit(1) from e
453
+ manager = get_agent_run_manager()
454
+ counts = manager.count_by_session(session_id)
455
+ total = sum(counts.values())
456
+
457
+ click.echo(f"Agent Statistics for session {session_id[:12]}:")
458
+ click.echo(f" Total Runs: {total}")
459
+ else:
460
+ # Global stats
461
+ row = db.fetchone(
462
+ """
463
+ SELECT
464
+ COUNT(*) as total,
465
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
466
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
467
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
468
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
469
+ SUM(CASE WHEN status = 'timeout' THEN 1 ELSE 0 END) as timeout,
470
+ SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled
471
+ FROM agent_runs
472
+ """
473
+ )
474
+
475
+ if row:
476
+ click.echo("Agent Run Statistics:")
477
+ click.echo(f" Total Runs: {row['total']}")
478
+ click.echo(f" Running: {row['running']}")
479
+ click.echo(f" Pending: {row['pending']}")
480
+ click.echo(f" Success: {row['success']}")
481
+ click.echo(f" Error: {row['error']}")
482
+ click.echo(f" Timeout: {row['timeout']}")
483
+ click.echo(f" Cancelled: {row['cancelled']}")
484
+
485
+ if row["total"] > 0:
486
+ success_rate = (row["success"] / row["total"]) * 100
487
+ click.echo(f"\n Success Rate: {success_rate:.1f}%")
488
+ else:
489
+ click.echo("No agent runs found.")
490
+
491
+
492
+ @agents.command("cleanup")
493
+ @click.option("--timeout", "-t", default=30, help="Timeout in minutes for stale runs")
494
+ @click.option("--dry-run", "-d", is_flag=True, help="Show what would be cleaned up")
495
+ def cleanup_agents(timeout: int, dry_run: bool) -> None:
496
+ """Clean up stale agent runs."""
497
+ manager = get_agent_run_manager()
498
+
499
+ if dry_run:
500
+ # Show what would be cleaned up
501
+ db = LocalDatabase()
502
+ stale_running = db.fetchall(
503
+ """
504
+ SELECT * FROM agent_runs
505
+ WHERE status = 'running'
506
+ AND datetime(started_at) < datetime('now', 'utc', ? || ' minutes')
507
+ """,
508
+ (f"-{timeout}",),
509
+ )
510
+ stale_pending = db.fetchall(
511
+ """
512
+ SELECT * FROM agent_runs
513
+ WHERE status = 'pending'
514
+ AND datetime(created_at) < datetime('now', 'utc', '-60 minutes')
515
+ """
516
+ )
517
+
518
+ click.echo(f"Stale running runs (>{timeout}m): {len(stale_running)}")
519
+ for row in stale_running[:5]:
520
+ click.echo(f" {row['id']}: started {row['started_at']}")
521
+
522
+ click.echo(f"Stale pending runs (>60m): {len(stale_pending)}")
523
+ for row in stale_pending[:5]:
524
+ click.echo(f" {row['id']}: created {row['created_at']}")
525
+ else:
526
+ timed_out = manager.cleanup_stale_runs(timeout_minutes=timeout)
527
+ failed = manager.cleanup_stale_pending_runs(timeout_minutes=60)
528
+
529
+ click.echo(f"Cleaned up {timed_out} timed-out runs and {failed} stale pending runs.")