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,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}")