gobby 0.2.8__py3-none-any.whl → 0.2.11__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 +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PTY Reader for streaming terminal output from embedded agents.
|
|
3
|
+
|
|
4
|
+
Reads from PTY master file descriptors and broadcasts output via callbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import select
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from gobby.agents.registry import RunningAgent
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Type for output callback: async function(run_id, data)
|
|
22
|
+
OutputCallback = Callable[[str, str], Awaitable[None]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PTYReaderManager:
|
|
26
|
+
"""
|
|
27
|
+
Manages PTY reading tasks for embedded agents.
|
|
28
|
+
|
|
29
|
+
Starts a reader task for each agent with a master_fd and
|
|
30
|
+
broadcasts output via the provided callback.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, output_callback: OutputCallback | None = None):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the PTY reader manager.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
output_callback: Async callback for terminal output (run_id, data)
|
|
39
|
+
"""
|
|
40
|
+
self._output_callback = output_callback
|
|
41
|
+
self._reader_tasks: dict[str, asyncio.Task[None]] = {}
|
|
42
|
+
self._stop_events: dict[str, asyncio.Event] = {}
|
|
43
|
+
self._lock = asyncio.Lock()
|
|
44
|
+
|
|
45
|
+
def set_output_callback(self, callback: OutputCallback) -> None:
|
|
46
|
+
"""Set the output callback for terminal data."""
|
|
47
|
+
self._output_callback = callback
|
|
48
|
+
|
|
49
|
+
async def start_reader(self, agent: RunningAgent) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Start a PTY reader for an agent.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
agent: RunningAgent with master_fd set
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if reader was started, False if already running or no fd
|
|
58
|
+
"""
|
|
59
|
+
if agent.master_fd is None:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
async with self._lock:
|
|
63
|
+
if agent.run_id in self._reader_tasks:
|
|
64
|
+
return False # Already reading
|
|
65
|
+
|
|
66
|
+
stop_event = asyncio.Event()
|
|
67
|
+
self._stop_events[agent.run_id] = stop_event
|
|
68
|
+
|
|
69
|
+
task = asyncio.create_task(
|
|
70
|
+
self._read_loop(agent.run_id, agent.master_fd, stop_event),
|
|
71
|
+
name=f"pty_reader_{agent.run_id}",
|
|
72
|
+
)
|
|
73
|
+
self._reader_tasks[agent.run_id] = task
|
|
74
|
+
|
|
75
|
+
logger.debug(f"Started PTY reader for agent {agent.run_id}")
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
async def stop_reader(self, run_id: str) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Stop a PTY reader for an agent.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
run_id: Agent run ID
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if reader was stopped, False if not running
|
|
87
|
+
"""
|
|
88
|
+
async with self._lock:
|
|
89
|
+
stop_event = self._stop_events.pop(run_id, None)
|
|
90
|
+
task = self._reader_tasks.pop(run_id, None)
|
|
91
|
+
|
|
92
|
+
if stop_event:
|
|
93
|
+
stop_event.set()
|
|
94
|
+
|
|
95
|
+
if task:
|
|
96
|
+
task.cancel()
|
|
97
|
+
try:
|
|
98
|
+
await asyncio.wait_for(task, timeout=1.0)
|
|
99
|
+
except (asyncio.CancelledError, TimeoutError):
|
|
100
|
+
pass
|
|
101
|
+
logger.debug(f"Stopped PTY reader for agent {run_id}")
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
async def stop_all(self) -> None:
|
|
107
|
+
"""Stop all PTY readers."""
|
|
108
|
+
async with self._lock:
|
|
109
|
+
run_ids = list(self._reader_tasks.keys())
|
|
110
|
+
|
|
111
|
+
for run_id in run_ids:
|
|
112
|
+
await self.stop_reader(run_id)
|
|
113
|
+
|
|
114
|
+
async def _read_loop(
|
|
115
|
+
self,
|
|
116
|
+
run_id: str,
|
|
117
|
+
master_fd: int,
|
|
118
|
+
stop_event: asyncio.Event,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Read loop for a single PTY.
|
|
122
|
+
|
|
123
|
+
Reads from the master_fd and broadcasts via callback.
|
|
124
|
+
Uses select to avoid blocking the event loop.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
run_id: Agent run ID
|
|
128
|
+
master_fd: PTY master file descriptor
|
|
129
|
+
stop_event: Event to signal stop
|
|
130
|
+
"""
|
|
131
|
+
loop = asyncio.get_running_loop()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
while not stop_event.is_set():
|
|
135
|
+
# Use select with timeout to check for data
|
|
136
|
+
try:
|
|
137
|
+
ready, _, _ = await loop.run_in_executor(
|
|
138
|
+
None,
|
|
139
|
+
lambda: select.select([master_fd], [], [], 0.1),
|
|
140
|
+
)
|
|
141
|
+
except (ValueError, OSError):
|
|
142
|
+
# FD closed or invalid
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if not ready:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Read available data
|
|
149
|
+
try:
|
|
150
|
+
data = await loop.run_in_executor(
|
|
151
|
+
None,
|
|
152
|
+
lambda: os.read(master_fd, 4096),
|
|
153
|
+
)
|
|
154
|
+
except OSError as e:
|
|
155
|
+
# FD closed or error
|
|
156
|
+
logger.debug(f"PTY read error for {run_id}: {e}")
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if not data:
|
|
160
|
+
# EOF
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# Decode and broadcast
|
|
164
|
+
try:
|
|
165
|
+
text = data.decode("utf-8", errors="replace")
|
|
166
|
+
except Exception:
|
|
167
|
+
text = data.decode("latin-1")
|
|
168
|
+
|
|
169
|
+
if self._output_callback:
|
|
170
|
+
try:
|
|
171
|
+
await self._output_callback(run_id, text)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.warning(f"Output callback error for {run_id}: {e}")
|
|
174
|
+
|
|
175
|
+
except asyncio.CancelledError:
|
|
176
|
+
pass
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"PTY reader error for {run_id}: {e}")
|
|
179
|
+
finally:
|
|
180
|
+
logger.debug(f"PTY reader finished for {run_id}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Global singleton
|
|
184
|
+
_pty_reader_manager: PTYReaderManager | None = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_pty_reader_manager() -> PTYReaderManager:
|
|
188
|
+
"""Get the global PTY reader manager singleton."""
|
|
189
|
+
global _pty_reader_manager
|
|
190
|
+
if _pty_reader_manager is None:
|
|
191
|
+
_pty_reader_manager = PTYReaderManager()
|
|
192
|
+
return _pty_reader_manager
|
gobby/agents/registry.py
CHANGED
|
@@ -338,9 +338,18 @@ class RunningAgentRegistry:
|
|
|
338
338
|
cmdline = ps_result.stdout.strip()
|
|
339
339
|
# Verify it's actually the agent process
|
|
340
340
|
# (contains session-id and matches expected CLI)
|
|
341
|
+
# Check both provider name and "claude" (for cursor/windsurf/copilot)
|
|
342
|
+
is_matched = agent.provider in cmdline.lower()
|
|
343
|
+
if not is_matched and agent.provider in (
|
|
344
|
+
"cursor",
|
|
345
|
+
"windsurf",
|
|
346
|
+
"copilot",
|
|
347
|
+
):
|
|
348
|
+
is_matched = "claude" in cmdline.lower()
|
|
349
|
+
|
|
341
350
|
if (
|
|
342
351
|
f"session-id {agent.session_id}" in cmdline
|
|
343
|
-
and
|
|
352
|
+
and is_matched
|
|
344
353
|
):
|
|
345
354
|
if matched_pid is not None:
|
|
346
355
|
# Multiple matches - ambiguous
|
gobby/agents/runner.py
CHANGED
|
@@ -49,7 +49,7 @@ class AgentConfig:
|
|
|
49
49
|
"""Machine identifier. Defaults to hostname if not provided."""
|
|
50
50
|
|
|
51
51
|
source: str = "claude"
|
|
52
|
-
"""CLI source (claude, gemini, codex)."""
|
|
52
|
+
"""CLI source (claude, gemini, codex, cursor, windsurf, copilot)."""
|
|
53
53
|
|
|
54
54
|
# New spec-aligned parameters
|
|
55
55
|
workflow: str | None = None
|
|
@@ -352,6 +352,20 @@ class AgentRunner:
|
|
|
352
352
|
),
|
|
353
353
|
turns_used=0,
|
|
354
354
|
)
|
|
355
|
+
# Agent spawning only supports WorkflowDefinition, not PipelineDefinition
|
|
356
|
+
if not isinstance(workflow_definition, WorkflowDefinition):
|
|
357
|
+
self.logger.error(
|
|
358
|
+
f"Cannot use pipeline '{effective_workflow}' for agent spawning"
|
|
359
|
+
)
|
|
360
|
+
return AgentResult(
|
|
361
|
+
output="",
|
|
362
|
+
status="error",
|
|
363
|
+
error=(
|
|
364
|
+
f"'{effective_workflow}' is a pipeline, not a step workflow. "
|
|
365
|
+
f"Agent spawning requires a step-based workflow."
|
|
366
|
+
),
|
|
367
|
+
turns_used=0,
|
|
368
|
+
)
|
|
355
369
|
|
|
356
370
|
# Create child session (now safe - workflow validated above)
|
|
357
371
|
try:
|
|
@@ -377,20 +391,22 @@ class AgentRunner:
|
|
|
377
391
|
)
|
|
378
392
|
|
|
379
393
|
# Initialize workflow state if workflow was loaded
|
|
394
|
+
# workflow_definition is WorkflowDefinition | None at this point (PipelineDefinition rejected above)
|
|
380
395
|
workflow_state = None
|
|
381
|
-
|
|
396
|
+
workflow_config: WorkflowDefinition | None = None
|
|
397
|
+
if workflow_definition and isinstance(workflow_definition, WorkflowDefinition):
|
|
398
|
+
workflow_config = workflow_definition
|
|
382
399
|
self.logger.info(
|
|
383
|
-
f"Loaded workflow '{effective_workflow}' for agent "
|
|
384
|
-
f"(type={workflow_definition.type})"
|
|
400
|
+
f"Loaded workflow '{effective_workflow}' for agent (type={workflow_config.type})"
|
|
385
401
|
)
|
|
386
402
|
|
|
387
403
|
# Initialize workflow state for child session
|
|
388
404
|
initial_step = ""
|
|
389
|
-
if
|
|
390
|
-
initial_step =
|
|
405
|
+
if workflow_config.steps:
|
|
406
|
+
initial_step = workflow_config.steps[0].name
|
|
391
407
|
|
|
392
408
|
# Build initial variables with agent depth information
|
|
393
|
-
initial_variables = dict(
|
|
409
|
+
initial_variables = dict(workflow_config.variables)
|
|
394
410
|
initial_variables["agent_depth"] = child_session.agent_depth
|
|
395
411
|
initial_variables["max_agent_depth"] = self._child_session_manager.max_agent_depth
|
|
396
412
|
initial_variables["can_spawn"] = (
|
|
@@ -448,7 +464,7 @@ class AgentRunner:
|
|
|
448
464
|
session=session_obj,
|
|
449
465
|
run=agent_run,
|
|
450
466
|
workflow_state=workflow_state,
|
|
451
|
-
workflow_config=
|
|
467
|
+
workflow_config=workflow_config,
|
|
452
468
|
)
|
|
453
469
|
|
|
454
470
|
async def execute_run(
|
gobby/agents/sandbox.py
CHANGED
|
@@ -7,6 +7,7 @@ built-in sandbox implementation - Gobby just passes the right flags.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from abc import ABC, abstractmethod
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
from typing import Literal
|
|
11
12
|
|
|
12
13
|
from pydantic import BaseModel, Field
|
|
@@ -202,7 +203,7 @@ def get_sandbox_resolver(cli: str) -> SandboxResolver:
|
|
|
202
203
|
Factory function to get the appropriate sandbox resolver for a CLI.
|
|
203
204
|
|
|
204
205
|
Args:
|
|
205
|
-
cli: The CLI name ("claude", "codex", or "
|
|
206
|
+
cli: The CLI name ("claude", "codex", "gemini", "cursor", "windsurf", or "copilot")
|
|
206
207
|
|
|
207
208
|
Returns:
|
|
208
209
|
The appropriate SandboxResolver subclass instance.
|
|
@@ -214,6 +215,9 @@ def get_sandbox_resolver(cli: str) -> SandboxResolver:
|
|
|
214
215
|
"claude": ClaudeSandboxResolver,
|
|
215
216
|
"codex": CodexSandboxResolver,
|
|
216
217
|
"gemini": GeminiSandboxResolver,
|
|
218
|
+
"cursor": ClaudeSandboxResolver,
|
|
219
|
+
"windsurf": ClaudeSandboxResolver,
|
|
220
|
+
"copilot": ClaudeSandboxResolver,
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
if cli not in resolvers:
|
|
@@ -249,8 +253,9 @@ def compute_sandbox_paths(
|
|
|
249
253
|
if path not in write_paths:
|
|
250
254
|
write_paths.append(path)
|
|
251
255
|
|
|
252
|
-
# Collect read paths
|
|
253
|
-
|
|
256
|
+
# Collect read paths - always include ~/.gobby/ for machine_id access
|
|
257
|
+
gobby_home = str(Path("~/.gobby").expanduser())
|
|
258
|
+
read_paths = [gobby_home] + list(config.extra_read_paths)
|
|
254
259
|
|
|
255
260
|
return ResolvedSandboxPaths(
|
|
256
261
|
workspace_path=workspace_path,
|
gobby/agents/session.py
CHANGED
|
@@ -53,6 +53,9 @@ class ChildSessionConfig:
|
|
|
53
53
|
lifecycle_variables: dict[str, Any] | None = None
|
|
54
54
|
"""Lifecycle variables for the session."""
|
|
55
55
|
|
|
56
|
+
step_variables: dict[str, Any] | None = None
|
|
57
|
+
"""Variables to pass during workflow auto-activation (e.g., assigned_task_id)."""
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
class ChildSessionManager:
|
|
58
61
|
"""
|
|
@@ -197,6 +200,7 @@ class ChildSessionManager:
|
|
|
197
200
|
agent_depth=child_depth,
|
|
198
201
|
spawned_by_agent_id=config.agent_id,
|
|
199
202
|
workflow_name=config.workflow_name,
|
|
203
|
+
step_variables=config.step_variables,
|
|
200
204
|
)
|
|
201
205
|
|
|
202
206
|
child_id = child.id
|
gobby/agents/spawn.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import logging
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
17
18
|
|
|
18
19
|
from gobby.agents.constants import get_terminal_env_vars
|
|
19
20
|
from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
|
|
@@ -239,7 +240,7 @@ class TerminalSpawner:
|
|
|
239
240
|
Spawn a CLI agent in a new terminal with Gobby environment variables.
|
|
240
241
|
|
|
241
242
|
Args:
|
|
242
|
-
cli: CLI to run (e.g., "claude", "gemini", "codex")
|
|
243
|
+
cli: CLI to run (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot")
|
|
243
244
|
cwd: Working directory (usually project root or worktree)
|
|
244
245
|
session_id: Pre-created child session ID
|
|
245
246
|
parent_session_id: Parent session for context resolution
|
|
@@ -366,6 +367,7 @@ def prepare_terminal_spawn(
|
|
|
366
367
|
source: str = "claude",
|
|
367
368
|
agent_id: str | None = None,
|
|
368
369
|
workflow_name: str | None = None,
|
|
370
|
+
step_variables: dict[str, Any] | None = None,
|
|
369
371
|
title: str | None = None,
|
|
370
372
|
git_branch: str | None = None,
|
|
371
373
|
prompt: str | None = None,
|
|
@@ -384,7 +386,7 @@ def prepare_terminal_spawn(
|
|
|
384
386
|
parent_session_id: Parent session ID
|
|
385
387
|
project_id: Project ID
|
|
386
388
|
machine_id: Machine ID
|
|
387
|
-
source: CLI source (claude, gemini, codex)
|
|
389
|
+
source: CLI source (claude, gemini, codex, cursor, windsurf, copilot)
|
|
388
390
|
agent_id: Optional agent ID
|
|
389
391
|
workflow_name: Optional workflow to activate
|
|
390
392
|
title: Optional session title
|
|
@@ -408,6 +410,7 @@ def prepare_terminal_spawn(
|
|
|
408
410
|
source=source,
|
|
409
411
|
agent_id=agent_id,
|
|
410
412
|
workflow_name=workflow_name,
|
|
413
|
+
step_variables=step_variables,
|
|
411
414
|
title=title,
|
|
412
415
|
git_branch=git_branch,
|
|
413
416
|
)
|
|
@@ -460,6 +463,7 @@ async def prepare_gemini_spawn_with_preflight(
|
|
|
460
463
|
machine_id: str,
|
|
461
464
|
agent_id: str | None = None,
|
|
462
465
|
workflow_name: str | None = None,
|
|
466
|
+
step_variables: dict[str, Any] | None = None,
|
|
463
467
|
title: str | None = None,
|
|
464
468
|
git_branch: str | None = None,
|
|
465
469
|
prompt: str | None = None,
|
|
@@ -512,6 +516,7 @@ async def prepare_gemini_spawn_with_preflight(
|
|
|
512
516
|
source="gemini",
|
|
513
517
|
agent_id=agent_id,
|
|
514
518
|
workflow_name=workflow_name,
|
|
519
|
+
step_variables=step_variables,
|
|
515
520
|
title=title,
|
|
516
521
|
git_branch=git_branch,
|
|
517
522
|
external_id=gemini_info.session_id, # Link to Gemini's session
|
|
@@ -569,6 +574,7 @@ async def prepare_codex_spawn_with_preflight(
|
|
|
569
574
|
machine_id: str,
|
|
570
575
|
agent_id: str | None = None,
|
|
571
576
|
workflow_name: str | None = None,
|
|
577
|
+
step_variables: dict[str, Any] | None = None,
|
|
572
578
|
title: str | None = None,
|
|
573
579
|
git_branch: str | None = None,
|
|
574
580
|
prompt: str | None = None,
|
|
@@ -621,6 +627,7 @@ async def prepare_codex_spawn_with_preflight(
|
|
|
621
627
|
source="codex",
|
|
622
628
|
agent_id=agent_id,
|
|
623
629
|
workflow_name=workflow_name,
|
|
630
|
+
step_variables=step_variables,
|
|
624
631
|
title=title,
|
|
625
632
|
git_branch=git_branch,
|
|
626
633
|
external_id=codex_info.session_id, # Link to Codex's session
|
gobby/agents/spawn_executor.py
CHANGED
|
@@ -20,10 +20,10 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from gobby.agents.session import ChildSessionManager
|
|
21
21
|
from gobby.agents.spawn import (
|
|
22
22
|
TerminalSpawner,
|
|
23
|
+
build_cli_command,
|
|
23
24
|
build_codex_command_with_resume,
|
|
24
|
-
build_gemini_command_with_resume,
|
|
25
25
|
prepare_codex_spawn_with_preflight,
|
|
26
|
-
|
|
26
|
+
prepare_terminal_spawn,
|
|
27
27
|
)
|
|
28
28
|
from gobby.agents.spawners.embedded import EmbeddedSpawner
|
|
29
29
|
from gobby.agents.spawners.headless import HeadlessSpawner
|
|
@@ -38,7 +38,7 @@ class SpawnRequest:
|
|
|
38
38
|
# Required fields
|
|
39
39
|
prompt: str
|
|
40
40
|
cwd: str
|
|
41
|
-
mode: Literal["terminal", "embedded", "headless"]
|
|
41
|
+
mode: Literal["terminal", "embedded", "headless", "self"]
|
|
42
42
|
provider: str
|
|
43
43
|
terminal: str
|
|
44
44
|
session_id: str
|
|
@@ -48,8 +48,10 @@ class SpawnRequest:
|
|
|
48
48
|
|
|
49
49
|
# Optional fields
|
|
50
50
|
workflow: str | None = None
|
|
51
|
+
step_variables: dict[str, Any] | None = None # Variables for step workflow activation
|
|
51
52
|
worktree_id: str | None = None
|
|
52
53
|
clone_id: str | None = None
|
|
54
|
+
branch_name: str | None = None # Git branch for worktree/clone isolation
|
|
53
55
|
agent_depth: int = 0
|
|
54
56
|
max_agent_depth: int = 3
|
|
55
57
|
session_manager: Any | None = None # Required for Gemini/Codex preflight
|
|
@@ -221,15 +223,16 @@ async def _spawn_headless(request: SpawnRequest) -> SpawnResult:
|
|
|
221
223
|
|
|
222
224
|
async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
|
|
223
225
|
"""
|
|
224
|
-
Spawn Gemini agent in terminal with
|
|
226
|
+
Spawn Gemini agent in terminal with direct spawn (no preflight).
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
1.
|
|
228
|
-
2.
|
|
229
|
-
3.
|
|
228
|
+
Session linkage approach:
|
|
229
|
+
1. Pre-create Gobby session with parent linkage (no external_id yet)
|
|
230
|
+
2. Pass GOBBY_SESSION_ID and other env vars to the terminal
|
|
231
|
+
3. Gemini's hook dispatcher reads env vars and includes in SessionStart
|
|
232
|
+
4. Daemon updates external_id when SessionStart fires with Gemini's native session_id
|
|
230
233
|
|
|
231
|
-
This
|
|
232
|
-
|
|
234
|
+
This avoids the preflight+resume approach which failed because Gemini
|
|
235
|
+
doesn't persist sessions when terminated.
|
|
233
236
|
"""
|
|
234
237
|
if request.session_manager is None:
|
|
235
238
|
return SpawnResult(
|
|
@@ -237,60 +240,36 @@ async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
|
|
|
237
240
|
run_id=request.run_id,
|
|
238
241
|
child_session_id=None,
|
|
239
242
|
status="failed",
|
|
240
|
-
error="session_manager is required for Gemini
|
|
243
|
+
error="session_manager is required for Gemini spawn",
|
|
241
244
|
)
|
|
242
245
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
)
|
|
246
|
+
# Prepare spawn context (creates child session, builds env vars)
|
|
247
|
+
spawn_context = prepare_terminal_spawn(
|
|
248
|
+
session_manager=cast("ChildSessionManager", request.session_manager),
|
|
249
|
+
parent_session_id=request.parent_session_id,
|
|
250
|
+
project_id=request.project_id,
|
|
251
|
+
machine_id=request.machine_id or "unknown",
|
|
252
|
+
source="gemini",
|
|
253
|
+
workflow_name=request.workflow,
|
|
254
|
+
step_variables=request.step_variables,
|
|
255
|
+
prompt=request.prompt,
|
|
256
|
+
max_agent_depth=request.max_agent_depth,
|
|
257
|
+
git_branch=request.branch_name,
|
|
258
|
+
)
|
|
280
259
|
|
|
281
|
-
# Extract IDs from prepared spawn context
|
|
282
260
|
gobby_session_id = spawn_context.session_id
|
|
283
|
-
gemini_session_id = spawn_context.env_vars["GOBBY_GEMINI_EXTERNAL_ID"]
|
|
284
261
|
|
|
285
|
-
# Build command
|
|
286
|
-
|
|
287
|
-
|
|
262
|
+
# Build command for fresh Gemini session (not resume)
|
|
263
|
+
# Session context is injected via additionalContext at SessionStart by the daemon
|
|
264
|
+
cmd = build_cli_command(
|
|
265
|
+
cli="gemini",
|
|
288
266
|
prompt=request.prompt,
|
|
289
267
|
auto_approve=True,
|
|
290
|
-
|
|
268
|
+
mode="terminal",
|
|
291
269
|
)
|
|
292
270
|
|
|
293
271
|
# Resolve sandbox config if provided
|
|
272
|
+
sandbox_args: list[str] = []
|
|
294
273
|
sandbox_env: dict[str, str] = {}
|
|
295
274
|
if request.sandbox_config and request.sandbox_config.enabled:
|
|
296
275
|
resolver = GeminiSandboxResolver()
|
|
@@ -299,16 +278,25 @@ async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
|
|
|
299
278
|
workspace_path=request.cwd,
|
|
300
279
|
)
|
|
301
280
|
sandbox_args, sandbox_env = resolver.resolve(request.sandbox_config, paths)
|
|
302
|
-
# Append sandbox args to command
|
|
281
|
+
# Append sandbox args to command
|
|
303
282
|
cmd.extend(sandbox_args)
|
|
304
283
|
|
|
305
|
-
#
|
|
284
|
+
# Merge env vars: spawn context + sandbox
|
|
285
|
+
env = spawn_context.env_vars.copy()
|
|
286
|
+
if sandbox_env:
|
|
287
|
+
env.update(sandbox_env)
|
|
288
|
+
|
|
289
|
+
# Pass machine_id as env var for sandboxed agents that can't read ~/.gobby/machine_id
|
|
290
|
+
if request.machine_id:
|
|
291
|
+
env["GOBBY_MACHINE_ID"] = request.machine_id
|
|
292
|
+
|
|
293
|
+
# Spawn in terminal with env vars
|
|
306
294
|
terminal_spawner = TerminalSpawner()
|
|
307
295
|
terminal_result = terminal_spawner.spawn(
|
|
308
296
|
command=cmd,
|
|
309
297
|
cwd=request.cwd,
|
|
310
298
|
terminal=request.terminal,
|
|
311
|
-
env=
|
|
299
|
+
env=env,
|
|
312
300
|
)
|
|
313
301
|
|
|
314
302
|
if not terminal_result.success:
|
|
@@ -322,11 +310,10 @@ async def _spawn_gemini_terminal(request: SpawnRequest) -> SpawnResult:
|
|
|
322
310
|
|
|
323
311
|
return SpawnResult(
|
|
324
312
|
success=True,
|
|
325
|
-
run_id=
|
|
326
|
-
child_session_id=gobby_session_id,
|
|
313
|
+
run_id=spawn_context.agent_run_id,
|
|
314
|
+
child_session_id=gobby_session_id,
|
|
327
315
|
status="pending",
|
|
328
316
|
pid=terminal_result.pid,
|
|
329
|
-
gemini_session_id=gemini_session_id,
|
|
330
317
|
message=f"Gemini agent spawned in terminal with session {gobby_session_id}",
|
|
331
318
|
)
|
|
332
319
|
|
|
@@ -354,7 +341,8 @@ async def _spawn_codex_terminal(request: SpawnRequest) -> SpawnResult:
|
|
|
354
341
|
project_id=request.project_id,
|
|
355
342
|
machine_id=request.machine_id or "unknown",
|
|
356
343
|
workflow_name=request.workflow,
|
|
357
|
-
|
|
344
|
+
step_variables=request.step_variables,
|
|
345
|
+
git_branch=request.branch_name,
|
|
358
346
|
)
|
|
359
347
|
except FileNotFoundError as e:
|
|
360
348
|
return SpawnResult(
|
|
@@ -35,9 +35,9 @@ def build_cli_command(
|
|
|
35
35
|
- Or: codex -c 'sandbox_permissions=["disk-full-read-access"]' -a never [PROMPT]
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
|
-
cli: CLI name (claude, gemini, codex)
|
|
38
|
+
cli: CLI name (claude, gemini, codex, cursor, windsurf, copilot)
|
|
39
39
|
prompt: Optional prompt to pass
|
|
40
|
-
session_id: Optional session ID (used by Claude
|
|
40
|
+
session_id: Optional session ID (used by Claude-compatible CLIs)
|
|
41
41
|
auto_approve: If True, add flags to auto-approve actions/permissions
|
|
42
42
|
working_directory: Optional working directory (used by Codex -C flag)
|
|
43
43
|
mode: Execution mode - "terminal" (interactive) or "headless" (non-interactive)
|
|
@@ -48,8 +48,8 @@ def build_cli_command(
|
|
|
48
48
|
"""
|
|
49
49
|
command = [cli]
|
|
50
50
|
|
|
51
|
-
if cli
|
|
52
|
-
# Claude CLI flags
|
|
51
|
+
if cli in ("claude", "cursor", "windsurf", "copilot"):
|
|
52
|
+
# Claude-compatible CLI flags
|
|
53
53
|
if session_id:
|
|
54
54
|
command.extend(["--session-id", session_id])
|
|
55
55
|
if auto_approve:
|