gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"""Task orchestration tool: orchestrate_ready_tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
|
|
8
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
9
|
+
from gobby.mcp_proxy.tools.task_readiness import _get_ready_descendants
|
|
10
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
11
|
+
|
|
12
|
+
from .utils import get_current_project_id
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.agents.runner import AgentRunner
|
|
16
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
17
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
18
|
+
from gobby.worktrees.git import WorktreeGitManager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_orchestrator(
|
|
24
|
+
registry: InternalToolRegistry,
|
|
25
|
+
task_manager: LocalTaskManager,
|
|
26
|
+
worktree_storage: LocalWorktreeManager,
|
|
27
|
+
git_manager: WorktreeGitManager | None = None,
|
|
28
|
+
agent_runner: AgentRunner | None = None,
|
|
29
|
+
default_project_id: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Register orchestrate_ready_tasks tool."""
|
|
32
|
+
|
|
33
|
+
# Lazy import to avoid circular dependency
|
|
34
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
35
|
+
|
|
36
|
+
async def orchestrate_ready_tasks(
|
|
37
|
+
parent_task_id: str,
|
|
38
|
+
provider: Literal["claude", "gemini", "codex", "antigravity"] = "gemini",
|
|
39
|
+
model: str | None = None,
|
|
40
|
+
terminal: str = "auto",
|
|
41
|
+
mode: str = "terminal",
|
|
42
|
+
workflow: str | None = "auto-task",
|
|
43
|
+
max_concurrent: int = 3,
|
|
44
|
+
parent_session_id: str | None = None,
|
|
45
|
+
project_path: str | None = None,
|
|
46
|
+
coding_provider: Literal["claude", "gemini", "codex", "antigravity"] | None = None,
|
|
47
|
+
coding_model: str | None = None,
|
|
48
|
+
base_branch: str | None = None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Orchestrate spawning agents in worktrees for ready subtasks.
|
|
52
|
+
|
|
53
|
+
Gets ready subtasks under a parent task, creates worktrees for each,
|
|
54
|
+
and spawns agents to work on them. Returns list of spawned agent/worktree pairs.
|
|
55
|
+
|
|
56
|
+
Used by the auto-orchestrator workflow to parallelize work.
|
|
57
|
+
|
|
58
|
+
Provider assignment:
|
|
59
|
+
- coding_provider/coding_model: Use these for implementation tasks (preferred)
|
|
60
|
+
- provider/model: Fallback if coding_* not specified
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
parent_task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
64
|
+
provider: Fallback LLM provider (default: gemini)
|
|
65
|
+
model: Fallback model override
|
|
66
|
+
terminal: Terminal for terminal mode (default: auto)
|
|
67
|
+
mode: Execution mode (terminal, embedded, headless)
|
|
68
|
+
workflow: Workflow for spawned agents (default: auto-task)
|
|
69
|
+
max_concurrent: Maximum concurrent agents to spawn (default: 3)
|
|
70
|
+
parent_session_id: Parent session ID for context (required)
|
|
71
|
+
project_path: Path to project directory
|
|
72
|
+
coding_provider: LLM provider for implementation tasks (overrides provider)
|
|
73
|
+
coding_model: Model for implementation tasks (overrides model)
|
|
74
|
+
base_branch: Branch to base worktrees on (auto-detected if not provided)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict with:
|
|
78
|
+
- success: bool
|
|
79
|
+
- spawned: List of {task_id, agent_id, worktree_id, branch_name}
|
|
80
|
+
- skipped: List of {task_id, reason} for tasks not spawned
|
|
81
|
+
- error: Optional error message
|
|
82
|
+
"""
|
|
83
|
+
# Validate mode parameter
|
|
84
|
+
valid_modes = {"terminal", "headless", "embedded"}
|
|
85
|
+
if mode not in valid_modes:
|
|
86
|
+
return {
|
|
87
|
+
"success": False,
|
|
88
|
+
"error": f"Invalid mode '{mode}'. Must be one of: {', '.join(sorted(valid_modes))}",
|
|
89
|
+
"spawned": [],
|
|
90
|
+
"skipped": [],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Resolve parent_task_id reference
|
|
94
|
+
try:
|
|
95
|
+
resolved_parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id)
|
|
96
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
97
|
+
return {
|
|
98
|
+
"success": False,
|
|
99
|
+
"error": f"Invalid parent_task_id: {e}",
|
|
100
|
+
"spawned": [],
|
|
101
|
+
"skipped": [],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if agent_runner is None:
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": "Agent runner not configured. Cannot orchestrate.",
|
|
108
|
+
"spawned": [],
|
|
109
|
+
"skipped": [],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if parent_session_id is None:
|
|
113
|
+
return {
|
|
114
|
+
"success": False,
|
|
115
|
+
"error": "parent_session_id is required for orchestration",
|
|
116
|
+
"spawned": [],
|
|
117
|
+
"skipped": [],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Resolve project ID
|
|
121
|
+
resolved_project_id = default_project_id
|
|
122
|
+
if project_path:
|
|
123
|
+
from pathlib import Path
|
|
124
|
+
|
|
125
|
+
from gobby.utils.project_context import get_project_context
|
|
126
|
+
|
|
127
|
+
ctx = get_project_context(Path(project_path))
|
|
128
|
+
if ctx:
|
|
129
|
+
resolved_project_id = ctx.get("id")
|
|
130
|
+
|
|
131
|
+
if not resolved_project_id:
|
|
132
|
+
resolved_project_id = get_current_project_id()
|
|
133
|
+
|
|
134
|
+
if not resolved_project_id:
|
|
135
|
+
return {
|
|
136
|
+
"success": False,
|
|
137
|
+
"error": "Could not resolve project ID",
|
|
138
|
+
"spawned": [],
|
|
139
|
+
"skipped": [],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Get ready subtasks under the parent task
|
|
143
|
+
ready_tasks = _get_ready_descendants(
|
|
144
|
+
task_manager=task_manager,
|
|
145
|
+
parent_task_id=resolved_parent_task_id,
|
|
146
|
+
project_id=resolved_project_id,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not ready_tasks:
|
|
150
|
+
return {
|
|
151
|
+
"success": True,
|
|
152
|
+
"message": f"No ready subtasks found under {resolved_parent_task_id}",
|
|
153
|
+
"spawned": [],
|
|
154
|
+
"skipped": [],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Check how many agents are currently running for this project
|
|
158
|
+
# Get exact count by not limiting the query - ensures max_concurrent is respected
|
|
159
|
+
from gobby.storage.worktrees import WorktreeStatus
|
|
160
|
+
|
|
161
|
+
active_worktrees = worktree_storage.list_worktrees(
|
|
162
|
+
project_id=resolved_project_id,
|
|
163
|
+
status=WorktreeStatus.ACTIVE.value,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Count worktrees claimed by active sessions (have agent_session_id)
|
|
167
|
+
current_running = sum(1 for wt in active_worktrees if wt.agent_session_id)
|
|
168
|
+
available_slots = max(0, max_concurrent - current_running)
|
|
169
|
+
|
|
170
|
+
if available_slots == 0:
|
|
171
|
+
return {
|
|
172
|
+
"success": True,
|
|
173
|
+
"message": f"Max concurrent limit reached ({max_concurrent} agents running)",
|
|
174
|
+
"spawned": [],
|
|
175
|
+
"skipped": [
|
|
176
|
+
{"task_id": t.id, "reason": "max_concurrent limit reached"} for t in ready_tasks
|
|
177
|
+
],
|
|
178
|
+
"current_running": current_running,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Limit to available slots
|
|
182
|
+
tasks_to_spawn = ready_tasks[:available_slots]
|
|
183
|
+
tasks_skipped = ready_tasks[available_slots:]
|
|
184
|
+
|
|
185
|
+
# Resolve effective provider and model for implementation tasks
|
|
186
|
+
# Priority: explicit parameter > workflow variable > default
|
|
187
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
188
|
+
|
|
189
|
+
workflow_vars: dict[str, Any] = {}
|
|
190
|
+
if parent_session_id:
|
|
191
|
+
state_manager = WorkflowStateManager(task_manager.db)
|
|
192
|
+
state = state_manager.get_state(parent_session_id)
|
|
193
|
+
if state:
|
|
194
|
+
workflow_vars = state.variables
|
|
195
|
+
|
|
196
|
+
# Provider assignment chain: parameter > workflow var > default
|
|
197
|
+
effective_provider = coding_provider or workflow_vars.get("coding_provider") or provider
|
|
198
|
+
effective_model = coding_model or workflow_vars.get("coding_model") or model
|
|
199
|
+
# Also capture terminal from workflow if not explicitly set
|
|
200
|
+
effective_terminal = (
|
|
201
|
+
terminal if terminal != "auto" else workflow_vars.get("terminal", "auto")
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
spawned: list[dict[str, Any]] = []
|
|
205
|
+
skipped: list[dict[str, Any]] = []
|
|
206
|
+
|
|
207
|
+
# Add skipped due to limit
|
|
208
|
+
for task in tasks_skipped:
|
|
209
|
+
skipped.append(
|
|
210
|
+
{
|
|
211
|
+
"task_id": task.id,
|
|
212
|
+
"title": task.title,
|
|
213
|
+
"reason": "max_concurrent limit reached",
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Import worktree tool helpers
|
|
218
|
+
import platform
|
|
219
|
+
import tempfile
|
|
220
|
+
from pathlib import Path
|
|
221
|
+
|
|
222
|
+
from gobby.mcp_proxy.tools.worktrees import (
|
|
223
|
+
_copy_project_json_to_worktree,
|
|
224
|
+
_install_provider_hooks,
|
|
225
|
+
)
|
|
226
|
+
from gobby.workflows.loader import WorkflowLoader
|
|
227
|
+
|
|
228
|
+
def get_worktree_base_dir() -> Path:
|
|
229
|
+
"""Get base directory for worktrees."""
|
|
230
|
+
if platform.system() == "Windows":
|
|
231
|
+
base = Path(tempfile.gettempdir()) / "gobby-worktrees"
|
|
232
|
+
else:
|
|
233
|
+
# nosec B108: /tmp is intentional - worktrees are temporary
|
|
234
|
+
base = Path("/tmp").resolve() / "gobby-worktrees" # nosec B108
|
|
235
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
return base
|
|
237
|
+
|
|
238
|
+
# Detect base_branch if not provided
|
|
239
|
+
effective_base_branch = base_branch
|
|
240
|
+
if not effective_base_branch and git_manager is not None:
|
|
241
|
+
effective_base_branch = git_manager.get_default_branch()
|
|
242
|
+
logger.debug(f"Auto-detected base branch: {effective_base_branch}")
|
|
243
|
+
elif not effective_base_branch:
|
|
244
|
+
effective_base_branch = "main" # Fallback when no git_manager
|
|
245
|
+
|
|
246
|
+
for task in tasks_to_spawn:
|
|
247
|
+
try:
|
|
248
|
+
# Generate branch name from task ID
|
|
249
|
+
branch_name = f"task/{task.id}"
|
|
250
|
+
safe_branch = branch_name.replace("/", "-")
|
|
251
|
+
|
|
252
|
+
# Check if worktree already exists for this task
|
|
253
|
+
existing_wt = worktree_storage.get_by_task(task.id)
|
|
254
|
+
if existing_wt and existing_wt.agent_session_id:
|
|
255
|
+
# Worktree exists and has active agent
|
|
256
|
+
skipped.append(
|
|
257
|
+
{
|
|
258
|
+
"task_id": task.id,
|
|
259
|
+
"title": task.title,
|
|
260
|
+
"reason": f"Already has active worktree: {existing_wt.id}",
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# Check if branch worktree exists
|
|
266
|
+
existing_branch_wt = worktree_storage.get_by_branch(
|
|
267
|
+
resolved_project_id, branch_name
|
|
268
|
+
)
|
|
269
|
+
if existing_branch_wt and existing_branch_wt.agent_session_id:
|
|
270
|
+
skipped.append(
|
|
271
|
+
{
|
|
272
|
+
"task_id": task.id,
|
|
273
|
+
"title": task.title,
|
|
274
|
+
"reason": f"Branch {branch_name} has active agent",
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Validate workflow early (before creating worktree to avoid cleanup)
|
|
280
|
+
if workflow:
|
|
281
|
+
workflow_loader = WorkflowLoader()
|
|
282
|
+
is_valid, error_msg = workflow_loader.validate_workflow_for_agent(
|
|
283
|
+
workflow, project_path=project_path
|
|
284
|
+
)
|
|
285
|
+
if not is_valid:
|
|
286
|
+
skipped.append(
|
|
287
|
+
{
|
|
288
|
+
"task_id": task.id,
|
|
289
|
+
"title": task.title,
|
|
290
|
+
"reason": f"Invalid workflow: {error_msg}",
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
# Determine worktree path
|
|
296
|
+
newly_created_worktree = False
|
|
297
|
+
if existing_wt:
|
|
298
|
+
worktree = existing_wt
|
|
299
|
+
elif existing_branch_wt:
|
|
300
|
+
worktree = existing_branch_wt
|
|
301
|
+
# Link task to existing worktree
|
|
302
|
+
worktree_storage.update(worktree.id, task_id=task.id)
|
|
303
|
+
else:
|
|
304
|
+
# Create new worktree
|
|
305
|
+
if git_manager is None:
|
|
306
|
+
skipped.append(
|
|
307
|
+
{
|
|
308
|
+
"task_id": task.id,
|
|
309
|
+
"title": task.title,
|
|
310
|
+
"reason": "No git manager configured",
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
# Generate path
|
|
316
|
+
project_name = Path(git_manager.repo_path).name
|
|
317
|
+
base_dir = get_worktree_base_dir()
|
|
318
|
+
worktree_path = str(base_dir / project_name / safe_branch)
|
|
319
|
+
|
|
320
|
+
# Create git worktree
|
|
321
|
+
result = git_manager.create_worktree(
|
|
322
|
+
worktree_path=worktree_path,
|
|
323
|
+
branch_name=branch_name,
|
|
324
|
+
base_branch=effective_base_branch,
|
|
325
|
+
create_branch=True,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if not result.success:
|
|
329
|
+
skipped.append(
|
|
330
|
+
{
|
|
331
|
+
"task_id": task.id,
|
|
332
|
+
"title": task.title,
|
|
333
|
+
"reason": f"Failed to create worktree: {result.error}",
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
# Record in database
|
|
339
|
+
worktree = worktree_storage.create(
|
|
340
|
+
project_id=resolved_project_id,
|
|
341
|
+
branch_name=branch_name,
|
|
342
|
+
worktree_path=worktree_path,
|
|
343
|
+
base_branch=effective_base_branch,
|
|
344
|
+
task_id=task.id,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Copy project.json and install hooks (with cleanup on failure)
|
|
348
|
+
try:
|
|
349
|
+
_copy_project_json_to_worktree(
|
|
350
|
+
git_manager.repo_path, worktree.worktree_path
|
|
351
|
+
)
|
|
352
|
+
_install_provider_hooks(effective_provider, worktree.worktree_path)
|
|
353
|
+
except Exception as init_error:
|
|
354
|
+
# Cleanup: delete DB record and git worktree
|
|
355
|
+
worktree_storage.delete(worktree.id)
|
|
356
|
+
git_manager.delete_worktree(
|
|
357
|
+
worktree_path=worktree_path,
|
|
358
|
+
force=True,
|
|
359
|
+
delete_branch=True,
|
|
360
|
+
)
|
|
361
|
+
skipped.append(
|
|
362
|
+
{
|
|
363
|
+
"task_id": task.id,
|
|
364
|
+
"title": task.title,
|
|
365
|
+
"reason": f"Worktree initialization failed: {init_error}",
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
continue
|
|
369
|
+
newly_created_worktree = True
|
|
370
|
+
|
|
371
|
+
# Build prompt with task context
|
|
372
|
+
prompt = _build_task_prompt(task)
|
|
373
|
+
|
|
374
|
+
# Check spawn depth
|
|
375
|
+
can_spawn, reason, _depth = agent_runner.can_spawn(parent_session_id)
|
|
376
|
+
if not can_spawn:
|
|
377
|
+
skipped.append(
|
|
378
|
+
{
|
|
379
|
+
"task_id": task.id,
|
|
380
|
+
"title": task.title,
|
|
381
|
+
"reason": reason,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
# Prepare agent run
|
|
387
|
+
from gobby.agents.runner import AgentConfig
|
|
388
|
+
from gobby.llm.executor import AgentResult
|
|
389
|
+
from gobby.utils.machine_id import get_machine_id
|
|
390
|
+
|
|
391
|
+
machine_id = get_machine_id()
|
|
392
|
+
|
|
393
|
+
config = AgentConfig(
|
|
394
|
+
prompt=prompt,
|
|
395
|
+
parent_session_id=parent_session_id,
|
|
396
|
+
project_id=resolved_project_id,
|
|
397
|
+
machine_id=machine_id,
|
|
398
|
+
source=effective_provider,
|
|
399
|
+
workflow=workflow,
|
|
400
|
+
task=task.id,
|
|
401
|
+
session_context="summary_markdown",
|
|
402
|
+
mode=mode,
|
|
403
|
+
terminal=effective_terminal,
|
|
404
|
+
worktree_id=worktree.id,
|
|
405
|
+
provider=effective_provider,
|
|
406
|
+
model=effective_model,
|
|
407
|
+
max_turns=50, # Allow substantial work
|
|
408
|
+
timeout=600.0, # 10 minutes
|
|
409
|
+
project_path=worktree.worktree_path,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
prepare_result = agent_runner.prepare_run(config)
|
|
413
|
+
if isinstance(prepare_result, AgentResult):
|
|
414
|
+
skipped.append(
|
|
415
|
+
{
|
|
416
|
+
"task_id": task.id,
|
|
417
|
+
"title": task.title,
|
|
418
|
+
"reason": prepare_result.error or "Failed to prepare agent run",
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
context = prepare_result
|
|
424
|
+
if context.session is None or context.run is None:
|
|
425
|
+
skipped.append(
|
|
426
|
+
{
|
|
427
|
+
"task_id": task.id,
|
|
428
|
+
"title": task.title,
|
|
429
|
+
"reason": "Internal error: context missing session or run",
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
continue
|
|
433
|
+
|
|
434
|
+
child_session = context.session
|
|
435
|
+
agent_run = context.run
|
|
436
|
+
|
|
437
|
+
# Claim worktree for child session
|
|
438
|
+
worktree_storage.claim(worktree.id, child_session.id)
|
|
439
|
+
|
|
440
|
+
# Note: Task status is updated to in_progress only after successful spawn
|
|
441
|
+
|
|
442
|
+
# Spawn in terminal
|
|
443
|
+
if mode == "terminal":
|
|
444
|
+
from gobby.agents.spawn import TerminalSpawner
|
|
445
|
+
|
|
446
|
+
spawner = TerminalSpawner()
|
|
447
|
+
spawn_result = spawner.spawn_agent(
|
|
448
|
+
cli=effective_provider,
|
|
449
|
+
cwd=worktree.worktree_path,
|
|
450
|
+
session_id=child_session.id,
|
|
451
|
+
parent_session_id=parent_session_id,
|
|
452
|
+
agent_run_id=agent_run.id,
|
|
453
|
+
project_id=resolved_project_id,
|
|
454
|
+
workflow_name=workflow,
|
|
455
|
+
agent_depth=child_session.agent_depth,
|
|
456
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
457
|
+
terminal=effective_terminal,
|
|
458
|
+
prompt=prompt,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if not spawn_result.success:
|
|
462
|
+
worktree_storage.release(worktree.id)
|
|
463
|
+
# Clean up newly created worktree on spawn failure
|
|
464
|
+
if newly_created_worktree and git_manager is not None:
|
|
465
|
+
worktree_storage.delete(worktree.id)
|
|
466
|
+
git_manager.delete_worktree(
|
|
467
|
+
worktree_path=worktree.worktree_path,
|
|
468
|
+
force=True,
|
|
469
|
+
delete_branch=True,
|
|
470
|
+
)
|
|
471
|
+
skipped.append(
|
|
472
|
+
{
|
|
473
|
+
"task_id": task.id,
|
|
474
|
+
"title": task.title,
|
|
475
|
+
"reason": spawn_result.error or "Terminal spawn failed",
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
# Mark task as in_progress only after successful spawn
|
|
481
|
+
task_manager.update_task(task.id, status="in_progress")
|
|
482
|
+
|
|
483
|
+
spawned.append(
|
|
484
|
+
{
|
|
485
|
+
"task_id": task.id,
|
|
486
|
+
"title": task.title,
|
|
487
|
+
"agent_id": agent_run.id,
|
|
488
|
+
"session_id": child_session.id,
|
|
489
|
+
"worktree_id": worktree.id,
|
|
490
|
+
"branch_name": worktree.branch_name,
|
|
491
|
+
"worktree_path": worktree.worktree_path,
|
|
492
|
+
"terminal_type": spawn_result.terminal_type,
|
|
493
|
+
"pid": spawn_result.pid,
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
elif mode == "embedded":
|
|
498
|
+
from gobby.agents.spawn import EmbeddedSpawner
|
|
499
|
+
|
|
500
|
+
embedded_spawner = EmbeddedSpawner()
|
|
501
|
+
embedded_result = embedded_spawner.spawn_agent(
|
|
502
|
+
cli=effective_provider,
|
|
503
|
+
cwd=worktree.worktree_path,
|
|
504
|
+
session_id=child_session.id,
|
|
505
|
+
parent_session_id=parent_session_id,
|
|
506
|
+
agent_run_id=agent_run.id,
|
|
507
|
+
project_id=resolved_project_id,
|
|
508
|
+
workflow_name=workflow,
|
|
509
|
+
agent_depth=child_session.agent_depth,
|
|
510
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
511
|
+
prompt=prompt,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
if not embedded_result.success:
|
|
515
|
+
worktree_storage.release(worktree.id)
|
|
516
|
+
# Clean up newly created worktree on spawn failure
|
|
517
|
+
if newly_created_worktree and git_manager is not None:
|
|
518
|
+
worktree_storage.delete(worktree.id)
|
|
519
|
+
git_manager.delete_worktree(
|
|
520
|
+
worktree_path=worktree.worktree_path,
|
|
521
|
+
force=True,
|
|
522
|
+
delete_branch=True,
|
|
523
|
+
)
|
|
524
|
+
skipped.append(
|
|
525
|
+
{
|
|
526
|
+
"task_id": task.id,
|
|
527
|
+
"title": task.title,
|
|
528
|
+
"reason": embedded_result.error or "Embedded spawn failed",
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
# Mark task as in_progress only after successful spawn
|
|
534
|
+
task_manager.update_task(task.id, status="in_progress")
|
|
535
|
+
|
|
536
|
+
spawned.append(
|
|
537
|
+
{
|
|
538
|
+
"task_id": task.id,
|
|
539
|
+
"title": task.title,
|
|
540
|
+
"agent_id": agent_run.id,
|
|
541
|
+
"session_id": child_session.id,
|
|
542
|
+
"worktree_id": worktree.id,
|
|
543
|
+
"branch_name": worktree.branch_name,
|
|
544
|
+
"worktree_path": worktree.worktree_path,
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
else: # headless
|
|
549
|
+
from gobby.agents.spawn import HeadlessSpawner
|
|
550
|
+
|
|
551
|
+
headless_spawner = HeadlessSpawner()
|
|
552
|
+
headless_result = headless_spawner.spawn_agent(
|
|
553
|
+
cli=effective_provider,
|
|
554
|
+
cwd=worktree.worktree_path,
|
|
555
|
+
session_id=child_session.id,
|
|
556
|
+
parent_session_id=parent_session_id,
|
|
557
|
+
agent_run_id=agent_run.id,
|
|
558
|
+
project_id=resolved_project_id,
|
|
559
|
+
workflow_name=workflow,
|
|
560
|
+
agent_depth=child_session.agent_depth,
|
|
561
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
562
|
+
prompt=prompt,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
if not headless_result.success:
|
|
566
|
+
worktree_storage.release(worktree.id)
|
|
567
|
+
# Clean up newly created worktree on spawn failure
|
|
568
|
+
if newly_created_worktree and git_manager is not None:
|
|
569
|
+
worktree_storage.delete(worktree.id)
|
|
570
|
+
git_manager.delete_worktree(
|
|
571
|
+
worktree_path=worktree.worktree_path,
|
|
572
|
+
force=True,
|
|
573
|
+
delete_branch=True,
|
|
574
|
+
)
|
|
575
|
+
skipped.append(
|
|
576
|
+
{
|
|
577
|
+
"task_id": task.id,
|
|
578
|
+
"title": task.title,
|
|
579
|
+
"reason": headless_result.error or "Headless spawn failed",
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
# Mark task as in_progress only after successful spawn
|
|
585
|
+
task_manager.update_task(task.id, status="in_progress")
|
|
586
|
+
|
|
587
|
+
spawned.append(
|
|
588
|
+
{
|
|
589
|
+
"task_id": task.id,
|
|
590
|
+
"title": task.title,
|
|
591
|
+
"agent_id": agent_run.id,
|
|
592
|
+
"session_id": child_session.id,
|
|
593
|
+
"worktree_id": worktree.id,
|
|
594
|
+
"branch_name": worktree.branch_name,
|
|
595
|
+
"worktree_path": worktree.worktree_path,
|
|
596
|
+
"pid": headless_result.pid,
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
except Exception as e:
|
|
601
|
+
logger.exception(f"Error orchestrating task {task.id}")
|
|
602
|
+
skipped.append(
|
|
603
|
+
{
|
|
604
|
+
"task_id": task.id,
|
|
605
|
+
"title": task.title,
|
|
606
|
+
"reason": str(e),
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Store spawned agents in workflow state for tracking
|
|
611
|
+
if spawned and parent_session_id:
|
|
612
|
+
try:
|
|
613
|
+
state_manager = WorkflowStateManager(task_manager.db)
|
|
614
|
+
state = state_manager.get_state(parent_session_id)
|
|
615
|
+
if state:
|
|
616
|
+
current_spawned = state.variables.get("spawned_agents", [])
|
|
617
|
+
# Append new agents to existing list
|
|
618
|
+
current_spawned.extend(spawned)
|
|
619
|
+
state.variables["spawned_agents"] = current_spawned
|
|
620
|
+
state_manager.save_state(state)
|
|
621
|
+
logger.info(
|
|
622
|
+
f"Updated spawned_agents in workflow state: {len(current_spawned)} total"
|
|
623
|
+
)
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.warning(f"Failed to update workflow state: {e}")
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
"success": True,
|
|
629
|
+
"parent_task_id": resolved_parent_task_id,
|
|
630
|
+
"spawned": spawned,
|
|
631
|
+
"skipped": skipped,
|
|
632
|
+
"spawned_count": len(spawned),
|
|
633
|
+
"skipped_count": len(skipped),
|
|
634
|
+
"current_running": current_running + len(spawned),
|
|
635
|
+
"max_concurrent": max_concurrent,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
registry.register(
|
|
639
|
+
name="orchestrate_ready_tasks",
|
|
640
|
+
description=(
|
|
641
|
+
"Spawn agents in worktrees for ready subtasks under a parent task. "
|
|
642
|
+
"Used by auto-orchestrator workflow for parallel execution. "
|
|
643
|
+
"Supports role-based provider assignment: explicitly passed params > workflow variables > defaults. "
|
|
644
|
+
"Workflow variables: coding_provider, coding_model, terminal."
|
|
645
|
+
),
|
|
646
|
+
input_schema={
|
|
647
|
+
"type": "object",
|
|
648
|
+
"properties": {
|
|
649
|
+
"parent_task_id": {
|
|
650
|
+
"type": "string",
|
|
651
|
+
"description": "Task reference: #N, N (seq_num), path (1.2.3), or UUID",
|
|
652
|
+
},
|
|
653
|
+
"provider": {
|
|
654
|
+
"type": "string",
|
|
655
|
+
"description": "Fallback LLM provider (claude, gemini, codex, antigravity)",
|
|
656
|
+
"default": "gemini",
|
|
657
|
+
},
|
|
658
|
+
"model": {
|
|
659
|
+
"type": "string",
|
|
660
|
+
"description": "Fallback model override",
|
|
661
|
+
"default": None,
|
|
662
|
+
},
|
|
663
|
+
"coding_provider": {
|
|
664
|
+
"type": "string",
|
|
665
|
+
"description": (
|
|
666
|
+
"LLM provider for implementation tasks. "
|
|
667
|
+
"Overrides 'provider' for coding work."
|
|
668
|
+
),
|
|
669
|
+
"default": None,
|
|
670
|
+
},
|
|
671
|
+
"coding_model": {
|
|
672
|
+
"type": "string",
|
|
673
|
+
"description": (
|
|
674
|
+
"Model for implementation tasks. Overrides 'model' for coding work."
|
|
675
|
+
),
|
|
676
|
+
"default": None,
|
|
677
|
+
},
|
|
678
|
+
"terminal": {
|
|
679
|
+
"type": "string",
|
|
680
|
+
"description": "Terminal for terminal mode (auto, ghostty, iterm2, etc.)",
|
|
681
|
+
"default": "auto",
|
|
682
|
+
},
|
|
683
|
+
"mode": {
|
|
684
|
+
"type": "string",
|
|
685
|
+
"description": "Execution mode (terminal, embedded, headless)",
|
|
686
|
+
"default": "terminal",
|
|
687
|
+
},
|
|
688
|
+
"workflow": {
|
|
689
|
+
"type": "string",
|
|
690
|
+
"description": "Workflow for spawned agents",
|
|
691
|
+
"default": "auto-task",
|
|
692
|
+
},
|
|
693
|
+
"max_concurrent": {
|
|
694
|
+
"type": "integer",
|
|
695
|
+
"description": "Maximum concurrent agents to spawn",
|
|
696
|
+
"default": 3,
|
|
697
|
+
},
|
|
698
|
+
"parent_session_id": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"description": "Parent session ID for context (required)",
|
|
701
|
+
},
|
|
702
|
+
"project_path": {
|
|
703
|
+
"type": "string",
|
|
704
|
+
"description": "Path to project directory",
|
|
705
|
+
"default": None,
|
|
706
|
+
},
|
|
707
|
+
"base_branch": {
|
|
708
|
+
"type": "string",
|
|
709
|
+
"description": (
|
|
710
|
+
"Branch to base worktrees on (e.g., main, master, develop). "
|
|
711
|
+
"Auto-detected from repository if not provided."
|
|
712
|
+
),
|
|
713
|
+
"default": None,
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
"required": ["parent_task_id", "parent_session_id"],
|
|
717
|
+
},
|
|
718
|
+
func=orchestrate_ready_tasks,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _build_task_prompt(task: Any) -> str:
|
|
723
|
+
"""Build a prompt for a task agent."""
|
|
724
|
+
prompt_parts = [
|
|
725
|
+
f"# Task: {task.title}",
|
|
726
|
+
f"Task ID: {task.id}",
|
|
727
|
+
]
|
|
728
|
+
|
|
729
|
+
if task.description:
|
|
730
|
+
prompt_parts.append(f"\n## Description\n{task.description}")
|
|
731
|
+
|
|
732
|
+
if task.category:
|
|
733
|
+
prompt_parts.append(f"\n## Category\n{task.category}")
|
|
734
|
+
|
|
735
|
+
if task.validation_criteria:
|
|
736
|
+
prompt_parts.append(f"\n## Validation Criteria\n{task.validation_criteria}")
|
|
737
|
+
|
|
738
|
+
prompt_parts.append(
|
|
739
|
+
"\n## Instructions\n"
|
|
740
|
+
"1. Implement the task as described\n"
|
|
741
|
+
"2. Write tests if applicable\n"
|
|
742
|
+
f"3. Commit your changes with the task ID in the message: [{task.id}]\n"
|
|
743
|
+
"4. Close the task when complete using close_task(commit_sha=...)"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
return "\n".join(prompt_parts)
|