gobby 0.2.6__py3-none-any.whl → 0.2.7__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 (146) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/codex_impl/__init__.py +28 -0
  4. gobby/adapters/codex_impl/adapter.py +722 -0
  5. gobby/adapters/codex_impl/client.py +679 -0
  6. gobby/adapters/codex_impl/protocol.py +20 -0
  7. gobby/adapters/codex_impl/types.py +68 -0
  8. gobby/agents/definitions.py +11 -1
  9. gobby/agents/isolation.py +395 -0
  10. gobby/agents/sandbox.py +261 -0
  11. gobby/agents/spawn.py +42 -287
  12. gobby/agents/spawn_executor.py +385 -0
  13. gobby/agents/spawners/__init__.py +24 -0
  14. gobby/agents/spawners/command_builder.py +189 -0
  15. gobby/agents/spawners/embedded.py +21 -2
  16. gobby/agents/spawners/headless.py +21 -2
  17. gobby/agents/spawners/prompt_manager.py +125 -0
  18. gobby/cli/install.py +4 -4
  19. gobby/cli/installers/claude.py +6 -0
  20. gobby/cli/installers/gemini.py +6 -0
  21. gobby/cli/installers/shared.py +103 -4
  22. gobby/cli/sessions.py +1 -1
  23. gobby/cli/utils.py +9 -2
  24. gobby/config/__init__.py +12 -97
  25. gobby/config/app.py +10 -94
  26. gobby/config/extensions.py +2 -2
  27. gobby/config/features.py +7 -130
  28. gobby/config/tasks.py +4 -28
  29. gobby/hooks/__init__.py +0 -13
  30. gobby/hooks/event_handlers.py +45 -2
  31. gobby/hooks/hook_manager.py +2 -2
  32. gobby/hooks/plugins.py +1 -1
  33. gobby/hooks/webhooks.py +1 -1
  34. gobby/llm/resolver.py +3 -2
  35. gobby/mcp_proxy/importer.py +62 -4
  36. gobby/mcp_proxy/instructions.py +2 -0
  37. gobby/mcp_proxy/registries.py +1 -4
  38. gobby/mcp_proxy/services/recommendation.py +43 -11
  39. gobby/mcp_proxy/tools/agents.py +31 -731
  40. gobby/mcp_proxy/tools/clones.py +0 -385
  41. gobby/mcp_proxy/tools/memory.py +2 -2
  42. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  43. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  44. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  45. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  46. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  47. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  48. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  49. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  50. gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
  51. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  52. gobby/mcp_proxy/tools/worktrees.py +0 -343
  53. gobby/memory/ingestion/__init__.py +5 -0
  54. gobby/memory/ingestion/multimodal.py +221 -0
  55. gobby/memory/manager.py +62 -283
  56. gobby/memory/search/__init__.py +10 -0
  57. gobby/memory/search/coordinator.py +248 -0
  58. gobby/memory/services/__init__.py +5 -0
  59. gobby/memory/services/crossref.py +142 -0
  60. gobby/prompts/loader.py +5 -2
  61. gobby/servers/http.py +1 -4
  62. gobby/servers/routes/admin.py +14 -0
  63. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  64. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  65. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  66. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  67. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  68. gobby/servers/routes/mcp/hooks.py +1 -1
  69. gobby/servers/routes/mcp/tools.py +48 -1506
  70. gobby/sessions/lifecycle.py +1 -1
  71. gobby/sessions/processor.py +10 -0
  72. gobby/sessions/transcripts/base.py +1 -0
  73. gobby/sessions/transcripts/claude.py +15 -5
  74. gobby/skills/parser.py +30 -2
  75. gobby/storage/migrations.py +159 -372
  76. gobby/storage/sessions.py +43 -7
  77. gobby/storage/skills.py +37 -4
  78. gobby/storage/tasks/_lifecycle.py +18 -3
  79. gobby/sync/memories.py +1 -1
  80. gobby/tasks/external_validator.py +1 -1
  81. gobby/tasks/validation.py +22 -20
  82. gobby/tools/summarizer.py +91 -10
  83. gobby/utils/project_context.py +2 -3
  84. gobby/utils/status.py +13 -0
  85. gobby/workflows/actions.py +221 -1217
  86. gobby/workflows/artifact_actions.py +31 -0
  87. gobby/workflows/autonomous_actions.py +11 -0
  88. gobby/workflows/context_actions.py +50 -1
  89. gobby/workflows/enforcement/__init__.py +47 -0
  90. gobby/workflows/enforcement/blocking.py +269 -0
  91. gobby/workflows/enforcement/commit_policy.py +283 -0
  92. gobby/workflows/enforcement/handlers.py +269 -0
  93. gobby/workflows/enforcement/task_policy.py +542 -0
  94. gobby/workflows/git_utils.py +106 -0
  95. gobby/workflows/llm_actions.py +30 -0
  96. gobby/workflows/mcp_actions.py +20 -1
  97. gobby/workflows/memory_actions.py +80 -0
  98. gobby/workflows/safe_evaluator.py +183 -0
  99. gobby/workflows/session_actions.py +44 -0
  100. gobby/workflows/state_actions.py +60 -1
  101. gobby/workflows/stop_signal_actions.py +55 -0
  102. gobby/workflows/summary_actions.py +94 -1
  103. gobby/workflows/task_sync_actions.py +347 -0
  104. gobby/workflows/todo_actions.py +34 -1
  105. gobby/workflows/webhook_actions.py +185 -0
  106. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
  107. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
  108. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  109. gobby/adapters/codex.py +0 -1332
  110. gobby/install/claude/commands/gobby/bug.md +0 -51
  111. gobby/install/claude/commands/gobby/chore.md +0 -51
  112. gobby/install/claude/commands/gobby/epic.md +0 -52
  113. gobby/install/claude/commands/gobby/eval.md +0 -235
  114. gobby/install/claude/commands/gobby/feat.md +0 -49
  115. gobby/install/claude/commands/gobby/nit.md +0 -52
  116. gobby/install/claude/commands/gobby/ref.md +0 -52
  117. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  118. gobby/prompts/defaults/expansion/system.md +0 -119
  119. gobby/prompts/defaults/expansion/user.md +0 -48
  120. gobby/prompts/defaults/external_validation/agent.md +0 -72
  121. gobby/prompts/defaults/external_validation/external.md +0 -63
  122. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  123. gobby/prompts/defaults/external_validation/system.md +0 -6
  124. gobby/prompts/defaults/features/import_mcp.md +0 -22
  125. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  126. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  127. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  128. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  129. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  130. gobby/prompts/defaults/features/server_description.md +0 -20
  131. gobby/prompts/defaults/features/server_description_system.md +0 -6
  132. gobby/prompts/defaults/features/task_description.md +0 -31
  133. gobby/prompts/defaults/features/task_description_system.md +0 -6
  134. gobby/prompts/defaults/features/tool_summary.md +0 -17
  135. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  136. gobby/prompts/defaults/handoff/compact.md +0 -63
  137. gobby/prompts/defaults/handoff/session_end.md +0 -57
  138. gobby/prompts/defaults/memory/extract.md +0 -61
  139. gobby/prompts/defaults/research/step.md +0 -58
  140. gobby/prompts/defaults/validation/criteria.md +0 -47
  141. gobby/prompts/defaults/validation/validate.md +0 -38
  142. gobby/storage/migrations_legacy.py +0 -1359
  143. gobby/workflows/task_enforcement_actions.py +0 -1343
  144. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  145. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  146. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,385 @@
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 SandboxConfig
14
+
15
+ if TYPE_CHECKING:
16
+ from gobby.agents.session import ChildSessionManager
17
+ from gobby.agents.spawn import (
18
+ TerminalSpawner,
19
+ build_codex_command_with_resume,
20
+ build_gemini_command_with_resume,
21
+ prepare_codex_spawn_with_preflight,
22
+ prepare_gemini_spawn_with_preflight,
23
+ )
24
+ from gobby.agents.spawners.embedded import EmbeddedSpawner
25
+ from gobby.agents.spawners.headless import HeadlessSpawner
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @dataclass
31
+ class SpawnRequest:
32
+ """Request for spawning an agent."""
33
+
34
+ # Required fields
35
+ prompt: str
36
+ cwd: str
37
+ mode: Literal["terminal", "embedded", "headless"]
38
+ provider: str
39
+ terminal: str
40
+ session_id: str
41
+ run_id: str
42
+ parent_session_id: str
43
+ project_id: str
44
+
45
+ # Optional fields
46
+ workflow: str | None = None
47
+ worktree_id: str | None = None
48
+ clone_id: str | None = None
49
+ agent_depth: int = 0
50
+ max_agent_depth: int = 3
51
+ session_manager: Any | None = None # Required for Gemini/Codex preflight
52
+ machine_id: str | None = None
53
+
54
+ # Sandbox configuration
55
+ sandbox_config: SandboxConfig | None = None
56
+ sandbox_args: list[str] | None = None
57
+ sandbox_env: dict[str, str] | None = field(default=None)
58
+
59
+
60
+ @dataclass
61
+ class SpawnResult:
62
+ """Result of a spawn operation."""
63
+
64
+ success: bool
65
+ run_id: str
66
+ child_session_id: str
67
+ status: str
68
+
69
+ # Optional result fields
70
+ pid: int | None = None
71
+ terminal_type: str | None = None
72
+ master_fd: int | None = None
73
+ error: str | None = None
74
+ message: str | None = None
75
+ process: Any | None = None # subprocess.Popen for headless
76
+ gemini_session_id: str | None = None # Gemini external session ID
77
+ codex_session_id: str | None = None # Codex external session ID
78
+
79
+
80
+ async def execute_spawn(request: SpawnRequest) -> SpawnResult:
81
+ """
82
+ Unified spawn dispatch for terminal/embedded/headless modes.
83
+
84
+ Consolidates duplicated logic from agents.py, worktrees.py, clones.py
85
+ into a single dispatch function.
86
+
87
+ Args:
88
+ request: SpawnRequest with all spawn parameters
89
+
90
+ Returns:
91
+ SpawnResult with spawn outcome and metadata
92
+ """
93
+ if request.mode == "terminal":
94
+ # Special handling for Gemini/Codex: requires preflight session capture
95
+ if request.provider == "gemini":
96
+ return await _spawn_gemini_terminal(request)
97
+ elif request.provider == "codex":
98
+ return await _spawn_codex_terminal(request)
99
+ return await _spawn_terminal(request)
100
+ elif request.mode == "embedded":
101
+ return await _spawn_embedded(request)
102
+ else: # headless
103
+ return await _spawn_headless(request)
104
+
105
+
106
+ async def _spawn_terminal(request: SpawnRequest) -> SpawnResult:
107
+ """Spawn agent in external terminal."""
108
+ spawner = TerminalSpawner()
109
+ result = spawner.spawn_agent(
110
+ cli=request.provider,
111
+ cwd=request.cwd,
112
+ session_id=request.session_id,
113
+ parent_session_id=request.parent_session_id,
114
+ agent_run_id=request.run_id,
115
+ project_id=request.project_id,
116
+ workflow_name=request.workflow,
117
+ agent_depth=request.agent_depth,
118
+ max_agent_depth=request.max_agent_depth,
119
+ terminal=request.terminal,
120
+ prompt=request.prompt,
121
+ sandbox_config=request.sandbox_config,
122
+ )
123
+
124
+ if not result.success:
125
+ return SpawnResult(
126
+ success=False,
127
+ run_id=request.run_id,
128
+ child_session_id=request.session_id,
129
+ status="failed",
130
+ error=result.error or result.message,
131
+ )
132
+
133
+ return SpawnResult(
134
+ success=True,
135
+ run_id=request.run_id,
136
+ child_session_id=request.session_id,
137
+ status="pending",
138
+ pid=result.pid,
139
+ terminal_type=result.terminal_type,
140
+ message=f"Agent spawned in {result.terminal_type} (PID: {result.pid})",
141
+ )
142
+
143
+
144
+ async def _spawn_embedded(request: SpawnRequest) -> SpawnResult:
145
+ """Spawn agent with PTY for UI attachment."""
146
+ spawner = EmbeddedSpawner()
147
+ result = spawner.spawn_agent(
148
+ cli=request.provider,
149
+ cwd=request.cwd,
150
+ session_id=request.session_id,
151
+ parent_session_id=request.parent_session_id,
152
+ agent_run_id=request.run_id,
153
+ project_id=request.project_id,
154
+ workflow_name=request.workflow,
155
+ agent_depth=request.agent_depth,
156
+ max_agent_depth=request.max_agent_depth,
157
+ prompt=request.prompt,
158
+ sandbox_config=request.sandbox_config,
159
+ )
160
+
161
+ if not result.success:
162
+ return SpawnResult(
163
+ success=False,
164
+ run_id=request.run_id,
165
+ child_session_id=request.session_id,
166
+ status="failed",
167
+ error=result.error or result.message,
168
+ )
169
+
170
+ return SpawnResult(
171
+ success=True,
172
+ run_id=request.run_id,
173
+ child_session_id=request.session_id,
174
+ status="pending",
175
+ pid=result.pid,
176
+ master_fd=result.master_fd,
177
+ message=f"Agent spawned with PTY (PID: {result.pid})",
178
+ )
179
+
180
+
181
+ async def _spawn_headless(request: SpawnRequest) -> SpawnResult:
182
+ """Spawn headless agent with output capture."""
183
+ spawner = HeadlessSpawner()
184
+ result = spawner.spawn_agent(
185
+ cli=request.provider,
186
+ cwd=request.cwd,
187
+ session_id=request.session_id,
188
+ parent_session_id=request.parent_session_id,
189
+ agent_run_id=request.run_id,
190
+ project_id=request.project_id,
191
+ workflow_name=request.workflow,
192
+ agent_depth=request.agent_depth,
193
+ max_agent_depth=request.max_agent_depth,
194
+ prompt=request.prompt,
195
+ sandbox_config=request.sandbox_config,
196
+ )
197
+
198
+ if not result.success:
199
+ return SpawnResult(
200
+ success=False,
201
+ run_id=request.run_id,
202
+ child_session_id=request.session_id,
203
+ status="failed",
204
+ error=result.error or result.message,
205
+ )
206
+
207
+ return SpawnResult(
208
+ success=True,
209
+ run_id=request.run_id,
210
+ child_session_id=request.session_id,
211
+ status="running", # Headless is immediately running
212
+ pid=result.pid,
213
+ process=result.process,
214
+ message=f"Agent spawned headless (PID: {result.pid})",
215
+ )
216
+
217
+
218
+ async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
219
+ """
220
+ Spawn Gemini agent in terminal with preflight session capture.
221
+
222
+ Gemini CLI in interactive mode can't introspect its session_id, so we:
223
+ 1. Launch preflight to capture session_id from stream-json output
224
+ 2. Create Gobby session with external_id = gemini's session_id
225
+ 3. Launch interactive with -r {session_id} to resume
226
+ """
227
+ if request.session_manager is None:
228
+ return SpawnResult(
229
+ success=False,
230
+ run_id=request.run_id,
231
+ child_session_id=request.session_id,
232
+ status="failed",
233
+ error="session_manager is required for Gemini preflight",
234
+ )
235
+
236
+ try:
237
+ # Preflight capture: gets Gemini's session_id and creates linked Gobby session
238
+ spawn_context = await prepare_gemini_spawn_with_preflight(
239
+ session_manager=cast("ChildSessionManager", request.session_manager),
240
+ parent_session_id=request.parent_session_id,
241
+ project_id=request.project_id,
242
+ machine_id=request.machine_id or "unknown",
243
+ workflow_name=request.workflow,
244
+ git_branch=None, # Will be detected by hook
245
+ )
246
+ except FileNotFoundError as e:
247
+ return SpawnResult(
248
+ success=False,
249
+ run_id=request.run_id,
250
+ child_session_id=request.session_id,
251
+ status="failed",
252
+ error=str(e),
253
+ )
254
+ except Exception as e:
255
+ logger.error(f"Gemini preflight capture failed: {e}", exc_info=True)
256
+ return SpawnResult(
257
+ success=False,
258
+ run_id=request.run_id,
259
+ child_session_id=request.session_id,
260
+ status="failed",
261
+ error=f"Gemini preflight capture failed: {e}",
262
+ )
263
+
264
+ # Extract IDs from prepared spawn context
265
+ gobby_session_id = spawn_context.session_id
266
+ gemini_session_id = spawn_context.env_vars["GOBBY_GEMINI_EXTERNAL_ID"]
267
+
268
+ # Build command with session context injected into prompt
269
+ cmd = build_gemini_command_with_resume(
270
+ gemini_external_id=gemini_session_id,
271
+ prompt=request.prompt,
272
+ auto_approve=True, # Subagents need to work autonomously
273
+ gobby_session_id=gobby_session_id,
274
+ )
275
+
276
+ # Spawn in terminal
277
+ terminal_spawner = TerminalSpawner()
278
+ terminal_result = terminal_spawner.spawn(
279
+ command=cmd,
280
+ cwd=request.cwd,
281
+ terminal=request.terminal,
282
+ )
283
+
284
+ if not terminal_result.success:
285
+ return SpawnResult(
286
+ success=False,
287
+ run_id=request.run_id,
288
+ child_session_id=gobby_session_id,
289
+ status="failed",
290
+ error=terminal_result.error or terminal_result.message,
291
+ )
292
+
293
+ return SpawnResult(
294
+ success=True,
295
+ run_id=f"gemini-{gemini_session_id[:8]}",
296
+ child_session_id=gobby_session_id,
297
+ status="pending",
298
+ pid=terminal_result.pid,
299
+ gemini_session_id=gemini_session_id,
300
+ message=f"Gemini agent spawned in terminal with session {gobby_session_id}",
301
+ )
302
+
303
+
304
+ async def _spawn_codex_terminal(request: SpawnRequest) -> SpawnResult:
305
+ """
306
+ Spawn Codex agent in terminal with preflight session capture.
307
+
308
+ Codex outputs session_id in startup banner, which we parse from `codex exec "exit"`.
309
+ """
310
+ if request.session_manager is None:
311
+ return SpawnResult(
312
+ success=False,
313
+ run_id=request.run_id,
314
+ child_session_id=request.session_id,
315
+ status="failed",
316
+ error="session_manager is required for Codex preflight",
317
+ )
318
+
319
+ try:
320
+ # Preflight capture: gets Codex's session_id and creates linked Gobby session
321
+ spawn_context = await prepare_codex_spawn_with_preflight(
322
+ session_manager=cast("ChildSessionManager", request.session_manager),
323
+ parent_session_id=request.parent_session_id,
324
+ project_id=request.project_id,
325
+ machine_id=request.machine_id or "unknown",
326
+ workflow_name=request.workflow,
327
+ git_branch=None, # Will be detected by hook
328
+ )
329
+ except FileNotFoundError as e:
330
+ return SpawnResult(
331
+ success=False,
332
+ run_id=request.run_id,
333
+ child_session_id=request.session_id,
334
+ status="failed",
335
+ error=str(e),
336
+ )
337
+ except Exception as e:
338
+ logger.error(f"Codex preflight capture failed: {e}", exc_info=True)
339
+ return SpawnResult(
340
+ success=False,
341
+ run_id=request.run_id,
342
+ child_session_id=request.session_id,
343
+ status="failed",
344
+ error=f"Codex preflight capture failed: {e}",
345
+ )
346
+
347
+ # Extract IDs from prepared spawn context
348
+ gobby_session_id = spawn_context.session_id
349
+ codex_session_id = spawn_context.env_vars["GOBBY_CODEX_EXTERNAL_ID"]
350
+
351
+ # Build command with session context injected into prompt
352
+ cmd = build_codex_command_with_resume(
353
+ codex_external_id=codex_session_id,
354
+ prompt=request.prompt,
355
+ auto_approve=True, # --full-auto for sandboxed autonomy
356
+ gobby_session_id=gobby_session_id,
357
+ working_directory=request.cwd,
358
+ )
359
+
360
+ # Spawn in terminal
361
+ terminal_spawner = TerminalSpawner()
362
+ terminal_result = terminal_spawner.spawn(
363
+ command=cmd,
364
+ cwd=request.cwd,
365
+ terminal=request.terminal,
366
+ )
367
+
368
+ if not terminal_result.success:
369
+ return SpawnResult(
370
+ success=False,
371
+ run_id=request.run_id,
372
+ child_session_id=gobby_session_id,
373
+ status="failed",
374
+ error=terminal_result.error or terminal_result.message,
375
+ )
376
+
377
+ return SpawnResult(
378
+ success=True,
379
+ run_id=f"codex-{codex_session_id[:8]}",
380
+ child_session_id=gobby_session_id,
381
+ status="pending",
382
+ pid=terminal_result.pid,
383
+ codex_session_id=codex_session_id,
384
+ message=f"Codex agent spawned in terminal with session {gobby_session_id}",
385
+ )
@@ -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
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from gobby.agents.constants import get_terminal_env_vars
11
+ from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
11
12
  from gobby.agents.spawners.base import EmbeddedPTYResult
12
13
 
13
14
  # pty is only available on Unix-like systems
@@ -37,11 +38,11 @@ def _get_spawn_utils() -> tuple[
37
38
  MAX_ENV_PROMPT_LENGTH as _MAX_ENV_PROMPT_LENGTH,
38
39
  )
39
40
  from gobby.agents.spawn import (
40
- _create_prompt_file,
41
41
  build_cli_command,
42
+ create_prompt_file,
42
43
  )
43
44
 
44
- return build_cli_command, _create_prompt_file, _MAX_ENV_PROMPT_LENGTH
45
+ return build_cli_command, create_prompt_file, _MAX_ENV_PROMPT_LENGTH
45
46
 
46
47
 
47
48
  class EmbeddedSpawner:
@@ -169,6 +170,7 @@ class EmbeddedSpawner:
169
170
  agent_depth: int = 1,
170
171
  max_agent_depth: int = 3,
171
172
  prompt: str | None = None,
173
+ sandbox_config: SandboxConfig | None = None,
172
174
  ) -> EmbeddedPTYResult:
173
175
  """
174
176
  Spawn a CLI agent with embedded PTY.
@@ -184,12 +186,24 @@ class EmbeddedSpawner:
184
186
  agent_depth: Current nesting depth
185
187
  max_agent_depth: Maximum allowed depth
186
188
  prompt: Optional initial prompt
189
+ sandbox_config: Optional sandbox configuration
187
190
 
188
191
  Returns:
189
192
  EmbeddedPTYResult with PTY info
190
193
  """
191
194
  build_cli_command, _create_prompt_file, max_env_prompt_length = _get_spawn_utils()
192
195
 
196
+ # Resolve sandbox configuration if enabled
197
+ sandbox_args: list[str] | None = None
198
+ sandbox_env: dict[str, str] = {}
199
+
200
+ if sandbox_config and sandbox_config.enabled:
201
+ # Compute sandbox paths based on cwd (workspace)
202
+ resolved_paths = compute_sandbox_paths(sandbox_config, str(cwd))
203
+ # Get CLI-specific resolver and generate args/env
204
+ resolver = get_sandbox_resolver(cli)
205
+ sandbox_args, sandbox_env = resolver.resolve(sandbox_config, resolved_paths)
206
+
193
207
  # Build command with prompt as CLI argument and auto-approve for autonomous work
194
208
  command = build_cli_command(
195
209
  cli,
@@ -197,6 +211,7 @@ class EmbeddedSpawner:
197
211
  session_id=session_id,
198
212
  auto_approve=True, # Subagents need to work autonomously
199
213
  working_directory=str(cwd) if cli == "codex" else None,
214
+ sandbox_args=sandbox_args,
200
215
  )
201
216
 
202
217
  # Handle prompt for environment variables (backup for hooks/context)
@@ -222,4 +237,8 @@ class EmbeddedSpawner:
222
237
  prompt_file=prompt_file,
223
238
  )
224
239
 
240
+ # Merge sandbox environment variables if present
241
+ if sandbox_env:
242
+ env.update(sandbox_env)
243
+
225
244
  return self.spawn(command, cwd, env)