spec-kitty-cli 0.12.1__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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
"""Integration module for connecting orchestration components.
|
|
2
|
+
|
|
3
|
+
This module integrates all orchestrator components into a working system:
|
|
4
|
+
- Main orchestration loop (T043)
|
|
5
|
+
- Progress display with Rich Live (T044)
|
|
6
|
+
- Summary report on completion (T045)
|
|
7
|
+
- Edge case handling (T046)
|
|
8
|
+
|
|
9
|
+
Implemented in WP09.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import signal
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
20
|
+
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.live import Live
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
|
|
27
|
+
from specify_cli.orchestrator.agents import detect_installed_agents, get_invoker, InvocationResult
|
|
28
|
+
from specify_cli.orchestrator.config import (
|
|
29
|
+
OrchestrationStatus,
|
|
30
|
+
OrchestratorConfig,
|
|
31
|
+
WPStatus,
|
|
32
|
+
)
|
|
33
|
+
from specify_cli.orchestrator.executor import (
|
|
34
|
+
ExecutionContext,
|
|
35
|
+
WorktreeCreationError,
|
|
36
|
+
create_worktree,
|
|
37
|
+
execute_with_logging,
|
|
38
|
+
get_log_path,
|
|
39
|
+
get_worktree_path,
|
|
40
|
+
worktree_exists,
|
|
41
|
+
)
|
|
42
|
+
from specify_cli.orchestrator.monitor import (
|
|
43
|
+
apply_fallback,
|
|
44
|
+
escalate_to_human,
|
|
45
|
+
execute_with_retry,
|
|
46
|
+
is_success,
|
|
47
|
+
transition_wp_lane,
|
|
48
|
+
update_wp_progress,
|
|
49
|
+
)
|
|
50
|
+
from specify_cli.orchestrator.scheduler import (
|
|
51
|
+
ConcurrencyManager,
|
|
52
|
+
DependencyGraphError,
|
|
53
|
+
SchedulerState,
|
|
54
|
+
build_wp_graph,
|
|
55
|
+
get_ready_wps,
|
|
56
|
+
is_single_agent_mode,
|
|
57
|
+
select_agent,
|
|
58
|
+
select_agent_from_user_config,
|
|
59
|
+
select_review_agent,
|
|
60
|
+
select_review_agent_from_user_config,
|
|
61
|
+
single_agent_review_delay,
|
|
62
|
+
validate_wp_graph,
|
|
63
|
+
)
|
|
64
|
+
from specify_cli.orchestrator.state import (
|
|
65
|
+
OrchestrationRun,
|
|
66
|
+
WPExecution,
|
|
67
|
+
save_state,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
logger = logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Exceptions (T046)
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OrchestrationError(Exception):
|
|
82
|
+
"""Base exception for orchestration errors."""
|
|
83
|
+
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CircularDependencyError(OrchestrationError):
|
|
88
|
+
"""Raised when circular dependencies detected in WP graph."""
|
|
89
|
+
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class NoAgentsError(OrchestrationError):
|
|
94
|
+
"""Raised when no agents are available for orchestration."""
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ValidationError(OrchestrationError):
|
|
100
|
+
"""Raised when pre-flight validation fails."""
|
|
101
|
+
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# Validation (T046)
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_feature(feature_dir: Path) -> dict[str, list[str]]:
|
|
111
|
+
"""Validate feature directory and build dependency graph.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
feature_dir: Path to feature directory.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Validated dependency graph.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValidationError: If feature is invalid.
|
|
121
|
+
CircularDependencyError: If circular dependencies detected.
|
|
122
|
+
"""
|
|
123
|
+
if not feature_dir.exists():
|
|
124
|
+
raise ValidationError(f"Feature directory not found: {feature_dir}")
|
|
125
|
+
|
|
126
|
+
tasks_dir = feature_dir / "tasks"
|
|
127
|
+
if not tasks_dir.exists():
|
|
128
|
+
raise ValidationError(
|
|
129
|
+
f"No tasks directory found. Run /spec-kitty.tasks first."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Build and validate graph
|
|
133
|
+
try:
|
|
134
|
+
graph = build_wp_graph(feature_dir)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise ValidationError(f"Failed to build dependency graph: {e}")
|
|
137
|
+
|
|
138
|
+
if not graph:
|
|
139
|
+
raise ValidationError("No work packages found in tasks directory.")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
validate_wp_graph(graph)
|
|
143
|
+
except DependencyGraphError as e:
|
|
144
|
+
if "Circular" in str(e):
|
|
145
|
+
raise CircularDependencyError(str(e))
|
|
146
|
+
raise ValidationError(str(e))
|
|
147
|
+
|
|
148
|
+
return graph
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def validate_agents(config: OrchestratorConfig) -> list[str]:
|
|
152
|
+
"""Validate that required agents are available.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
config: Orchestrator configuration.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of available agent IDs.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
NoAgentsError: If no agents are available.
|
|
162
|
+
"""
|
|
163
|
+
installed = detect_installed_agents()
|
|
164
|
+
enabled = [
|
|
165
|
+
aid for aid, ac in config.agents.items()
|
|
166
|
+
if ac.enabled and aid in installed
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if not enabled:
|
|
170
|
+
raise NoAgentsError(
|
|
171
|
+
"No agents available for orchestration.\n\n"
|
|
172
|
+
"Install at least one agent:\n"
|
|
173
|
+
" npm install -g @anthropic-ai/claude-code\n"
|
|
174
|
+
" npm install -g codex\n"
|
|
175
|
+
" npm install -g opencode\n\n"
|
|
176
|
+
"Or enable an installed agent in .kittify/agents.yaml"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return enabled
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# =============================================================================
|
|
183
|
+
# Progress Display (T044)
|
|
184
|
+
# =============================================================================
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def create_status_table(state: OrchestrationRun) -> Table:
|
|
188
|
+
"""Create status table for live display.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
state: Current orchestration state.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Rich Table with WP statuses.
|
|
195
|
+
"""
|
|
196
|
+
table = Table(
|
|
197
|
+
title=f"[bold]Orchestration: {state.feature_slug}[/bold]",
|
|
198
|
+
show_header=True,
|
|
199
|
+
header_style="bold cyan",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
table.add_column("WP", style="cyan", width=8)
|
|
203
|
+
table.add_column("Status", width=14)
|
|
204
|
+
table.add_column("Agent", style="green", width=15)
|
|
205
|
+
table.add_column("Time", style="yellow", width=10)
|
|
206
|
+
|
|
207
|
+
# Sort WPs by ID for consistent display
|
|
208
|
+
sorted_wps = sorted(state.work_packages.items(), key=lambda x: x[0])
|
|
209
|
+
|
|
210
|
+
for wp_id, wp in sorted_wps:
|
|
211
|
+
# Status with color
|
|
212
|
+
status_styles = {
|
|
213
|
+
WPStatus.PENDING: "[dim]pending[/dim]",
|
|
214
|
+
WPStatus.READY: "[yellow]ready[/yellow]",
|
|
215
|
+
WPStatus.IMPLEMENTATION: "[blue]implementing[/blue]",
|
|
216
|
+
WPStatus.REVIEW: "[magenta]reviewing[/magenta]",
|
|
217
|
+
WPStatus.REWORK: "[yellow]rework[/yellow]",
|
|
218
|
+
WPStatus.COMPLETED: "[green]done[/green]",
|
|
219
|
+
WPStatus.FAILED: "[red]failed[/red]",
|
|
220
|
+
}
|
|
221
|
+
status = status_styles.get(wp.status, wp.status.value)
|
|
222
|
+
|
|
223
|
+
# Agent info
|
|
224
|
+
if wp.status == WPStatus.IMPLEMENTATION:
|
|
225
|
+
agent = wp.implementation_agent or "-"
|
|
226
|
+
started = wp.implementation_started
|
|
227
|
+
elif wp.status == WPStatus.REVIEW:
|
|
228
|
+
agent = wp.review_agent or "-"
|
|
229
|
+
started = wp.review_started
|
|
230
|
+
else:
|
|
231
|
+
agent = "-"
|
|
232
|
+
started = None
|
|
233
|
+
|
|
234
|
+
# Elapsed time
|
|
235
|
+
if started:
|
|
236
|
+
elapsed = (datetime.now(timezone.utc) - started).total_seconds()
|
|
237
|
+
if elapsed < 60:
|
|
238
|
+
time_str = f"{int(elapsed)}s"
|
|
239
|
+
elif elapsed < 3600:
|
|
240
|
+
time_str = f"{int(elapsed // 60)}m {int(elapsed % 60)}s"
|
|
241
|
+
else:
|
|
242
|
+
time_str = f"{int(elapsed // 3600)}h {int((elapsed % 3600) // 60)}m"
|
|
243
|
+
else:
|
|
244
|
+
time_str = "-"
|
|
245
|
+
|
|
246
|
+
table.add_row(wp_id, status, agent, time_str)
|
|
247
|
+
|
|
248
|
+
return table
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def create_progress_panel(state: OrchestrationRun) -> Panel:
|
|
252
|
+
"""Create progress panel with overall status.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
state: Current orchestration state.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Rich Panel with progress info.
|
|
259
|
+
"""
|
|
260
|
+
total = state.wps_total
|
|
261
|
+
completed = state.wps_completed
|
|
262
|
+
failed = state.wps_failed
|
|
263
|
+
in_progress = sum(
|
|
264
|
+
1 for wp in state.work_packages.values()
|
|
265
|
+
if wp.status in [WPStatus.IMPLEMENTATION, WPStatus.REVIEW]
|
|
266
|
+
)
|
|
267
|
+
pending = total - completed - failed - in_progress
|
|
268
|
+
|
|
269
|
+
# Progress bar
|
|
270
|
+
pct = (completed / total * 100) if total > 0 else 0
|
|
271
|
+
filled = int(pct / 5) # 20 chars
|
|
272
|
+
bar = "[green]" + "█" * filled + "[/green]" + "░" * (20 - filled)
|
|
273
|
+
|
|
274
|
+
# Elapsed time
|
|
275
|
+
elapsed = (datetime.now(timezone.utc) - state.started_at).total_seconds()
|
|
276
|
+
if elapsed < 60:
|
|
277
|
+
elapsed_str = f"{int(elapsed)}s"
|
|
278
|
+
elif elapsed < 3600:
|
|
279
|
+
elapsed_str = f"{int(elapsed // 60)}m {int(elapsed % 60)}s"
|
|
280
|
+
else:
|
|
281
|
+
elapsed_str = f"{int(elapsed // 3600)}h {int((elapsed % 3600) // 60)}m"
|
|
282
|
+
|
|
283
|
+
# Status color
|
|
284
|
+
status_color = {
|
|
285
|
+
OrchestrationStatus.RUNNING: "green",
|
|
286
|
+
OrchestrationStatus.PAUSED: "yellow",
|
|
287
|
+
OrchestrationStatus.COMPLETED: "bright_green",
|
|
288
|
+
OrchestrationStatus.FAILED: "red",
|
|
289
|
+
}.get(state.status, "white")
|
|
290
|
+
|
|
291
|
+
content = (
|
|
292
|
+
f"[bold]Status:[/bold] [{status_color}]{state.status.value}[/{status_color}]\n"
|
|
293
|
+
f"[bold]Progress:[/bold] {bar} {completed}/{total} ({pct:.0f}%)\n"
|
|
294
|
+
f"[bold]Elapsed:[/bold] {elapsed_str}\n"
|
|
295
|
+
f"[bold]Active:[/bold] {in_progress} [bold]Pending:[/bold] {pending} "
|
|
296
|
+
f"[bold]Failed:[/bold] {failed}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return Panel(content, border_style="blue")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def create_live_display(state: OrchestrationRun) -> Table:
|
|
303
|
+
"""Create combined live display.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
state: Current orchestration state.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Rich Table combining progress and status.
|
|
310
|
+
"""
|
|
311
|
+
from rich.layout import Layout
|
|
312
|
+
|
|
313
|
+
# Just return the status table for simplicity
|
|
314
|
+
# Progress is shown in the table title area
|
|
315
|
+
table = create_status_table(state)
|
|
316
|
+
|
|
317
|
+
# Add progress summary row
|
|
318
|
+
total = state.wps_total
|
|
319
|
+
completed = state.wps_completed
|
|
320
|
+
pct = (completed / total * 100) if total > 0 else 0
|
|
321
|
+
elapsed = (datetime.now(timezone.utc) - state.started_at).total_seconds()
|
|
322
|
+
elapsed_str = f"{int(elapsed)}s" if elapsed < 60 else f"{int(elapsed // 60)}m"
|
|
323
|
+
|
|
324
|
+
table.caption = f"Progress: {completed}/{total} ({pct:.0f}%) | Elapsed: {elapsed_str}"
|
|
325
|
+
|
|
326
|
+
return table
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# =============================================================================
|
|
330
|
+
# Summary Report (T045)
|
|
331
|
+
# =============================================================================
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def print_summary(state: OrchestrationRun, console: Console) -> None:
|
|
335
|
+
"""Print orchestration summary report.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
state: Completed orchestration state.
|
|
339
|
+
console: Rich console for output.
|
|
340
|
+
"""
|
|
341
|
+
# Calculate duration
|
|
342
|
+
if state.completed_at:
|
|
343
|
+
duration = (state.completed_at - state.started_at).total_seconds()
|
|
344
|
+
else:
|
|
345
|
+
duration = (datetime.now(timezone.utc) - state.started_at).total_seconds()
|
|
346
|
+
|
|
347
|
+
# Format duration
|
|
348
|
+
if duration < 60:
|
|
349
|
+
duration_str = f"{duration:.1f} seconds"
|
|
350
|
+
elif duration < 3600:
|
|
351
|
+
duration_str = f"{duration / 60:.1f} minutes"
|
|
352
|
+
else:
|
|
353
|
+
duration_str = f"{duration / 3600:.1f} hours"
|
|
354
|
+
|
|
355
|
+
# Collect agent usage stats
|
|
356
|
+
agents_used: set[str] = set()
|
|
357
|
+
for wp in state.work_packages.values():
|
|
358
|
+
if wp.implementation_agent:
|
|
359
|
+
agents_used.add(wp.implementation_agent)
|
|
360
|
+
if wp.review_agent:
|
|
361
|
+
agents_used.add(wp.review_agent)
|
|
362
|
+
|
|
363
|
+
# Status color
|
|
364
|
+
if state.status == OrchestrationStatus.COMPLETED and state.wps_failed == 0:
|
|
365
|
+
status_color = "green"
|
|
366
|
+
status_text = "COMPLETED SUCCESSFULLY"
|
|
367
|
+
elif state.status == OrchestrationStatus.COMPLETED:
|
|
368
|
+
status_color = "yellow"
|
|
369
|
+
status_text = "COMPLETED WITH FAILURES"
|
|
370
|
+
elif state.status == OrchestrationStatus.PAUSED:
|
|
371
|
+
status_color = "yellow"
|
|
372
|
+
status_text = "PAUSED"
|
|
373
|
+
else:
|
|
374
|
+
status_color = "red"
|
|
375
|
+
status_text = "FAILED"
|
|
376
|
+
|
|
377
|
+
# Build summary content
|
|
378
|
+
content = (
|
|
379
|
+
f"[bold {status_color}]{status_text}[/bold {status_color}]\n\n"
|
|
380
|
+
f"[bold]Feature:[/bold] {state.feature_slug}\n"
|
|
381
|
+
f"[bold]Duration:[/bold] {duration_str}\n"
|
|
382
|
+
f"\n"
|
|
383
|
+
f"[bold]Work Packages:[/bold]\n"
|
|
384
|
+
f" Total: {state.wps_total}\n"
|
|
385
|
+
f" Completed: {state.wps_completed}\n"
|
|
386
|
+
f" Failed: {state.wps_failed}\n"
|
|
387
|
+
f"\n"
|
|
388
|
+
f"[bold]Execution Stats:[/bold]\n"
|
|
389
|
+
f" Agents Used: {', '.join(sorted(agents_used)) if agents_used else 'None'}\n"
|
|
390
|
+
f" Peak Parallelism: {state.parallel_peak}\n"
|
|
391
|
+
f" Total Invocations: {state.total_agent_invocations}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
console.print()
|
|
395
|
+
console.print("=" * 60)
|
|
396
|
+
console.print(
|
|
397
|
+
Panel(
|
|
398
|
+
content,
|
|
399
|
+
title="Orchestration Summary",
|
|
400
|
+
border_style=status_color,
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Show failed WPs if any
|
|
405
|
+
failed_wps = [
|
|
406
|
+
(wp_id, wp)
|
|
407
|
+
for wp_id, wp in state.work_packages.items()
|
|
408
|
+
if wp.status == WPStatus.FAILED
|
|
409
|
+
]
|
|
410
|
+
if failed_wps:
|
|
411
|
+
console.print(f"\n[red]Failed Work Packages:[/red]")
|
|
412
|
+
for wp_id, wp in failed_wps:
|
|
413
|
+
error = wp.last_error or "Unknown error"
|
|
414
|
+
if len(error) > 80:
|
|
415
|
+
error = error[:80] + "..."
|
|
416
|
+
console.print(f" {wp_id}: {error}")
|
|
417
|
+
console.print("\nCheck logs in .kittify/logs/ for details")
|
|
418
|
+
|
|
419
|
+
console.print()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# =============================================================================
|
|
423
|
+
# WP Processing
|
|
424
|
+
# =============================================================================
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
async def process_wp_implementation(
|
|
428
|
+
wp_id: str,
|
|
429
|
+
state: OrchestrationRun,
|
|
430
|
+
config: OrchestratorConfig,
|
|
431
|
+
feature_dir: Path,
|
|
432
|
+
repo_root: Path,
|
|
433
|
+
agent_id: str,
|
|
434
|
+
console: Console,
|
|
435
|
+
) -> bool:
|
|
436
|
+
"""Process implementation phase for a single WP.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
wp_id: Work package ID.
|
|
440
|
+
state: Orchestration state.
|
|
441
|
+
config: Orchestrator config.
|
|
442
|
+
feature_dir: Feature directory path.
|
|
443
|
+
repo_root: Repository root.
|
|
444
|
+
agent_id: Agent to use.
|
|
445
|
+
console: Rich console.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
True if implementation succeeded.
|
|
449
|
+
"""
|
|
450
|
+
wp = state.work_packages[wp_id]
|
|
451
|
+
feature_slug = feature_dir.name
|
|
452
|
+
|
|
453
|
+
# Update state
|
|
454
|
+
wp.status = WPStatus.IMPLEMENTATION
|
|
455
|
+
wp.implementation_agent = agent_id
|
|
456
|
+
wp.implementation_started = datetime.now(timezone.utc)
|
|
457
|
+
state.total_agent_invocations += 1
|
|
458
|
+
save_state(state, repo_root)
|
|
459
|
+
|
|
460
|
+
# Update lane
|
|
461
|
+
await transition_wp_lane(wp, "start_implementation", repo_root)
|
|
462
|
+
|
|
463
|
+
logger.info(f"Starting implementation of {wp_id} with {agent_id}")
|
|
464
|
+
|
|
465
|
+
# Get or create worktree
|
|
466
|
+
worktree_path = get_worktree_path(feature_slug, wp_id, repo_root)
|
|
467
|
+
if not worktree_path.exists():
|
|
468
|
+
try:
|
|
469
|
+
# Determine base WP from dependencies
|
|
470
|
+
from specify_cli.core.dependency_graph import build_dependency_graph
|
|
471
|
+
|
|
472
|
+
graph = build_dependency_graph(feature_dir)
|
|
473
|
+
deps = graph.get(wp_id, [])
|
|
474
|
+
|
|
475
|
+
# Use most recently completed dependency as base
|
|
476
|
+
base_wp = None
|
|
477
|
+
for dep_id in deps:
|
|
478
|
+
dep_state = state.work_packages.get(dep_id)
|
|
479
|
+
if dep_state and dep_state.status == WPStatus.COMPLETED:
|
|
480
|
+
base_wp = dep_id
|
|
481
|
+
|
|
482
|
+
worktree_path = await create_worktree(
|
|
483
|
+
feature_slug, wp_id, base_wp, repo_root
|
|
484
|
+
)
|
|
485
|
+
wp.worktree_path = worktree_path
|
|
486
|
+
except WorktreeCreationError as e:
|
|
487
|
+
logger.error(f"Failed to create worktree for {wp_id}: {e}")
|
|
488
|
+
wp.status = WPStatus.FAILED
|
|
489
|
+
wp.last_error = str(e)
|
|
490
|
+
state.wps_failed += 1
|
|
491
|
+
save_state(state, repo_root)
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
# Get invoker and prompt
|
|
495
|
+
invoker = get_invoker(agent_id)
|
|
496
|
+
prompt_path = feature_dir / "tasks" / f"{wp_id}-*.md"
|
|
497
|
+
|
|
498
|
+
# Find actual prompt file
|
|
499
|
+
prompt_files = list((feature_dir / "tasks").glob(f"{wp_id}-*.md"))
|
|
500
|
+
if not prompt_files:
|
|
501
|
+
logger.error(f"No prompt file found for {wp_id}")
|
|
502
|
+
wp.status = WPStatus.FAILED
|
|
503
|
+
wp.last_error = f"No prompt file found for {wp_id}"
|
|
504
|
+
state.wps_failed += 1
|
|
505
|
+
save_state(state, repo_root)
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
prompt_path = prompt_files[0]
|
|
509
|
+
prompt_content = prompt_path.read_text()
|
|
510
|
+
|
|
511
|
+
# If this is a re-implementation after review rejection, include the feedback
|
|
512
|
+
if wp.review_feedback and wp.implementation_retries > 0:
|
|
513
|
+
rework_header = f"""
|
|
514
|
+
## ⚠️ RE-IMPLEMENTATION REQUIRED (Attempt {wp.implementation_retries + 1})
|
|
515
|
+
|
|
516
|
+
The previous implementation was reviewed and **rejected**. Please address the following feedback:
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
{wp.review_feedback}
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
Fix the issues described above and ensure all requirements are met.
|
|
523
|
+
"""
|
|
524
|
+
prompt_content = rework_header + "\n\n" + prompt_content
|
|
525
|
+
logger.info(f"{wp_id} re-implementation with feedback from review")
|
|
526
|
+
|
|
527
|
+
# Get log path
|
|
528
|
+
log_path = get_log_path(repo_root, wp_id, "implementation", datetime.now())
|
|
529
|
+
wp.log_file = log_path
|
|
530
|
+
|
|
531
|
+
# Execute with retry
|
|
532
|
+
async def execute_fn():
|
|
533
|
+
return await execute_with_logging(
|
|
534
|
+
invoker,
|
|
535
|
+
prompt_content,
|
|
536
|
+
worktree_path,
|
|
537
|
+
"implementation",
|
|
538
|
+
config.global_timeout,
|
|
539
|
+
log_path,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
result = await execute_with_retry(execute_fn, wp, config, "implementation", agent_id)
|
|
543
|
+
|
|
544
|
+
# Update progress
|
|
545
|
+
update_wp_progress(wp, result, "implementation")
|
|
546
|
+
wp.implementation_completed = datetime.now(timezone.utc)
|
|
547
|
+
|
|
548
|
+
if is_success(result):
|
|
549
|
+
logger.info(f"{wp_id} implementation completed successfully")
|
|
550
|
+
save_state(state, repo_root)
|
|
551
|
+
return True
|
|
552
|
+
|
|
553
|
+
# Handle failure - try fallback
|
|
554
|
+
logger.warning(f"{wp_id} implementation failed with {agent_id}")
|
|
555
|
+
next_agent = apply_fallback(wp_id, "implementation", agent_id, config, state)
|
|
556
|
+
|
|
557
|
+
if next_agent:
|
|
558
|
+
# Reset and retry with fallback agent
|
|
559
|
+
wp.status = WPStatus.PENDING
|
|
560
|
+
wp.implementation_started = None
|
|
561
|
+
wp.implementation_completed = None
|
|
562
|
+
save_state(state, repo_root)
|
|
563
|
+
|
|
564
|
+
return await process_wp_implementation(
|
|
565
|
+
wp_id, state, config, feature_dir, repo_root, next_agent, console
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# No fallback - escalate to human
|
|
569
|
+
await escalate_to_human(wp_id, "implementation", state, repo_root, console)
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class ReviewResult:
|
|
574
|
+
"""Result of a review phase."""
|
|
575
|
+
|
|
576
|
+
APPROVED = "approved"
|
|
577
|
+
REJECTED = "rejected"
|
|
578
|
+
ERROR = "error"
|
|
579
|
+
|
|
580
|
+
def __init__(self, outcome: str, feedback: str | None = None):
|
|
581
|
+
self.outcome = outcome
|
|
582
|
+
self.feedback = feedback
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def is_approved(self) -> bool:
|
|
586
|
+
return self.outcome == self.APPROVED
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def is_rejected(self) -> bool:
|
|
590
|
+
return self.outcome == self.REJECTED
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def parse_review_outcome(result: InvocationResult, log_path: Path | None = None) -> ReviewResult:
|
|
594
|
+
"""Parse review result to determine if approved or rejected.
|
|
595
|
+
|
|
596
|
+
Looks for rejection signals in the output:
|
|
597
|
+
- Explicit "REJECTED" or "CHANGES_REQUESTED" markers
|
|
598
|
+
- "needs work", "please fix", "issues found" phrases
|
|
599
|
+
- Non-zero exit code with feedback
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
result: InvocationResult from agent execution.
|
|
603
|
+
log_path: Optional path to log file for detailed output.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
ReviewResult with outcome and feedback.
|
|
607
|
+
"""
|
|
608
|
+
exit_code = result.exit_code
|
|
609
|
+
stdout = result.stdout or ""
|
|
610
|
+
stderr = result.stderr or ""
|
|
611
|
+
output = stdout + "\n" + stderr
|
|
612
|
+
|
|
613
|
+
# Check for explicit markers (case-insensitive)
|
|
614
|
+
output_lower = output.lower()
|
|
615
|
+
|
|
616
|
+
# Rejection patterns
|
|
617
|
+
rejection_patterns = [
|
|
618
|
+
"rejected",
|
|
619
|
+
"changes_requested",
|
|
620
|
+
"changes requested",
|
|
621
|
+
"needs rework",
|
|
622
|
+
"needs work",
|
|
623
|
+
"please fix",
|
|
624
|
+
"issues found",
|
|
625
|
+
"not approved",
|
|
626
|
+
"review failed",
|
|
627
|
+
"failing tests",
|
|
628
|
+
"tests failing",
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
# Approval patterns
|
|
632
|
+
approval_patterns = [
|
|
633
|
+
"approved",
|
|
634
|
+
"lgtm",
|
|
635
|
+
"looks good",
|
|
636
|
+
"review complete",
|
|
637
|
+
"review passed",
|
|
638
|
+
"all tests pass",
|
|
639
|
+
"no issues found",
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
# Check patterns
|
|
643
|
+
is_rejected = any(p in output_lower for p in rejection_patterns)
|
|
644
|
+
is_approved = any(p in output_lower for p in approval_patterns)
|
|
645
|
+
|
|
646
|
+
# If both or neither, use exit code
|
|
647
|
+
if is_rejected and not is_approved:
|
|
648
|
+
# Extract feedback - look for content after rejection marker
|
|
649
|
+
feedback = output.strip()
|
|
650
|
+
if len(feedback) > 500:
|
|
651
|
+
feedback = feedback[:500] + "..."
|
|
652
|
+
return ReviewResult(ReviewResult.REJECTED, feedback)
|
|
653
|
+
|
|
654
|
+
if is_approved and not is_rejected:
|
|
655
|
+
return ReviewResult(ReviewResult.APPROVED)
|
|
656
|
+
|
|
657
|
+
# Fall back to exit code
|
|
658
|
+
if exit_code == 0:
|
|
659
|
+
return ReviewResult(ReviewResult.APPROVED)
|
|
660
|
+
|
|
661
|
+
# Non-zero exit with no clear pattern - treat as error, not rejection
|
|
662
|
+
return ReviewResult(ReviewResult.ERROR, output.strip()[:500] if output else None)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
async def process_wp_review(
|
|
666
|
+
wp_id: str,
|
|
667
|
+
state: OrchestrationRun,
|
|
668
|
+
config: OrchestratorConfig,
|
|
669
|
+
feature_dir: Path,
|
|
670
|
+
repo_root: Path,
|
|
671
|
+
agent_id: str,
|
|
672
|
+
console: Console,
|
|
673
|
+
) -> ReviewResult:
|
|
674
|
+
"""Process review phase for a single WP.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
wp_id: Work package ID.
|
|
678
|
+
state: Orchestration state.
|
|
679
|
+
config: Orchestrator config.
|
|
680
|
+
feature_dir: Feature directory path.
|
|
681
|
+
repo_root: Repository root.
|
|
682
|
+
agent_id: Agent to use.
|
|
683
|
+
console: Rich console.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
ReviewResult indicating approved, rejected, or error.
|
|
687
|
+
"""
|
|
688
|
+
wp = state.work_packages[wp_id]
|
|
689
|
+
feature_slug = feature_dir.name
|
|
690
|
+
|
|
691
|
+
# Update state
|
|
692
|
+
wp.status = WPStatus.REVIEW
|
|
693
|
+
wp.review_agent = agent_id
|
|
694
|
+
wp.review_started = datetime.now(timezone.utc)
|
|
695
|
+
state.total_agent_invocations += 1
|
|
696
|
+
save_state(state, repo_root)
|
|
697
|
+
|
|
698
|
+
# Update lane
|
|
699
|
+
await transition_wp_lane(wp, "complete_implementation", repo_root)
|
|
700
|
+
|
|
701
|
+
logger.info(f"Starting review of {wp_id} with {agent_id}")
|
|
702
|
+
|
|
703
|
+
# Get worktree
|
|
704
|
+
worktree_path = get_worktree_path(feature_slug, wp_id, repo_root)
|
|
705
|
+
if not worktree_path.exists():
|
|
706
|
+
logger.error(f"Worktree not found for {wp_id} review")
|
|
707
|
+
wp.last_error = "Worktree not found for review"
|
|
708
|
+
return ReviewResult(ReviewResult.ERROR, "Worktree not found")
|
|
709
|
+
|
|
710
|
+
# Get invoker
|
|
711
|
+
invoker = get_invoker(agent_id)
|
|
712
|
+
|
|
713
|
+
# Build review prompt - ask for explicit approval/rejection signal
|
|
714
|
+
review_prompt = f"""Review the implementation in this workspace for work package {wp_id}.
|
|
715
|
+
|
|
716
|
+
Check for:
|
|
717
|
+
- Code correctness and completeness
|
|
718
|
+
- Test coverage
|
|
719
|
+
- Documentation
|
|
720
|
+
- Following project conventions
|
|
721
|
+
|
|
722
|
+
IMPORTANT: At the end of your review, you MUST output one of these markers:
|
|
723
|
+
- If implementation is good: "APPROVED - review complete"
|
|
724
|
+
- If changes are needed: "REJECTED - <reason>" and describe what needs to be fixed
|
|
725
|
+
|
|
726
|
+
If you find issues, describe them clearly so they can be addressed in re-implementation.
|
|
727
|
+
Do NOT fix issues yourself during review - just identify them.
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
# Get log path
|
|
731
|
+
log_path = get_log_path(repo_root, wp_id, "review", datetime.now())
|
|
732
|
+
|
|
733
|
+
# Execute with retry
|
|
734
|
+
async def execute_fn():
|
|
735
|
+
return await execute_with_logging(
|
|
736
|
+
invoker,
|
|
737
|
+
review_prompt,
|
|
738
|
+
worktree_path,
|
|
739
|
+
"review",
|
|
740
|
+
config.global_timeout,
|
|
741
|
+
log_path,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
result = await execute_with_retry(execute_fn, wp, config, "review", agent_id)
|
|
745
|
+
|
|
746
|
+
# Update progress
|
|
747
|
+
update_wp_progress(wp, result, "review")
|
|
748
|
+
wp.review_completed = datetime.now(timezone.utc)
|
|
749
|
+
|
|
750
|
+
# Parse the outcome
|
|
751
|
+
review_result = parse_review_outcome(result, log_path)
|
|
752
|
+
logger.info(f"{wp_id} review outcome: {review_result.outcome}")
|
|
753
|
+
|
|
754
|
+
save_state(state, repo_root)
|
|
755
|
+
return review_result
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
async def process_wp(
|
|
759
|
+
wp_id: str,
|
|
760
|
+
state: OrchestrationRun,
|
|
761
|
+
config: OrchestratorConfig,
|
|
762
|
+
feature_dir: Path,
|
|
763
|
+
repo_root: Path,
|
|
764
|
+
concurrency: ConcurrencyManager,
|
|
765
|
+
console: Console,
|
|
766
|
+
override_impl_agent: str | None = None,
|
|
767
|
+
override_review_agent: str | None = None,
|
|
768
|
+
) -> bool:
|
|
769
|
+
"""Process a single WP through the implement→review state machine.
|
|
770
|
+
|
|
771
|
+
This is the core state machine loop that continues until:
|
|
772
|
+
- WP is COMPLETED (review approved)
|
|
773
|
+
- WP is FAILED (max retries exceeded or unrecoverable error)
|
|
774
|
+
|
|
775
|
+
State machine:
|
|
776
|
+
READY/PENDING/REWORK → IMPLEMENTATION → REVIEW
|
|
777
|
+
↓
|
|
778
|
+
COMPLETED ← (approved)
|
|
779
|
+
or
|
|
780
|
+
REWORK ← (rejected) → back to IMPLEMENTATION
|
|
781
|
+
or
|
|
782
|
+
FAILED ← (max retries exceeded)
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
wp_id: Work package ID.
|
|
786
|
+
state: Orchestration state.
|
|
787
|
+
config: Orchestrator config.
|
|
788
|
+
feature_dir: Feature directory path.
|
|
789
|
+
repo_root: Repository root.
|
|
790
|
+
concurrency: Concurrency manager.
|
|
791
|
+
console: Rich console.
|
|
792
|
+
override_impl_agent: CLI override for implementation agent.
|
|
793
|
+
override_review_agent: CLI override for review agent.
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
True if WP completed successfully.
|
|
797
|
+
"""
|
|
798
|
+
wp = state.work_packages[wp_id]
|
|
799
|
+
max_review_cycles = config.max_retries
|
|
800
|
+
|
|
801
|
+
# State machine loop
|
|
802
|
+
while wp.status not in [WPStatus.COMPLETED, WPStatus.FAILED]:
|
|
803
|
+
logger.info(f"{wp_id} state machine: current status = {wp.status.value}")
|
|
804
|
+
|
|
805
|
+
# ===== IMPLEMENTATION PHASE =====
|
|
806
|
+
if wp.status in [WPStatus.READY, WPStatus.PENDING, WPStatus.REWORK]:
|
|
807
|
+
# Check max retries before starting
|
|
808
|
+
if wp.implementation_retries >= max_review_cycles:
|
|
809
|
+
logger.error(f"{wp_id} exceeded max review cycles ({max_review_cycles})")
|
|
810
|
+
wp.status = WPStatus.FAILED
|
|
811
|
+
wp.last_error = f"Exceeded max review cycles ({max_review_cycles})"
|
|
812
|
+
state.wps_failed += 1
|
|
813
|
+
save_state(state, repo_root)
|
|
814
|
+
return False
|
|
815
|
+
|
|
816
|
+
# Select implementation agent using user config from spec-kitty init
|
|
817
|
+
impl_agent = select_agent_from_user_config(
|
|
818
|
+
repo_root, "implementation", override_agent=override_impl_agent
|
|
819
|
+
)
|
|
820
|
+
if not impl_agent:
|
|
821
|
+
# Fall back to legacy config-based selection
|
|
822
|
+
impl_agent = select_agent(config, "implementation", state=state)
|
|
823
|
+
if not impl_agent:
|
|
824
|
+
logger.error(f"No agent available for {wp_id} implementation")
|
|
825
|
+
wp.status = WPStatus.FAILED
|
|
826
|
+
wp.last_error = "No agent available"
|
|
827
|
+
state.wps_failed += 1
|
|
828
|
+
save_state(state, repo_root)
|
|
829
|
+
return False
|
|
830
|
+
|
|
831
|
+
# Run implementation
|
|
832
|
+
async with concurrency.throttle(impl_agent):
|
|
833
|
+
impl_success = await process_wp_implementation(
|
|
834
|
+
wp_id, state, config, feature_dir, repo_root, impl_agent, console
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
if not impl_success:
|
|
838
|
+
# Implementation failed (not rejection - actual error)
|
|
839
|
+
wp.status = WPStatus.FAILED
|
|
840
|
+
state.wps_failed += 1
|
|
841
|
+
save_state(state, repo_root)
|
|
842
|
+
return False
|
|
843
|
+
|
|
844
|
+
# Implementation succeeded - move to review
|
|
845
|
+
# (status is already updated by process_wp_implementation)
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
# ===== REVIEW PHASE =====
|
|
849
|
+
if wp.status == WPStatus.IMPLEMENTATION:
|
|
850
|
+
# Implementation just completed, start review
|
|
851
|
+
|
|
852
|
+
# Check if review is needed (skip in single-agent mode with no review config)
|
|
853
|
+
skip_review = is_single_agent_mode(config) and not config.defaults.get("review")
|
|
854
|
+
|
|
855
|
+
if skip_review:
|
|
856
|
+
# Mark as completed without review
|
|
857
|
+
wp.status = WPStatus.COMPLETED
|
|
858
|
+
state.wps_completed += 1
|
|
859
|
+
await transition_wp_lane(wp, "complete_review", repo_root)
|
|
860
|
+
save_state(state, repo_root)
|
|
861
|
+
return True
|
|
862
|
+
|
|
863
|
+
# Single-agent delay before review
|
|
864
|
+
if is_single_agent_mode(config):
|
|
865
|
+
await single_agent_review_delay(config.single_agent_delay)
|
|
866
|
+
|
|
867
|
+
# Select review agent using user config (prefers different agent for cross-review)
|
|
868
|
+
review_agent = select_review_agent_from_user_config(
|
|
869
|
+
repo_root, wp.implementation_agent, override_agent=override_review_agent
|
|
870
|
+
)
|
|
871
|
+
if not review_agent:
|
|
872
|
+
# Fall back to legacy config-based selection
|
|
873
|
+
review_agent = select_review_agent(config, wp.implementation_agent, state=state)
|
|
874
|
+
if not review_agent:
|
|
875
|
+
logger.warning(f"No review agent available for {wp_id}, marking as complete")
|
|
876
|
+
wp.status = WPStatus.COMPLETED
|
|
877
|
+
state.wps_completed += 1
|
|
878
|
+
save_state(state, repo_root)
|
|
879
|
+
return True
|
|
880
|
+
|
|
881
|
+
# Run review
|
|
882
|
+
async with concurrency.throttle(review_agent):
|
|
883
|
+
review_result = await process_wp_review(
|
|
884
|
+
wp_id, state, config, feature_dir, repo_root, review_agent, console
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# Handle review outcome
|
|
888
|
+
if review_result.is_approved:
|
|
889
|
+
# Review approved - WP is done!
|
|
890
|
+
wp.status = WPStatus.COMPLETED
|
|
891
|
+
state.wps_completed += 1
|
|
892
|
+
await transition_wp_lane(wp, "complete_review", repo_root)
|
|
893
|
+
logger.info(f"{wp_id} COMPLETED - review approved")
|
|
894
|
+
save_state(state, repo_root)
|
|
895
|
+
return True
|
|
896
|
+
|
|
897
|
+
elif review_result.is_rejected:
|
|
898
|
+
# Review rejected - go back to implementation
|
|
899
|
+
wp.status = WPStatus.REWORK
|
|
900
|
+
wp.review_feedback = review_result.feedback
|
|
901
|
+
wp.implementation_retries += 1
|
|
902
|
+
wp.review_retries += 1
|
|
903
|
+
|
|
904
|
+
# Clear review timestamps for next cycle
|
|
905
|
+
wp.review_started = None
|
|
906
|
+
wp.review_completed = None
|
|
907
|
+
|
|
908
|
+
logger.info(
|
|
909
|
+
f"{wp_id} REWORK - review rejected (cycle {wp.implementation_retries}/{max_review_cycles})"
|
|
910
|
+
)
|
|
911
|
+
if review_result.feedback:
|
|
912
|
+
logger.info(f"{wp_id} feedback: {review_result.feedback[:200]}...")
|
|
913
|
+
|
|
914
|
+
save_state(state, repo_root)
|
|
915
|
+
# Loop continues - will go back to implementation
|
|
916
|
+
continue
|
|
917
|
+
|
|
918
|
+
else:
|
|
919
|
+
# Review error (not rejection) - try fallback agent or fail
|
|
920
|
+
logger.warning(f"{wp_id} review error: {review_result.feedback}")
|
|
921
|
+
next_agent = apply_fallback(wp_id, "review", review_agent, config, state)
|
|
922
|
+
|
|
923
|
+
if next_agent:
|
|
924
|
+
# Retry review with different agent
|
|
925
|
+
wp.review_started = None
|
|
926
|
+
wp.review_completed = None
|
|
927
|
+
save_state(state, repo_root)
|
|
928
|
+
|
|
929
|
+
async with concurrency.throttle(next_agent):
|
|
930
|
+
review_result = await process_wp_review(
|
|
931
|
+
wp_id, state, config, feature_dir, repo_root, next_agent, console
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Re-check outcome after fallback
|
|
935
|
+
if review_result.is_approved:
|
|
936
|
+
wp.status = WPStatus.COMPLETED
|
|
937
|
+
state.wps_completed += 1
|
|
938
|
+
await transition_wp_lane(wp, "complete_review", repo_root)
|
|
939
|
+
save_state(state, repo_root)
|
|
940
|
+
return True
|
|
941
|
+
elif review_result.is_rejected:
|
|
942
|
+
wp.status = WPStatus.REWORK
|
|
943
|
+
wp.review_feedback = review_result.feedback
|
|
944
|
+
wp.implementation_retries += 1
|
|
945
|
+
wp.review_retries += 1
|
|
946
|
+
wp.review_started = None
|
|
947
|
+
wp.review_completed = None
|
|
948
|
+
save_state(state, repo_root)
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
# No fallback or fallback also errored - escalate
|
|
952
|
+
await escalate_to_human(wp_id, "review", state, repo_root, console)
|
|
953
|
+
return False
|
|
954
|
+
|
|
955
|
+
# ===== REVIEW STATUS (already in review, resuming) =====
|
|
956
|
+
if wp.status == WPStatus.REVIEW:
|
|
957
|
+
# We're resuming a WP that was in review
|
|
958
|
+
# This shouldn't normally happen as review is synchronous
|
|
959
|
+
# Treat as needing implementation
|
|
960
|
+
wp.status = WPStatus.REWORK
|
|
961
|
+
wp.review_feedback = "Review interrupted - restarting"
|
|
962
|
+
save_state(state, repo_root)
|
|
963
|
+
continue
|
|
964
|
+
|
|
965
|
+
# Unknown status - shouldn't happen
|
|
966
|
+
logger.error(f"{wp_id} in unexpected status: {wp.status}")
|
|
967
|
+
wp.status = WPStatus.FAILED
|
|
968
|
+
wp.last_error = f"Unexpected status: {wp.status}"
|
|
969
|
+
state.wps_failed += 1
|
|
970
|
+
save_state(state, repo_root)
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
# Should not reach here, but handle gracefully
|
|
974
|
+
return wp.status == WPStatus.COMPLETED
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
# =============================================================================
|
|
978
|
+
# Main Orchestration Loop (T043)
|
|
979
|
+
# =============================================================================
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
async def run_orchestration_loop(
|
|
983
|
+
state: OrchestrationRun,
|
|
984
|
+
config: OrchestratorConfig,
|
|
985
|
+
feature_dir: Path,
|
|
986
|
+
repo_root: Path,
|
|
987
|
+
console: Console | None = None,
|
|
988
|
+
live_display: bool = True,
|
|
989
|
+
override_impl_agent: str | None = None,
|
|
990
|
+
override_review_agent: str | None = None,
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Main orchestration loop connecting all components.
|
|
993
|
+
|
|
994
|
+
Coordinates scheduler, executor, and monitor to process WPs in parallel.
|
|
995
|
+
|
|
996
|
+
Args:
|
|
997
|
+
state: Orchestration state.
|
|
998
|
+
config: Orchestrator config.
|
|
999
|
+
feature_dir: Feature directory path.
|
|
1000
|
+
repo_root: Repository root.
|
|
1001
|
+
console: Rich console for output.
|
|
1002
|
+
live_display: Whether to show live progress display.
|
|
1003
|
+
override_impl_agent: CLI override for implementation agent.
|
|
1004
|
+
override_review_agent: CLI override for review agent.
|
|
1005
|
+
"""
|
|
1006
|
+
if console is None:
|
|
1007
|
+
console = Console()
|
|
1008
|
+
|
|
1009
|
+
# Build graph and validate
|
|
1010
|
+
graph = build_wp_graph(feature_dir)
|
|
1011
|
+
|
|
1012
|
+
# Initialize concurrency manager
|
|
1013
|
+
concurrency = ConcurrencyManager(config)
|
|
1014
|
+
|
|
1015
|
+
# Initialize WP states
|
|
1016
|
+
for wp_id in graph:
|
|
1017
|
+
if wp_id not in state.work_packages:
|
|
1018
|
+
state.work_packages[wp_id] = WPExecution(wp_id=wp_id)
|
|
1019
|
+
|
|
1020
|
+
state.wps_total = len(graph)
|
|
1021
|
+
state.status = OrchestrationStatus.RUNNING
|
|
1022
|
+
save_state(state, repo_root)
|
|
1023
|
+
|
|
1024
|
+
# Set up shutdown handler
|
|
1025
|
+
shutdown_requested = False
|
|
1026
|
+
original_sigint = signal.getsignal(signal.SIGINT)
|
|
1027
|
+
original_sigterm = signal.getsignal(signal.SIGTERM)
|
|
1028
|
+
|
|
1029
|
+
def signal_handler(sig, frame):
|
|
1030
|
+
nonlocal shutdown_requested
|
|
1031
|
+
if shutdown_requested:
|
|
1032
|
+
# Second signal - force exit
|
|
1033
|
+
console.print("\n[red]Force shutdown...[/red]")
|
|
1034
|
+
raise SystemExit(1)
|
|
1035
|
+
|
|
1036
|
+
console.print("\n[yellow]Shutdown requested, finishing current tasks...[/yellow]")
|
|
1037
|
+
shutdown_requested = True
|
|
1038
|
+
state.status = OrchestrationStatus.PAUSED
|
|
1039
|
+
save_state(state, repo_root)
|
|
1040
|
+
|
|
1041
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
1042
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
1043
|
+
|
|
1044
|
+
# Track running tasks
|
|
1045
|
+
running_tasks: dict[str, asyncio.Task] = {}
|
|
1046
|
+
|
|
1047
|
+
try:
|
|
1048
|
+
# Run with or without live display
|
|
1049
|
+
if live_display:
|
|
1050
|
+
with Live(create_live_display(state), refresh_per_second=1, console=console) as live:
|
|
1051
|
+
await _orchestration_main_loop(
|
|
1052
|
+
state, config, graph, feature_dir, repo_root,
|
|
1053
|
+
concurrency, console, running_tasks,
|
|
1054
|
+
lambda: shutdown_requested,
|
|
1055
|
+
lambda: live.update(create_live_display(state)),
|
|
1056
|
+
override_impl_agent=override_impl_agent,
|
|
1057
|
+
override_review_agent=override_review_agent,
|
|
1058
|
+
)
|
|
1059
|
+
else:
|
|
1060
|
+
await _orchestration_main_loop(
|
|
1061
|
+
state, config, graph, feature_dir, repo_root,
|
|
1062
|
+
concurrency, console, running_tasks,
|
|
1063
|
+
lambda: shutdown_requested,
|
|
1064
|
+
lambda: None, # No display update
|
|
1065
|
+
override_impl_agent=override_impl_agent,
|
|
1066
|
+
override_review_agent=override_review_agent,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
finally:
|
|
1070
|
+
# Restore signal handlers
|
|
1071
|
+
signal.signal(signal.SIGINT, original_sigint)
|
|
1072
|
+
signal.signal(signal.SIGTERM, original_sigterm)
|
|
1073
|
+
|
|
1074
|
+
# Cancel any remaining tasks
|
|
1075
|
+
for task in running_tasks.values():
|
|
1076
|
+
if not task.done():
|
|
1077
|
+
task.cancel()
|
|
1078
|
+
|
|
1079
|
+
# Finalize state
|
|
1080
|
+
if not shutdown_requested:
|
|
1081
|
+
if state.wps_failed > 0:
|
|
1082
|
+
state.status = OrchestrationStatus.COMPLETED
|
|
1083
|
+
else:
|
|
1084
|
+
all_done = all(
|
|
1085
|
+
wp.status in [WPStatus.COMPLETED, WPStatus.FAILED]
|
|
1086
|
+
for wp in state.work_packages.values()
|
|
1087
|
+
)
|
|
1088
|
+
if all_done:
|
|
1089
|
+
state.status = OrchestrationStatus.COMPLETED
|
|
1090
|
+
else:
|
|
1091
|
+
state.status = OrchestrationStatus.FAILED
|
|
1092
|
+
|
|
1093
|
+
state.completed_at = datetime.now(timezone.utc)
|
|
1094
|
+
save_state(state, repo_root)
|
|
1095
|
+
|
|
1096
|
+
# Print summary
|
|
1097
|
+
print_summary(state, console)
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
async def _orchestration_main_loop(
|
|
1101
|
+
state: OrchestrationRun,
|
|
1102
|
+
config: OrchestratorConfig,
|
|
1103
|
+
graph: dict[str, list[str]],
|
|
1104
|
+
feature_dir: Path,
|
|
1105
|
+
repo_root: Path,
|
|
1106
|
+
concurrency: ConcurrencyManager,
|
|
1107
|
+
console: Console,
|
|
1108
|
+
running_tasks: dict[str, asyncio.Task],
|
|
1109
|
+
is_shutdown: Callable[[], bool],
|
|
1110
|
+
update_display: Callable[[], None],
|
|
1111
|
+
override_impl_agent: str | None = None,
|
|
1112
|
+
override_review_agent: str | None = None,
|
|
1113
|
+
) -> None:
|
|
1114
|
+
"""Inner orchestration loop.
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
state: Orchestration state.
|
|
1118
|
+
config: Orchestrator config.
|
|
1119
|
+
graph: Dependency graph.
|
|
1120
|
+
feature_dir: Feature directory.
|
|
1121
|
+
repo_root: Repository root.
|
|
1122
|
+
concurrency: Concurrency manager.
|
|
1123
|
+
console: Rich console.
|
|
1124
|
+
running_tasks: Dict tracking running asyncio tasks.
|
|
1125
|
+
is_shutdown: Callback to check if shutdown requested.
|
|
1126
|
+
update_display: Callback to update live display.
|
|
1127
|
+
override_impl_agent: CLI override for implementation agent.
|
|
1128
|
+
override_review_agent: CLI override for review agent.
|
|
1129
|
+
"""
|
|
1130
|
+
while not is_shutdown():
|
|
1131
|
+
# Update display
|
|
1132
|
+
update_display()
|
|
1133
|
+
|
|
1134
|
+
# Check completion
|
|
1135
|
+
all_done = all(
|
|
1136
|
+
wp.status in [WPStatus.COMPLETED, WPStatus.FAILED]
|
|
1137
|
+
for wp in state.work_packages.values()
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
if all_done:
|
|
1141
|
+
logger.info("All work packages complete")
|
|
1142
|
+
break
|
|
1143
|
+
|
|
1144
|
+
# Check for paused state (human intervention)
|
|
1145
|
+
if state.status == OrchestrationStatus.PAUSED:
|
|
1146
|
+
logger.info("Orchestration paused")
|
|
1147
|
+
break
|
|
1148
|
+
|
|
1149
|
+
# Get ready WPs
|
|
1150
|
+
ready = get_ready_wps(graph, state)
|
|
1151
|
+
|
|
1152
|
+
# Start tasks for ready WPs (up to available slots)
|
|
1153
|
+
for wp_id in ready:
|
|
1154
|
+
if wp_id in running_tasks:
|
|
1155
|
+
continue # Already running
|
|
1156
|
+
|
|
1157
|
+
if concurrency.get_available_slots() <= 0:
|
|
1158
|
+
break # At global limit
|
|
1159
|
+
|
|
1160
|
+
# Create task
|
|
1161
|
+
task = asyncio.create_task(
|
|
1162
|
+
process_wp(
|
|
1163
|
+
wp_id, state, config, feature_dir, repo_root,
|
|
1164
|
+
concurrency, console,
|
|
1165
|
+
override_impl_agent=override_impl_agent,
|
|
1166
|
+
override_review_agent=override_review_agent,
|
|
1167
|
+
)
|
|
1168
|
+
)
|
|
1169
|
+
running_tasks[wp_id] = task
|
|
1170
|
+
logger.info(f"Started task for {wp_id}")
|
|
1171
|
+
|
|
1172
|
+
# Update peak parallelism
|
|
1173
|
+
active_count = sum(1 for t in running_tasks.values() if not t.done())
|
|
1174
|
+
if active_count > state.parallel_peak:
|
|
1175
|
+
state.parallel_peak = active_count
|
|
1176
|
+
|
|
1177
|
+
# Clean up completed tasks
|
|
1178
|
+
completed_wp_ids = [
|
|
1179
|
+
wp_id for wp_id, task in running_tasks.items()
|
|
1180
|
+
if task.done()
|
|
1181
|
+
]
|
|
1182
|
+
for wp_id in completed_wp_ids:
|
|
1183
|
+
task = running_tasks.pop(wp_id)
|
|
1184
|
+
try:
|
|
1185
|
+
task.result() # Raises if task failed
|
|
1186
|
+
except Exception as e:
|
|
1187
|
+
logger.error(f"Task for {wp_id} raised exception: {e}")
|
|
1188
|
+
|
|
1189
|
+
# Check if nothing can progress
|
|
1190
|
+
if not ready and not running_tasks:
|
|
1191
|
+
# Deadlock or all blocked on failed WPs
|
|
1192
|
+
remaining = [
|
|
1193
|
+
wp_id for wp_id, wp in state.work_packages.items()
|
|
1194
|
+
if wp.status not in [WPStatus.COMPLETED, WPStatus.FAILED]
|
|
1195
|
+
]
|
|
1196
|
+
if remaining:
|
|
1197
|
+
logger.warning(f"No progress possible. Remaining: {remaining}")
|
|
1198
|
+
for wp_id in remaining:
|
|
1199
|
+
state.work_packages[wp_id].status = WPStatus.FAILED
|
|
1200
|
+
state.work_packages[wp_id].last_error = "Blocked by failed dependencies"
|
|
1201
|
+
state.wps_failed += 1
|
|
1202
|
+
save_state(state, repo_root)
|
|
1203
|
+
break
|
|
1204
|
+
|
|
1205
|
+
# Wait a bit before next iteration
|
|
1206
|
+
await asyncio.sleep(2)
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
__all__ = [
|
|
1210
|
+
# Exceptions
|
|
1211
|
+
"OrchestrationError",
|
|
1212
|
+
"CircularDependencyError",
|
|
1213
|
+
"NoAgentsError",
|
|
1214
|
+
"ValidationError",
|
|
1215
|
+
# Validation (T046)
|
|
1216
|
+
"validate_feature",
|
|
1217
|
+
"validate_agents",
|
|
1218
|
+
# Progress display (T044)
|
|
1219
|
+
"create_status_table",
|
|
1220
|
+
"create_progress_panel",
|
|
1221
|
+
"create_live_display",
|
|
1222
|
+
# Summary report (T045)
|
|
1223
|
+
"print_summary",
|
|
1224
|
+
# WP processing
|
|
1225
|
+
"process_wp",
|
|
1226
|
+
"process_wp_implementation",
|
|
1227
|
+
"process_wp_review",
|
|
1228
|
+
# Main loop (T043)
|
|
1229
|
+
"run_orchestration_loop",
|
|
1230
|
+
]
|