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.
Files changed (242) hide show
  1. spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
  2. spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
  3. spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
  4. spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
  5. spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
  6. specify_cli/__init__.py +171 -0
  7. specify_cli/acceptance.py +627 -0
  8. specify_cli/agent_utils/README.md +157 -0
  9. specify_cli/agent_utils/__init__.py +9 -0
  10. specify_cli/agent_utils/status.py +356 -0
  11. specify_cli/cli/__init__.py +6 -0
  12. specify_cli/cli/commands/__init__.py +46 -0
  13. specify_cli/cli/commands/accept.py +189 -0
  14. specify_cli/cli/commands/agent/__init__.py +22 -0
  15. specify_cli/cli/commands/agent/config.py +382 -0
  16. specify_cli/cli/commands/agent/context.py +191 -0
  17. specify_cli/cli/commands/agent/feature.py +1057 -0
  18. specify_cli/cli/commands/agent/release.py +11 -0
  19. specify_cli/cli/commands/agent/tasks.py +1253 -0
  20. specify_cli/cli/commands/agent/workflow.py +801 -0
  21. specify_cli/cli/commands/context.py +246 -0
  22. specify_cli/cli/commands/dashboard.py +85 -0
  23. specify_cli/cli/commands/implement.py +973 -0
  24. specify_cli/cli/commands/init.py +827 -0
  25. specify_cli/cli/commands/init_help.py +62 -0
  26. specify_cli/cli/commands/merge.py +755 -0
  27. specify_cli/cli/commands/mission.py +240 -0
  28. specify_cli/cli/commands/ops.py +265 -0
  29. specify_cli/cli/commands/orchestrate.py +640 -0
  30. specify_cli/cli/commands/repair.py +175 -0
  31. specify_cli/cli/commands/research.py +165 -0
  32. specify_cli/cli/commands/sync.py +364 -0
  33. specify_cli/cli/commands/upgrade.py +249 -0
  34. specify_cli/cli/commands/validate_encoding.py +186 -0
  35. specify_cli/cli/commands/validate_tasks.py +186 -0
  36. specify_cli/cli/commands/verify.py +310 -0
  37. specify_cli/cli/helpers.py +123 -0
  38. specify_cli/cli/step_tracker.py +91 -0
  39. specify_cli/cli/ui.py +192 -0
  40. specify_cli/core/__init__.py +53 -0
  41. specify_cli/core/agent_context.py +311 -0
  42. specify_cli/core/config.py +96 -0
  43. specify_cli/core/context_validation.py +362 -0
  44. specify_cli/core/dependency_graph.py +351 -0
  45. specify_cli/core/git_ops.py +129 -0
  46. specify_cli/core/multi_parent_merge.py +323 -0
  47. specify_cli/core/paths.py +260 -0
  48. specify_cli/core/project_resolver.py +110 -0
  49. specify_cli/core/stale_detection.py +263 -0
  50. specify_cli/core/tool_checker.py +79 -0
  51. specify_cli/core/utils.py +43 -0
  52. specify_cli/core/vcs/__init__.py +114 -0
  53. specify_cli/core/vcs/detection.py +341 -0
  54. specify_cli/core/vcs/exceptions.py +85 -0
  55. specify_cli/core/vcs/git.py +1304 -0
  56. specify_cli/core/vcs/jujutsu.py +1208 -0
  57. specify_cli/core/vcs/protocol.py +285 -0
  58. specify_cli/core/vcs/types.py +249 -0
  59. specify_cli/core/version_checker.py +261 -0
  60. specify_cli/core/worktree.py +506 -0
  61. specify_cli/dashboard/__init__.py +28 -0
  62. specify_cli/dashboard/diagnostics.py +204 -0
  63. specify_cli/dashboard/handlers/__init__.py +17 -0
  64. specify_cli/dashboard/handlers/api.py +143 -0
  65. specify_cli/dashboard/handlers/base.py +65 -0
  66. specify_cli/dashboard/handlers/features.py +390 -0
  67. specify_cli/dashboard/handlers/router.py +81 -0
  68. specify_cli/dashboard/handlers/static.py +50 -0
  69. specify_cli/dashboard/lifecycle.py +541 -0
  70. specify_cli/dashboard/scanner.py +437 -0
  71. specify_cli/dashboard/server.py +123 -0
  72. specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
  73. specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
  74. specify_cli/dashboard/static/spec-kitty.png +0 -0
  75. specify_cli/dashboard/templates/__init__.py +36 -0
  76. specify_cli/dashboard/templates/index.html +258 -0
  77. specify_cli/doc_generators.py +621 -0
  78. specify_cli/doc_state.py +408 -0
  79. specify_cli/frontmatter.py +384 -0
  80. specify_cli/gap_analysis.py +915 -0
  81. specify_cli/gitignore_manager.py +300 -0
  82. specify_cli/guards.py +145 -0
  83. specify_cli/legacy_detector.py +83 -0
  84. specify_cli/manifest.py +286 -0
  85. specify_cli/merge/__init__.py +63 -0
  86. specify_cli/merge/executor.py +653 -0
  87. specify_cli/merge/forecast.py +215 -0
  88. specify_cli/merge/ordering.py +126 -0
  89. specify_cli/merge/preflight.py +230 -0
  90. specify_cli/merge/state.py +185 -0
  91. specify_cli/merge/status_resolver.py +354 -0
  92. specify_cli/mission.py +654 -0
  93. specify_cli/missions/documentation/command-templates/implement.md +309 -0
  94. specify_cli/missions/documentation/command-templates/plan.md +275 -0
  95. specify_cli/missions/documentation/command-templates/review.md +344 -0
  96. specify_cli/missions/documentation/command-templates/specify.md +206 -0
  97. specify_cli/missions/documentation/command-templates/tasks.md +189 -0
  98. specify_cli/missions/documentation/mission.yaml +113 -0
  99. specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
  100. specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
  101. specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
  102. specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
  103. specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
  104. specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
  105. specify_cli/missions/documentation/templates/plan-template.md +269 -0
  106. specify_cli/missions/documentation/templates/release-template.md +222 -0
  107. specify_cli/missions/documentation/templates/spec-template.md +172 -0
  108. specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
  109. specify_cli/missions/documentation/templates/tasks-template.md +159 -0
  110. specify_cli/missions/research/command-templates/merge.md +388 -0
  111. specify_cli/missions/research/command-templates/plan.md +125 -0
  112. specify_cli/missions/research/command-templates/review.md +144 -0
  113. specify_cli/missions/research/command-templates/tasks.md +225 -0
  114. specify_cli/missions/research/mission.yaml +115 -0
  115. specify_cli/missions/research/templates/data-model-template.md +33 -0
  116. specify_cli/missions/research/templates/plan-template.md +161 -0
  117. specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
  118. specify_cli/missions/research/templates/research/source-register.csv +18 -0
  119. specify_cli/missions/research/templates/research-template.md +35 -0
  120. specify_cli/missions/research/templates/spec-template.md +64 -0
  121. specify_cli/missions/research/templates/task-prompt-template.md +148 -0
  122. specify_cli/missions/research/templates/tasks-template.md +114 -0
  123. specify_cli/missions/software-dev/command-templates/accept.md +75 -0
  124. specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
  125. specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
  126. specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
  127. specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
  128. specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
  129. specify_cli/missions/software-dev/command-templates/implement.md +41 -0
  130. specify_cli/missions/software-dev/command-templates/merge.md +383 -0
  131. specify_cli/missions/software-dev/command-templates/plan.md +171 -0
  132. specify_cli/missions/software-dev/command-templates/review.md +32 -0
  133. specify_cli/missions/software-dev/command-templates/specify.md +321 -0
  134. specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
  135. specify_cli/missions/software-dev/mission.yaml +100 -0
  136. specify_cli/missions/software-dev/templates/plan-template.md +132 -0
  137. specify_cli/missions/software-dev/templates/spec-template.md +116 -0
  138. specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
  139. specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
  140. specify_cli/orchestrator/__init__.py +75 -0
  141. specify_cli/orchestrator/agent_config.py +224 -0
  142. specify_cli/orchestrator/agents/__init__.py +170 -0
  143. specify_cli/orchestrator/agents/augment.py +112 -0
  144. specify_cli/orchestrator/agents/base.py +243 -0
  145. specify_cli/orchestrator/agents/claude.py +112 -0
  146. specify_cli/orchestrator/agents/codex.py +106 -0
  147. specify_cli/orchestrator/agents/copilot.py +137 -0
  148. specify_cli/orchestrator/agents/cursor.py +139 -0
  149. specify_cli/orchestrator/agents/gemini.py +115 -0
  150. specify_cli/orchestrator/agents/kilocode.py +94 -0
  151. specify_cli/orchestrator/agents/opencode.py +132 -0
  152. specify_cli/orchestrator/agents/qwen.py +96 -0
  153. specify_cli/orchestrator/config.py +455 -0
  154. specify_cli/orchestrator/executor.py +642 -0
  155. specify_cli/orchestrator/integration.py +1230 -0
  156. specify_cli/orchestrator/monitor.py +898 -0
  157. specify_cli/orchestrator/scheduler.py +832 -0
  158. specify_cli/orchestrator/state.py +508 -0
  159. specify_cli/orchestrator/testing/__init__.py +122 -0
  160. specify_cli/orchestrator/testing/availability.py +346 -0
  161. specify_cli/orchestrator/testing/fixtures.py +684 -0
  162. specify_cli/orchestrator/testing/paths.py +218 -0
  163. specify_cli/plan_validation.py +107 -0
  164. specify_cli/scripts/debug-dashboard-scan.py +61 -0
  165. specify_cli/scripts/tasks/acceptance_support.py +695 -0
  166. specify_cli/scripts/tasks/task_helpers.py +506 -0
  167. specify_cli/scripts/tasks/tasks_cli.py +848 -0
  168. specify_cli/scripts/validate_encoding.py +180 -0
  169. specify_cli/task_metadata_validation.py +274 -0
  170. specify_cli/tasks_support.py +447 -0
  171. specify_cli/template/__init__.py +47 -0
  172. specify_cli/template/asset_generator.py +206 -0
  173. specify_cli/template/github_client.py +334 -0
  174. specify_cli/template/manager.py +193 -0
  175. specify_cli/template/renderer.py +99 -0
  176. specify_cli/templates/AGENTS.md +190 -0
  177. specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
  178. specify_cli/templates/agent-file-template.md +35 -0
  179. specify_cli/templates/checklist-template.md +42 -0
  180. specify_cli/templates/claudeignore-template +58 -0
  181. specify_cli/templates/command-templates/accept.md +141 -0
  182. specify_cli/templates/command-templates/analyze.md +253 -0
  183. specify_cli/templates/command-templates/checklist.md +352 -0
  184. specify_cli/templates/command-templates/clarify.md +224 -0
  185. specify_cli/templates/command-templates/constitution.md +432 -0
  186. specify_cli/templates/command-templates/dashboard.md +175 -0
  187. specify_cli/templates/command-templates/implement.md +190 -0
  188. specify_cli/templates/command-templates/merge.md +374 -0
  189. specify_cli/templates/command-templates/plan.md +171 -0
  190. specify_cli/templates/command-templates/research.md +88 -0
  191. specify_cli/templates/command-templates/review.md +510 -0
  192. specify_cli/templates/command-templates/specify.md +321 -0
  193. specify_cli/templates/command-templates/status.md +92 -0
  194. specify_cli/templates/command-templates/tasks.md +199 -0
  195. specify_cli/templates/git-hooks/pre-commit +22 -0
  196. specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
  197. specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
  198. specify_cli/templates/plan-template.md +108 -0
  199. specify_cli/templates/spec-template.md +118 -0
  200. specify_cli/templates/task-prompt-template.md +165 -0
  201. specify_cli/templates/tasks-template.md +161 -0
  202. specify_cli/templates/vscode-settings.json +13 -0
  203. specify_cli/text_sanitization.py +225 -0
  204. specify_cli/upgrade/__init__.py +18 -0
  205. specify_cli/upgrade/detector.py +239 -0
  206. specify_cli/upgrade/metadata.py +182 -0
  207. specify_cli/upgrade/migrations/__init__.py +65 -0
  208. specify_cli/upgrade/migrations/base.py +80 -0
  209. specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
  210. specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
  211. specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
  212. specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
  213. specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
  214. specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
  215. specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
  216. specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
  217. specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
  218. specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
  219. specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
  220. specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
  221. specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
  222. specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
  223. specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
  224. specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
  225. specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
  226. specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
  227. specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
  228. specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
  229. specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
  230. specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
  231. specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
  232. specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
  233. specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
  234. specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
  235. specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
  236. specify_cli/upgrade/registry.py +121 -0
  237. specify_cli/upgrade/runner.py +284 -0
  238. specify_cli/validators/__init__.py +14 -0
  239. specify_cli/validators/paths.py +154 -0
  240. specify_cli/validators/research.py +428 -0
  241. specify_cli/verify_enhanced.py +270 -0
  242. 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
+ ]