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,736 @@
|
|
|
1
|
+
"""Task orchestration tools: review (spawn_review_agent, process_completed_agents)."""
|
|
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.storage.tasks import TaskNotFoundError
|
|
10
|
+
|
|
11
|
+
from .utils import get_current_project_id
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.agents.runner import AgentRunner
|
|
15
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
16
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_reviewer(
|
|
22
|
+
registry: InternalToolRegistry,
|
|
23
|
+
task_manager: LocalTaskManager,
|
|
24
|
+
worktree_storage: LocalWorktreeManager,
|
|
25
|
+
agent_runner: AgentRunner | None = None,
|
|
26
|
+
default_project_id: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Register review tools."""
|
|
29
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
30
|
+
|
|
31
|
+
async def spawn_review_agent(
|
|
32
|
+
task_id: str,
|
|
33
|
+
review_provider: Literal["claude", "gemini", "codex", "antigravity"] = "claude",
|
|
34
|
+
review_model: str | None = "claude-opus-4-5",
|
|
35
|
+
terminal: str = "auto",
|
|
36
|
+
mode: str = "terminal",
|
|
37
|
+
parent_session_id: str | None = None,
|
|
38
|
+
project_path: str | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Spawn a review agent for a completed task.
|
|
42
|
+
|
|
43
|
+
Used by the auto-orchestrator workflow's review step to validate
|
|
44
|
+
completed work before merging/cleanup.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
48
|
+
review_provider: LLM provider for review (default: claude)
|
|
49
|
+
review_model: Model for review (default: claude-opus-4-5 for thorough analysis)
|
|
50
|
+
terminal: Terminal for terminal mode (default: auto)
|
|
51
|
+
mode: Execution mode (terminal, embedded, headless)
|
|
52
|
+
parent_session_id: Parent session ID for context (required)
|
|
53
|
+
project_path: Path to project directory
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict with:
|
|
57
|
+
- success: bool
|
|
58
|
+
- agent_id: ID of spawned review agent
|
|
59
|
+
- session_id: Child session ID
|
|
60
|
+
- error: Optional error message
|
|
61
|
+
"""
|
|
62
|
+
# Validate mode and review_provider
|
|
63
|
+
allowed_modes = {"terminal", "embedded", "headless"}
|
|
64
|
+
allowed_providers = {"claude", "gemini", "codex", "antigravity"}
|
|
65
|
+
|
|
66
|
+
mode_lower = mode.lower() if mode else "terminal"
|
|
67
|
+
if mode_lower not in allowed_modes:
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": f"Invalid mode '{mode}'. Must be one of: {sorted(allowed_modes)}",
|
|
71
|
+
}
|
|
72
|
+
mode = mode_lower # Use normalized value
|
|
73
|
+
|
|
74
|
+
if review_provider not in allowed_providers:
|
|
75
|
+
return {
|
|
76
|
+
"success": False,
|
|
77
|
+
"error": f"Invalid review_provider '{review_provider}'. Must be one of: {sorted(allowed_providers)}",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Resolve task_id reference
|
|
81
|
+
try:
|
|
82
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
83
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
84
|
+
return {
|
|
85
|
+
"success": False,
|
|
86
|
+
"error": f"Invalid task_id: {e}",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if agent_runner is None:
|
|
90
|
+
return {
|
|
91
|
+
"success": False,
|
|
92
|
+
"error": "Agent runner not configured. Cannot spawn review agent.",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if parent_session_id is None:
|
|
96
|
+
return {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": "parent_session_id is required for spawning review agent",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Resolve project ID
|
|
102
|
+
resolved_project_id = default_project_id
|
|
103
|
+
if project_path:
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
from gobby.utils.project_context import get_project_context
|
|
107
|
+
|
|
108
|
+
ctx = get_project_context(Path(project_path))
|
|
109
|
+
if ctx:
|
|
110
|
+
resolved_project_id = ctx.get("id")
|
|
111
|
+
|
|
112
|
+
if not resolved_project_id:
|
|
113
|
+
resolved_project_id = get_current_project_id()
|
|
114
|
+
|
|
115
|
+
if not resolved_project_id:
|
|
116
|
+
return {
|
|
117
|
+
"success": False,
|
|
118
|
+
"error": "Could not resolve project ID",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Get the task
|
|
122
|
+
try:
|
|
123
|
+
task = task_manager.get_task(resolved_task_id)
|
|
124
|
+
except ValueError as e:
|
|
125
|
+
return {
|
|
126
|
+
"success": False,
|
|
127
|
+
"error": f"Task {task_id} not found: {e}",
|
|
128
|
+
}
|
|
129
|
+
if not task:
|
|
130
|
+
return {
|
|
131
|
+
"success": False,
|
|
132
|
+
"error": f"Task {task_id} not found",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Get worktree for the task
|
|
136
|
+
worktree = worktree_storage.get_by_task(resolved_task_id)
|
|
137
|
+
if not worktree:
|
|
138
|
+
return {
|
|
139
|
+
"success": False,
|
|
140
|
+
"error": f"No worktree found for task {resolved_task_id}",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Build review prompt
|
|
144
|
+
review_prompt = _build_review_prompt(task, worktree)
|
|
145
|
+
|
|
146
|
+
# Check spawn depth
|
|
147
|
+
can_spawn, reason, _depth = agent_runner.can_spawn(parent_session_id)
|
|
148
|
+
if not can_spawn:
|
|
149
|
+
return {
|
|
150
|
+
"success": False,
|
|
151
|
+
"error": reason,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Prepare agent run
|
|
155
|
+
from gobby.agents.runner import AgentConfig
|
|
156
|
+
from gobby.llm.executor import AgentResult
|
|
157
|
+
from gobby.utils.machine_id import get_machine_id
|
|
158
|
+
|
|
159
|
+
machine_id = get_machine_id()
|
|
160
|
+
|
|
161
|
+
config = AgentConfig(
|
|
162
|
+
prompt=review_prompt,
|
|
163
|
+
parent_session_id=parent_session_id,
|
|
164
|
+
project_id=resolved_project_id,
|
|
165
|
+
machine_id=machine_id,
|
|
166
|
+
source=review_provider,
|
|
167
|
+
workflow=None, # Review doesn't need a workflow
|
|
168
|
+
task=resolved_task_id,
|
|
169
|
+
session_context="summary_markdown",
|
|
170
|
+
mode=mode,
|
|
171
|
+
terminal=terminal,
|
|
172
|
+
worktree_id=worktree.id,
|
|
173
|
+
provider=review_provider,
|
|
174
|
+
model=review_model,
|
|
175
|
+
max_turns=20, # Reviews should be shorter
|
|
176
|
+
timeout=300.0, # 5 minutes
|
|
177
|
+
project_path=worktree.worktree_path,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
prepare_result = agent_runner.prepare_run(config)
|
|
181
|
+
if isinstance(prepare_result, AgentResult):
|
|
182
|
+
return {
|
|
183
|
+
"success": False,
|
|
184
|
+
"error": prepare_result.error or "Failed to prepare review agent run",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
context = prepare_result
|
|
188
|
+
if context.session is None or context.run is None:
|
|
189
|
+
return {
|
|
190
|
+
"success": False,
|
|
191
|
+
"error": "Internal error: context missing session or run",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
child_session = context.session
|
|
195
|
+
agent_run = context.run
|
|
196
|
+
|
|
197
|
+
# Spawn the review agent
|
|
198
|
+
if mode == "terminal":
|
|
199
|
+
from gobby.agents.spawn import TerminalSpawner
|
|
200
|
+
|
|
201
|
+
spawner = TerminalSpawner()
|
|
202
|
+
spawn_result = spawner.spawn_agent(
|
|
203
|
+
cli=review_provider,
|
|
204
|
+
cwd=worktree.worktree_path,
|
|
205
|
+
session_id=child_session.id,
|
|
206
|
+
parent_session_id=parent_session_id,
|
|
207
|
+
agent_run_id=agent_run.id,
|
|
208
|
+
project_id=resolved_project_id,
|
|
209
|
+
workflow_name=None,
|
|
210
|
+
agent_depth=child_session.agent_depth,
|
|
211
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
212
|
+
terminal=terminal,
|
|
213
|
+
prompt=review_prompt,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not spawn_result.success:
|
|
217
|
+
return {
|
|
218
|
+
"success": False,
|
|
219
|
+
"error": spawn_result.error or "Terminal spawn failed",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"success": True,
|
|
224
|
+
"task_id": resolved_task_id,
|
|
225
|
+
"agent_id": agent_run.id,
|
|
226
|
+
"session_id": child_session.id,
|
|
227
|
+
"worktree_id": worktree.id,
|
|
228
|
+
"terminal_type": spawn_result.terminal_type,
|
|
229
|
+
"pid": spawn_result.pid,
|
|
230
|
+
"provider": review_provider,
|
|
231
|
+
"model": review_model,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
elif mode == "embedded":
|
|
235
|
+
from gobby.agents.spawn import EmbeddedSpawner
|
|
236
|
+
|
|
237
|
+
embedded_spawner = EmbeddedSpawner()
|
|
238
|
+
embedded_result = embedded_spawner.spawn_agent(
|
|
239
|
+
cli=review_provider,
|
|
240
|
+
cwd=worktree.worktree_path,
|
|
241
|
+
session_id=child_session.id,
|
|
242
|
+
parent_session_id=parent_session_id,
|
|
243
|
+
agent_run_id=agent_run.id,
|
|
244
|
+
project_id=resolved_project_id,
|
|
245
|
+
workflow_name=None,
|
|
246
|
+
agent_depth=child_session.agent_depth,
|
|
247
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
248
|
+
prompt=review_prompt,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not embedded_result.success:
|
|
252
|
+
return {
|
|
253
|
+
"success": False,
|
|
254
|
+
"error": embedded_result.error or "Embedded spawn failed",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"success": True,
|
|
259
|
+
"task_id": resolved_task_id,
|
|
260
|
+
"agent_id": agent_run.id,
|
|
261
|
+
"session_id": child_session.id,
|
|
262
|
+
"worktree_id": worktree.id,
|
|
263
|
+
"provider": review_provider,
|
|
264
|
+
"model": review_model,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
else: # headless
|
|
268
|
+
from gobby.agents.spawn import HeadlessSpawner
|
|
269
|
+
|
|
270
|
+
headless_spawner = HeadlessSpawner()
|
|
271
|
+
headless_result = headless_spawner.spawn_agent(
|
|
272
|
+
cli=review_provider,
|
|
273
|
+
cwd=worktree.worktree_path,
|
|
274
|
+
session_id=child_session.id,
|
|
275
|
+
parent_session_id=parent_session_id,
|
|
276
|
+
agent_run_id=agent_run.id,
|
|
277
|
+
project_id=resolved_project_id,
|
|
278
|
+
workflow_name=None,
|
|
279
|
+
agent_depth=child_session.agent_depth,
|
|
280
|
+
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
281
|
+
prompt=review_prompt,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not headless_result.success:
|
|
285
|
+
return {
|
|
286
|
+
"success": False,
|
|
287
|
+
"error": headless_result.error or "Headless spawn failed",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"success": True,
|
|
292
|
+
"task_id": resolved_task_id,
|
|
293
|
+
"agent_id": agent_run.id,
|
|
294
|
+
"session_id": child_session.id,
|
|
295
|
+
"worktree_id": worktree.id,
|
|
296
|
+
"pid": headless_result.pid,
|
|
297
|
+
"provider": review_provider,
|
|
298
|
+
"model": review_model,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
registry.register(
|
|
302
|
+
name="spawn_review_agent",
|
|
303
|
+
description=(
|
|
304
|
+
"Spawn a review agent for a completed task. "
|
|
305
|
+
"Used by auto-orchestrator workflow for code review. "
|
|
306
|
+
"Uses review_provider/review_model for thorough analysis."
|
|
307
|
+
),
|
|
308
|
+
input_schema={
|
|
309
|
+
"type": "object",
|
|
310
|
+
"properties": {
|
|
311
|
+
"task_id": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"description": "Task reference: #N, N (seq_num), path (1.2.3), or UUID",
|
|
314
|
+
},
|
|
315
|
+
"review_provider": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "LLM provider for review (claude, gemini, codex, antigravity)",
|
|
318
|
+
"default": "claude",
|
|
319
|
+
},
|
|
320
|
+
"review_model": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "Model for review (default: claude-opus-4-5 for thorough analysis)",
|
|
323
|
+
"default": "claude-opus-4-5",
|
|
324
|
+
},
|
|
325
|
+
"terminal": {
|
|
326
|
+
"type": "string",
|
|
327
|
+
"description": "Terminal for terminal mode (auto, ghostty, iterm2, etc.)",
|
|
328
|
+
"default": "auto",
|
|
329
|
+
},
|
|
330
|
+
"mode": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"description": "Execution mode (terminal, embedded, headless)",
|
|
333
|
+
"default": "terminal",
|
|
334
|
+
},
|
|
335
|
+
"parent_session_id": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"description": "Parent session ID for context (required)",
|
|
338
|
+
},
|
|
339
|
+
"project_path": {
|
|
340
|
+
"type": ["string", "null"],
|
|
341
|
+
"description": "Path to project directory",
|
|
342
|
+
"default": None,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
"required": ["task_id", "parent_session_id"],
|
|
346
|
+
},
|
|
347
|
+
func=spawn_review_agent,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def process_completed_agents(
|
|
351
|
+
parent_session_id: str,
|
|
352
|
+
spawn_reviews: bool = True,
|
|
353
|
+
review_provider: Literal["claude", "gemini", "codex", "antigravity"] | None = None,
|
|
354
|
+
review_model: str | None = None,
|
|
355
|
+
terminal: str = "auto",
|
|
356
|
+
mode: str = "terminal",
|
|
357
|
+
project_path: str | None = None,
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Process completed agents and route them to review or cleanup.
|
|
361
|
+
|
|
362
|
+
Takes agents from completed_agents list and either:
|
|
363
|
+
- Spawns review agents for validation (if spawn_reviews=True)
|
|
364
|
+
- Moves directly to reviewed_agents list (if already validated)
|
|
365
|
+
|
|
366
|
+
For failed agents, optionally retries or escalates.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
parent_session_id: Parent session ID (orchestrator session)
|
|
370
|
+
spawn_reviews: Whether to spawn review agents for completed tasks
|
|
371
|
+
review_provider: LLM provider for reviews (uses workflow variable if not set)
|
|
372
|
+
review_model: Model for reviews (uses workflow variable if not set)
|
|
373
|
+
terminal: Terminal for terminal mode
|
|
374
|
+
mode: Execution mode for review agents
|
|
375
|
+
project_path: Path to project directory
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Dict with:
|
|
379
|
+
- reviews_spawned: List of review agents spawned
|
|
380
|
+
- ready_for_cleanup: List of agents ready for worktree cleanup
|
|
381
|
+
- retries_scheduled: List of failed agents scheduled for retry
|
|
382
|
+
- escalated: List of agents escalated for manual intervention
|
|
383
|
+
"""
|
|
384
|
+
if agent_runner is None:
|
|
385
|
+
return {
|
|
386
|
+
"success": False,
|
|
387
|
+
"error": "Agent runner not configured",
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Get workflow state
|
|
391
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
392
|
+
|
|
393
|
+
state_manager = WorkflowStateManager(task_manager.db)
|
|
394
|
+
state = state_manager.get_state(parent_session_id)
|
|
395
|
+
if not state:
|
|
396
|
+
return {
|
|
397
|
+
"success": True,
|
|
398
|
+
"reviews_spawned": [],
|
|
399
|
+
"ready_for_cleanup": [],
|
|
400
|
+
"retries_scheduled": [],
|
|
401
|
+
"escalated": [],
|
|
402
|
+
"message": "No workflow state found",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
workflow_vars = state.variables
|
|
406
|
+
|
|
407
|
+
# Defensive type coercion - ensure lists of dicts, handle None/wrong types
|
|
408
|
+
def _safe_list_of_dicts(val: Any) -> list[dict[str, Any]]:
|
|
409
|
+
"""Coerce value to list of dicts, filtering out non-dict entries."""
|
|
410
|
+
if not val:
|
|
411
|
+
return []
|
|
412
|
+
if not isinstance(val, list):
|
|
413
|
+
return []
|
|
414
|
+
return [x for x in val if isinstance(x, dict)]
|
|
415
|
+
|
|
416
|
+
completed_agents = _safe_list_of_dicts(workflow_vars.get("completed_agents"))
|
|
417
|
+
failed_agents = _safe_list_of_dicts(workflow_vars.get("failed_agents"))
|
|
418
|
+
# Create a fresh list for newly reviewed agents to avoid aliasing the stored list
|
|
419
|
+
newly_reviewed: list[dict[str, Any]] = []
|
|
420
|
+
# Shallow copy to avoid aliasing
|
|
421
|
+
review_agents_spawned = list(
|
|
422
|
+
_safe_list_of_dicts(workflow_vars.get("review_agents_spawned"))
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Resolve review provider from workflow vars or parameters
|
|
426
|
+
effective_review_provider = (
|
|
427
|
+
review_provider or workflow_vars.get("review_provider") or "claude"
|
|
428
|
+
)
|
|
429
|
+
effective_review_model = (
|
|
430
|
+
review_model or workflow_vars.get("review_model") or "claude-opus-4-5"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
reviews_spawned: list[dict[str, Any]] = []
|
|
434
|
+
ready_for_cleanup: list[dict[str, Any]] = []
|
|
435
|
+
retries_scheduled: list[dict[str, Any]] = []
|
|
436
|
+
escalated: list[dict[str, Any]] = []
|
|
437
|
+
|
|
438
|
+
# Process completed agents
|
|
439
|
+
still_pending_review: list[dict[str, Any]] = []
|
|
440
|
+
|
|
441
|
+
for agent_info in completed_agents:
|
|
442
|
+
task_id = agent_info.get("task_id")
|
|
443
|
+
if not task_id:
|
|
444
|
+
# Invalid agent info
|
|
445
|
+
escalated.append(
|
|
446
|
+
{
|
|
447
|
+
**agent_info,
|
|
448
|
+
"escalation_reason": "Missing task_id",
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Check task validation status
|
|
454
|
+
try:
|
|
455
|
+
task = task_manager.get_task(task_id)
|
|
456
|
+
except ValueError as e:
|
|
457
|
+
escalated.append(
|
|
458
|
+
{
|
|
459
|
+
**agent_info,
|
|
460
|
+
"escalation_reason": f"Task lookup failed: {e}",
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
continue
|
|
464
|
+
if not task:
|
|
465
|
+
escalated.append(
|
|
466
|
+
{
|
|
467
|
+
**agent_info,
|
|
468
|
+
"escalation_reason": "Task not found",
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
# Check if task is already validated (passed validation)
|
|
474
|
+
if task.validation_status == "valid":
|
|
475
|
+
# Ready for cleanup
|
|
476
|
+
ready_for_cleanup.append(
|
|
477
|
+
{
|
|
478
|
+
**agent_info,
|
|
479
|
+
"validation_status": "valid",
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
newly_reviewed.append(agent_info)
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# Check if task validation failed - may need retry
|
|
486
|
+
if task.validation_status == "invalid":
|
|
487
|
+
# Check failure count
|
|
488
|
+
fail_count = task.validation_fail_count or 0
|
|
489
|
+
max_retries = 3
|
|
490
|
+
|
|
491
|
+
if fail_count >= max_retries:
|
|
492
|
+
# Escalate - too many failures
|
|
493
|
+
escalated.append(
|
|
494
|
+
{
|
|
495
|
+
**agent_info,
|
|
496
|
+
"escalation_reason": f"Validation failed {fail_count} times",
|
|
497
|
+
"validation_feedback": task.validation_feedback,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
# Retry - reopen task and add back to queue
|
|
502
|
+
try:
|
|
503
|
+
task_manager.reopen_task(task_id, reason="Validation failed, retrying")
|
|
504
|
+
retries_scheduled.append(
|
|
505
|
+
{
|
|
506
|
+
**agent_info,
|
|
507
|
+
"retry_count": fail_count + 1,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
escalated.append(
|
|
512
|
+
{
|
|
513
|
+
**agent_info,
|
|
514
|
+
"escalation_reason": f"Failed to reopen task: {e}",
|
|
515
|
+
}
|
|
516
|
+
)
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
# Task needs review - spawn review agent if enabled
|
|
520
|
+
if spawn_reviews:
|
|
521
|
+
# Check if review agent already spawned for this task
|
|
522
|
+
already_spawned = any(ra.get("task_id") == task_id for ra in review_agents_spawned)
|
|
523
|
+
if already_spawned:
|
|
524
|
+
# Keep in pending review list
|
|
525
|
+
still_pending_review.append(agent_info)
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
# Spawn review agent
|
|
529
|
+
review_result = await spawn_review_agent(
|
|
530
|
+
task_id=task_id,
|
|
531
|
+
review_provider=effective_review_provider,
|
|
532
|
+
review_model=effective_review_model,
|
|
533
|
+
terminal=terminal,
|
|
534
|
+
mode=mode,
|
|
535
|
+
parent_session_id=parent_session_id,
|
|
536
|
+
project_path=project_path,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if review_result.get("success"):
|
|
540
|
+
reviews_spawned.append(
|
|
541
|
+
{
|
|
542
|
+
"task_id": task_id,
|
|
543
|
+
"agent_id": review_result.get("agent_id"),
|
|
544
|
+
"session_id": review_result.get("session_id"),
|
|
545
|
+
"worktree_id": review_result.get("worktree_id"),
|
|
546
|
+
}
|
|
547
|
+
)
|
|
548
|
+
review_agents_spawned.append(
|
|
549
|
+
{
|
|
550
|
+
"task_id": task_id,
|
|
551
|
+
"agent_id": review_result.get("agent_id"),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
# Keep agent in completed list until review completes
|
|
555
|
+
still_pending_review.append(agent_info)
|
|
556
|
+
else:
|
|
557
|
+
# Review spawn failed - escalate
|
|
558
|
+
escalated.append(
|
|
559
|
+
{
|
|
560
|
+
**agent_info,
|
|
561
|
+
"escalation_reason": f"Review spawn failed: {review_result.get('error')}",
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
# Not spawning reviews - move to ready_for_cleanup
|
|
566
|
+
ready_for_cleanup.append(
|
|
567
|
+
{
|
|
568
|
+
**agent_info,
|
|
569
|
+
"skipped_review": True,
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
newly_reviewed.append(agent_info)
|
|
573
|
+
|
|
574
|
+
# Process failed agents
|
|
575
|
+
still_failed: list[dict[str, Any]] = []
|
|
576
|
+
|
|
577
|
+
for agent_info in failed_agents:
|
|
578
|
+
task_id = agent_info.get("task_id")
|
|
579
|
+
failure_reason = agent_info.get("failure_reason") or "Unknown"
|
|
580
|
+
|
|
581
|
+
# Check if this is a retriable failure
|
|
582
|
+
if "crashed" in failure_reason.lower() or "exited" in failure_reason.lower():
|
|
583
|
+
# Potentially retriable - reopen task
|
|
584
|
+
if task_id:
|
|
585
|
+
retry_task: Any = None
|
|
586
|
+
try:
|
|
587
|
+
retry_task = task_manager.get_task(task_id)
|
|
588
|
+
except ValueError:
|
|
589
|
+
# Task was deleted concurrently - skip
|
|
590
|
+
pass
|
|
591
|
+
if retry_task and retry_task.status == "in_progress":
|
|
592
|
+
# Reopen for retry
|
|
593
|
+
try:
|
|
594
|
+
task_manager.update_task(task_id, status="open")
|
|
595
|
+
retries_scheduled.append(
|
|
596
|
+
{
|
|
597
|
+
**agent_info,
|
|
598
|
+
"retry_reason": "Agent crashed, reopened task",
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
continue
|
|
602
|
+
except Exception as e:
|
|
603
|
+
# Task update failed - keep in still_failed for next cycle
|
|
604
|
+
still_failed.append(
|
|
605
|
+
{
|
|
606
|
+
**agent_info,
|
|
607
|
+
"pending_retry": True,
|
|
608
|
+
"retry_error": str(e),
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
# Non-retriable - escalate
|
|
614
|
+
escalated.append(
|
|
615
|
+
{
|
|
616
|
+
**agent_info,
|
|
617
|
+
"escalation_reason": failure_reason,
|
|
618
|
+
}
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Update workflow state
|
|
622
|
+
try:
|
|
623
|
+
state = state_manager.get_state(parent_session_id)
|
|
624
|
+
if state:
|
|
625
|
+
# Update completed_agents to only include pending review
|
|
626
|
+
state.variables["completed_agents"] = still_pending_review
|
|
627
|
+
# Update reviewed_agents - copy existing to avoid aliasing, then extend
|
|
628
|
+
existing_reviewed = list(state.variables.get("reviewed_agents", []))
|
|
629
|
+
existing_reviewed.extend(newly_reviewed)
|
|
630
|
+
state.variables["reviewed_agents"] = existing_reviewed
|
|
631
|
+
# Update review_agents_spawned
|
|
632
|
+
state.variables["review_agents_spawned"] = review_agents_spawned
|
|
633
|
+
# Update failed_agents
|
|
634
|
+
state.variables["failed_agents"] = still_failed
|
|
635
|
+
# Track escalated agents
|
|
636
|
+
existing_escalated = list(state.variables.get("escalated_agents", []))
|
|
637
|
+
existing_escalated.extend(escalated)
|
|
638
|
+
state.variables["escalated_agents"] = existing_escalated
|
|
639
|
+
|
|
640
|
+
state_manager.save_state(state)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
logger.warning(f"Failed to update workflow state during processing: {e}")
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
"success": True,
|
|
646
|
+
"reviews_spawned": reviews_spawned,
|
|
647
|
+
"ready_for_cleanup": ready_for_cleanup,
|
|
648
|
+
"retries_scheduled": retries_scheduled,
|
|
649
|
+
"escalated": escalated,
|
|
650
|
+
"summary": {
|
|
651
|
+
"reviews_spawned": len(reviews_spawned),
|
|
652
|
+
"ready_for_cleanup": len(ready_for_cleanup),
|
|
653
|
+
"retries_scheduled": len(retries_scheduled),
|
|
654
|
+
"escalated": len(escalated),
|
|
655
|
+
"pending_review": len(still_pending_review),
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
registry.register(
|
|
660
|
+
name="process_completed_agents",
|
|
661
|
+
description=(
|
|
662
|
+
"Process completed agents and route to review or cleanup. "
|
|
663
|
+
"Spawns review agents for validation, handles retries for failures, "
|
|
664
|
+
"escalates unrecoverable errors. Used by auto-orchestrator review step."
|
|
665
|
+
),
|
|
666
|
+
input_schema={
|
|
667
|
+
"type": "object",
|
|
668
|
+
"properties": {
|
|
669
|
+
"parent_session_id": {
|
|
670
|
+
"type": "string",
|
|
671
|
+
"description": "Parent session ID (orchestrator session)",
|
|
672
|
+
},
|
|
673
|
+
"spawn_reviews": {
|
|
674
|
+
"type": "boolean",
|
|
675
|
+
"description": "Whether to spawn review agents for completed tasks",
|
|
676
|
+
"default": True,
|
|
677
|
+
},
|
|
678
|
+
"review_provider": {
|
|
679
|
+
"type": ["string", "null"],
|
|
680
|
+
"description": "LLM provider for reviews (uses workflow variable if not set)",
|
|
681
|
+
"default": None,
|
|
682
|
+
},
|
|
683
|
+
"review_model": {
|
|
684
|
+
"type": ["string", "null"],
|
|
685
|
+
"description": "Model for reviews (uses workflow variable if not set)",
|
|
686
|
+
"default": None,
|
|
687
|
+
},
|
|
688
|
+
"terminal": {
|
|
689
|
+
"type": "string",
|
|
690
|
+
"description": "Terminal for terminal mode",
|
|
691
|
+
"default": "auto",
|
|
692
|
+
},
|
|
693
|
+
"mode": {
|
|
694
|
+
"type": "string",
|
|
695
|
+
"description": "Execution mode for review agents",
|
|
696
|
+
"default": "terminal",
|
|
697
|
+
},
|
|
698
|
+
"project_path": {
|
|
699
|
+
"type": ["string", "null"],
|
|
700
|
+
"description": "Path to project directory",
|
|
701
|
+
"default": None,
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
"required": ["parent_session_id"],
|
|
705
|
+
},
|
|
706
|
+
func=process_completed_agents,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _build_review_prompt(task: Any, worktree: Any) -> str:
|
|
711
|
+
"""Build a review prompt for a completed task."""
|
|
712
|
+
prompt_parts = [
|
|
713
|
+
"# Code Review Request",
|
|
714
|
+
f"\n## Task: {task.title}",
|
|
715
|
+
f"Task ID: {task.id}",
|
|
716
|
+
f"Branch: {worktree.branch_name}",
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
if task.description:
|
|
720
|
+
prompt_parts.append(f"\n## Task Description\n{task.description}")
|
|
721
|
+
|
|
722
|
+
if task.validation_criteria:
|
|
723
|
+
prompt_parts.append(f"\n## Validation Criteria\n{task.validation_criteria}")
|
|
724
|
+
|
|
725
|
+
prompt_parts.append(
|
|
726
|
+
"\n## Review Instructions\n"
|
|
727
|
+
"1. Review the code changes on this branch\n"
|
|
728
|
+
"2. Check that the implementation matches the task description\n"
|
|
729
|
+
"3. Verify tests exist and pass (if applicable)\n"
|
|
730
|
+
"4. Check for code quality, security issues, and best practices\n"
|
|
731
|
+
"5. Use validate_task() to mark as valid/invalid with feedback\n"
|
|
732
|
+
"6. If valid, the task can proceed to merge\n"
|
|
733
|
+
"7. If invalid, provide clear feedback for the implementer"
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
return "\n".join(prompt_parts)
|