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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +217 -51
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {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
|