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/agents/spawn.py ADDED
@@ -0,0 +1,916 @@
1
+ """Terminal spawning for agent execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import logging
7
+ import os
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from gobby.agents.constants import get_terminal_env_vars
13
+ from gobby.agents.session import ChildSessionConfig, ChildSessionManager
14
+ from gobby.agents.spawners import (
15
+ AlacrittySpawner,
16
+ CmdSpawner,
17
+ EmbeddedSpawner,
18
+ GhosttySpawner,
19
+ GnomeTerminalSpawner,
20
+ HeadlessSpawner,
21
+ ITermSpawner,
22
+ KittySpawner,
23
+ KonsoleSpawner,
24
+ PowerShellSpawner,
25
+ SpawnMode,
26
+ SpawnResult,
27
+ TerminalAppSpawner,
28
+ TerminalSpawnerBase,
29
+ TerminalType,
30
+ TmuxSpawner,
31
+ WindowsTerminalSpawner,
32
+ WSLSpawner,
33
+ )
34
+ from gobby.agents.spawners.base import EmbeddedPTYResult, HeadlessResult
35
+ from gobby.agents.tty_config import get_tty_config
36
+
37
+ # Re-export for backward compatibility - these types moved to spawners/ package
38
+ __all__ = [
39
+ # Enums
40
+ "SpawnMode",
41
+ "TerminalType",
42
+ # Result dataclasses
43
+ "SpawnResult",
44
+ "EmbeddedPTYResult",
45
+ "HeadlessResult",
46
+ # Base class
47
+ "TerminalSpawnerBase",
48
+ # Orchestrator
49
+ "TerminalSpawner",
50
+ # Spawner implementations
51
+ "GhosttySpawner",
52
+ "ITermSpawner",
53
+ "TerminalAppSpawner",
54
+ "KittySpawner",
55
+ "AlacrittySpawner",
56
+ "GnomeTerminalSpawner",
57
+ "KonsoleSpawner",
58
+ "WindowsTerminalSpawner",
59
+ "CmdSpawner",
60
+ "PowerShellSpawner",
61
+ "WSLSpawner",
62
+ "TmuxSpawner",
63
+ "EmbeddedSpawner",
64
+ "HeadlessSpawner",
65
+ # Helpers
66
+ "PreparedSpawn",
67
+ "prepare_terminal_spawn",
68
+ "prepare_gemini_spawn_with_preflight",
69
+ "prepare_codex_spawn_with_preflight",
70
+ "read_prompt_from_env",
71
+ "build_cli_command",
72
+ "build_gemini_command_with_resume",
73
+ "build_codex_command_with_resume",
74
+ "MAX_ENV_PROMPT_LENGTH",
75
+ ]
76
+
77
+ # Maximum prompt length to pass via environment variable
78
+ # Longer prompts will be written to a temp file
79
+ MAX_ENV_PROMPT_LENGTH = 4096
80
+
81
+ logger = logging.getLogger(__name__)
82
+
83
+ # Module-level set for tracking prompt files to clean up on exit
84
+ # This avoids registering a new atexit handler for each prompt file
85
+ _prompt_files_to_cleanup: set[Path] = set()
86
+ _atexit_registered = False
87
+
88
+
89
+ def _cleanup_all_prompt_files() -> None:
90
+ """Clean up all tracked prompt files on process exit."""
91
+ for prompt_path in list(_prompt_files_to_cleanup):
92
+ try:
93
+ if prompt_path.exists():
94
+ prompt_path.unlink()
95
+ except OSError:
96
+ pass
97
+ _prompt_files_to_cleanup.clear()
98
+
99
+
100
+ def _create_prompt_file(prompt: str, session_id: str) -> str:
101
+ """
102
+ Create a prompt file with secure permissions.
103
+
104
+ The file is created in the system temp directory with restrictive
105
+ permissions (owner read/write only) and tracked for cleanup on exit.
106
+
107
+ Args:
108
+ prompt: The prompt content to write
109
+ session_id: Session ID for naming the file
110
+
111
+ Returns:
112
+ Path to the created temp file
113
+ """
114
+ global _atexit_registered
115
+
116
+ # Create temp directory with restrictive permissions
117
+ temp_dir = Path(tempfile.gettempdir()) / "gobby-prompts"
118
+ temp_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
119
+
120
+ # Create the prompt file path
121
+ prompt_path = temp_dir / f"prompt-{session_id}.txt"
122
+
123
+ # Write with secure permissions atomically - create with mode 0o600 from the start
124
+ # This avoids the TOCTOU window between write_text and chmod
125
+ fd = os.open(str(prompt_path), os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
126
+ try:
127
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
128
+ f.write(prompt)
129
+ f.flush()
130
+ os.fsync(f.fileno())
131
+ except Exception:
132
+ # fd is closed by fdopen, but if fdopen fails we need to close it
133
+ try:
134
+ os.close(fd)
135
+ except OSError:
136
+ pass
137
+ raise
138
+
139
+ # Track for cleanup
140
+ _prompt_files_to_cleanup.add(prompt_path)
141
+
142
+ # Register cleanup handler once
143
+ if not _atexit_registered:
144
+ atexit.register(_cleanup_all_prompt_files)
145
+ _atexit_registered = True
146
+
147
+ logger.debug(f"Created secure prompt file: {prompt_path}")
148
+ return str(prompt_path)
149
+
150
+
151
+ def build_cli_command(
152
+ cli: str,
153
+ prompt: str | None = None,
154
+ session_id: str | None = None,
155
+ auto_approve: bool = False,
156
+ working_directory: str | None = None,
157
+ mode: str = "terminal",
158
+ ) -> list[str]:
159
+ """
160
+ Build the CLI command with proper prompt passing and permission flags.
161
+
162
+ Each CLI has different syntax for passing prompts and handling permissions:
163
+
164
+ Claude Code:
165
+ - claude --session-id <uuid> --dangerously-skip-permissions [prompt]
166
+ - Use --dangerously-skip-permissions for autonomous subagent operation
167
+
168
+ Gemini CLI:
169
+ - gemini -i "prompt" (interactive mode with initial prompt)
170
+ - gemini --approval-mode yolo -i "prompt" (YOLO + interactive)
171
+ - gemini "prompt" (one-shot non-interactive for headless)
172
+
173
+ Codex CLI:
174
+ - codex --full-auto -C <dir> [PROMPT]
175
+ - Or: codex -c 'sandbox_permissions=["disk-full-read-access"]' -a never [PROMPT]
176
+
177
+ Args:
178
+ cli: CLI name (claude, gemini, codex)
179
+ prompt: Optional prompt to pass
180
+ session_id: Optional session ID (used by Claude CLI)
181
+ auto_approve: If True, add flags to auto-approve actions/permissions
182
+ working_directory: Optional working directory (used by Codex -C flag)
183
+ mode: Execution mode - "terminal" (interactive) or "headless" (non-interactive)
184
+
185
+ Returns:
186
+ Command list for subprocess execution
187
+ """
188
+ command = [cli]
189
+
190
+ if cli == "claude":
191
+ # Claude CLI flags
192
+ if session_id:
193
+ command.extend(["--session-id", session_id])
194
+ if auto_approve:
195
+ # Skip all permission prompts for autonomous subagent operation
196
+ command.append("--dangerously-skip-permissions")
197
+ if prompt:
198
+ # Use -p (print mode) for non-interactive execution.
199
+ # NOTE: Print mode bypasses hooks - headless spawner manually tracks status.
200
+ command.append("-p")
201
+
202
+ elif cli == "gemini":
203
+ # Gemini CLI flags
204
+ if auto_approve:
205
+ command.extend(["--approval-mode", "yolo"])
206
+ # For terminal mode, use -i (prompt-interactive) to execute prompt and stay interactive
207
+ # For headless mode, use positional prompt for one-shot execution
208
+ if prompt:
209
+ if mode == "terminal":
210
+ command.extend(["-i", prompt])
211
+ return command # Don't add prompt again as positional
212
+ # else: fall through to add as positional for headless
213
+
214
+ elif cli == "codex":
215
+ # Codex CLI flags
216
+ if auto_approve:
217
+ # --full-auto: low-friction sandboxed automatic execution
218
+ command.append("--full-auto")
219
+ if working_directory:
220
+ command.extend(["-C", working_directory])
221
+
222
+ # All three CLIs accept prompt as positional argument (must come last)
223
+ # For Gemini terminal mode, this is skipped (handled above with -i flag)
224
+ if prompt:
225
+ command.append(prompt)
226
+
227
+ return command
228
+
229
+
230
+ class TerminalSpawner:
231
+ """
232
+ Main terminal spawner that auto-detects and uses available terminals.
233
+
234
+ Provides a unified interface for spawning terminal processes across
235
+ different platforms and terminal emulators. Terminal preferences and
236
+ configurations are loaded from ~/.gobby/tty_config.yaml.
237
+ """
238
+
239
+ # Map terminal names to spawner classes
240
+ SPAWNER_CLASSES: dict[str, type[TerminalSpawnerBase]] = {
241
+ "ghostty": GhosttySpawner,
242
+ "iterm": ITermSpawner,
243
+ "terminal.app": TerminalAppSpawner,
244
+ "kitty": KittySpawner,
245
+ "alacritty": AlacrittySpawner,
246
+ "gnome-terminal": GnomeTerminalSpawner,
247
+ "konsole": KonsoleSpawner,
248
+ "windows-terminal": WindowsTerminalSpawner,
249
+ "cmd": CmdSpawner,
250
+ "powershell": PowerShellSpawner,
251
+ "wsl": WSLSpawner,
252
+ "tmux": TmuxSpawner,
253
+ }
254
+
255
+ def __init__(self) -> None:
256
+ """Initialize with platform-specific terminal preferences."""
257
+ self._spawners: dict[TerminalType, TerminalSpawnerBase] = {}
258
+ self._register_spawners()
259
+
260
+ def _register_spawners(self) -> None:
261
+ """Register all available spawners."""
262
+ all_spawners = [
263
+ GhosttySpawner(),
264
+ ITermSpawner(),
265
+ TerminalAppSpawner(),
266
+ KittySpawner(),
267
+ AlacrittySpawner(),
268
+ GnomeTerminalSpawner(),
269
+ KonsoleSpawner(),
270
+ WindowsTerminalSpawner(),
271
+ CmdSpawner(),
272
+ PowerShellSpawner(),
273
+ WSLSpawner(),
274
+ TmuxSpawner(),
275
+ ]
276
+
277
+ for spawner in all_spawners:
278
+ self._spawners[spawner.terminal_type] = spawner
279
+
280
+ def get_available_terminals(self) -> list[TerminalType]:
281
+ """Get list of available terminals on this system."""
282
+ return [
283
+ term_type for term_type, spawner in self._spawners.items() if spawner.is_available()
284
+ ]
285
+
286
+ def get_preferred_terminal(self) -> TerminalType | None:
287
+ """Get the preferred available terminal for this platform based on config."""
288
+ config = get_tty_config()
289
+ preferences = config.get_preferences()
290
+
291
+ for terminal_name in preferences:
292
+ spawner_cls = self.SPAWNER_CLASSES.get(terminal_name)
293
+ if spawner_cls is None:
294
+ continue
295
+ spawner = spawner_cls()
296
+ if spawner.is_available():
297
+ return spawner.terminal_type
298
+
299
+ return None
300
+
301
+ def spawn(
302
+ self,
303
+ command: list[str],
304
+ cwd: str | Path,
305
+ terminal: TerminalType | str = TerminalType.AUTO,
306
+ env: dict[str, str] | None = None,
307
+ title: str | None = None,
308
+ ) -> SpawnResult:
309
+ """
310
+ Spawn a command in a new terminal window.
311
+
312
+ Args:
313
+ command: Command to run
314
+ cwd: Working directory
315
+ terminal: Terminal type or "auto" for auto-detection
316
+ env: Environment variables to set
317
+ title: Optional window title
318
+
319
+ Returns:
320
+ SpawnResult with success status
321
+ """
322
+ # Convert string to enum if needed
323
+ if isinstance(terminal, str):
324
+ try:
325
+ terminal = TerminalType(terminal)
326
+ except ValueError:
327
+ return SpawnResult(
328
+ success=False,
329
+ message=f"Unknown terminal type: {terminal}",
330
+ )
331
+
332
+ # Auto-detect if requested
333
+ if terminal == TerminalType.AUTO:
334
+ preferred = self.get_preferred_terminal()
335
+ if preferred is None:
336
+ return SpawnResult(
337
+ success=False,
338
+ message="No supported terminal found on this system",
339
+ )
340
+ terminal = preferred
341
+
342
+ # Get spawner
343
+ spawner = self._spawners.get(terminal)
344
+ if spawner is None:
345
+ return SpawnResult(
346
+ success=False,
347
+ message=f"No spawner registered for terminal: {terminal}",
348
+ )
349
+
350
+ if not spawner.is_available():
351
+ return SpawnResult(
352
+ success=False,
353
+ message=f"Terminal {terminal.value} is not available on this system",
354
+ )
355
+
356
+ # Spawn the terminal
357
+ return spawner.spawn(command, cwd, env, title)
358
+
359
+ def spawn_agent(
360
+ self,
361
+ cli: str,
362
+ cwd: str | Path,
363
+ session_id: str,
364
+ parent_session_id: str,
365
+ agent_run_id: str,
366
+ project_id: str,
367
+ workflow_name: str | None = None,
368
+ agent_depth: int = 1,
369
+ max_agent_depth: int = 3,
370
+ terminal: TerminalType | str = TerminalType.AUTO,
371
+ prompt: str | None = None,
372
+ ) -> SpawnResult:
373
+ """
374
+ Spawn a CLI agent in a new terminal with Gobby environment variables.
375
+
376
+ Args:
377
+ cli: CLI to run (e.g., "claude", "gemini", "codex")
378
+ cwd: Working directory (usually project root or worktree)
379
+ session_id: Pre-created child session ID
380
+ parent_session_id: Parent session for context resolution
381
+ agent_run_id: Agent run record ID
382
+ project_id: Project ID
383
+ workflow_name: Optional workflow to activate
384
+ agent_depth: Current nesting depth
385
+ max_agent_depth: Maximum allowed depth
386
+ terminal: Terminal type or "auto"
387
+ prompt: Optional initial prompt
388
+
389
+ Returns:
390
+ SpawnResult with success status
391
+ """
392
+ # Build command with prompt as CLI argument and auto-approve for autonomous work
393
+ command = build_cli_command(
394
+ cli,
395
+ prompt=prompt,
396
+ session_id=session_id,
397
+ auto_approve=True, # Subagents need to work autonomously
398
+ working_directory=str(cwd) if cli == "codex" else None,
399
+ mode="terminal", # Interactive terminal mode
400
+ )
401
+
402
+ # Handle prompt for environment variables (backup for hooks/context)
403
+ prompt_env: str | None = None
404
+ prompt_file: str | None = None
405
+
406
+ if prompt:
407
+ if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
408
+ prompt_env = prompt
409
+ else:
410
+ prompt_file = self._write_prompt_file(prompt, session_id)
411
+
412
+ # Build environment
413
+ env = get_terminal_env_vars(
414
+ session_id=session_id,
415
+ parent_session_id=parent_session_id,
416
+ agent_run_id=agent_run_id,
417
+ project_id=project_id,
418
+ workflow_name=workflow_name,
419
+ agent_depth=agent_depth,
420
+ max_agent_depth=max_agent_depth,
421
+ prompt=prompt_env,
422
+ prompt_file=prompt_file,
423
+ )
424
+
425
+ # Set title (avoid colons/parentheses which Ghostty interprets as config syntax)
426
+ title = f"gobby-{cli}-d{agent_depth}"
427
+
428
+ return self.spawn(
429
+ command=command,
430
+ cwd=cwd,
431
+ terminal=terminal,
432
+ env=env,
433
+ title=title,
434
+ )
435
+
436
+ def _write_prompt_file(self, prompt: str, session_id: str) -> str:
437
+ """
438
+ Write prompt to a temp file for passing to spawned agent.
439
+
440
+ Delegates to the module-level _create_prompt_file helper which
441
+ handles secure permissions and cleanup tracking.
442
+
443
+ Args:
444
+ prompt: The prompt content
445
+ session_id: Session ID for naming the file
446
+
447
+ Returns:
448
+ Path to the created temp file
449
+ """
450
+ return _create_prompt_file(prompt, session_id)
451
+
452
+
453
+ @dataclass
454
+ class PreparedSpawn:
455
+ """Configuration for a prepared terminal spawn."""
456
+
457
+ session_id: str
458
+ """The pre-created child session ID."""
459
+
460
+ agent_run_id: str
461
+ """The agent run record ID."""
462
+
463
+ parent_session_id: str
464
+ """The parent session ID."""
465
+
466
+ project_id: str
467
+ """The project ID."""
468
+
469
+ workflow_name: str | None
470
+ """Workflow to activate (if any)."""
471
+
472
+ agent_depth: int
473
+ """Current agent depth."""
474
+
475
+ env_vars: dict[str, str]
476
+ """Environment variables to set."""
477
+
478
+
479
+ def prepare_terminal_spawn(
480
+ session_manager: ChildSessionManager,
481
+ parent_session_id: str,
482
+ project_id: str,
483
+ machine_id: str,
484
+ source: str = "claude",
485
+ agent_id: str | None = None,
486
+ workflow_name: str | None = None,
487
+ title: str | None = None,
488
+ git_branch: str | None = None,
489
+ prompt: str | None = None,
490
+ max_agent_depth: int = 3,
491
+ ) -> PreparedSpawn:
492
+ """
493
+ Prepare a terminal spawn by creating the child session.
494
+
495
+ This should be called before spawning a terminal to:
496
+ 1. Create the child session in the database
497
+ 2. Generate the agent run ID
498
+ 3. Build the environment variables
499
+
500
+ Args:
501
+ session_manager: ChildSessionManager for session creation
502
+ parent_session_id: Parent session ID
503
+ project_id: Project ID
504
+ machine_id: Machine ID
505
+ source: CLI source (claude, gemini, codex)
506
+ agent_id: Optional agent ID
507
+ workflow_name: Optional workflow to activate
508
+ title: Optional session title
509
+ git_branch: Optional git branch
510
+ prompt: Optional initial prompt
511
+ max_agent_depth: Maximum agent depth
512
+
513
+ Returns:
514
+ PreparedSpawn with all necessary spawn configuration
515
+
516
+ Raises:
517
+ ValueError: If max agent depth exceeded
518
+ """
519
+ import uuid
520
+
521
+ # Create child session config
522
+ config = ChildSessionConfig(
523
+ parent_session_id=parent_session_id,
524
+ project_id=project_id,
525
+ machine_id=machine_id,
526
+ source=source,
527
+ agent_id=agent_id,
528
+ workflow_name=workflow_name,
529
+ title=title,
530
+ git_branch=git_branch,
531
+ )
532
+
533
+ # Create the child session
534
+ child_session = session_manager.create_child_session(config)
535
+
536
+ # Generate agent run ID
537
+ agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
538
+
539
+ # Handle prompt - decide env var vs file
540
+ prompt_env: str | None = None
541
+ prompt_file: str | None = None
542
+
543
+ if prompt:
544
+ if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
545
+ prompt_env = prompt
546
+ else:
547
+ # Write to temp file with secure permissions
548
+ prompt_file = _create_prompt_file(prompt, child_session.id)
549
+
550
+ # Build environment variables
551
+ env_vars = get_terminal_env_vars(
552
+ session_id=child_session.id,
553
+ parent_session_id=parent_session_id,
554
+ agent_run_id=agent_run_id,
555
+ project_id=project_id,
556
+ workflow_name=workflow_name,
557
+ agent_depth=child_session.agent_depth,
558
+ max_agent_depth=max_agent_depth,
559
+ prompt=prompt_env,
560
+ prompt_file=prompt_file,
561
+ )
562
+
563
+ return PreparedSpawn(
564
+ session_id=child_session.id,
565
+ agent_run_id=agent_run_id,
566
+ parent_session_id=parent_session_id,
567
+ project_id=project_id,
568
+ workflow_name=workflow_name,
569
+ agent_depth=child_session.agent_depth,
570
+ env_vars=env_vars,
571
+ )
572
+
573
+
574
+ def read_prompt_from_env() -> str | None:
575
+ """
576
+ Read initial prompt from environment variables.
577
+
578
+ Checks GOBBY_PROMPT_FILE first (for long prompts),
579
+ then falls back to GOBBY_PROMPT (for short prompts).
580
+
581
+ Returns:
582
+ Prompt string or None if not set
583
+ """
584
+ from gobby.agents.constants import GOBBY_PROMPT, GOBBY_PROMPT_FILE
585
+
586
+ # Check for prompt file first
587
+ prompt_file = os.environ.get(GOBBY_PROMPT_FILE)
588
+ if prompt_file:
589
+ try:
590
+ prompt_path = Path(prompt_file)
591
+ if prompt_path.exists():
592
+ return prompt_path.read_text(encoding="utf-8")
593
+ else:
594
+ logger.warning(f"Prompt file not found: {prompt_file}")
595
+ except Exception as e:
596
+ logger.error(f"Error reading prompt file: {e}")
597
+
598
+ # Fall back to inline prompt
599
+ return os.environ.get(GOBBY_PROMPT)
600
+
601
+
602
+ async def prepare_gemini_spawn_with_preflight(
603
+ session_manager: ChildSessionManager,
604
+ parent_session_id: str,
605
+ project_id: str,
606
+ machine_id: str,
607
+ agent_id: str | None = None,
608
+ workflow_name: str | None = None,
609
+ title: str | None = None,
610
+ git_branch: str | None = None,
611
+ prompt: str | None = None,
612
+ max_agent_depth: int = 3,
613
+ preflight_timeout: float = 10.0,
614
+ ) -> PreparedSpawn:
615
+ """
616
+ Prepare a Gemini terminal spawn with preflight session ID capture.
617
+
618
+ This is necessary because Gemini CLI in interactive mode cannot introspect
619
+ its own session_id. We use preflight capture to:
620
+ 1. Launch Gemini with stream-json to capture its session_id
621
+ 2. Create the Gobby session with that external_id
622
+ 3. Resume the Gemini session with -r flag
623
+
624
+ Args:
625
+ session_manager: ChildSessionManager for session creation
626
+ parent_session_id: Parent session ID
627
+ project_id: Project ID
628
+ machine_id: Machine ID
629
+ agent_id: Optional agent ID
630
+ workflow_name: Optional workflow to activate
631
+ title: Optional session title
632
+ git_branch: Optional git branch
633
+ prompt: Optional initial prompt
634
+ max_agent_depth: Maximum agent depth
635
+ preflight_timeout: Timeout for preflight capture (default 10s)
636
+
637
+ Returns:
638
+ PreparedSpawn with gemini_external_id set in env_vars
639
+
640
+ Raises:
641
+ ValueError: If max agent depth exceeded
642
+ asyncio.TimeoutError: If preflight capture times out
643
+ """
644
+ import uuid
645
+
646
+ from gobby.agents.gemini_session import capture_gemini_session_id
647
+
648
+ # 1. Preflight: capture Gemini's session_id
649
+ logger.info("Starting Gemini preflight capture...")
650
+ gemini_info = await capture_gemini_session_id(timeout=preflight_timeout)
651
+ logger.info(f"Captured Gemini session_id: {gemini_info.session_id}")
652
+
653
+ # 2. Create child session config with Gemini's session_id as external_id
654
+ config = ChildSessionConfig(
655
+ parent_session_id=parent_session_id,
656
+ project_id=project_id,
657
+ machine_id=machine_id,
658
+ source="gemini",
659
+ agent_id=agent_id,
660
+ workflow_name=workflow_name,
661
+ title=title,
662
+ git_branch=git_branch,
663
+ external_id=gemini_info.session_id, # Link to Gemini's session
664
+ )
665
+
666
+ # Create the child session
667
+ child_session = session_manager.create_child_session(config)
668
+
669
+ # Generate agent run ID
670
+ agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
671
+
672
+ # Handle prompt - decide env var vs file
673
+ prompt_env: str | None = None
674
+ prompt_file: str | None = None
675
+
676
+ if prompt:
677
+ if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
678
+ prompt_env = prompt
679
+ else:
680
+ prompt_file = _create_prompt_file(prompt, child_session.id)
681
+
682
+ # Build environment variables
683
+ env_vars = get_terminal_env_vars(
684
+ session_id=child_session.id,
685
+ parent_session_id=parent_session_id,
686
+ agent_run_id=agent_run_id,
687
+ project_id=project_id,
688
+ workflow_name=workflow_name,
689
+ agent_depth=child_session.agent_depth,
690
+ max_agent_depth=max_agent_depth,
691
+ prompt=prompt_env,
692
+ prompt_file=prompt_file,
693
+ )
694
+
695
+ # Add Gemini-specific env vars for session linking
696
+ env_vars["GOBBY_GEMINI_EXTERNAL_ID"] = gemini_info.session_id
697
+ if gemini_info.model:
698
+ env_vars["GOBBY_GEMINI_MODEL"] = gemini_info.model
699
+
700
+ return PreparedSpawn(
701
+ session_id=child_session.id,
702
+ agent_run_id=agent_run_id,
703
+ parent_session_id=parent_session_id,
704
+ project_id=project_id,
705
+ workflow_name=workflow_name,
706
+ agent_depth=child_session.agent_depth,
707
+ env_vars=env_vars,
708
+ )
709
+
710
+
711
+ def build_gemini_command_with_resume(
712
+ gemini_external_id: str,
713
+ prompt: str | None = None,
714
+ auto_approve: bool = False,
715
+ gobby_session_id: str | None = None,
716
+ ) -> list[str]:
717
+ """
718
+ Build Gemini CLI command with session resume.
719
+
720
+ Uses -r flag to resume a preflight-captured session, with session context
721
+ injected into the initial prompt.
722
+
723
+ Args:
724
+ gemini_external_id: Gemini's session_id from preflight capture
725
+ prompt: Optional user prompt
726
+ auto_approve: If True, add --approval-mode yolo
727
+ gobby_session_id: Gobby session ID to inject into context
728
+
729
+ Returns:
730
+ Command list for subprocess execution
731
+ """
732
+ command = ["gemini"]
733
+
734
+ # Resume the preflight session
735
+ command.extend(["-r", gemini_external_id])
736
+
737
+ if auto_approve:
738
+ command.extend(["--approval-mode", "yolo"])
739
+
740
+ # Build prompt with session context
741
+ if gobby_session_id:
742
+ context_prefix = (
743
+ f"Your Gobby session_id is: {gobby_session_id}\n"
744
+ f"Use this when calling Gobby MCP tools.\n\n"
745
+ )
746
+ full_prompt = context_prefix + (prompt or "")
747
+ else:
748
+ full_prompt = prompt or ""
749
+
750
+ # Use -i for interactive mode with initial prompt
751
+ if full_prompt:
752
+ command.extend(["-i", full_prompt])
753
+
754
+ return command
755
+
756
+
757
+ # =============================================================================
758
+ # Codex Preflight Capture
759
+ # =============================================================================
760
+
761
+
762
+ async def prepare_codex_spawn_with_preflight(
763
+ session_manager: ChildSessionManager,
764
+ parent_session_id: str,
765
+ project_id: str,
766
+ machine_id: str,
767
+ agent_id: str | None = None,
768
+ workflow_name: str | None = None,
769
+ title: str | None = None,
770
+ git_branch: str | None = None,
771
+ prompt: str | None = None,
772
+ max_agent_depth: int = 3,
773
+ preflight_timeout: float = 30.0,
774
+ ) -> PreparedSpawn:
775
+ """
776
+ Prepare a Codex terminal spawn with preflight session ID capture.
777
+
778
+ This is necessary because we need Codex's session_id before launching
779
+ interactive mode to properly link sessions. We use preflight capture to:
780
+ 1. Launch Codex with `exec "exit"` to capture its session_id
781
+ 2. Create the Gobby session with that external_id
782
+ 3. Resume the Codex session with `codex resume {session_id}`
783
+
784
+ Args:
785
+ session_manager: ChildSessionManager for session creation
786
+ parent_session_id: Parent session ID
787
+ project_id: Project ID
788
+ machine_id: Machine ID
789
+ agent_id: Optional agent ID
790
+ workflow_name: Optional workflow to activate
791
+ title: Optional session title
792
+ git_branch: Optional git branch
793
+ prompt: Optional initial prompt
794
+ max_agent_depth: Maximum agent depth
795
+ preflight_timeout: Timeout for preflight capture (default 30s)
796
+
797
+ Returns:
798
+ PreparedSpawn with codex_external_id set in env_vars
799
+
800
+ Raises:
801
+ ValueError: If max agent depth exceeded
802
+ asyncio.TimeoutError: If preflight capture times out
803
+ """
804
+ import uuid
805
+
806
+ from gobby.agents.codex_session import capture_codex_session_id
807
+
808
+ # 1. Preflight: capture Codex's session_id
809
+ logger.info("Starting Codex preflight capture...")
810
+ codex_info = await capture_codex_session_id(timeout=preflight_timeout)
811
+ logger.info(f"Captured Codex session_id: {codex_info.session_id}")
812
+
813
+ # 2. Create child session config with Codex's session_id as external_id
814
+ config = ChildSessionConfig(
815
+ parent_session_id=parent_session_id,
816
+ project_id=project_id,
817
+ machine_id=machine_id,
818
+ source="codex",
819
+ agent_id=agent_id,
820
+ workflow_name=workflow_name,
821
+ title=title,
822
+ git_branch=git_branch,
823
+ external_id=codex_info.session_id, # Link to Codex's session
824
+ )
825
+
826
+ # Create the child session
827
+ child_session = session_manager.create_child_session(config)
828
+
829
+ # Generate agent run ID
830
+ agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
831
+
832
+ # Handle prompt - decide env var vs file
833
+ prompt_env: str | None = None
834
+ prompt_file: str | None = None
835
+
836
+ if prompt:
837
+ if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
838
+ prompt_env = prompt
839
+ else:
840
+ prompt_file = _create_prompt_file(prompt, child_session.id)
841
+
842
+ # Build environment variables
843
+ env_vars = get_terminal_env_vars(
844
+ session_id=child_session.id,
845
+ parent_session_id=parent_session_id,
846
+ agent_run_id=agent_run_id,
847
+ project_id=project_id,
848
+ workflow_name=workflow_name,
849
+ agent_depth=child_session.agent_depth,
850
+ max_agent_depth=max_agent_depth,
851
+ prompt=prompt_env,
852
+ prompt_file=prompt_file,
853
+ )
854
+
855
+ # Add Codex-specific env vars for session linking
856
+ env_vars["GOBBY_CODEX_EXTERNAL_ID"] = codex_info.session_id
857
+ if codex_info.model:
858
+ env_vars["GOBBY_CODEX_MODEL"] = codex_info.model
859
+
860
+ return PreparedSpawn(
861
+ session_id=child_session.id,
862
+ agent_run_id=agent_run_id,
863
+ parent_session_id=parent_session_id,
864
+ project_id=project_id,
865
+ workflow_name=workflow_name,
866
+ agent_depth=child_session.agent_depth,
867
+ env_vars=env_vars,
868
+ )
869
+
870
+
871
+ def build_codex_command_with_resume(
872
+ codex_external_id: str,
873
+ prompt: str | None = None,
874
+ auto_approve: bool = False,
875
+ gobby_session_id: str | None = None,
876
+ working_directory: str | None = None,
877
+ ) -> list[str]:
878
+ """
879
+ Build Codex CLI command with session resume.
880
+
881
+ Uses `codex resume {session_id}` to resume a preflight-captured session,
882
+ with session context injected into the prompt.
883
+
884
+ Args:
885
+ codex_external_id: Codex's session_id from preflight capture
886
+ prompt: Optional user prompt
887
+ auto_approve: If True, add --full-auto flag
888
+ gobby_session_id: Gobby session ID to inject into context
889
+ working_directory: Optional working directory override
890
+
891
+ Returns:
892
+ Command list for subprocess execution
893
+ """
894
+ command = ["codex", "resume", codex_external_id]
895
+
896
+ if auto_approve:
897
+ command.append("--full-auto")
898
+
899
+ if working_directory:
900
+ command.extend(["-C", working_directory])
901
+
902
+ # Build prompt with session context
903
+ if gobby_session_id:
904
+ context_prefix = (
905
+ f"Your Gobby session_id is: {gobby_session_id}\n"
906
+ f"Use this when calling Gobby MCP tools.\n\n"
907
+ )
908
+ full_prompt = context_prefix + (prompt or "")
909
+ else:
910
+ full_prompt = prompt or ""
911
+
912
+ # Prompt is a positional argument after session_id
913
+ if full_prompt:
914
+ command.append(full_prompt)
915
+
916
+ return command