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,77 @@
1
+ """Terminal spawner implementations for agent execution.
2
+
3
+ This package provides Strategy pattern implementations for spawning
4
+ agents in various terminal types across different platforms.
5
+
6
+ Usage:
7
+ from gobby.agents.spawners import (
8
+ TerminalSpawnerBase,
9
+ SpawnResult,
10
+ TerminalType,
11
+ SpawnMode,
12
+ # Platform-specific spawners
13
+ GhosttySpawner,
14
+ ITermSpawner,
15
+ # etc.
16
+ )
17
+ """
18
+
19
+ from gobby.agents.spawners.base import (
20
+ EmbeddedPTYResult,
21
+ HeadlessResult,
22
+ SpawnMode,
23
+ SpawnResult,
24
+ TerminalSpawnerBase,
25
+ TerminalType,
26
+ )
27
+ from gobby.agents.spawners.cross_platform import (
28
+ AlacrittySpawner,
29
+ KittySpawner,
30
+ TmuxSpawner,
31
+ )
32
+ from gobby.agents.spawners.embedded import EmbeddedSpawner
33
+ from gobby.agents.spawners.headless import HeadlessSpawner
34
+ from gobby.agents.spawners.linux import (
35
+ GnomeTerminalSpawner,
36
+ KonsoleSpawner,
37
+ )
38
+ from gobby.agents.spawners.macos import (
39
+ GhosttySpawner,
40
+ ITermSpawner,
41
+ TerminalAppSpawner,
42
+ )
43
+ from gobby.agents.spawners.windows import (
44
+ CmdSpawner,
45
+ PowerShellSpawner,
46
+ WindowsTerminalSpawner,
47
+ WSLSpawner,
48
+ )
49
+
50
+ __all__ = [
51
+ # Base types
52
+ "SpawnMode",
53
+ "TerminalType",
54
+ "SpawnResult",
55
+ "EmbeddedPTYResult",
56
+ "HeadlessResult",
57
+ "TerminalSpawnerBase",
58
+ # macOS spawners
59
+ "GhosttySpawner",
60
+ "ITermSpawner",
61
+ "TerminalAppSpawner",
62
+ # Linux spawners
63
+ "GnomeTerminalSpawner",
64
+ "KonsoleSpawner",
65
+ # Windows spawners
66
+ "WindowsTerminalSpawner",
67
+ "CmdSpawner",
68
+ "PowerShellSpawner",
69
+ "WSLSpawner",
70
+ # Cross-platform spawners
71
+ "KittySpawner",
72
+ "AlacrittySpawner",
73
+ "TmuxSpawner",
74
+ # Embedded/Headless spawners
75
+ "EmbeddedSpawner",
76
+ "HeadlessSpawner",
77
+ ]
@@ -0,0 +1,142 @@
1
+ """Base classes and types for terminal spawners."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess # nosec B404 - subprocess needed for terminal spawning
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ class SpawnMode(str, Enum):
15
+ """Agent execution mode."""
16
+
17
+ TERMINAL = "terminal" # Spawn in external terminal window
18
+ EMBEDDED = "embedded" # Return PTY handle for UI attachment
19
+ HEADLESS = "headless" # Daemon captures output, no terminal visible
20
+ IN_PROCESS = "in_process" # Run via SDK in daemon process
21
+
22
+
23
+ class TerminalType(str, Enum):
24
+ """Supported terminal types."""
25
+
26
+ # macOS
27
+ GHOSTTY = "ghostty"
28
+ ITERM = "iterm"
29
+ TERMINAL_APP = "terminal.app"
30
+ KITTY = "kitty"
31
+ ALACRITTY = "alacritty"
32
+
33
+ # Linux
34
+ GNOME_TERMINAL = "gnome-terminal"
35
+ KONSOLE = "konsole"
36
+
37
+ # Windows
38
+ WINDOWS_TERMINAL = "windows-terminal"
39
+ CMD = "cmd"
40
+ POWERSHELL = "powershell"
41
+ WSL = "wsl"
42
+
43
+ # Cross-platform multiplexer
44
+ TMUX = "tmux"
45
+
46
+ # Auto-detect
47
+ AUTO = "auto"
48
+
49
+
50
+ @dataclass
51
+ class SpawnResult:
52
+ """Result of spawning a terminal process."""
53
+
54
+ success: bool
55
+ message: str
56
+ pid: int | None = None
57
+ terminal_type: str | None = None
58
+ error: str | None = None
59
+
60
+
61
+ @dataclass
62
+ class EmbeddedPTYResult:
63
+ """Result of spawning an embedded PTY process."""
64
+
65
+ success: bool
66
+ message: str
67
+ master_fd: int | None = None
68
+ """Master file descriptor for reading/writing to PTY."""
69
+ slave_fd: int | None = None
70
+ """Slave file descriptor (used by child process)."""
71
+ pid: int | None = None
72
+ """Child process PID."""
73
+ error: str | None = None
74
+
75
+ def close(self) -> None:
76
+ """Close the PTY file descriptors."""
77
+ if self.master_fd is not None:
78
+ try:
79
+ os.close(self.master_fd)
80
+ except OSError:
81
+ pass
82
+ if self.slave_fd is not None:
83
+ try:
84
+ os.close(self.slave_fd)
85
+ except OSError:
86
+ pass
87
+
88
+
89
+ @dataclass
90
+ class HeadlessResult:
91
+ """Result of spawning a headless process."""
92
+
93
+ success: bool
94
+ message: str
95
+ pid: int | None = None
96
+ """Child process PID."""
97
+ process: subprocess.Popen[Any] | None = None
98
+ """Subprocess handle for output capture."""
99
+ output_buffer: list[str] = field(default_factory=list)
100
+ """Captured output lines."""
101
+ error: str | None = None
102
+
103
+ def get_output(self) -> str:
104
+ """Get all captured output as a string."""
105
+ return "\n".join(self.output_buffer)
106
+
107
+
108
+ class TerminalSpawnerBase(ABC):
109
+ """Base class for terminal spawners."""
110
+
111
+ @property
112
+ @abstractmethod
113
+ def terminal_type(self) -> TerminalType:
114
+ """The terminal type this spawner handles."""
115
+ pass
116
+
117
+ @abstractmethod
118
+ def is_available(self) -> bool:
119
+ """Check if this terminal is available on the system."""
120
+ pass
121
+
122
+ @abstractmethod
123
+ def spawn(
124
+ self,
125
+ command: list[str],
126
+ cwd: str | Path,
127
+ env: dict[str, str] | None = None,
128
+ title: str | None = None,
129
+ ) -> SpawnResult:
130
+ """
131
+ Spawn a new terminal window with the given command.
132
+
133
+ Args:
134
+ command: Command to run in the terminal
135
+ cwd: Working directory
136
+ env: Environment variables to set
137
+ title: Optional window title
138
+
139
+ Returns:
140
+ SpawnResult with success status and process info
141
+ """
142
+ pass
@@ -0,0 +1,266 @@
1
+ """Cross-platform terminal spawners: Kitty, Alacritty, and tmux."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import shlex
8
+ import shutil
9
+ import subprocess # nosec B404 - subprocess needed for terminal spawning
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from gobby.agents.spawners.base import SpawnResult, TerminalSpawnerBase, TerminalType
14
+ from gobby.agents.tty_config import get_tty_config
15
+
16
+ __all__ = ["KittySpawner", "AlacrittySpawner", "TmuxSpawner"]
17
+
18
+
19
+ class KittySpawner(TerminalSpawnerBase):
20
+ """Spawner for Kitty terminal."""
21
+
22
+ @property
23
+ def terminal_type(self) -> TerminalType:
24
+ return TerminalType.KITTY
25
+
26
+ def is_available(self) -> bool:
27
+ config = get_tty_config().get_terminal_config("kitty")
28
+ if not config.enabled:
29
+ return False
30
+ # On macOS, check app bundle; on other platforms check CLI
31
+ if platform.system() == "Darwin":
32
+ app_path = config.app_path or "/Applications/kitty.app"
33
+ return Path(app_path).exists()
34
+ command = config.command or "kitty"
35
+ return shutil.which(command) is not None
36
+
37
+ def spawn(
38
+ self,
39
+ command: list[str],
40
+ cwd: str | Path,
41
+ env: dict[str, str] | None = None,
42
+ title: str | None = None,
43
+ ) -> SpawnResult:
44
+ try:
45
+ tty_config = get_tty_config().get_terminal_config("kitty")
46
+ if platform.system() == "Darwin":
47
+ # On macOS, --detach doesn't work properly - command doesn't execute
48
+ # Use direct path without --detach, subprocess handles backgrounding
49
+ app_path = tty_config.app_path or "/Applications/kitty.app"
50
+ kitty_path = f"{app_path}/Contents/MacOS/kitty"
51
+ args = [kitty_path, "--directory", str(cwd)]
52
+ else:
53
+ # On Linux, --detach works correctly
54
+ cli_command = tty_config.command or "kitty"
55
+ args = [cli_command, "--detach", "--directory", str(cwd)]
56
+
57
+ # Add extra options from config (includes confirm_os_window_close=0 by default)
58
+ args.extend(tty_config.options)
59
+
60
+ if title:
61
+ args.extend(["--title", title])
62
+ # Add end-of-options separator before the user command
63
+ # This ensures command arguments starting with '-' are not interpreted as Kitty options
64
+ args.append("--")
65
+ args.extend(command)
66
+
67
+ spawn_env = os.environ.copy()
68
+ if env:
69
+ spawn_env.update(env)
70
+
71
+ process = subprocess.Popen( # nosec B603 - args built from config
72
+ args,
73
+ env=spawn_env,
74
+ start_new_session=True,
75
+ )
76
+
77
+ return SpawnResult(
78
+ success=True,
79
+ message=f"Spawned Kitty with PID {process.pid}",
80
+ pid=process.pid,
81
+ terminal_type=self.terminal_type.value,
82
+ )
83
+
84
+ except Exception as e:
85
+ return SpawnResult(
86
+ success=False,
87
+ message=f"Failed to spawn Kitty: {e}",
88
+ error=str(e),
89
+ )
90
+
91
+
92
+ class AlacrittySpawner(TerminalSpawnerBase):
93
+ """Spawner for Alacritty terminal."""
94
+
95
+ @property
96
+ def terminal_type(self) -> TerminalType:
97
+ return TerminalType.ALACRITTY
98
+
99
+ def is_available(self) -> bool:
100
+ config = get_tty_config().get_terminal_config("alacritty")
101
+ if not config.enabled:
102
+ return False
103
+ command = config.command or "alacritty"
104
+ return shutil.which(command) is not None
105
+
106
+ def spawn(
107
+ self,
108
+ command: list[str],
109
+ cwd: str | Path,
110
+ env: dict[str, str] | None = None,
111
+ title: str | None = None,
112
+ ) -> SpawnResult:
113
+ try:
114
+ tty_config = get_tty_config().get_terminal_config("alacritty")
115
+ cli_command = tty_config.command or "alacritty"
116
+ args = [cli_command, "--working-directory", str(cwd)]
117
+ # Add extra options from config
118
+ args.extend(tty_config.options)
119
+ if title:
120
+ args.extend(["--title", title])
121
+ args.extend(["-e"] + command)
122
+
123
+ spawn_env = os.environ.copy()
124
+ if env:
125
+ spawn_env.update(env)
126
+
127
+ process = subprocess.Popen( # nosec B603 - args built from config
128
+ args,
129
+ env=spawn_env,
130
+ start_new_session=True,
131
+ )
132
+
133
+ return SpawnResult(
134
+ success=True,
135
+ message=f"Spawned Alacritty with PID {process.pid}",
136
+ pid=process.pid,
137
+ terminal_type=self.terminal_type.value,
138
+ )
139
+
140
+ except Exception as e:
141
+ return SpawnResult(
142
+ success=False,
143
+ message=f"Failed to spawn Alacritty: {e}",
144
+ error=str(e),
145
+ )
146
+
147
+
148
+ class TmuxSpawner(TerminalSpawnerBase):
149
+ """
150
+ Spawner for tmux terminal multiplexer.
151
+
152
+ Creates a new detached tmux session that runs the command.
153
+ The session can be attached to later with `tmux attach -t <session>`.
154
+ """
155
+
156
+ @property
157
+ def terminal_type(self) -> TerminalType:
158
+ return TerminalType.TMUX
159
+
160
+ def is_available(self) -> bool:
161
+ # tmux is available on macOS and Linux (not Windows natively)
162
+ if platform.system() == "Windows":
163
+ return False
164
+ config = get_tty_config().get_terminal_config("tmux")
165
+ if not config.enabled:
166
+ return False
167
+ command = config.command or "tmux"
168
+ return shutil.which(command) is not None
169
+
170
+ def spawn(
171
+ self,
172
+ command: list[str],
173
+ cwd: str | Path,
174
+ env: dict[str, str] | None = None,
175
+ title: str | None = None,
176
+ ) -> SpawnResult:
177
+ try:
178
+ tty_config = get_tty_config().get_terminal_config("tmux")
179
+ cli_command = tty_config.command or "tmux"
180
+
181
+ # Generate a unique session name based on title or timestamp
182
+ session_name = title or f"gobby-{int(time.time())}"
183
+ # Sanitize session name (tmux doesn't like dots or colons)
184
+ session_name = session_name.replace(".", "-").replace(":", "-")
185
+
186
+ # Build tmux command:
187
+ # tmux new-session -d -s <name> -n <name> -c <cwd> <command> \
188
+ # \; set-option destroy-unattached off \
189
+ # \; set-environment VAR value ...
190
+ # -d: detached (runs in background)
191
+ # -s: session name
192
+ # -n: window name (title)
193
+ # -c: starting directory
194
+ # The chained set-option prevents session destruction when user has
195
+ # global destroy-unattached on (must be atomic with session creation)
196
+ args = [
197
+ cli_command,
198
+ "new-session",
199
+ "-d", # Detached
200
+ "-s",
201
+ session_name,
202
+ "-n",
203
+ session_name, # Window title
204
+ "-c",
205
+ str(cwd),
206
+ ]
207
+
208
+ # Add extra options from config
209
+ args.extend(tty_config.options)
210
+
211
+ # Build the command to run, injecting env vars if provided
212
+ # We export env vars in the shell command so they're available to the process
213
+ # (tmux set-environment only affects new processes, not the initial command)
214
+ if env:
215
+ # Build export statements for each env var
216
+ exports = " ".join(
217
+ f"export {shlex.quote(k)}={shlex.quote(v)};" for k, v in env.items()
218
+ )
219
+ # Wrap command with exports
220
+ shell_cmd = f"{exports} exec {shlex.join(command)}"
221
+ args.extend(["sh", "-c", shell_cmd])
222
+ elif len(command) == 1:
223
+ args.append(command[0])
224
+ else:
225
+ # Use shell to handle complex commands with arguments
226
+ args.extend(["sh", "-c", shlex.join(command)])
227
+
228
+ # Chain set-option to disable destroy-unattached atomically
229
+ # This prevents the session from being destroyed before we can configure it
230
+ args.extend([";", "set-option", "-t", session_name, "destroy-unattached", "off"])
231
+
232
+ spawn_env = os.environ.copy()
233
+ if env:
234
+ spawn_env.update(env)
235
+
236
+ process = subprocess.Popen( # nosec B603 - args built from config
237
+ args,
238
+ cwd=cwd,
239
+ env=spawn_env,
240
+ start_new_session=True,
241
+ )
242
+
243
+ # Wait for tmux to start (it exits quickly after creating the session)
244
+ process.wait()
245
+
246
+ if process.returncode != 0:
247
+ return SpawnResult(
248
+ success=False,
249
+ message=f"tmux exited with code {process.returncode}",
250
+ error=f"tmux failed to create session '{session_name}'",
251
+ )
252
+
253
+ # pid=None since tmux process has exited; session_name is the identifier
254
+ return SpawnResult(
255
+ success=True,
256
+ message=f"Spawned tmux session '{session_name}' (attach with: tmux attach -t {session_name})",
257
+ pid=None,
258
+ terminal_type=self.terminal_type.value,
259
+ )
260
+
261
+ except Exception as e:
262
+ return SpawnResult(
263
+ success=False,
264
+ message=f"Failed to spawn tmux: {e}",
265
+ error=str(e),
266
+ )
@@ -0,0 +1,225 @@
1
+ """Embedded PTY spawner for agent execution with UI attachment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from gobby.agents.constants import get_terminal_env_vars
11
+ from gobby.agents.spawners.base import EmbeddedPTYResult
12
+
13
+ # pty is only available on Unix-like systems
14
+ try:
15
+ import pty
16
+ except ImportError:
17
+ pty = None # type: ignore[assignment]
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable
21
+
22
+ __all__ = ["EmbeddedSpawner"]
23
+
24
+ # Maximum prompt length to pass via environment variable
25
+ # Longer prompts will be written to a temp file
26
+ MAX_ENV_PROMPT_LENGTH = 4096
27
+
28
+
29
+ # Import these from spawn.py to avoid duplication
30
+ def _get_spawn_utils() -> tuple[
31
+ Callable[..., list[str]],
32
+ Callable[[str, str], str],
33
+ int,
34
+ ]:
35
+ """Lazy import to avoid circular dependencies."""
36
+ from gobby.agents.spawn import (
37
+ MAX_ENV_PROMPT_LENGTH as _MAX_ENV_PROMPT_LENGTH,
38
+ )
39
+ from gobby.agents.spawn import (
40
+ _create_prompt_file,
41
+ build_cli_command,
42
+ )
43
+
44
+ return build_cli_command, _create_prompt_file, _MAX_ENV_PROMPT_LENGTH
45
+
46
+
47
+ class EmbeddedSpawner:
48
+ """
49
+ Spawner for embedded mode with PTY.
50
+
51
+ Creates a pseudo-terminal that can be attached to a UI component.
52
+ The master file descriptor can be used to read/write to the process.
53
+ """
54
+
55
+ def spawn(
56
+ self,
57
+ command: list[str],
58
+ cwd: str | Path,
59
+ env: dict[str, str] | None = None,
60
+ ) -> EmbeddedPTYResult:
61
+ """
62
+ Spawn a process with a PTY for embedded mode.
63
+
64
+ Args:
65
+ command: Command to run
66
+ cwd: Working directory
67
+ env: Environment variables to set
68
+
69
+ Returns:
70
+ EmbeddedPTYResult with PTY file descriptors and process info
71
+ """
72
+ if not command or len(command) == 0:
73
+ return EmbeddedPTYResult(
74
+ success=False,
75
+ message="Cannot spawn process with empty command",
76
+ error="Empty command list provided",
77
+ )
78
+
79
+ if platform.system() == "Windows" or pty is None:
80
+ return EmbeddedPTYResult(
81
+ success=False,
82
+ message="Embedded PTY mode not supported on Windows",
83
+ error="Windows does not support Unix PTY",
84
+ )
85
+
86
+ master_fd: int | None = None
87
+ slave_fd: int | None = None
88
+
89
+ try:
90
+ # Create pseudo-terminal
91
+ master_fd, slave_fd = pty.openpty()
92
+
93
+ # Merge environment
94
+ spawn_env = os.environ.copy()
95
+ if env:
96
+ spawn_env.update(env)
97
+
98
+ # Fork and exec
99
+ pid = os.fork()
100
+
101
+ if pid == 0:
102
+ # Child process
103
+ try:
104
+ # Close master fd in child - not needed
105
+ os.close(master_fd)
106
+
107
+ # Create new session
108
+ os.setsid()
109
+
110
+ # Set slave as controlling terminal
111
+ os.dup2(slave_fd, 0) # stdin
112
+ os.dup2(slave_fd, 1) # stdout
113
+ os.dup2(slave_fd, 2) # stderr
114
+
115
+ # Close original slave fd after duplication
116
+ os.close(slave_fd)
117
+
118
+ # Change to working directory
119
+ os.chdir(cwd)
120
+
121
+ # Execute command
122
+ os.execvpe(command[0], command, spawn_env) # nosec B606 - config
123
+ except Exception:
124
+ # Ensure we exit on any failure
125
+ os._exit(1)
126
+
127
+ # Should never reach here, but just in case
128
+ os._exit(1)
129
+ else:
130
+ # Parent process - close slave fd (child has its own copy)
131
+ os.close(slave_fd)
132
+ slave_fd = None # Mark as closed
133
+
134
+ return EmbeddedPTYResult(
135
+ success=True,
136
+ message=f"Spawned embedded PTY with PID {pid}",
137
+ master_fd=master_fd,
138
+ slave_fd=None, # Closed in parent
139
+ pid=pid,
140
+ )
141
+
142
+ except Exception as e:
143
+ # Clean up file descriptors on any error
144
+ if master_fd is not None:
145
+ try:
146
+ os.close(master_fd)
147
+ except OSError:
148
+ pass
149
+ if slave_fd is not None:
150
+ try:
151
+ os.close(slave_fd)
152
+ except OSError:
153
+ pass
154
+ return EmbeddedPTYResult(
155
+ success=False,
156
+ message=f"Failed to spawn embedded PTY: {e}",
157
+ error=str(e),
158
+ )
159
+
160
+ def spawn_agent(
161
+ self,
162
+ cli: str,
163
+ cwd: str | Path,
164
+ session_id: str,
165
+ parent_session_id: str,
166
+ agent_run_id: str,
167
+ project_id: str,
168
+ workflow_name: str | None = None,
169
+ agent_depth: int = 1,
170
+ max_agent_depth: int = 3,
171
+ prompt: str | None = None,
172
+ ) -> EmbeddedPTYResult:
173
+ """
174
+ Spawn a CLI agent with embedded PTY.
175
+
176
+ Args:
177
+ cli: CLI to run
178
+ cwd: Working directory
179
+ session_id: Pre-created child session ID
180
+ parent_session_id: Parent session ID
181
+ agent_run_id: Agent run record ID
182
+ project_id: Project ID
183
+ workflow_name: Optional workflow to activate
184
+ agent_depth: Current nesting depth
185
+ max_agent_depth: Maximum allowed depth
186
+ prompt: Optional initial prompt
187
+
188
+ Returns:
189
+ EmbeddedPTYResult with PTY info
190
+ """
191
+ build_cli_command, _create_prompt_file, max_env_prompt_length = _get_spawn_utils()
192
+
193
+ # Build command with prompt as CLI argument and auto-approve for autonomous work
194
+ command = build_cli_command(
195
+ cli,
196
+ prompt=prompt,
197
+ session_id=session_id,
198
+ auto_approve=True, # Subagents need to work autonomously
199
+ working_directory=str(cwd) if cli == "codex" else None,
200
+ )
201
+
202
+ # Handle prompt for environment variables (backup for hooks/context)
203
+ prompt_env: str | None = None
204
+ prompt_file: str | None = None
205
+
206
+ if prompt:
207
+ if len(prompt) <= max_env_prompt_length:
208
+ prompt_env = prompt
209
+ else:
210
+ # Write to temp file with secure permissions
211
+ prompt_file = _create_prompt_file(prompt, session_id)
212
+
213
+ env = get_terminal_env_vars(
214
+ session_id=session_id,
215
+ parent_session_id=parent_session_id,
216
+ agent_run_id=agent_run_id,
217
+ project_id=project_id,
218
+ workflow_name=workflow_name,
219
+ agent_depth=agent_depth,
220
+ max_agent_depth=max_agent_depth,
221
+ prompt=prompt_env,
222
+ prompt_file=prompt_file,
223
+ )
224
+
225
+ return self.spawn(command, cwd, env)