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.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {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. Useful for tracking which commits implement 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