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,832 @@
|
|
|
1
|
+
"""Scheduler for orchestrating work package execution.
|
|
2
|
+
|
|
3
|
+
This module handles:
|
|
4
|
+
- Dependency graph reading from WP frontmatter
|
|
5
|
+
- Ready WP detection (dependencies satisfied)
|
|
6
|
+
- Agent selection by role and priority
|
|
7
|
+
- Concurrency management via semaphores
|
|
8
|
+
- Single-agent mode handling
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, AsyncIterator
|
|
18
|
+
|
|
19
|
+
from specify_cli.core.dependency_graph import (
|
|
20
|
+
build_dependency_graph,
|
|
21
|
+
detect_cycles,
|
|
22
|
+
topological_sort,
|
|
23
|
+
)
|
|
24
|
+
from specify_cli.orchestrator.config import OrchestratorConfig, WPStatus
|
|
25
|
+
from specify_cli.orchestrator.state import OrchestrationRun, WPExecution
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Exceptions
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SchedulerError(Exception):
|
|
39
|
+
"""Base exception for scheduler errors."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DependencyGraphError(SchedulerError):
|
|
45
|
+
"""Raised when dependency graph is invalid."""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NoAgentAvailableError(SchedulerError):
|
|
51
|
+
"""Raised when no agent is available for a role."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Dependency Graph (T022)
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_wp_graph(feature_dir: Path) -> dict[str, list[str]]:
|
|
62
|
+
"""Build WP dependency graph from task frontmatter.
|
|
63
|
+
|
|
64
|
+
Wraps the existing build_dependency_graph function from core module.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
feature_dir: Path to feature directory (contains tasks/ subdirectory)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dict mapping WP ID to list of dependency WP IDs.
|
|
71
|
+
e.g., {"WP02": ["WP01"], "WP03": ["WP01", "WP02"]}
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
DependencyGraphError: If graph has cycles or invalid references.
|
|
75
|
+
"""
|
|
76
|
+
graph = build_dependency_graph(feature_dir)
|
|
77
|
+
|
|
78
|
+
if not graph:
|
|
79
|
+
logger.warning(f"No work packages found in {feature_dir}")
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
logger.info(f"Built dependency graph with {len(graph)} work packages")
|
|
83
|
+
return graph
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def validate_wp_graph(graph: dict[str, list[str]]) -> None:
|
|
87
|
+
"""Validate WP dependency graph.
|
|
88
|
+
|
|
89
|
+
Checks for:
|
|
90
|
+
- Circular dependencies
|
|
91
|
+
- Invalid dependency references
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
graph: Dependency graph from build_wp_graph()
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
DependencyGraphError: If validation fails.
|
|
98
|
+
"""
|
|
99
|
+
if not graph:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Check for cycles
|
|
103
|
+
cycles = detect_cycles(graph)
|
|
104
|
+
if cycles:
|
|
105
|
+
cycle_strs = [" -> ".join(cycle) for cycle in cycles]
|
|
106
|
+
raise DependencyGraphError(
|
|
107
|
+
f"Circular dependencies detected:\n " + "\n ".join(cycle_strs)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Check all dependencies exist in graph
|
|
111
|
+
all_wp_ids = set(graph.keys())
|
|
112
|
+
for wp_id, deps in graph.items():
|
|
113
|
+
for dep in deps:
|
|
114
|
+
if dep not in all_wp_ids:
|
|
115
|
+
raise DependencyGraphError(
|
|
116
|
+
f"WP {wp_id} depends on {dep}, but {dep} does not exist"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.debug("Dependency graph validation passed")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_topological_order(graph: dict[str, list[str]]) -> list[str]:
|
|
123
|
+
"""Get work packages in topological order.
|
|
124
|
+
|
|
125
|
+
Returns WPs ordered so that dependencies come before dependents.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
graph: Validated dependency graph
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of WP IDs in topological order
|
|
132
|
+
"""
|
|
133
|
+
if not graph:
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
return topological_sort(graph)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# Ready WP Detection (T023)
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_ready_wps(
|
|
145
|
+
graph: dict[str, list[str]],
|
|
146
|
+
state: OrchestrationRun,
|
|
147
|
+
) -> list[str]:
|
|
148
|
+
"""Return WP IDs that are ready to execute.
|
|
149
|
+
|
|
150
|
+
A WP is ready if:
|
|
151
|
+
1. All dependencies have completed successfully
|
|
152
|
+
2. WP itself is in "pending" or "rework" status
|
|
153
|
+
|
|
154
|
+
REWORK status means the WP was reviewed and rejected, needing
|
|
155
|
+
re-implementation with the review feedback.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
graph: Dependency graph from build_wp_graph()
|
|
159
|
+
state: Current orchestration state
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of WP IDs ready for execution, sorted by topological order
|
|
163
|
+
"""
|
|
164
|
+
ready = []
|
|
165
|
+
|
|
166
|
+
# Statuses that indicate a WP can be started/restarted
|
|
167
|
+
startable_statuses = {WPStatus.PENDING, WPStatus.REWORK}
|
|
168
|
+
|
|
169
|
+
for wp_id, deps in graph.items():
|
|
170
|
+
# Get WP state, defaulting to pending if not tracked yet
|
|
171
|
+
wp_state = state.work_packages.get(wp_id)
|
|
172
|
+
|
|
173
|
+
# Skip if not in a startable status
|
|
174
|
+
if wp_state and wp_state.status not in startable_statuses:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Check all dependencies completed successfully
|
|
178
|
+
all_deps_done = True
|
|
179
|
+
for dep_id in deps:
|
|
180
|
+
dep_state = state.work_packages.get(dep_id)
|
|
181
|
+
if not dep_state or dep_state.status != WPStatus.COMPLETED:
|
|
182
|
+
all_deps_done = False
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if all_deps_done:
|
|
186
|
+
ready.append(wp_id)
|
|
187
|
+
|
|
188
|
+
# Sort by topological order for determinism
|
|
189
|
+
if ready:
|
|
190
|
+
try:
|
|
191
|
+
topo_order = get_topological_order(graph)
|
|
192
|
+
order_map = {wp: i for i, wp in enumerate(topo_order)}
|
|
193
|
+
ready.sort(key=lambda wp: order_map.get(wp, 999))
|
|
194
|
+
except ValueError:
|
|
195
|
+
# If topo sort fails (shouldn't after validation), just sort by ID
|
|
196
|
+
ready.sort()
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Ready WPs: {ready}")
|
|
199
|
+
return ready
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_blocked_wps(
|
|
203
|
+
graph: dict[str, list[str]],
|
|
204
|
+
state: OrchestrationRun,
|
|
205
|
+
) -> dict[str, list[str]]:
|
|
206
|
+
"""Get WPs that are blocked waiting on dependencies.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
graph: Dependency graph
|
|
210
|
+
state: Current orchestration state
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict mapping blocked WP ID to list of blocking dependency IDs
|
|
214
|
+
"""
|
|
215
|
+
blocked = {}
|
|
216
|
+
|
|
217
|
+
for wp_id, deps in graph.items():
|
|
218
|
+
wp_state = state.work_packages.get(wp_id)
|
|
219
|
+
|
|
220
|
+
# Only check pending WPs
|
|
221
|
+
if wp_state and wp_state.status != WPStatus.PENDING:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
# Find incomplete dependencies
|
|
225
|
+
blocking_deps = []
|
|
226
|
+
for dep_id in deps:
|
|
227
|
+
dep_state = state.work_packages.get(dep_id)
|
|
228
|
+
if not dep_state or dep_state.status != WPStatus.COMPLETED:
|
|
229
|
+
blocking_deps.append(dep_id)
|
|
230
|
+
|
|
231
|
+
if blocking_deps:
|
|
232
|
+
blocked[wp_id] = blocking_deps
|
|
233
|
+
|
|
234
|
+
return blocked
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# =============================================================================
|
|
238
|
+
# Agent Selection (T024)
|
|
239
|
+
# =============================================================================
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _count_active_agent_tasks(
|
|
243
|
+
agent_id: str,
|
|
244
|
+
state: OrchestrationRun | None,
|
|
245
|
+
) -> int:
|
|
246
|
+
"""Count how many tasks an agent is currently running.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
agent_id: Agent identifier
|
|
250
|
+
state: Current orchestration state
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Number of active tasks for this agent
|
|
254
|
+
"""
|
|
255
|
+
if not state:
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
count = 0
|
|
259
|
+
for wp in state.work_packages.values():
|
|
260
|
+
# Count implementation tasks
|
|
261
|
+
if (
|
|
262
|
+
wp.status == WPStatus.IMPLEMENTATION
|
|
263
|
+
and wp.implementation_agent == agent_id
|
|
264
|
+
):
|
|
265
|
+
count += 1
|
|
266
|
+
|
|
267
|
+
# Count review tasks
|
|
268
|
+
if wp.status == WPStatus.REVIEW and wp.review_agent == agent_id:
|
|
269
|
+
count += 1
|
|
270
|
+
|
|
271
|
+
return count
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _agent_at_limit(
|
|
275
|
+
agent_id: str,
|
|
276
|
+
config: OrchestratorConfig,
|
|
277
|
+
state: OrchestrationRun | None,
|
|
278
|
+
) -> bool:
|
|
279
|
+
"""Check if agent has reached its concurrency limit.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
agent_id: Agent identifier
|
|
283
|
+
config: Orchestrator configuration
|
|
284
|
+
state: Current orchestration state
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if agent is at its max_concurrent limit
|
|
288
|
+
"""
|
|
289
|
+
agent_config = config.agents.get(agent_id)
|
|
290
|
+
if not agent_config:
|
|
291
|
+
return True # Unknown agent treated as at limit
|
|
292
|
+
|
|
293
|
+
active_count = _count_active_agent_tasks(agent_id, state)
|
|
294
|
+
return active_count >= agent_config.max_concurrent
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def select_agent_from_user_config(
|
|
298
|
+
repo_root: Path,
|
|
299
|
+
role: str,
|
|
300
|
+
exclude_agent: str | None = None,
|
|
301
|
+
override_agent: str | None = None,
|
|
302
|
+
) -> str | None:
|
|
303
|
+
"""Select agent using user configuration from spec-kitty init.
|
|
304
|
+
|
|
305
|
+
This is the preferred way to select agents - uses the configuration
|
|
306
|
+
set by the user during `spec-kitty init`.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
repo_root: Repository root for loading config
|
|
310
|
+
role: "implementation" or "review"
|
|
311
|
+
exclude_agent: Agent to exclude (for cross-review)
|
|
312
|
+
override_agent: CLI override to use specific agent
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Canonical agent ID (normalized from aliases) or None if no agents configured
|
|
316
|
+
"""
|
|
317
|
+
from specify_cli.orchestrator.agent_config import load_agent_config
|
|
318
|
+
from specify_cli.orchestrator.agents import normalize_agent_id
|
|
319
|
+
|
|
320
|
+
# CLI override takes precedence
|
|
321
|
+
if override_agent:
|
|
322
|
+
logger.info(f"Using CLI override agent: {override_agent}")
|
|
323
|
+
return normalize_agent_id(override_agent)
|
|
324
|
+
|
|
325
|
+
config = load_agent_config(repo_root)
|
|
326
|
+
|
|
327
|
+
if not config.available:
|
|
328
|
+
logger.warning("No agents configured in .kittify/config.yaml")
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
# Select agent and normalize to canonical ID
|
|
332
|
+
if role == "implementation":
|
|
333
|
+
selected = config.select_implementer(exclude=exclude_agent)
|
|
334
|
+
elif role == "review":
|
|
335
|
+
selected = config.select_reviewer(implementer=exclude_agent)
|
|
336
|
+
else:
|
|
337
|
+
logger.warning(f"Unknown role: {role}")
|
|
338
|
+
selected = config.available[0] if config.available else None
|
|
339
|
+
|
|
340
|
+
return normalize_agent_id(selected) if selected else None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def select_agent(
|
|
344
|
+
config: OrchestratorConfig,
|
|
345
|
+
role: str,
|
|
346
|
+
exclude_agent: str | None = None,
|
|
347
|
+
state: OrchestrationRun | None = None,
|
|
348
|
+
) -> str | None:
|
|
349
|
+
"""Select highest-priority available agent for role.
|
|
350
|
+
|
|
351
|
+
NOTE: This is the legacy selection method using OrchestratorConfig.
|
|
352
|
+
Prefer select_agent_from_user_config() which uses the configuration
|
|
353
|
+
set during `spec-kitty init`.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
config: Orchestrator configuration
|
|
357
|
+
role: "implementation" or "review"
|
|
358
|
+
exclude_agent: Agent to exclude (for cross-agent review)
|
|
359
|
+
state: Current state (for concurrency tracking)
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Agent ID or None if no agent available
|
|
363
|
+
"""
|
|
364
|
+
# Get candidates from defaults, maintaining priority order
|
|
365
|
+
candidates = config.defaults.get(role, [])
|
|
366
|
+
|
|
367
|
+
if not candidates:
|
|
368
|
+
# Fall back to all enabled agents with this role
|
|
369
|
+
candidates = [
|
|
370
|
+
agent_id
|
|
371
|
+
for agent_id, agent_config in config.agents.items()
|
|
372
|
+
if agent_config.enabled and role in agent_config.roles
|
|
373
|
+
]
|
|
374
|
+
# Sort by priority
|
|
375
|
+
candidates.sort(
|
|
376
|
+
key=lambda aid: config.agents[aid].priority
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
for agent_id in candidates:
|
|
380
|
+
agent_config = config.agents.get(agent_id)
|
|
381
|
+
if not agent_config:
|
|
382
|
+
logger.debug(f"Agent {agent_id} not found in config")
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
if not agent_config.enabled:
|
|
386
|
+
logger.debug(f"Agent {agent_id} is disabled")
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
if role not in agent_config.roles:
|
|
390
|
+
logger.debug(f"Agent {agent_id} does not support role {role}")
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
if agent_id == exclude_agent:
|
|
394
|
+
logger.debug(f"Agent {agent_id} excluded for cross-agent review")
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
# Check concurrency limit
|
|
398
|
+
if _agent_at_limit(agent_id, config, state):
|
|
399
|
+
logger.debug(f"Agent {agent_id} at concurrency limit")
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
logger.info(f"Selected agent {agent_id} for {role}")
|
|
403
|
+
return agent_id
|
|
404
|
+
|
|
405
|
+
logger.warning(f"No agent available for role {role}")
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def select_review_agent_from_user_config(
|
|
410
|
+
repo_root: Path,
|
|
411
|
+
implementation_agent: str,
|
|
412
|
+
override_agent: str | None = None,
|
|
413
|
+
) -> str | None:
|
|
414
|
+
"""Select review agent using user configuration from spec-kitty init.
|
|
415
|
+
|
|
416
|
+
Prefers a different agent than implementation for cross-review.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
repo_root: Repository root for loading config
|
|
420
|
+
implementation_agent: Agent that did implementation (may be alias or canonical)
|
|
421
|
+
override_agent: CLI override to use specific agent
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Canonical agent ID (normalized from aliases) for review
|
|
425
|
+
"""
|
|
426
|
+
from specify_cli.orchestrator.agent_config import load_agent_config
|
|
427
|
+
from specify_cli.orchestrator.agents import normalize_agent_id
|
|
428
|
+
|
|
429
|
+
# CLI override takes precedence
|
|
430
|
+
if override_agent:
|
|
431
|
+
logger.info(f"Using CLI override review agent: {override_agent}")
|
|
432
|
+
return normalize_agent_id(override_agent)
|
|
433
|
+
|
|
434
|
+
config = load_agent_config(repo_root)
|
|
435
|
+
|
|
436
|
+
if not config.available:
|
|
437
|
+
logger.warning("No agents configured, using implementer for review")
|
|
438
|
+
return normalize_agent_id(implementation_agent)
|
|
439
|
+
|
|
440
|
+
selected = config.select_reviewer(implementer=implementation_agent)
|
|
441
|
+
return normalize_agent_id(selected) if selected else None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def select_review_agent(
|
|
445
|
+
config: OrchestratorConfig,
|
|
446
|
+
implementation_agent: str,
|
|
447
|
+
state: OrchestrationRun | None = None,
|
|
448
|
+
) -> str | None:
|
|
449
|
+
"""Select review agent, excluding the implementation agent for cross-review.
|
|
450
|
+
|
|
451
|
+
NOTE: This is the legacy selection method using OrchestratorConfig.
|
|
452
|
+
Prefer select_review_agent_from_user_config() which uses the configuration
|
|
453
|
+
set during `spec-kitty init`.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
config: Orchestrator configuration
|
|
457
|
+
implementation_agent: Agent that did implementation
|
|
458
|
+
state: Current state for concurrency tracking
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Agent ID for review, or None if unavailable
|
|
462
|
+
"""
|
|
463
|
+
# In single-agent mode, use the same agent
|
|
464
|
+
if is_single_agent_mode(config):
|
|
465
|
+
logger.info(
|
|
466
|
+
f"Single-agent mode: using {implementation_agent} for review"
|
|
467
|
+
)
|
|
468
|
+
return implementation_agent
|
|
469
|
+
|
|
470
|
+
# Try to find a different agent for cross-review
|
|
471
|
+
review_agent = select_agent(
|
|
472
|
+
config,
|
|
473
|
+
role="review",
|
|
474
|
+
exclude_agent=implementation_agent,
|
|
475
|
+
state=state,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if review_agent:
|
|
479
|
+
return review_agent
|
|
480
|
+
|
|
481
|
+
# If no other agent available, fall back to same agent with warning
|
|
482
|
+
logger.warning(
|
|
483
|
+
f"No cross-review agent available, using {implementation_agent}"
|
|
484
|
+
)
|
|
485
|
+
return implementation_agent
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# =============================================================================
|
|
489
|
+
# Concurrency Management (T025)
|
|
490
|
+
# =============================================================================
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class ConcurrencyManager:
|
|
494
|
+
"""Manages concurrency limits for orchestration.
|
|
495
|
+
|
|
496
|
+
Uses asyncio.Semaphore to limit:
|
|
497
|
+
- Global concurrent processes
|
|
498
|
+
- Per-agent concurrent processes
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
def __init__(self, config: OrchestratorConfig):
|
|
502
|
+
"""Initialize concurrency manager.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
config: Orchestrator configuration with concurrency limits
|
|
506
|
+
"""
|
|
507
|
+
self.config = config
|
|
508
|
+
self.global_semaphore = asyncio.Semaphore(config.global_concurrency)
|
|
509
|
+
self.agent_semaphores: dict[str, asyncio.Semaphore] = {}
|
|
510
|
+
|
|
511
|
+
# Create per-agent semaphores
|
|
512
|
+
for agent_id, agent_config in config.agents.items():
|
|
513
|
+
self.agent_semaphores[agent_id] = asyncio.Semaphore(
|
|
514
|
+
agent_config.max_concurrent
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
logger.info(
|
|
518
|
+
f"ConcurrencyManager initialized: global={config.global_concurrency}, "
|
|
519
|
+
f"agents={len(self.agent_semaphores)}"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def _get_agent_semaphore(self, agent_id: str) -> asyncio.Semaphore:
|
|
523
|
+
"""Get or create semaphore for agent.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
agent_id: Agent identifier
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Semaphore for the agent
|
|
530
|
+
"""
|
|
531
|
+
if agent_id not in self.agent_semaphores:
|
|
532
|
+
# Create with default limit if not configured
|
|
533
|
+
default_limit = 2
|
|
534
|
+
self.agent_semaphores[agent_id] = asyncio.Semaphore(default_limit)
|
|
535
|
+
logger.warning(
|
|
536
|
+
f"Created default semaphore for unconfigured agent {agent_id}"
|
|
537
|
+
)
|
|
538
|
+
return self.agent_semaphores[agent_id]
|
|
539
|
+
|
|
540
|
+
async def acquire(self, agent_id: str) -> None:
|
|
541
|
+
"""Acquire both global and agent-specific semaphores.
|
|
542
|
+
|
|
543
|
+
Always acquires global first to prevent deadlocks.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
agent_id: Agent identifier
|
|
547
|
+
"""
|
|
548
|
+
# Always acquire global first to prevent deadlock
|
|
549
|
+
await self.global_semaphore.acquire()
|
|
550
|
+
try:
|
|
551
|
+
agent_sem = self._get_agent_semaphore(agent_id)
|
|
552
|
+
await agent_sem.acquire()
|
|
553
|
+
except Exception:
|
|
554
|
+
# Release global if agent acquisition fails
|
|
555
|
+
self.global_semaphore.release()
|
|
556
|
+
raise
|
|
557
|
+
|
|
558
|
+
logger.debug(f"Acquired semaphores for {agent_id}")
|
|
559
|
+
|
|
560
|
+
def release(self, agent_id: str) -> None:
|
|
561
|
+
"""Release both semaphores.
|
|
562
|
+
|
|
563
|
+
Releases in reverse order of acquisition.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
agent_id: Agent identifier
|
|
567
|
+
"""
|
|
568
|
+
agent_sem = self._get_agent_semaphore(agent_id)
|
|
569
|
+
agent_sem.release()
|
|
570
|
+
self.global_semaphore.release()
|
|
571
|
+
logger.debug(f"Released semaphores for {agent_id}")
|
|
572
|
+
|
|
573
|
+
@asynccontextmanager
|
|
574
|
+
async def throttle(self, agent_id: str) -> AsyncIterator[None]:
|
|
575
|
+
"""Context manager for throttled execution.
|
|
576
|
+
|
|
577
|
+
Acquires semaphores on entry, releases on exit.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
agent_id: Agent identifier
|
|
581
|
+
|
|
582
|
+
Yields:
|
|
583
|
+
None after acquiring semaphores
|
|
584
|
+
"""
|
|
585
|
+
await self.acquire(agent_id)
|
|
586
|
+
try:
|
|
587
|
+
yield
|
|
588
|
+
finally:
|
|
589
|
+
self.release(agent_id)
|
|
590
|
+
|
|
591
|
+
def get_available_slots(self) -> int:
|
|
592
|
+
"""Get number of available global execution slots.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
Number of slots available (0 if at limit)
|
|
596
|
+
"""
|
|
597
|
+
# Semaphore doesn't expose count directly, so we track it
|
|
598
|
+
# This is a heuristic based on the initial value
|
|
599
|
+
return self.config.global_concurrency - (
|
|
600
|
+
self.config.global_concurrency - self.global_semaphore._value
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def get_agent_available_slots(self, agent_id: str) -> int:
|
|
604
|
+
"""Get number of available slots for specific agent.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
agent_id: Agent identifier
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Number of slots available for this agent
|
|
611
|
+
"""
|
|
612
|
+
agent_sem = self._get_agent_semaphore(agent_id)
|
|
613
|
+
agent_config = self.config.agents.get(agent_id)
|
|
614
|
+
max_concurrent = agent_config.max_concurrent if agent_config else 2
|
|
615
|
+
return max_concurrent - (max_concurrent - agent_sem._value)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# =============================================================================
|
|
619
|
+
# Single-Agent Mode (T026)
|
|
620
|
+
# =============================================================================
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# Default delay between implementation and review in single-agent mode
|
|
624
|
+
DEFAULT_SINGLE_AGENT_DELAY = 60 # seconds
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def is_single_agent_mode(config: OrchestratorConfig) -> bool:
|
|
628
|
+
"""Check if operating in single-agent mode.
|
|
629
|
+
|
|
630
|
+
Single-agent mode is active when:
|
|
631
|
+
- Explicitly enabled via config.single_agent_mode
|
|
632
|
+
- Only one agent is enabled (auto-detected)
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
config: Orchestrator configuration
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
True if single-agent mode is active
|
|
639
|
+
"""
|
|
640
|
+
# Explicit configuration
|
|
641
|
+
if config.single_agent_mode:
|
|
642
|
+
return True
|
|
643
|
+
|
|
644
|
+
# Auto-detect: only one agent enabled
|
|
645
|
+
enabled_agents = [
|
|
646
|
+
aid for aid, ac in config.agents.items()
|
|
647
|
+
if ac.enabled
|
|
648
|
+
]
|
|
649
|
+
return len(enabled_agents) == 1
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def get_single_agent(config: OrchestratorConfig) -> str | None:
|
|
653
|
+
"""Get the single agent ID when in single-agent mode.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
config: Orchestrator configuration
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
Agent ID if in single-agent mode, None otherwise
|
|
660
|
+
"""
|
|
661
|
+
if not is_single_agent_mode(config):
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
# Use explicitly configured agent
|
|
665
|
+
if config.single_agent:
|
|
666
|
+
return config.single_agent
|
|
667
|
+
|
|
668
|
+
# Auto-detect: return the only enabled agent
|
|
669
|
+
enabled_agents = [
|
|
670
|
+
aid for aid, ac in config.agents.items()
|
|
671
|
+
if ac.enabled
|
|
672
|
+
]
|
|
673
|
+
return enabled_agents[0] if enabled_agents else None
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
async def single_agent_review_delay(
|
|
677
|
+
delay_seconds: int | None = None,
|
|
678
|
+
) -> None:
|
|
679
|
+
"""Apply delay before single-agent review.
|
|
680
|
+
|
|
681
|
+
The delay helps the agent "forget" its implementation context
|
|
682
|
+
and review with a fresher perspective.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
delay_seconds: Delay in seconds (defaults to 60)
|
|
686
|
+
"""
|
|
687
|
+
delay = delay_seconds or DEFAULT_SINGLE_AGENT_DELAY
|
|
688
|
+
logger.info(
|
|
689
|
+
f"Single-agent mode: waiting {delay}s before review "
|
|
690
|
+
"to provide fresh perspective"
|
|
691
|
+
)
|
|
692
|
+
await asyncio.sleep(delay)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# =============================================================================
|
|
696
|
+
# Scheduler State
|
|
697
|
+
# =============================================================================
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class SchedulerState:
|
|
701
|
+
"""Tracks scheduler state during orchestration.
|
|
702
|
+
|
|
703
|
+
Combines configuration, dependency graph, and concurrency management.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
def __init__(
|
|
707
|
+
self,
|
|
708
|
+
config: OrchestratorConfig,
|
|
709
|
+
feature_dir: Path,
|
|
710
|
+
):
|
|
711
|
+
"""Initialize scheduler state.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
config: Orchestrator configuration
|
|
715
|
+
feature_dir: Path to feature directory
|
|
716
|
+
|
|
717
|
+
Raises:
|
|
718
|
+
DependencyGraphError: If dependency graph is invalid
|
|
719
|
+
"""
|
|
720
|
+
self.config = config
|
|
721
|
+
self.feature_dir = feature_dir
|
|
722
|
+
|
|
723
|
+
# Build and validate dependency graph
|
|
724
|
+
self.graph = build_wp_graph(feature_dir)
|
|
725
|
+
validate_wp_graph(self.graph)
|
|
726
|
+
|
|
727
|
+
# Initialize concurrency manager
|
|
728
|
+
self.concurrency = ConcurrencyManager(config)
|
|
729
|
+
|
|
730
|
+
# Track single-agent mode
|
|
731
|
+
self.single_agent_mode = is_single_agent_mode(config)
|
|
732
|
+
self.single_agent = get_single_agent(config)
|
|
733
|
+
|
|
734
|
+
if self.single_agent_mode:
|
|
735
|
+
logger.warning(
|
|
736
|
+
f"Single-agent mode active: {self.single_agent}. "
|
|
737
|
+
"Cross-agent review will not be available."
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def get_ready_wps(self, state: OrchestrationRun) -> list[str]:
|
|
741
|
+
"""Get WPs ready for execution.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
state: Current orchestration state
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
List of ready WP IDs
|
|
748
|
+
"""
|
|
749
|
+
return get_ready_wps(self.graph, state)
|
|
750
|
+
|
|
751
|
+
def select_implementation_agent(
|
|
752
|
+
self,
|
|
753
|
+
state: OrchestrationRun,
|
|
754
|
+
) -> str | None:
|
|
755
|
+
"""Select agent for implementation.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
state: Current orchestration state
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Agent ID or None
|
|
762
|
+
"""
|
|
763
|
+
if self.single_agent_mode:
|
|
764
|
+
return self.single_agent
|
|
765
|
+
return select_agent(self.config, "implementation", state=state)
|
|
766
|
+
|
|
767
|
+
def select_review_agent(
|
|
768
|
+
self,
|
|
769
|
+
implementation_agent: str,
|
|
770
|
+
state: OrchestrationRun,
|
|
771
|
+
) -> str | None:
|
|
772
|
+
"""Select agent for review.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
implementation_agent: Agent that did implementation
|
|
776
|
+
state: Current orchestration state
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
Agent ID or None
|
|
780
|
+
"""
|
|
781
|
+
return select_review_agent(
|
|
782
|
+
self.config,
|
|
783
|
+
implementation_agent,
|
|
784
|
+
state=state,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
def initialize_wp_state(
|
|
788
|
+
self,
|
|
789
|
+
state: OrchestrationRun,
|
|
790
|
+
) -> None:
|
|
791
|
+
"""Initialize WP execution state for all WPs in graph.
|
|
792
|
+
|
|
793
|
+
Creates WPExecution entries for WPs not already in state.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
state: Orchestration state to update
|
|
797
|
+
"""
|
|
798
|
+
for wp_id in self.graph:
|
|
799
|
+
if wp_id not in state.work_packages:
|
|
800
|
+
state.work_packages[wp_id] = WPExecution(wp_id=wp_id)
|
|
801
|
+
|
|
802
|
+
state.wps_total = len(self.graph)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
__all__ = [
|
|
806
|
+
# Exceptions
|
|
807
|
+
"SchedulerError",
|
|
808
|
+
"DependencyGraphError",
|
|
809
|
+
"NoAgentAvailableError",
|
|
810
|
+
# Graph functions (T022)
|
|
811
|
+
"build_wp_graph",
|
|
812
|
+
"validate_wp_graph",
|
|
813
|
+
"get_topological_order",
|
|
814
|
+
# Ready detection (T023)
|
|
815
|
+
"get_ready_wps",
|
|
816
|
+
"get_blocked_wps",
|
|
817
|
+
# Agent selection (T024) - user config based (preferred)
|
|
818
|
+
"select_agent_from_user_config",
|
|
819
|
+
"select_review_agent_from_user_config",
|
|
820
|
+
# Agent selection (T024) - legacy (for backwards compatibility)
|
|
821
|
+
"select_agent",
|
|
822
|
+
"select_review_agent",
|
|
823
|
+
# Concurrency (T025)
|
|
824
|
+
"ConcurrencyManager",
|
|
825
|
+
# Single-agent mode (T026)
|
|
826
|
+
"is_single_agent_mode",
|
|
827
|
+
"get_single_agent",
|
|
828
|
+
"single_agent_review_delay",
|
|
829
|
+
"DEFAULT_SINGLE_AGENT_DELAY",
|
|
830
|
+
# State
|
|
831
|
+
"SchedulerState",
|
|
832
|
+
]
|