gobby 0.2.6__py3-none-any.whl → 0.2.8__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
1
+ """
2
+ Unified Spawn Executor for Agent Spawning.
3
+
4
+ This module consolidates the spawn dispatch logic from agents.py, worktrees.py,
5
+ and clones.py into a single unified executor that handles terminal, embedded,
6
+ and headless modes.
7
+ """
8
+
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from typing import TYPE_CHECKING, Any, Literal, cast
12
+
13
+ from gobby.agents.sandbox import (
14
+ GeminiSandboxResolver,
15
+ SandboxConfig,
16
+ compute_sandbox_paths,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.agents.session import ChildSessionManager
21
+ from gobby.agents.spawn import (
22
+ TerminalSpawner,
23
+ build_codex_command_with_resume,
24
+ build_gemini_command_with_resume,
25
+ prepare_codex_spawn_with_preflight,
26
+ prepare_gemini_spawn_with_preflight,
27
+ )
28
+ from gobby.agents.spawners.embedded import EmbeddedSpawner
29
+ from gobby.agents.spawners.headless import HeadlessSpawner
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ @dataclass
35
+ class SpawnRequest:
36
+ """Request for spawning an agent."""
37
+
38
+ # Required fields
39
+ prompt: str
40
+ cwd: str
41
+ mode: Literal["terminal", "embedded", "headless"]
42
+ provider: str
43
+ terminal: str
44
+ session_id: str
45
+ run_id: str
46
+ parent_session_id: str
47
+ project_id: str
48
+
49
+ # Optional fields
50
+ workflow: str | None = None
51
+ worktree_id: str | None = None
52
+ clone_id: str | None = None
53
+ agent_depth: int = 0
54
+ max_agent_depth: int = 3
55
+ session_manager: Any | None = None # Required for Gemini/Codex preflight
56
+ machine_id: str | None = None
57
+
58
+ # Sandbox configuration
59
+ sandbox_config: SandboxConfig | None = None
60
+ sandbox_args: list[str] | None = None
61
+ sandbox_env: dict[str, str] | None = field(default=None)
62
+
63
+
64
+ @dataclass
65
+ class SpawnResult:
66
+ """Result of a spawn operation."""
67
+
68
+ success: bool
69
+ run_id: str
70
+ child_session_id: str | None
71
+ status: str
72
+
73
+ # Optional result fields
74
+ pid: int | None = None
75
+ terminal_type: str | None = None
76
+ master_fd: int | None = None
77
+ error: str | None = None
78
+ message: str | None = None
79
+ process: Any | None = None # subprocess.Popen for headless
80
+ gemini_session_id: str | None = None # Gemini external session ID
81
+ codex_session_id: str | None = None # Codex external session ID
82
+
83
+
84
+ async def execute_spawn(request: SpawnRequest) -> SpawnResult:
85
+ """
86
+ Unified spawn dispatch for terminal/embedded/headless modes.
87
+
88
+ Consolidates duplicated logic from agents.py, worktrees.py, clones.py
89
+ into a single dispatch function.
90
+
91
+ Args:
92
+ request: SpawnRequest with all spawn parameters
93
+
94
+ Returns:
95
+ SpawnResult with spawn outcome and metadata
96
+ """
97
+ if request.mode == "terminal":
98
+ # Special handling for Gemini/Codex: requires preflight session capture
99
+ if request.provider == "gemini":
100
+ return await _spawn_gemini_terminal(request)
101
+ elif request.provider == "codex":
102
+ return await _spawn_codex_terminal(request)
103
+ return await _spawn_terminal(request)
104
+ elif request.mode == "embedded":
105
+ return await _spawn_embedded(request)
106
+ else: # headless
107
+ return await _spawn_headless(request)
108
+
109
+
110
+ async def _spawn_terminal(request: SpawnRequest) -> SpawnResult:
111
+ """Spawn agent in external terminal."""
112
+ spawner = TerminalSpawner()
113
+ result = spawner.spawn_agent(
114
+ cli=request.provider,
115
+ cwd=request.cwd,
116
+ session_id=request.session_id,
117
+ parent_session_id=request.parent_session_id,
118
+ agent_run_id=request.run_id,
119
+ project_id=request.project_id,
120
+ workflow_name=request.workflow,
121
+ agent_depth=request.agent_depth,
122
+ max_agent_depth=request.max_agent_depth,
123
+ terminal=request.terminal,
124
+ prompt=request.prompt,
125
+ sandbox_config=request.sandbox_config,
126
+ )
127
+
128
+ if not result.success:
129
+ return SpawnResult(
130
+ success=False,
131
+ run_id=request.run_id,
132
+ child_session_id=request.session_id,
133
+ status="failed",
134
+ error=result.error or result.message,
135
+ )
136
+
137
+ return SpawnResult(
138
+ success=True,
139
+ run_id=request.run_id,
140
+ child_session_id=request.session_id,
141
+ status="pending",
142
+ pid=result.pid,
143
+ terminal_type=result.terminal_type,
144
+ message=f"Agent spawned in {result.terminal_type} (PID: {result.pid})",
145
+ )
146
+
147
+
148
+ async def _spawn_embedded(request: SpawnRequest) -> SpawnResult:
149
+ """Spawn agent with PTY for UI attachment."""
150
+ spawner = EmbeddedSpawner()
151
+ result = spawner.spawn_agent(
152
+ cli=request.provider,
153
+ cwd=request.cwd,
154
+ session_id=request.session_id,
155
+ parent_session_id=request.parent_session_id,
156
+ agent_run_id=request.run_id,
157
+ project_id=request.project_id,
158
+ workflow_name=request.workflow,
159
+ agent_depth=request.agent_depth,
160
+ max_agent_depth=request.max_agent_depth,
161
+ prompt=request.prompt,
162
+ sandbox_config=request.sandbox_config,
163
+ )
164
+
165
+ if not result.success:
166
+ return SpawnResult(
167
+ success=False,
168
+ run_id=request.run_id,
169
+ child_session_id=request.session_id,
170
+ status="failed",
171
+ error=result.error or result.message,
172
+ )
173
+
174
+ return SpawnResult(
175
+ success=True,
176
+ run_id=request.run_id,
177
+ child_session_id=request.session_id,
178
+ status="pending",
179
+ pid=result.pid,
180
+ master_fd=result.master_fd,
181
+ message=f"Agent spawned with PTY (PID: {result.pid})",
182
+ )
183
+
184
+
185
+ async def _spawn_headless(request: SpawnRequest) -> SpawnResult:
186
+ """Spawn headless agent with output capture."""
187
+ spawner = HeadlessSpawner()
188
+ result = spawner.spawn_agent(
189
+ cli=request.provider,
190
+ cwd=request.cwd,
191
+ session_id=request.session_id,
192
+ parent_session_id=request.parent_session_id,
193
+ agent_run_id=request.run_id,
194
+ project_id=request.project_id,
195
+ workflow_name=request.workflow,
196
+ agent_depth=request.agent_depth,
197
+ max_agent_depth=request.max_agent_depth,
198
+ prompt=request.prompt,
199
+ sandbox_config=request.sandbox_config,
200
+ )
201
+
202
+ if not result.success:
203
+ return SpawnResult(
204
+ success=False,
205
+ run_id=request.run_id,
206
+ child_session_id=request.session_id,
207
+ status="failed",
208
+ error=result.error or result.message,
209
+ )
210
+
211
+ return SpawnResult(
212
+ success=True,
213
+ run_id=request.run_id,
214
+ child_session_id=request.session_id,
215
+ status="running", # Headless is immediately running
216
+ pid=result.pid,
217
+ process=result.process,
218
+ message=f"Agent spawned headless (PID: {result.pid})",
219
+ )
220
+
221
+
222
+ async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
223
+ """
224
+ Spawn Gemini agent in terminal with preflight session capture.
225
+
226
+ Uses preflight to capture Gemini's session_id before launching interactive mode:
227
+ 1. Run `gemini --output-format stream-json` to capture Gemini's session_id
228
+ 2. Pre-create Gobby session with parent_session_id linked and external_id set
229
+ 3. Resume Gemini session with `-r {session_id}` flag
230
+
231
+ This approach ensures session linkage works without relying on env vars,
232
+ which don't pass through macOS's `open` command.
233
+ """
234
+ if request.session_manager is None:
235
+ return SpawnResult(
236
+ success=False,
237
+ run_id=request.run_id,
238
+ child_session_id=None,
239
+ status="failed",
240
+ error="session_manager is required for Gemini preflight",
241
+ )
242
+
243
+ try:
244
+ # Preflight capture: gets Gemini's session_id and creates linked Gobby session
245
+ spawn_context = await prepare_gemini_spawn_with_preflight(
246
+ session_manager=cast("ChildSessionManager", request.session_manager),
247
+ parent_session_id=request.parent_session_id,
248
+ project_id=request.project_id,
249
+ machine_id=request.machine_id or "unknown",
250
+ workflow_name=request.workflow,
251
+ git_branch=None, # Will be detected by hook
252
+ prompt=request.prompt,
253
+ max_agent_depth=request.max_agent_depth,
254
+ )
255
+ except FileNotFoundError as e:
256
+ logger.error(
257
+ f"Gemini spawn failed - command not found: {e}",
258
+ extra={"project_id": request.project_id, "run_id": request.run_id},
259
+ )
260
+ return SpawnResult(
261
+ success=False,
262
+ run_id=request.run_id,
263
+ child_session_id=None,
264
+ status="failed",
265
+ error=str(e),
266
+ )
267
+ except Exception as e:
268
+ logger.error(
269
+ f"Gemini preflight capture failed: {e}",
270
+ extra={"project_id": request.project_id, "run_id": request.run_id},
271
+ exc_info=True,
272
+ )
273
+ return SpawnResult(
274
+ success=False,
275
+ run_id=request.run_id,
276
+ child_session_id=None,
277
+ status="failed",
278
+ error=f"Gemini preflight capture failed: {e}",
279
+ )
280
+
281
+ # Extract IDs from prepared spawn context
282
+ gobby_session_id = spawn_context.session_id
283
+ gemini_session_id = spawn_context.env_vars["GOBBY_GEMINI_EXTERNAL_ID"]
284
+
285
+ # Build command with resume (no env vars needed - session already linked)
286
+ cmd = build_gemini_command_with_resume(
287
+ gemini_external_id=gemini_session_id,
288
+ prompt=request.prompt,
289
+ auto_approve=True,
290
+ gobby_session_id=gobby_session_id,
291
+ )
292
+
293
+ # Resolve sandbox config if provided
294
+ sandbox_env: dict[str, str] = {}
295
+ if request.sandbox_config and request.sandbox_config.enabled:
296
+ resolver = GeminiSandboxResolver()
297
+ paths = compute_sandbox_paths(
298
+ config=request.sandbox_config,
299
+ workspace_path=request.cwd,
300
+ )
301
+ sandbox_args, sandbox_env = resolver.resolve(request.sandbox_config, paths)
302
+ # Append sandbox args to command (e.g., -s flag)
303
+ cmd.extend(sandbox_args)
304
+
305
+ # Spawn in terminal
306
+ terminal_spawner = TerminalSpawner()
307
+ terminal_result = terminal_spawner.spawn(
308
+ command=cmd,
309
+ cwd=request.cwd,
310
+ terminal=request.terminal,
311
+ env=sandbox_env if sandbox_env else None,
312
+ )
313
+
314
+ if not terminal_result.success:
315
+ return SpawnResult(
316
+ success=False,
317
+ run_id=request.run_id,
318
+ child_session_id=gobby_session_id,
319
+ status="failed",
320
+ error=terminal_result.error or terminal_result.message,
321
+ )
322
+
323
+ return SpawnResult(
324
+ success=True,
325
+ run_id=f"gemini-{gemini_session_id[:8]}",
326
+ child_session_id=gobby_session_id, # Now properly set!
327
+ status="pending",
328
+ pid=terminal_result.pid,
329
+ gemini_session_id=gemini_session_id,
330
+ message=f"Gemini agent spawned in terminal with session {gobby_session_id}",
331
+ )
332
+
333
+
334
+ async def _spawn_codex_terminal(request: SpawnRequest) -> SpawnResult:
335
+ """
336
+ Spawn Codex agent in terminal with preflight session capture.
337
+
338
+ Codex outputs session_id in startup banner, which we parse from `codex exec "exit"`.
339
+ """
340
+ if request.session_manager is None:
341
+ return SpawnResult(
342
+ success=False,
343
+ run_id=request.run_id,
344
+ child_session_id=request.session_id,
345
+ status="failed",
346
+ error="session_manager is required for Codex preflight",
347
+ )
348
+
349
+ try:
350
+ # Preflight capture: gets Codex's session_id and creates linked Gobby session
351
+ spawn_context = await prepare_codex_spawn_with_preflight(
352
+ session_manager=cast("ChildSessionManager", request.session_manager),
353
+ parent_session_id=request.parent_session_id,
354
+ project_id=request.project_id,
355
+ machine_id=request.machine_id or "unknown",
356
+ workflow_name=request.workflow,
357
+ git_branch=None, # Will be detected by hook
358
+ )
359
+ except FileNotFoundError as e:
360
+ return SpawnResult(
361
+ success=False,
362
+ run_id=request.run_id,
363
+ child_session_id=request.session_id,
364
+ status="failed",
365
+ error=str(e),
366
+ )
367
+ except Exception as e:
368
+ logger.error(f"Codex preflight capture failed: {e}", exc_info=True)
369
+ return SpawnResult(
370
+ success=False,
371
+ run_id=request.run_id,
372
+ child_session_id=request.session_id,
373
+ status="failed",
374
+ error=f"Codex preflight capture failed: {e}",
375
+ )
376
+
377
+ # Extract IDs from prepared spawn context
378
+ gobby_session_id = spawn_context.session_id
379
+ codex_session_id = spawn_context.env_vars["GOBBY_CODEX_EXTERNAL_ID"]
380
+
381
+ # Build command with session context injected into prompt
382
+ cmd = build_codex_command_with_resume(
383
+ codex_external_id=codex_session_id,
384
+ prompt=request.prompt,
385
+ auto_approve=True, # --full-auto for sandboxed autonomy
386
+ gobby_session_id=gobby_session_id,
387
+ working_directory=request.cwd,
388
+ )
389
+
390
+ # Spawn in terminal
391
+ terminal_spawner = TerminalSpawner()
392
+ terminal_result = terminal_spawner.spawn(
393
+ command=cmd,
394
+ cwd=request.cwd,
395
+ terminal=request.terminal,
396
+ )
397
+
398
+ if not terminal_result.success:
399
+ return SpawnResult(
400
+ success=False,
401
+ run_id=request.run_id,
402
+ child_session_id=gobby_session_id,
403
+ status="failed",
404
+ error=terminal_result.error or terminal_result.message,
405
+ )
406
+
407
+ return SpawnResult(
408
+ success=True,
409
+ run_id=f"codex-{codex_session_id[:8]}",
410
+ child_session_id=gobby_session_id,
411
+ status="pending",
412
+ pid=terminal_result.pid,
413
+ codex_session_id=codex_session_id,
414
+ message=f"Codex agent spawned in terminal with session {gobby_session_id}",
415
+ )
@@ -14,6 +14,12 @@ Usage:
14
14
  ITermSpawner,
15
15
  # etc.
16
16
  )
17
+
18
+ # Command building
19
+ from gobby.agents.spawners import build_cli_command
20
+
21
+ # Prompt file management
22
+ from gobby.agents.spawners import create_prompt_file, read_prompt_from_env
17
23
  """
18
24
 
19
25
  from gobby.agents.spawners.base import (
@@ -24,6 +30,11 @@ from gobby.agents.spawners.base import (
24
30
  TerminalSpawnerBase,
25
31
  TerminalType,
26
32
  )
33
+ from gobby.agents.spawners.command_builder import (
34
+ build_cli_command,
35
+ build_codex_command_with_resume,
36
+ build_gemini_command_with_resume,
37
+ )
27
38
  from gobby.agents.spawners.cross_platform import (
28
39
  AlacrittySpawner,
29
40
  KittySpawner,
@@ -40,6 +51,11 @@ from gobby.agents.spawners.macos import (
40
51
  ITermSpawner,
41
52
  TerminalAppSpawner,
42
53
  )
54
+ from gobby.agents.spawners.prompt_manager import (
55
+ MAX_ENV_PROMPT_LENGTH,
56
+ create_prompt_file,
57
+ read_prompt_from_env,
58
+ )
43
59
  from gobby.agents.spawners.windows import (
44
60
  CmdSpawner,
45
61
  PowerShellSpawner,
@@ -74,4 +90,12 @@ __all__ = [
74
90
  # Embedded/Headless spawners
75
91
  "EmbeddedSpawner",
76
92
  "HeadlessSpawner",
93
+ # Command building
94
+ "build_cli_command",
95
+ "build_gemini_command_with_resume",
96
+ "build_codex_command_with_resume",
97
+ # Prompt management
98
+ "MAX_ENV_PROMPT_LENGTH",
99
+ "create_prompt_file",
100
+ "read_prompt_from_env",
77
101
  ]
@@ -0,0 +1,189 @@
1
+ """CLI command building for agent spawning.
2
+
3
+ Provides functions to construct CLI commands for Claude, Gemini, and Codex
4
+ with proper flags for prompts, permissions, and session management.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ def build_cli_command(
11
+ cli: str,
12
+ prompt: str | None = None,
13
+ session_id: str | None = None,
14
+ auto_approve: bool = False,
15
+ working_directory: str | None = None,
16
+ mode: str = "terminal",
17
+ sandbox_args: list[str] | None = None,
18
+ ) -> list[str]:
19
+ """
20
+ Build the CLI command with proper prompt passing and permission flags.
21
+
22
+ Each CLI has different syntax for passing prompts and handling permissions:
23
+
24
+ Claude Code:
25
+ - claude --session-id <uuid> --dangerously-skip-permissions [prompt]
26
+ - Use --dangerously-skip-permissions for autonomous subagent operation
27
+
28
+ Gemini CLI:
29
+ - gemini -i "prompt" (interactive mode with initial prompt)
30
+ - gemini --approval-mode yolo -i "prompt" (YOLO + interactive)
31
+ - gemini "prompt" (one-shot non-interactive for headless)
32
+
33
+ Codex CLI:
34
+ - codex --full-auto -C <dir> [PROMPT]
35
+ - Or: codex -c 'sandbox_permissions=["disk-full-read-access"]' -a never [PROMPT]
36
+
37
+ Args:
38
+ cli: CLI name (claude, gemini, codex)
39
+ prompt: Optional prompt to pass
40
+ session_id: Optional session ID (used by Claude CLI)
41
+ auto_approve: If True, add flags to auto-approve actions/permissions
42
+ working_directory: Optional working directory (used by Codex -C flag)
43
+ mode: Execution mode - "terminal" (interactive) or "headless" (non-interactive)
44
+ sandbox_args: Optional list of CLI args for sandbox configuration
45
+
46
+ Returns:
47
+ Command list for subprocess execution
48
+ """
49
+ command = [cli]
50
+
51
+ if cli == "claude":
52
+ # Claude CLI flags
53
+ if session_id:
54
+ command.extend(["--session-id", session_id])
55
+ if auto_approve:
56
+ # Skip all permission prompts for autonomous subagent operation
57
+ command.append("--dangerously-skip-permissions")
58
+ # For headless mode, use -p (print mode) for single-turn execution
59
+ # For terminal mode, don't use -p to allow multi-turn interaction
60
+ if prompt and mode != "terminal":
61
+ command.append("-p")
62
+
63
+ elif cli == "gemini":
64
+ # Gemini CLI flags
65
+ if auto_approve:
66
+ command.extend(["--approval-mode", "yolo"])
67
+ # For terminal mode, use -i (prompt-interactive) to execute prompt and stay interactive
68
+ # For headless mode, use positional prompt for one-shot execution
69
+ if prompt:
70
+ if mode == "terminal":
71
+ command.extend(["-i", prompt])
72
+ # Add sandbox args before returning (prompt already added via -i flag)
73
+ if sandbox_args:
74
+ command.extend(sandbox_args)
75
+ return command # Don't add prompt again as positional
76
+ # else: fall through to add as positional for headless
77
+
78
+ elif cli == "codex":
79
+ # Codex CLI flags
80
+ if auto_approve:
81
+ # --full-auto: low-friction sandboxed automatic execution
82
+ command.append("--full-auto")
83
+ if working_directory:
84
+ command.extend(["-C", working_directory])
85
+
86
+ # Add sandbox args before prompt (prompt must be last)
87
+ if sandbox_args:
88
+ command.extend(sandbox_args)
89
+
90
+ # All three CLIs accept prompt as positional argument (must come last)
91
+ # For Gemini terminal mode, this is skipped (handled above with -i flag)
92
+ if prompt:
93
+ command.append(prompt)
94
+
95
+ return command
96
+
97
+
98
+ def build_gemini_command_with_resume(
99
+ gemini_external_id: str,
100
+ prompt: str | None = None,
101
+ auto_approve: bool = False,
102
+ gobby_session_id: str | None = None,
103
+ ) -> list[str]:
104
+ """
105
+ Build Gemini CLI command with session resume.
106
+
107
+ Uses -r flag to resume a preflight-captured session, with session context
108
+ injected into the initial prompt.
109
+
110
+ Args:
111
+ gemini_external_id: Gemini's session_id from preflight capture
112
+ prompt: Optional user prompt
113
+ auto_approve: If True, add --approval-mode yolo
114
+ gobby_session_id: Gobby session ID to inject into context
115
+
116
+ Returns:
117
+ Command list for subprocess execution
118
+ """
119
+ command = ["gemini"]
120
+
121
+ # Resume the preflight session
122
+ command.extend(["-r", gemini_external_id])
123
+
124
+ if auto_approve:
125
+ command.extend(["--approval-mode", "yolo"])
126
+
127
+ # Build prompt with session context
128
+ if gobby_session_id:
129
+ context_prefix = (
130
+ f"Your Gobby session_id is: {gobby_session_id}\n"
131
+ f"Use this when calling Gobby MCP tools.\n\n"
132
+ )
133
+ full_prompt = context_prefix + (prompt or "")
134
+ else:
135
+ full_prompt = prompt or ""
136
+
137
+ # Use -i for interactive mode with initial prompt
138
+ if full_prompt:
139
+ command.extend(["-i", full_prompt])
140
+
141
+ return command
142
+
143
+
144
+ def build_codex_command_with_resume(
145
+ codex_external_id: str,
146
+ prompt: str | None = None,
147
+ auto_approve: bool = False,
148
+ gobby_session_id: str | None = None,
149
+ working_directory: str | None = None,
150
+ ) -> list[str]:
151
+ """
152
+ Build Codex CLI command with session resume.
153
+
154
+ Uses `codex resume {session_id}` to resume a preflight-captured session,
155
+ with session context injected into the prompt.
156
+
157
+ Args:
158
+ codex_external_id: Codex's session_id from preflight capture
159
+ prompt: Optional user prompt
160
+ auto_approve: If True, add --full-auto flag
161
+ gobby_session_id: Gobby session ID to inject into context
162
+ working_directory: Optional working directory override
163
+
164
+ Returns:
165
+ Command list for subprocess execution
166
+ """
167
+ command = ["codex", "resume", codex_external_id]
168
+
169
+ if auto_approve:
170
+ command.append("--full-auto")
171
+
172
+ if working_directory:
173
+ command.extend(["-C", working_directory])
174
+
175
+ # Build prompt with session context
176
+ if gobby_session_id:
177
+ context_prefix = (
178
+ f"Your Gobby session_id is: {gobby_session_id}\n"
179
+ f"Use this when calling Gobby MCP tools.\n\n"
180
+ )
181
+ full_prompt = context_prefix + (prompt or "")
182
+ else:
183
+ full_prompt = prompt or ""
184
+
185
+ # Prompt is a positional argument after session_id
186
+ if full_prompt:
187
+ command.append(full_prompt)
188
+
189
+ return command