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,455 @@
|
|
|
1
|
+
"""Configuration module for the orchestrator.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Status enums (OrchestrationStatus, WPStatus, FallbackStrategy)
|
|
5
|
+
- Config dataclasses (AgentConfig, OrchestratorConfig)
|
|
6
|
+
- YAML parsing and validation
|
|
7
|
+
- Default config generation based on installed agents
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import shutil
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ruamel.yaml import YAML
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Enums (T002)
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OrchestrationStatus(str, Enum):
|
|
30
|
+
"""Status of an orchestration run."""
|
|
31
|
+
|
|
32
|
+
PENDING = "pending"
|
|
33
|
+
RUNNING = "running"
|
|
34
|
+
PAUSED = "paused"
|
|
35
|
+
COMPLETED = "completed"
|
|
36
|
+
FAILED = "failed"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WPStatus(str, Enum):
|
|
40
|
+
"""Status of a work package execution.
|
|
41
|
+
|
|
42
|
+
State machine transitions:
|
|
43
|
+
PENDING → READY (dependencies satisfied)
|
|
44
|
+
READY → IMPLEMENTATION (agent starts)
|
|
45
|
+
IMPLEMENTATION → REVIEW (implementation completes)
|
|
46
|
+
REVIEW → COMPLETED (review approves)
|
|
47
|
+
REVIEW → REWORK (review rejects with feedback)
|
|
48
|
+
REWORK → IMPLEMENTATION (re-implementation starts)
|
|
49
|
+
Any → FAILED (max retries exceeded or unrecoverable error)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
PENDING = "pending"
|
|
53
|
+
READY = "ready"
|
|
54
|
+
IMPLEMENTATION = "implementation"
|
|
55
|
+
REVIEW = "review"
|
|
56
|
+
REWORK = "rework" # Review rejected, needs re-implementation
|
|
57
|
+
COMPLETED = "completed"
|
|
58
|
+
FAILED = "failed"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FallbackStrategy(str, Enum):
|
|
62
|
+
"""Strategy for handling agent failures."""
|
|
63
|
+
|
|
64
|
+
NEXT_IN_LIST = "next_in_list"
|
|
65
|
+
SAME_AGENT = "same_agent"
|
|
66
|
+
FAIL = "fail"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Exceptions
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigValidationError(Exception):
|
|
75
|
+
"""Raised when configuration validation fails."""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NoAgentsError(Exception):
|
|
81
|
+
"""Raised when no agents are installed or enabled."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Dataclasses (T003)
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class AgentConfig:
|
|
93
|
+
"""Configuration for a single AI agent."""
|
|
94
|
+
|
|
95
|
+
agent_id: str
|
|
96
|
+
enabled: bool = True
|
|
97
|
+
roles: list[str] = field(default_factory=lambda: ["implementation", "review"])
|
|
98
|
+
priority: int = 50
|
|
99
|
+
max_concurrent: int = 100 # Effectively unlimited - let dependency graph be the limit
|
|
100
|
+
timeout_seconds: int = 600
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class OrchestratorConfig:
|
|
105
|
+
"""Main orchestrator configuration."""
|
|
106
|
+
|
|
107
|
+
version: str = "1.0"
|
|
108
|
+
defaults: dict[str, list[str]] = field(default_factory=dict)
|
|
109
|
+
agents: dict[str, AgentConfig] = field(default_factory=dict)
|
|
110
|
+
fallback_strategy: FallbackStrategy = FallbackStrategy.NEXT_IN_LIST
|
|
111
|
+
max_retries: int = 3
|
|
112
|
+
single_agent_mode: bool = False
|
|
113
|
+
single_agent: str | None = None
|
|
114
|
+
global_concurrency: int = 100 # Effectively unlimited - let dependency graph be the limit
|
|
115
|
+
global_timeout: int = 3600
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# =============================================================================
|
|
119
|
+
# Agent Detection
|
|
120
|
+
# =============================================================================
|
|
121
|
+
|
|
122
|
+
# Map of agent ID to CLI command name for detection
|
|
123
|
+
AGENT_COMMANDS: dict[str, str] = {
|
|
124
|
+
"claude-code": "claude",
|
|
125
|
+
"codex": "codex",
|
|
126
|
+
"copilot": "gh", # GitHub Copilot uses gh CLI
|
|
127
|
+
"gemini": "gemini",
|
|
128
|
+
"qwen": "qwen",
|
|
129
|
+
"opencode": "opencode",
|
|
130
|
+
"kilocode": "kilocode",
|
|
131
|
+
"augment": "auggie",
|
|
132
|
+
"cursor": "cursor",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Default priority order (lower = higher priority)
|
|
136
|
+
AGENT_PRIORITIES: dict[str, int] = {
|
|
137
|
+
"claude-code": 10,
|
|
138
|
+
"codex": 20,
|
|
139
|
+
"copilot": 30,
|
|
140
|
+
"gemini": 40,
|
|
141
|
+
"qwen": 50,
|
|
142
|
+
"opencode": 60,
|
|
143
|
+
"kilocode": 70,
|
|
144
|
+
"augment": 80,
|
|
145
|
+
"cursor": 90,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def detect_installed_agents() -> list[str]:
|
|
150
|
+
"""Detect which AI agents are installed on the system.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of agent IDs that have their CLI tools available.
|
|
154
|
+
"""
|
|
155
|
+
installed = []
|
|
156
|
+
for agent_id, command in AGENT_COMMANDS.items():
|
|
157
|
+
if shutil.which(command):
|
|
158
|
+
installed.append(agent_id)
|
|
159
|
+
logger.debug(f"Detected agent: {agent_id} ({command})")
|
|
160
|
+
else:
|
|
161
|
+
logger.debug(f"Agent not found: {agent_id} ({command})")
|
|
162
|
+
|
|
163
|
+
return installed
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# Config Parsing (T004)
|
|
168
|
+
# =============================================================================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _parse_agent_config(agent_id: str, data: dict[str, Any]) -> AgentConfig:
|
|
172
|
+
"""Parse a single agent configuration from YAML data."""
|
|
173
|
+
return AgentConfig(
|
|
174
|
+
agent_id=agent_id,
|
|
175
|
+
enabled=data.get("enabled", True),
|
|
176
|
+
roles=data.get("roles", ["implementation", "review"]),
|
|
177
|
+
priority=data.get("priority", AGENT_PRIORITIES.get(agent_id, 50)),
|
|
178
|
+
max_concurrent=data.get("max_concurrent", 2),
|
|
179
|
+
timeout_seconds=data.get("timeout_seconds", 600),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _parse_fallback_strategy(value: str) -> FallbackStrategy:
|
|
184
|
+
"""Parse fallback strategy from string."""
|
|
185
|
+
try:
|
|
186
|
+
return FallbackStrategy(value)
|
|
187
|
+
except ValueError:
|
|
188
|
+
valid = [s.value for s in FallbackStrategy]
|
|
189
|
+
raise ConfigValidationError(
|
|
190
|
+
f"Invalid fallback_strategy '{value}'. Must be one of: {valid}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_config(data: dict[str, Any]) -> OrchestratorConfig:
|
|
195
|
+
"""Parse raw YAML data into OrchestratorConfig.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
data: Dictionary loaded from YAML file.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Parsed OrchestratorConfig instance.
|
|
202
|
+
"""
|
|
203
|
+
# Parse agents
|
|
204
|
+
agents: dict[str, AgentConfig] = {}
|
|
205
|
+
agents_data = data.get("agents", {})
|
|
206
|
+
for agent_id, agent_data in agents_data.items():
|
|
207
|
+
if isinstance(agent_data, dict):
|
|
208
|
+
agents[agent_id] = _parse_agent_config(agent_id, agent_data)
|
|
209
|
+
else:
|
|
210
|
+
# Simple enabled/disabled format
|
|
211
|
+
agents[agent_id] = AgentConfig(
|
|
212
|
+
agent_id=agent_id,
|
|
213
|
+
enabled=bool(agent_data),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Parse single_agent_mode
|
|
217
|
+
single_agent_mode_data = data.get("single_agent_mode", {})
|
|
218
|
+
if isinstance(single_agent_mode_data, dict):
|
|
219
|
+
single_agent_mode = single_agent_mode_data.get("enabled", False)
|
|
220
|
+
single_agent = single_agent_mode_data.get("agent")
|
|
221
|
+
else:
|
|
222
|
+
single_agent_mode = bool(single_agent_mode_data)
|
|
223
|
+
single_agent = None
|
|
224
|
+
|
|
225
|
+
# Parse fallback strategy
|
|
226
|
+
fallback_str = data.get("fallback_strategy", "next_in_list")
|
|
227
|
+
fallback_strategy = _parse_fallback_strategy(fallback_str)
|
|
228
|
+
|
|
229
|
+
return OrchestratorConfig(
|
|
230
|
+
version=data.get("version", "1.0"),
|
|
231
|
+
defaults=data.get("defaults", {}),
|
|
232
|
+
agents=agents,
|
|
233
|
+
fallback_strategy=fallback_strategy,
|
|
234
|
+
max_retries=data.get("max_retries", 3),
|
|
235
|
+
single_agent_mode=single_agent_mode,
|
|
236
|
+
single_agent=single_agent,
|
|
237
|
+
global_concurrency=data.get("global_concurrency", 5),
|
|
238
|
+
global_timeout=data.get("global_timeout", 3600),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_config(config: OrchestratorConfig) -> None:
|
|
243
|
+
"""Validate orchestrator configuration.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
config: Configuration to validate.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
ConfigValidationError: If validation fails.
|
|
250
|
+
"""
|
|
251
|
+
errors: list[str] = []
|
|
252
|
+
|
|
253
|
+
# Check defaults reference existing agents
|
|
254
|
+
for role, agent_ids in config.defaults.items():
|
|
255
|
+
for agent_id in agent_ids:
|
|
256
|
+
if agent_id not in config.agents:
|
|
257
|
+
errors.append(
|
|
258
|
+
f"defaults.{role} references unknown agent '{agent_id}'"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Check single_agent_mode configuration
|
|
262
|
+
if config.single_agent_mode:
|
|
263
|
+
if not config.single_agent:
|
|
264
|
+
errors.append(
|
|
265
|
+
"single_agent_mode is enabled but no agent specified"
|
|
266
|
+
)
|
|
267
|
+
elif config.single_agent not in config.agents:
|
|
268
|
+
errors.append(
|
|
269
|
+
f"single_agent '{config.single_agent}' not found in agents"
|
|
270
|
+
)
|
|
271
|
+
elif not config.agents[config.single_agent].enabled:
|
|
272
|
+
errors.append(
|
|
273
|
+
f"single_agent '{config.single_agent}' is not enabled"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Check numeric constraints
|
|
277
|
+
if config.max_retries < 0:
|
|
278
|
+
errors.append(f"max_retries must be >= 0, got {config.max_retries}")
|
|
279
|
+
|
|
280
|
+
if config.global_concurrency < 1:
|
|
281
|
+
errors.append(
|
|
282
|
+
f"global_concurrency must be >= 1, got {config.global_concurrency}"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if config.global_timeout < 1:
|
|
286
|
+
errors.append(
|
|
287
|
+
f"global_timeout must be >= 1, got {config.global_timeout}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Check at least one agent is enabled
|
|
291
|
+
enabled_agents = [
|
|
292
|
+
aid for aid, ac in config.agents.items()
|
|
293
|
+
if ac.enabled
|
|
294
|
+
]
|
|
295
|
+
if not enabled_agents:
|
|
296
|
+
errors.append("No agents are enabled in configuration")
|
|
297
|
+
|
|
298
|
+
if errors:
|
|
299
|
+
raise ConfigValidationError(
|
|
300
|
+
"Configuration validation failed:\n - " + "\n - ".join(errors)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def load_config(config_path: Path) -> OrchestratorConfig:
|
|
305
|
+
"""Load and validate orchestrator configuration from YAML file.
|
|
306
|
+
|
|
307
|
+
If the config file doesn't exist, generates a default configuration
|
|
308
|
+
based on installed agents.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
config_path: Path to agents.yaml file.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Validated OrchestratorConfig instance.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ConfigValidationError: If configuration is invalid.
|
|
318
|
+
NoAgentsError: If no agents are installed.
|
|
319
|
+
"""
|
|
320
|
+
if not config_path.exists():
|
|
321
|
+
logger.info(f"Config file not found at {config_path}, generating defaults")
|
|
322
|
+
return generate_default_config()
|
|
323
|
+
|
|
324
|
+
yaml = YAML()
|
|
325
|
+
yaml.preserve_quotes = True
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
with open(config_path) as f:
|
|
329
|
+
data = yaml.load(f)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
raise ConfigValidationError(f"Failed to parse YAML: {e}")
|
|
332
|
+
|
|
333
|
+
if not data:
|
|
334
|
+
logger.info("Config file is empty, generating defaults")
|
|
335
|
+
return generate_default_config()
|
|
336
|
+
|
|
337
|
+
config = parse_config(data)
|
|
338
|
+
validate_config(config)
|
|
339
|
+
|
|
340
|
+
logger.info(f"Loaded config from {config_path}")
|
|
341
|
+
return config
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# =============================================================================
|
|
345
|
+
# Default Config Generation (T005)
|
|
346
|
+
# =============================================================================
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def generate_default_config() -> OrchestratorConfig:
|
|
350
|
+
"""Generate default configuration based on installed agents.
|
|
351
|
+
|
|
352
|
+
Detects which agents are installed and creates a configuration
|
|
353
|
+
with sensible defaults.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
OrchestratorConfig with detected agents.
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
NoAgentsError: If no agents are installed.
|
|
360
|
+
"""
|
|
361
|
+
installed = detect_installed_agents()
|
|
362
|
+
|
|
363
|
+
if not installed:
|
|
364
|
+
raise NoAgentsError(
|
|
365
|
+
"No AI agents are installed.\n\n"
|
|
366
|
+
"Install at least one agent to use orchestration:\n"
|
|
367
|
+
" npm install -g @anthropic-ai/claude-code\n"
|
|
368
|
+
" npm install -g codex\n"
|
|
369
|
+
" npm install -g opencode\n\n"
|
|
370
|
+
"See documentation for other supported agents."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
logger.info(f"Detected {len(installed)} installed agents: {', '.join(installed)}")
|
|
374
|
+
|
|
375
|
+
# Create agent configs sorted by priority
|
|
376
|
+
# No artificial per-agent limits - let dependency graph determine parallelism
|
|
377
|
+
agents: dict[str, AgentConfig] = {}
|
|
378
|
+
for agent_id in installed:
|
|
379
|
+
agents[agent_id] = AgentConfig(
|
|
380
|
+
agent_id=agent_id,
|
|
381
|
+
enabled=True,
|
|
382
|
+
roles=["implementation", "review"],
|
|
383
|
+
priority=AGENT_PRIORITIES.get(agent_id, 50),
|
|
384
|
+
max_concurrent=100, # Effectively unlimited
|
|
385
|
+
timeout_seconds=600,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Sort by priority for defaults
|
|
389
|
+
sorted_agents = sorted(installed, key=lambda x: AGENT_PRIORITIES.get(x, 50))
|
|
390
|
+
|
|
391
|
+
# Set defaults based on installed agents
|
|
392
|
+
defaults = {
|
|
393
|
+
"implementation": sorted_agents.copy(),
|
|
394
|
+
"review": sorted_agents.copy(),
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Determine if single-agent mode should be auto-enabled
|
|
398
|
+
single_agent_mode = len(installed) == 1
|
|
399
|
+
single_agent = installed[0] if single_agent_mode else None
|
|
400
|
+
|
|
401
|
+
config = OrchestratorConfig(
|
|
402
|
+
version="1.0",
|
|
403
|
+
defaults=defaults,
|
|
404
|
+
agents=agents,
|
|
405
|
+
fallback_strategy=FallbackStrategy.NEXT_IN_LIST,
|
|
406
|
+
max_retries=3,
|
|
407
|
+
single_agent_mode=single_agent_mode,
|
|
408
|
+
single_agent=single_agent,
|
|
409
|
+
global_concurrency=100, # Effectively unlimited - dependency graph is the limit
|
|
410
|
+
global_timeout=3600,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return config
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def save_config(config: OrchestratorConfig, config_path: Path) -> None:
|
|
417
|
+
"""Save orchestrator configuration to YAML file.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
config: Configuration to save.
|
|
421
|
+
config_path: Path to write the YAML file.
|
|
422
|
+
"""
|
|
423
|
+
yaml = YAML()
|
|
424
|
+
yaml.default_flow_style = False
|
|
425
|
+
|
|
426
|
+
# Convert to serializable dict
|
|
427
|
+
data = {
|
|
428
|
+
"version": config.version,
|
|
429
|
+
"defaults": config.defaults,
|
|
430
|
+
"agents": {
|
|
431
|
+
agent_id: {
|
|
432
|
+
"enabled": ac.enabled,
|
|
433
|
+
"roles": ac.roles,
|
|
434
|
+
"priority": ac.priority,
|
|
435
|
+
"max_concurrent": ac.max_concurrent,
|
|
436
|
+
"timeout_seconds": ac.timeout_seconds,
|
|
437
|
+
}
|
|
438
|
+
for agent_id, ac in config.agents.items()
|
|
439
|
+
},
|
|
440
|
+
"fallback_strategy": config.fallback_strategy.value,
|
|
441
|
+
"max_retries": config.max_retries,
|
|
442
|
+
"single_agent_mode": {
|
|
443
|
+
"enabled": config.single_agent_mode,
|
|
444
|
+
"agent": config.single_agent,
|
|
445
|
+
},
|
|
446
|
+
"global_concurrency": config.global_concurrency,
|
|
447
|
+
"global_timeout": config.global_timeout,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
451
|
+
|
|
452
|
+
with open(config_path, "w") as f:
|
|
453
|
+
yaml.dump(data, f)
|
|
454
|
+
|
|
455
|
+
logger.info(f"Saved config to {config_path}")
|