gobby 0.2.5__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- 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/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -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/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -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 +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- 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 +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -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 +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -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 +111 -1
- 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.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- 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/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- 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/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/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified spawn_agent MCP tool.
|
|
3
|
+
|
|
4
|
+
Consolidates three separate agent spawning tools into one:
|
|
5
|
+
- start_agent
|
|
6
|
+
- spawn_agent_in_worktree
|
|
7
|
+
- spawn_agent_in_clone
|
|
8
|
+
|
|
9
|
+
One tool: spawn_agent(prompt, agent="generic", isolation="current"|"worktree"|"clone", ...)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import socket
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
19
|
+
|
|
20
|
+
from gobby.agents.definitions import AgentDefinition, AgentDefinitionLoader
|
|
21
|
+
from gobby.agents.isolation import (
|
|
22
|
+
SpawnConfig,
|
|
23
|
+
get_isolation_handler,
|
|
24
|
+
)
|
|
25
|
+
from gobby.agents.sandbox import SandboxConfig
|
|
26
|
+
from gobby.agents.spawn_executor import SpawnRequest, execute_spawn
|
|
27
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
28
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
29
|
+
from gobby.utils.project_context import get_project_context
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from gobby.agents.runner import AgentRunner
|
|
33
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def spawn_agent_impl(
|
|
39
|
+
prompt: str,
|
|
40
|
+
runner: AgentRunner,
|
|
41
|
+
agent_def: AgentDefinition | None = None,
|
|
42
|
+
task_id: str | None = None,
|
|
43
|
+
task_manager: LocalTaskManager | None = None,
|
|
44
|
+
# Isolation
|
|
45
|
+
isolation: Literal["current", "worktree", "clone"] | None = None,
|
|
46
|
+
branch_name: str | None = None,
|
|
47
|
+
base_branch: str | None = None,
|
|
48
|
+
# Storage/managers for isolation
|
|
49
|
+
worktree_storage: Any | None = None,
|
|
50
|
+
git_manager: Any | None = None,
|
|
51
|
+
clone_storage: Any | None = None,
|
|
52
|
+
clone_manager: Any | None = None,
|
|
53
|
+
# Execution
|
|
54
|
+
workflow: str | None = None,
|
|
55
|
+
mode: Literal["terminal", "embedded", "headless"] | None = None,
|
|
56
|
+
terminal: str = "auto",
|
|
57
|
+
provider: str | None = None,
|
|
58
|
+
model: str | None = None,
|
|
59
|
+
# Limits
|
|
60
|
+
timeout: float | None = None,
|
|
61
|
+
max_turns: int | None = None,
|
|
62
|
+
# Sandbox
|
|
63
|
+
sandbox: bool | None = None,
|
|
64
|
+
sandbox_mode: Literal["permissive", "restrictive"] | None = None,
|
|
65
|
+
sandbox_allow_network: bool | None = None,
|
|
66
|
+
sandbox_extra_paths: list[str] | None = None,
|
|
67
|
+
# Context
|
|
68
|
+
parent_session_id: str | None = None,
|
|
69
|
+
project_path: str | None = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Core spawn_agent implementation that can be called directly.
|
|
73
|
+
|
|
74
|
+
This is the internal implementation used by both the spawn_agent MCP tool
|
|
75
|
+
and the deprecated spawn_agent_in_worktree/spawn_agent_in_clone tools.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
prompt: Required - what the agent should do
|
|
79
|
+
runner: AgentRunner instance for executing agents
|
|
80
|
+
agent_def: Optional loaded agent definition
|
|
81
|
+
task_id: Optional - link to task (supports N, #N, UUID)
|
|
82
|
+
task_manager: Task manager for task resolution
|
|
83
|
+
isolation: Isolation mode (current/worktree/clone)
|
|
84
|
+
branch_name: Git branch name (auto-generated from task if not provided)
|
|
85
|
+
base_branch: Base branch for worktree/clone
|
|
86
|
+
worktree_storage: Storage for worktree records
|
|
87
|
+
git_manager: Git manager for worktree operations
|
|
88
|
+
clone_storage: Storage for clone records
|
|
89
|
+
clone_manager: Git manager for clone operations
|
|
90
|
+
workflow: Workflow to use
|
|
91
|
+
mode: Execution mode (terminal/embedded/headless)
|
|
92
|
+
terminal: Terminal type for terminal mode
|
|
93
|
+
provider: AI provider (claude/gemini/codex)
|
|
94
|
+
model: Model to use
|
|
95
|
+
timeout: Timeout in seconds
|
|
96
|
+
max_turns: Maximum conversation turns
|
|
97
|
+
sandbox: Enable sandbox (True/False/None). None inherits from agent_def.
|
|
98
|
+
sandbox_mode: Sandbox mode (permissive/restrictive). Overrides agent_def.
|
|
99
|
+
sandbox_allow_network: Allow network access. Overrides agent_def.
|
|
100
|
+
sandbox_extra_paths: Extra paths for sandbox write access.
|
|
101
|
+
parent_session_id: Parent session ID
|
|
102
|
+
project_path: Project path override
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dict with success status, run_id, child_session_id, isolation metadata
|
|
106
|
+
"""
|
|
107
|
+
# 1. Merge config: agent_def defaults < params
|
|
108
|
+
effective_isolation = isolation
|
|
109
|
+
if effective_isolation is None and agent_def:
|
|
110
|
+
effective_isolation = agent_def.isolation
|
|
111
|
+
effective_isolation = effective_isolation or "current"
|
|
112
|
+
|
|
113
|
+
effective_provider = provider
|
|
114
|
+
if effective_provider is None and agent_def:
|
|
115
|
+
effective_provider = agent_def.provider
|
|
116
|
+
effective_provider = effective_provider or "claude"
|
|
117
|
+
|
|
118
|
+
effective_mode: Literal["terminal", "embedded", "headless"] | None = mode
|
|
119
|
+
if effective_mode is None and agent_def:
|
|
120
|
+
effective_mode = cast(Literal["terminal", "embedded", "headless"], agent_def.mode)
|
|
121
|
+
effective_mode = effective_mode or "terminal"
|
|
122
|
+
|
|
123
|
+
effective_workflow = workflow
|
|
124
|
+
if effective_workflow is None and agent_def:
|
|
125
|
+
effective_workflow = agent_def.workflow
|
|
126
|
+
|
|
127
|
+
effective_base_branch = base_branch
|
|
128
|
+
if effective_base_branch is None and agent_def:
|
|
129
|
+
effective_base_branch = agent_def.base_branch
|
|
130
|
+
effective_base_branch = effective_base_branch or "main"
|
|
131
|
+
|
|
132
|
+
effective_branch_prefix = None
|
|
133
|
+
if agent_def:
|
|
134
|
+
effective_branch_prefix = agent_def.branch_prefix
|
|
135
|
+
|
|
136
|
+
# Build effective sandbox config (merge agent_def.sandbox with params)
|
|
137
|
+
effective_sandbox_config: SandboxConfig | None = None
|
|
138
|
+
|
|
139
|
+
# Start with agent_def.sandbox if present
|
|
140
|
+
base_sandbox = agent_def.sandbox if agent_def and hasattr(agent_def, "sandbox") else None
|
|
141
|
+
|
|
142
|
+
# Determine if sandbox should be enabled
|
|
143
|
+
sandbox_enabled = sandbox # Explicit param takes precedence
|
|
144
|
+
if sandbox_enabled is None and base_sandbox is not None:
|
|
145
|
+
sandbox_enabled = base_sandbox.enabled
|
|
146
|
+
|
|
147
|
+
# Build sandbox config if enabled or if we have params to apply
|
|
148
|
+
if sandbox_enabled is True or (
|
|
149
|
+
sandbox_enabled is None
|
|
150
|
+
and (sandbox_mode is not None or sandbox_allow_network is not None or sandbox_extra_paths)
|
|
151
|
+
):
|
|
152
|
+
# Start from base or create new
|
|
153
|
+
if base_sandbox is not None:
|
|
154
|
+
effective_sandbox_config = SandboxConfig(
|
|
155
|
+
enabled=True if sandbox_enabled is None else sandbox_enabled,
|
|
156
|
+
mode=sandbox_mode if sandbox_mode is not None else base_sandbox.mode,
|
|
157
|
+
allow_network=(
|
|
158
|
+
sandbox_allow_network
|
|
159
|
+
if sandbox_allow_network is not None
|
|
160
|
+
else base_sandbox.allow_network
|
|
161
|
+
),
|
|
162
|
+
extra_read_paths=base_sandbox.extra_read_paths,
|
|
163
|
+
extra_write_paths=(
|
|
164
|
+
list(base_sandbox.extra_write_paths) + (sandbox_extra_paths or [])
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
effective_sandbox_config = SandboxConfig(
|
|
169
|
+
enabled=True,
|
|
170
|
+
mode=sandbox_mode or "permissive",
|
|
171
|
+
allow_network=sandbox_allow_network if sandbox_allow_network is not None else True,
|
|
172
|
+
extra_write_paths=sandbox_extra_paths or [],
|
|
173
|
+
)
|
|
174
|
+
elif sandbox_enabled is False:
|
|
175
|
+
# Explicitly disabled - set config with enabled=False
|
|
176
|
+
effective_sandbox_config = SandboxConfig(enabled=False)
|
|
177
|
+
|
|
178
|
+
# 2. Resolve project context
|
|
179
|
+
ctx = get_project_context(Path(project_path) if project_path else None)
|
|
180
|
+
if ctx is None:
|
|
181
|
+
return {"success": False, "error": "Could not resolve project context"}
|
|
182
|
+
|
|
183
|
+
project_id = ctx.get("id") or ctx.get("project_id")
|
|
184
|
+
resolved_project_path = ctx.get("project_path")
|
|
185
|
+
|
|
186
|
+
if not project_id or not isinstance(project_id, str):
|
|
187
|
+
return {"success": False, "error": "Could not resolve project_id from context"}
|
|
188
|
+
if not resolved_project_path or not isinstance(resolved_project_path, str):
|
|
189
|
+
return {"success": False, "error": "Could not resolve project_path from context"}
|
|
190
|
+
|
|
191
|
+
# 3. Validate parent_session_id and spawn depth
|
|
192
|
+
if parent_session_id is None:
|
|
193
|
+
return {"success": False, "error": "parent_session_id is required"}
|
|
194
|
+
|
|
195
|
+
can_spawn, reason, _depth = runner.can_spawn(parent_session_id)
|
|
196
|
+
if not can_spawn:
|
|
197
|
+
return {"success": False, "error": reason}
|
|
198
|
+
|
|
199
|
+
# 4. Resolve task_id if provided (supports N, #N, UUID)
|
|
200
|
+
resolved_task_id: str | None = None
|
|
201
|
+
task_title: str | None = None
|
|
202
|
+
task_seq_num: int | None = None
|
|
203
|
+
|
|
204
|
+
if task_id and task_manager:
|
|
205
|
+
try:
|
|
206
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id, project_id)
|
|
207
|
+
task = task_manager.get_task(resolved_task_id)
|
|
208
|
+
if task:
|
|
209
|
+
task_title = task.title
|
|
210
|
+
task_seq_num = task.seq_num
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"Failed to resolve task_id {task_id}: {e}")
|
|
213
|
+
|
|
214
|
+
# 5. Get isolation handler
|
|
215
|
+
handler = get_isolation_handler(
|
|
216
|
+
effective_isolation,
|
|
217
|
+
git_manager=git_manager,
|
|
218
|
+
worktree_storage=worktree_storage,
|
|
219
|
+
clone_manager=clone_manager,
|
|
220
|
+
clone_storage=clone_storage,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# 6. Build spawn config
|
|
224
|
+
spawn_config = SpawnConfig(
|
|
225
|
+
prompt=prompt,
|
|
226
|
+
task_id=resolved_task_id,
|
|
227
|
+
task_title=task_title,
|
|
228
|
+
task_seq_num=task_seq_num,
|
|
229
|
+
branch_name=branch_name,
|
|
230
|
+
branch_prefix=effective_branch_prefix,
|
|
231
|
+
base_branch=effective_base_branch,
|
|
232
|
+
project_id=project_id,
|
|
233
|
+
project_path=resolved_project_path,
|
|
234
|
+
provider=effective_provider,
|
|
235
|
+
parent_session_id=parent_session_id,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# 7. Prepare environment (worktree/clone creation)
|
|
239
|
+
try:
|
|
240
|
+
isolation_ctx = await handler.prepare_environment(spawn_config)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to prepare environment: {e}", exc_info=True)
|
|
243
|
+
return {"success": False, "error": f"Failed to prepare environment: {e}"}
|
|
244
|
+
|
|
245
|
+
# 8. Build enhanced prompt with isolation context
|
|
246
|
+
enhanced_prompt = handler.build_context_prompt(prompt, isolation_ctx)
|
|
247
|
+
|
|
248
|
+
# 9. Generate session and run IDs
|
|
249
|
+
session_id = str(uuid.uuid4())
|
|
250
|
+
run_id = str(uuid.uuid4())
|
|
251
|
+
|
|
252
|
+
# 10. Execute spawn via SpawnExecutor
|
|
253
|
+
spawn_request = SpawnRequest(
|
|
254
|
+
prompt=enhanced_prompt,
|
|
255
|
+
cwd=isolation_ctx.cwd,
|
|
256
|
+
mode=effective_mode,
|
|
257
|
+
provider=effective_provider,
|
|
258
|
+
terminal=terminal,
|
|
259
|
+
session_id=session_id,
|
|
260
|
+
run_id=run_id,
|
|
261
|
+
parent_session_id=parent_session_id,
|
|
262
|
+
project_id=project_id,
|
|
263
|
+
workflow=effective_workflow,
|
|
264
|
+
worktree_id=isolation_ctx.worktree_id,
|
|
265
|
+
clone_id=isolation_ctx.clone_id,
|
|
266
|
+
session_manager=runner._child_session_manager,
|
|
267
|
+
machine_id=socket.gethostname(),
|
|
268
|
+
sandbox_config=effective_sandbox_config,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
spawn_result = await execute_spawn(spawn_request)
|
|
272
|
+
|
|
273
|
+
# 11. Return response with isolation metadata
|
|
274
|
+
return {
|
|
275
|
+
"success": spawn_result.success,
|
|
276
|
+
"run_id": spawn_result.run_id,
|
|
277
|
+
"child_session_id": spawn_result.child_session_id,
|
|
278
|
+
"status": spawn_result.status,
|
|
279
|
+
"isolation": effective_isolation,
|
|
280
|
+
"branch_name": isolation_ctx.branch_name,
|
|
281
|
+
"worktree_id": isolation_ctx.worktree_id,
|
|
282
|
+
"worktree_path": isolation_ctx.cwd if effective_isolation == "worktree" else None,
|
|
283
|
+
"clone_id": isolation_ctx.clone_id,
|
|
284
|
+
"pid": spawn_result.pid,
|
|
285
|
+
"error": spawn_result.error,
|
|
286
|
+
"message": spawn_result.message,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def create_spawn_agent_registry(
|
|
291
|
+
runner: AgentRunner,
|
|
292
|
+
agent_loader: AgentDefinitionLoader | None = None,
|
|
293
|
+
task_manager: LocalTaskManager | None = None,
|
|
294
|
+
worktree_storage: Any | None = None,
|
|
295
|
+
git_manager: Any | None = None,
|
|
296
|
+
clone_storage: Any | None = None,
|
|
297
|
+
clone_manager: Any | None = None,
|
|
298
|
+
) -> InternalToolRegistry:
|
|
299
|
+
"""
|
|
300
|
+
Create a spawn_agent tool registry with the unified spawn_agent tool.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
runner: AgentRunner instance for executing agents.
|
|
304
|
+
agent_loader: Loader for agent definitions.
|
|
305
|
+
task_manager: Task manager for task resolution.
|
|
306
|
+
worktree_storage: Storage for worktree records.
|
|
307
|
+
git_manager: Git manager for worktree operations.
|
|
308
|
+
clone_storage: Storage for clone records.
|
|
309
|
+
clone_manager: Git manager for clone operations.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
InternalToolRegistry with spawn_agent tool registered.
|
|
313
|
+
"""
|
|
314
|
+
registry = InternalToolRegistry(
|
|
315
|
+
name="gobby-spawn-agent",
|
|
316
|
+
description="Unified agent spawning with isolation support",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Use provided loader or create default
|
|
320
|
+
loader = agent_loader or AgentDefinitionLoader()
|
|
321
|
+
|
|
322
|
+
@registry.tool(
|
|
323
|
+
name="spawn_agent",
|
|
324
|
+
description=(
|
|
325
|
+
"Spawn a subagent to execute a task. Supports isolation modes: "
|
|
326
|
+
"'current' (work in current directory), 'worktree' (create git worktree), "
|
|
327
|
+
"'clone' (create shallow clone). Can use named agent definitions or raw parameters."
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
async def spawn_agent(
|
|
331
|
+
prompt: str,
|
|
332
|
+
agent: str = "generic",
|
|
333
|
+
task_id: str | None = None,
|
|
334
|
+
# Isolation
|
|
335
|
+
isolation: Literal["current", "worktree", "clone"] | None = None,
|
|
336
|
+
branch_name: str | None = None,
|
|
337
|
+
base_branch: str | None = None,
|
|
338
|
+
# Execution
|
|
339
|
+
workflow: str | None = None,
|
|
340
|
+
mode: Literal["terminal", "embedded", "headless"] | None = None,
|
|
341
|
+
terminal: str = "auto",
|
|
342
|
+
provider: str | None = None,
|
|
343
|
+
model: str | None = None,
|
|
344
|
+
# Limits
|
|
345
|
+
timeout: float | None = None,
|
|
346
|
+
max_turns: int | None = None,
|
|
347
|
+
# Sandbox
|
|
348
|
+
sandbox: bool | None = None,
|
|
349
|
+
sandbox_mode: Literal["permissive", "restrictive"] | None = None,
|
|
350
|
+
sandbox_allow_network: bool | None = None,
|
|
351
|
+
sandbox_extra_paths: list[str] | None = None,
|
|
352
|
+
# Context
|
|
353
|
+
parent_session_id: str | None = None,
|
|
354
|
+
project_path: str | None = None,
|
|
355
|
+
) -> dict[str, Any]:
|
|
356
|
+
"""
|
|
357
|
+
Spawn a subagent with the specified configuration.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
prompt: Required - what the agent should do
|
|
361
|
+
agent: Agent definition name (defaults to "generic")
|
|
362
|
+
task_id: Optional - link to task (supports N, #N, UUID)
|
|
363
|
+
isolation: Isolation mode (current/worktree/clone)
|
|
364
|
+
branch_name: Git branch name (auto-generated from task if not provided)
|
|
365
|
+
base_branch: Base branch for worktree/clone
|
|
366
|
+
workflow: Workflow to use
|
|
367
|
+
mode: Execution mode (terminal/embedded/headless)
|
|
368
|
+
terminal: Terminal type for terminal mode
|
|
369
|
+
provider: AI provider (claude/gemini/codex)
|
|
370
|
+
model: Model to use
|
|
371
|
+
timeout: Timeout in seconds
|
|
372
|
+
max_turns: Maximum conversation turns
|
|
373
|
+
sandbox: Enable sandbox (True/False/None). None inherits from agent_def.
|
|
374
|
+
sandbox_mode: Sandbox mode (permissive/restrictive). Overrides agent_def.
|
|
375
|
+
sandbox_allow_network: Allow network access. Overrides agent_def.
|
|
376
|
+
sandbox_extra_paths: Extra paths for sandbox write access.
|
|
377
|
+
parent_session_id: Parent session ID
|
|
378
|
+
project_path: Project path override
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Dict with success status, run_id, child_session_id, isolation metadata
|
|
382
|
+
"""
|
|
383
|
+
# Load agent definition (defaults to "generic")
|
|
384
|
+
agent_def = loader.load(agent)
|
|
385
|
+
if agent_def is None and agent != "generic":
|
|
386
|
+
return {"success": False, "error": f"Agent '{agent}' not found"}
|
|
387
|
+
|
|
388
|
+
# Delegate to spawn_agent_impl
|
|
389
|
+
return await spawn_agent_impl(
|
|
390
|
+
prompt=prompt,
|
|
391
|
+
runner=runner,
|
|
392
|
+
agent_def=agent_def,
|
|
393
|
+
task_id=task_id,
|
|
394
|
+
task_manager=task_manager,
|
|
395
|
+
isolation=isolation,
|
|
396
|
+
branch_name=branch_name,
|
|
397
|
+
base_branch=base_branch,
|
|
398
|
+
worktree_storage=worktree_storage,
|
|
399
|
+
git_manager=git_manager,
|
|
400
|
+
clone_storage=clone_storage,
|
|
401
|
+
clone_manager=clone_manager,
|
|
402
|
+
workflow=workflow,
|
|
403
|
+
mode=mode,
|
|
404
|
+
terminal=terminal,
|
|
405
|
+
provider=provider,
|
|
406
|
+
model=model,
|
|
407
|
+
timeout=timeout,
|
|
408
|
+
max_turns=max_turns,
|
|
409
|
+
sandbox=sandbox,
|
|
410
|
+
sandbox_mode=sandbox_mode,
|
|
411
|
+
sandbox_allow_network=sandbox_allow_network,
|
|
412
|
+
sandbox_extra_paths=sandbox_extra_paths,
|
|
413
|
+
parent_session_id=parent_session_id,
|
|
414
|
+
project_path=project_path,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return registry
|
|
@@ -11,6 +11,7 @@ from gobby.mcp_proxy.tools.orchestration.monitor import register_monitor
|
|
|
11
11
|
from gobby.mcp_proxy.tools.orchestration.orchestrate import register_orchestrator
|
|
12
12
|
from gobby.mcp_proxy.tools.orchestration.review import register_reviewer
|
|
13
13
|
from gobby.mcp_proxy.tools.orchestration.utils import get_current_project_id
|
|
14
|
+
from gobby.mcp_proxy.tools.orchestration.wait import register_wait
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from gobby.agents.runner import AgentRunner
|
|
@@ -74,4 +75,10 @@ def create_orchestration_registry(
|
|
|
74
75
|
default_project_id=default_project_id,
|
|
75
76
|
)
|
|
76
77
|
|
|
78
|
+
# Register wait tools
|
|
79
|
+
register_wait(
|
|
80
|
+
registry=registry,
|
|
81
|
+
task_manager=task_manager,
|
|
82
|
+
)
|
|
83
|
+
|
|
77
84
|
return registry
|
|
@@ -9,6 +9,7 @@ Provides tools for task readiness management:
|
|
|
9
9
|
Extracted from tasks.py using Strangler Fig pattern for code decomposition.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
from collections.abc import Callable
|
|
13
14
|
from typing import TYPE_CHECKING, Any
|
|
14
15
|
|
|
@@ -20,6 +21,8 @@ from gobby.workflows.state_manager import WorkflowStateManager
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from gobby.storage.tasks import LocalTaskManager
|
|
22
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
23
26
|
__all__ = [
|
|
24
27
|
"create_readiness_registry",
|
|
25
28
|
"is_descendant_of",
|
|
@@ -474,6 +477,16 @@ def create_readiness_registry(
|
|
|
474
477
|
if best_proximity > 0:
|
|
475
478
|
reasons.append("same branch as current work")
|
|
476
479
|
|
|
480
|
+
# Get recommended skills based on task category
|
|
481
|
+
recommended_skills: list[str] = []
|
|
482
|
+
try:
|
|
483
|
+
from gobby.workflows.context_actions import recommend_skills_for_task
|
|
484
|
+
|
|
485
|
+
task_brief = best_task.to_brief()
|
|
486
|
+
recommended_skills = recommend_skills_for_task(task_brief)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
logger.debug(f"Skill recommendation failed: {e}")
|
|
489
|
+
|
|
477
490
|
return {
|
|
478
491
|
"suggestion": best_task.to_brief(),
|
|
479
492
|
"score": best_score,
|
|
@@ -482,6 +495,7 @@ def create_readiness_registry(
|
|
|
482
495
|
{"ref": t.to_brief()["ref"], "title": t.title, "score": s}
|
|
483
496
|
for t, s, _, _ in scored[1:4] # Show top 3 alternatives
|
|
484
497
|
],
|
|
498
|
+
"recommended_skills": recommended_skills,
|
|
485
499
|
}
|
|
486
500
|
|
|
487
501
|
registry.register(
|
|
@@ -159,7 +159,7 @@ def create_sync_registry(
|
|
|
159
159
|
|
|
160
160
|
registry.register(
|
|
161
161
|
name="link_commit",
|
|
162
|
-
description="Link a git commit to a task.
|
|
162
|
+
description="Link a git commit to a task. NOTE: For closing tasks, prefer close_task(task_id, commit_sha='...') which links and closes in one call. Use link_commit only when you need to link without closing.",
|
|
163
163
|
input_schema={
|
|
164
164
|
"type": "object",
|
|
165
165
|
"properties": {
|
|
@@ -20,7 +20,6 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from gobby.config.app import DaemonConfig
|
|
21
21
|
from gobby.config.tasks import TaskValidationConfig
|
|
22
22
|
from gobby.sync.tasks import TaskSyncManager
|
|
23
|
-
from gobby.tasks.expansion import TaskExpander
|
|
24
23
|
from gobby.tasks.validation import TaskValidator
|
|
25
24
|
|
|
26
25
|
|
|
@@ -36,7 +35,6 @@ class RegistryContext:
|
|
|
36
35
|
sync_manager: "TaskSyncManager"
|
|
37
36
|
|
|
38
37
|
# Optional managers
|
|
39
|
-
task_expander: "TaskExpander | None" = None
|
|
40
38
|
task_validator: "TaskValidator | None" = None
|
|
41
39
|
agent_runner: "AgentRunner | None" = None
|
|
42
40
|
config: "DaemonConfig | None" = None
|
|
@@ -50,7 +48,6 @@ class RegistryContext:
|
|
|
50
48
|
# Config settings (initialized in __post_init__)
|
|
51
49
|
show_result_on_create: bool = field(init=False)
|
|
52
50
|
auto_generate_on_expand: bool = field(init=False)
|
|
53
|
-
tdd_mode_config: bool = field(init=False)
|
|
54
51
|
validation_config: "TaskValidationConfig | None" = field(init=False)
|
|
55
52
|
|
|
56
53
|
def __post_init__(self) -> None:
|
|
@@ -65,7 +62,6 @@ class RegistryContext:
|
|
|
65
62
|
# Initialize config settings
|
|
66
63
|
self.show_result_on_create = False
|
|
67
64
|
self.auto_generate_on_expand = True
|
|
68
|
-
self.tdd_mode_config = False
|
|
69
65
|
self.validation_config = None
|
|
70
66
|
|
|
71
67
|
if self.config is not None:
|
|
@@ -73,7 +69,6 @@ class RegistryContext:
|
|
|
73
69
|
self.show_result_on_create = tasks_config.show_result_on_create
|
|
74
70
|
self.validation_config = tasks_config.validation
|
|
75
71
|
self.auto_generate_on_expand = self.validation_config.auto_generate_on_expand
|
|
76
|
-
self.tdd_mode_config = tasks_config.expansion.tdd_mode
|
|
77
72
|
|
|
78
73
|
def get_project_repo_path(self, project_id: str | None) -> str | None:
|
|
79
74
|
"""Get the repo_path for a project by ID."""
|
|
@@ -95,18 +90,3 @@ class RegistryContext:
|
|
|
95
90
|
if not session_id:
|
|
96
91
|
return None
|
|
97
92
|
return self.workflow_state_manager.get_state(session_id)
|
|
98
|
-
|
|
99
|
-
def resolve_tdd_mode(self, session_id: str | None) -> bool:
|
|
100
|
-
"""
|
|
101
|
-
Resolve tdd_mode from workflow state > config hierarchy.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
True if TDD mode is enabled, False otherwise.
|
|
105
|
-
"""
|
|
106
|
-
# Check workflow state first (takes precedence)
|
|
107
|
-
state = self.get_workflow_state(session_id)
|
|
108
|
-
if state and "tdd_mode" in state.variables:
|
|
109
|
-
return bool(state.variables["tdd_mode"])
|
|
110
|
-
|
|
111
|
-
# Fall back to config
|
|
112
|
-
return self.tdd_mode_config
|