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,508 @@
1
+ """State management for orchestration runs.
2
+
3
+ This module handles:
4
+ - OrchestrationRun and WPExecution dataclasses
5
+ - State persistence to .kittify/orchestration-state.json
6
+ - State loading for resume capability
7
+ - State updates during execution
8
+ - Active orchestration detection
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import shutil
17
+ import tempfile
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from specify_cli.orchestrator.config import OrchestrationStatus, WPStatus
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # State file location
28
+ STATE_FILENAME = "orchestration-state.json"
29
+ STATE_BACKUP_SUFFIX = ".bak"
30
+
31
+
32
+ # =============================================================================
33
+ # Exceptions
34
+ # =============================================================================
35
+
36
+
37
+ class StateValidationError(Exception):
38
+ """Raised when state validation fails."""
39
+
40
+ pass
41
+
42
+
43
+ class StateLoadError(Exception):
44
+ """Raised when state cannot be loaded."""
45
+
46
+ pass
47
+
48
+
49
+ # =============================================================================
50
+ # WPExecution Dataclass (T018)
51
+ # =============================================================================
52
+
53
+
54
+ @dataclass
55
+ class WPExecution:
56
+ """Tracks individual work package execution state.
57
+
58
+ Captures all relevant information for a single WP's progression
59
+ through implementation and review phases.
60
+ """
61
+
62
+ wp_id: str
63
+ status: WPStatus = WPStatus.PENDING
64
+
65
+ # Implementation phase
66
+ implementation_agent: str | None = None
67
+ implementation_started: datetime | None = None
68
+ implementation_completed: datetime | None = None
69
+ implementation_exit_code: int | None = None
70
+ implementation_retries: int = 0
71
+
72
+ # Review phase
73
+ review_agent: str | None = None
74
+ review_started: datetime | None = None
75
+ review_completed: datetime | None = None
76
+ review_exit_code: int | None = None
77
+ review_retries: int = 0
78
+ review_feedback: str | None = None # Feedback from rejected review for re-implementation
79
+
80
+ # Output tracking
81
+ log_file: Path | None = None
82
+ worktree_path: Path | None = None
83
+
84
+ # Error tracking
85
+ last_error: str | None = None
86
+ fallback_agents_tried: list[str] = field(default_factory=list)
87
+
88
+ def validate(self) -> None:
89
+ """Validate state transitions per data-model.md rules.
90
+
91
+ Raises:
92
+ StateValidationError: If state is invalid.
93
+ """
94
+ # Implementation completion requires start
95
+ if self.implementation_completed and not self.implementation_started:
96
+ raise StateValidationError(
97
+ f"WP {self.wp_id}: implementation_completed requires implementation_started"
98
+ )
99
+
100
+ # Review start requires implementation completion
101
+ if self.review_started and not self.implementation_completed:
102
+ raise StateValidationError(
103
+ f"WP {self.wp_id}: review_started requires implementation_completed"
104
+ )
105
+
106
+ # Review completion requires review start
107
+ if self.review_completed and not self.review_started:
108
+ raise StateValidationError(
109
+ f"WP {self.wp_id}: review_completed requires review_started"
110
+ )
111
+
112
+ # COMPLETED status requires review_completed (or single-agent mode)
113
+ if self.status == WPStatus.COMPLETED:
114
+ if not self.implementation_completed:
115
+ raise StateValidationError(
116
+ f"WP {self.wp_id}: COMPLETED status requires implementation_completed"
117
+ )
118
+
119
+ # IMPLEMENTATION status requires implementation_started
120
+ if self.status == WPStatus.IMPLEMENTATION and not self.implementation_started:
121
+ raise StateValidationError(
122
+ f"WP {self.wp_id}: IMPLEMENTATION status requires implementation_started"
123
+ )
124
+
125
+ # REVIEW status requires review_started
126
+ if self.status == WPStatus.REVIEW and not self.review_started:
127
+ raise StateValidationError(
128
+ f"WP {self.wp_id}: REVIEW status requires review_started"
129
+ )
130
+
131
+ # REWORK status requires review_feedback (rejection reason)
132
+ if self.status == WPStatus.REWORK and not self.review_feedback:
133
+ raise StateValidationError(
134
+ f"WP {self.wp_id}: REWORK status requires review_feedback"
135
+ )
136
+
137
+ def to_dict(self) -> dict[str, Any]:
138
+ """Serialize to JSON-compatible dict."""
139
+ return {
140
+ "wp_id": self.wp_id,
141
+ "status": self.status.value,
142
+ "implementation_agent": self.implementation_agent,
143
+ "implementation_started": (
144
+ self.implementation_started.isoformat()
145
+ if self.implementation_started
146
+ else None
147
+ ),
148
+ "implementation_completed": (
149
+ self.implementation_completed.isoformat()
150
+ if self.implementation_completed
151
+ else None
152
+ ),
153
+ "implementation_exit_code": self.implementation_exit_code,
154
+ "implementation_retries": self.implementation_retries,
155
+ "review_agent": self.review_agent,
156
+ "review_started": (
157
+ self.review_started.isoformat() if self.review_started else None
158
+ ),
159
+ "review_completed": (
160
+ self.review_completed.isoformat() if self.review_completed else None
161
+ ),
162
+ "review_exit_code": self.review_exit_code,
163
+ "review_retries": self.review_retries,
164
+ "review_feedback": self.review_feedback,
165
+ "log_file": str(self.log_file) if self.log_file else None,
166
+ "worktree_path": str(self.worktree_path) if self.worktree_path else None,
167
+ "last_error": self.last_error,
168
+ "fallback_agents_tried": self.fallback_agents_tried,
169
+ }
170
+
171
+ @classmethod
172
+ def from_dict(cls, data: dict[str, Any]) -> "WPExecution":
173
+ """Deserialize from dict."""
174
+ return cls(
175
+ wp_id=data["wp_id"],
176
+ status=WPStatus(data.get("status", "pending")),
177
+ implementation_agent=data.get("implementation_agent"),
178
+ implementation_started=(
179
+ datetime.fromisoformat(data["implementation_started"])
180
+ if data.get("implementation_started")
181
+ else None
182
+ ),
183
+ implementation_completed=(
184
+ datetime.fromisoformat(data["implementation_completed"])
185
+ if data.get("implementation_completed")
186
+ else None
187
+ ),
188
+ implementation_exit_code=data.get("implementation_exit_code"),
189
+ implementation_retries=data.get("implementation_retries", 0),
190
+ review_agent=data.get("review_agent"),
191
+ review_started=(
192
+ datetime.fromisoformat(data["review_started"])
193
+ if data.get("review_started")
194
+ else None
195
+ ),
196
+ review_completed=(
197
+ datetime.fromisoformat(data["review_completed"])
198
+ if data.get("review_completed")
199
+ else None
200
+ ),
201
+ review_exit_code=data.get("review_exit_code"),
202
+ review_retries=data.get("review_retries", 0),
203
+ review_feedback=data.get("review_feedback"),
204
+ log_file=Path(data["log_file"]) if data.get("log_file") else None,
205
+ worktree_path=(
206
+ Path(data["worktree_path"]) if data.get("worktree_path") else None
207
+ ),
208
+ last_error=data.get("last_error"),
209
+ fallback_agents_tried=data.get("fallback_agents_tried", []),
210
+ )
211
+
212
+
213
+ # =============================================================================
214
+ # OrchestrationRun Dataclass (T017)
215
+ # =============================================================================
216
+
217
+
218
+ @dataclass
219
+ class OrchestrationRun:
220
+ """Tracks complete orchestration execution state.
221
+
222
+ This is the top-level state object persisted to disk, containing
223
+ all information needed to resume an interrupted orchestration.
224
+ """
225
+
226
+ run_id: str
227
+ feature_slug: str
228
+ started_at: datetime
229
+ status: OrchestrationStatus = OrchestrationStatus.PENDING
230
+ completed_at: datetime | None = None
231
+
232
+ # Configuration snapshot
233
+ config_hash: str = ""
234
+ concurrency_limit: int = 5
235
+
236
+ # Progress tracking
237
+ wps_total: int = 0
238
+ wps_completed: int = 0
239
+ wps_failed: int = 0
240
+
241
+ # Metrics
242
+ parallel_peak: int = 0
243
+ total_agent_invocations: int = 0
244
+
245
+ # Work package states
246
+ work_packages: dict[str, WPExecution] = field(default_factory=dict)
247
+
248
+ def validate(self) -> None:
249
+ """Validate overall orchestration state.
250
+
251
+ Raises:
252
+ StateValidationError: If state is invalid.
253
+ """
254
+ # Validate each WP
255
+ for wp in self.work_packages.values():
256
+ wp.validate()
257
+
258
+ # Completed count should match
259
+ completed_count = sum(
260
+ 1
261
+ for wp in self.work_packages.values()
262
+ if wp.status == WPStatus.COMPLETED
263
+ )
264
+ if completed_count != self.wps_completed:
265
+ logger.warning(
266
+ f"wps_completed mismatch: stored={self.wps_completed}, "
267
+ f"actual={completed_count}"
268
+ )
269
+
270
+ # Failed count should match
271
+ failed_count = sum(
272
+ 1
273
+ for wp in self.work_packages.values()
274
+ if wp.status == WPStatus.FAILED
275
+ )
276
+ if failed_count != self.wps_failed:
277
+ logger.warning(
278
+ f"wps_failed mismatch: stored={self.wps_failed}, "
279
+ f"actual={failed_count}"
280
+ )
281
+
282
+ def to_dict(self) -> dict[str, Any]:
283
+ """Serialize to JSON-compatible dict."""
284
+ return {
285
+ "run_id": self.run_id,
286
+ "feature_slug": self.feature_slug,
287
+ "started_at": self.started_at.isoformat(),
288
+ "completed_at": (
289
+ self.completed_at.isoformat() if self.completed_at else None
290
+ ),
291
+ "status": self.status.value,
292
+ "config_hash": self.config_hash,
293
+ "concurrency_limit": self.concurrency_limit,
294
+ "wps_total": self.wps_total,
295
+ "wps_completed": self.wps_completed,
296
+ "wps_failed": self.wps_failed,
297
+ "parallel_peak": self.parallel_peak,
298
+ "total_agent_invocations": self.total_agent_invocations,
299
+ "work_packages": {
300
+ wp_id: wp.to_dict() for wp_id, wp in self.work_packages.items()
301
+ },
302
+ }
303
+
304
+ @classmethod
305
+ def from_dict(cls, data: dict[str, Any]) -> "OrchestrationRun":
306
+ """Deserialize from dict."""
307
+ work_packages = {
308
+ wp_id: WPExecution.from_dict(wp_data)
309
+ for wp_id, wp_data in data.get("work_packages", {}).items()
310
+ }
311
+
312
+ return cls(
313
+ run_id=data["run_id"],
314
+ feature_slug=data["feature_slug"],
315
+ started_at=datetime.fromisoformat(data["started_at"]),
316
+ completed_at=(
317
+ datetime.fromisoformat(data["completed_at"])
318
+ if data.get("completed_at")
319
+ else None
320
+ ),
321
+ status=OrchestrationStatus(data.get("status", "pending")),
322
+ config_hash=data.get("config_hash", ""),
323
+ concurrency_limit=data.get("concurrency_limit", 5),
324
+ wps_total=data.get("wps_total", 0),
325
+ wps_completed=data.get("wps_completed", 0),
326
+ wps_failed=data.get("wps_failed", 0),
327
+ parallel_peak=data.get("parallel_peak", 0),
328
+ total_agent_invocations=data.get("total_agent_invocations", 0),
329
+ work_packages=work_packages,
330
+ )
331
+
332
+
333
+ # =============================================================================
334
+ # JSON Serialization Helpers (T021)
335
+ # =============================================================================
336
+
337
+
338
+ def _json_serializer(obj: Any) -> Any:
339
+ """JSON serializer for datetime and Path objects."""
340
+ if isinstance(obj, datetime):
341
+ return obj.isoformat()
342
+ if isinstance(obj, Path):
343
+ return str(obj)
344
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
345
+
346
+
347
+ def _atomic_write(path: Path, data: dict[str, Any]) -> None:
348
+ """Write JSON atomically via temp file rename.
349
+
350
+ Creates a backup of existing state before writing.
351
+ Uses atomic rename to ensure either old or new state exists,
352
+ never a partial write.
353
+
354
+ Args:
355
+ path: Target file path.
356
+ data: Data to serialize as JSON.
357
+ """
358
+ # Create backup of existing state
359
+ if path.exists():
360
+ backup_path = path.with_suffix(path.suffix + STATE_BACKUP_SUFFIX)
361
+ try:
362
+ shutil.copy2(path, backup_path)
363
+ except OSError as e:
364
+ logger.warning(f"Failed to create backup: {e}")
365
+
366
+ # Ensure parent directory exists
367
+ path.parent.mkdir(parents=True, exist_ok=True)
368
+
369
+ # Write to temp file in same directory (ensures same filesystem for atomic rename)
370
+ fd, temp_path = tempfile.mkstemp(
371
+ dir=path.parent,
372
+ prefix=".orchestration-state-",
373
+ suffix=".tmp",
374
+ )
375
+ try:
376
+ with os.fdopen(fd, "w") as f:
377
+ json.dump(data, f, indent=2, default=_json_serializer)
378
+ # Atomic rename
379
+ os.rename(temp_path, path)
380
+ logger.debug(f"State saved to {path}")
381
+ except Exception:
382
+ # Clean up temp file on failure
383
+ if os.path.exists(temp_path):
384
+ os.unlink(temp_path)
385
+ raise
386
+
387
+
388
+ # =============================================================================
389
+ # State Persistence Functions (T020)
390
+ # =============================================================================
391
+
392
+
393
+ def get_state_path(repo_root: Path) -> Path:
394
+ """Get the path to the state file.
395
+
396
+ Args:
397
+ repo_root: Repository root directory.
398
+
399
+ Returns:
400
+ Path to orchestration-state.json.
401
+ """
402
+ return repo_root / ".kittify" / STATE_FILENAME
403
+
404
+
405
+ def save_state(state: OrchestrationRun, repo_root: Path) -> None:
406
+ """Save orchestration state to JSON file.
407
+
408
+ Uses atomic writes to prevent corruption on crash.
409
+
410
+ Args:
411
+ state: Orchestration state to save.
412
+ repo_root: Repository root directory.
413
+ """
414
+ state_file = get_state_path(repo_root)
415
+ data = state.to_dict()
416
+ _atomic_write(state_file, data)
417
+ logger.info(f"Saved orchestration state for {state.feature_slug}")
418
+
419
+
420
+ def load_state(repo_root: Path) -> OrchestrationRun | None:
421
+ """Load orchestration state from JSON file.
422
+
423
+ Args:
424
+ repo_root: Repository root directory.
425
+
426
+ Returns:
427
+ Loaded OrchestrationRun or None if no state file exists.
428
+
429
+ Raises:
430
+ StateLoadError: If state file exists but cannot be parsed.
431
+ """
432
+ state_file = get_state_path(repo_root)
433
+ if not state_file.exists():
434
+ return None
435
+
436
+ try:
437
+ with open(state_file) as f:
438
+ data = json.load(f)
439
+ state = OrchestrationRun.from_dict(data)
440
+ logger.info(f"Loaded orchestration state for {state.feature_slug}")
441
+ return state
442
+ except json.JSONDecodeError as e:
443
+ raise StateLoadError(f"Failed to parse state file: {e}")
444
+ except KeyError as e:
445
+ raise StateLoadError(f"Missing required field in state file: {e}")
446
+ except Exception as e:
447
+ raise StateLoadError(f"Failed to load state: {e}")
448
+
449
+
450
+ def has_active_orchestration(repo_root: Path) -> bool:
451
+ """Check if there's an active (running/paused) orchestration.
452
+
453
+ Args:
454
+ repo_root: Repository root directory.
455
+
456
+ Returns:
457
+ True if an orchestration is running or paused.
458
+ """
459
+ state = load_state(repo_root)
460
+ if state is None:
461
+ return False
462
+ return state.status in [OrchestrationStatus.RUNNING, OrchestrationStatus.PAUSED]
463
+
464
+
465
+ def clear_state(repo_root: Path) -> None:
466
+ """Remove state file and its backup.
467
+
468
+ Args:
469
+ repo_root: Repository root directory.
470
+ """
471
+ state_file = get_state_path(repo_root)
472
+ backup_file = state_file.with_suffix(state_file.suffix + STATE_BACKUP_SUFFIX)
473
+
474
+ if state_file.exists():
475
+ state_file.unlink()
476
+ logger.info(f"Removed state file: {state_file}")
477
+
478
+ if backup_file.exists():
479
+ backup_file.unlink()
480
+ logger.debug(f"Removed backup file: {backup_file}")
481
+
482
+
483
+ def restore_from_backup(repo_root: Path) -> OrchestrationRun | None:
484
+ """Attempt to restore state from backup file.
485
+
486
+ Useful if the main state file was corrupted.
487
+
488
+ Args:
489
+ repo_root: Repository root directory.
490
+
491
+ Returns:
492
+ Restored OrchestrationRun or None if backup doesn't exist.
493
+ """
494
+ state_file = get_state_path(repo_root)
495
+ backup_file = state_file.with_suffix(state_file.suffix + STATE_BACKUP_SUFFIX)
496
+
497
+ if not backup_file.exists():
498
+ return None
499
+
500
+ try:
501
+ with open(backup_file) as f:
502
+ data = json.load(f)
503
+ state = OrchestrationRun.from_dict(data)
504
+ logger.info(f"Restored orchestration state from backup for {state.feature_slug}")
505
+ return state
506
+ except Exception as e:
507
+ logger.error(f"Failed to restore from backup: {e}")
508
+ return None
@@ -0,0 +1,122 @@
1
+ """Testing utilities for the orchestrator.
2
+
3
+ This subpackage provides infrastructure for end-to-end testing of the
4
+ multi-agent orchestrator. It includes:
5
+
6
+ - Agent availability detection (which agents are installed and authenticated)
7
+ - Test path selection (1-agent, 2-agent, or 3+-agent test paths)
8
+ - Fixture management (checkpoint snapshots for deterministic testing)
9
+
10
+ Example usage:
11
+ from specify_cli.orchestrator.testing import (
12
+ AgentAvailability,
13
+ detect_all_agents,
14
+ CORE_AGENTS,
15
+ EXTENDED_AGENTS,
16
+ TestPath,
17
+ select_test_path,
18
+ FixtureCheckpoint,
19
+ TestContext,
20
+ load_checkpoint,
21
+ )
22
+
23
+ # Detect available agents
24
+ agents = await detect_all_agents()
25
+ available = [a for a in agents.values() if a.is_available]
26
+
27
+ # Select test path based on available agents
28
+ test_path = await select_test_path()
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ # Availability detection (WP01)
34
+ from specify_cli.orchestrator.testing.availability import (
35
+ CORE_AGENTS,
36
+ EXTENDED_AGENTS,
37
+ ALL_AGENTS,
38
+ AgentAvailability,
39
+ detect_all_agents,
40
+ detect_agent,
41
+ get_available_agents,
42
+ clear_agent_cache,
43
+ check_installed,
44
+ probe_agent_auth,
45
+ )
46
+
47
+ # Test path selection (WP02)
48
+ from specify_cli.orchestrator.testing.paths import (
49
+ TestPath,
50
+ assign_agents,
51
+ clear_test_path_cache,
52
+ determine_path_type,
53
+ select_test_path,
54
+ select_test_path_sync,
55
+ )
56
+
57
+ # Fixture management (WP03 + WP04)
58
+ from specify_cli.orchestrator.testing.fixtures import (
59
+ FixtureCheckpoint,
60
+ GitError,
61
+ StateFileError,
62
+ TestContext,
63
+ WorktreeMetadata,
64
+ WorktreesFileError,
65
+ cleanup_temp_dir,
66
+ cleanup_test_context,
67
+ copy_fixture_to_temp,
68
+ create_worktrees_from_metadata,
69
+ init_git_repo,
70
+ load_checkpoint,
71
+ load_orchestration_state,
72
+ load_state_file,
73
+ load_worktrees_file,
74
+ register_for_cleanup,
75
+ save_state_file,
76
+ save_worktrees_file,
77
+ )
78
+
79
+ __all__ = [
80
+ # Tier constants
81
+ "CORE_AGENTS",
82
+ "EXTENDED_AGENTS",
83
+ "ALL_AGENTS",
84
+ # Availability detection (WP01)
85
+ "AgentAvailability",
86
+ "detect_all_agents",
87
+ "detect_agent",
88
+ "get_available_agents",
89
+ "clear_agent_cache",
90
+ "check_installed",
91
+ "probe_agent_auth",
92
+ # Test path selection (WP02)
93
+ "TestPath",
94
+ "assign_agents",
95
+ "clear_test_path_cache",
96
+ "determine_path_type",
97
+ "select_test_path",
98
+ "select_test_path_sync",
99
+ # Data structures (WP03)
100
+ "FixtureCheckpoint",
101
+ "WorktreeMetadata",
102
+ "TestContext",
103
+ # Exceptions
104
+ "WorktreesFileError",
105
+ "StateFileError",
106
+ "GitError",
107
+ # File I/O
108
+ "load_worktrees_file",
109
+ "save_worktrees_file",
110
+ "load_state_file",
111
+ "save_state_file",
112
+ # Loader functions (WP04)
113
+ "copy_fixture_to_temp",
114
+ "init_git_repo",
115
+ "create_worktrees_from_metadata",
116
+ "load_orchestration_state",
117
+ "load_checkpoint",
118
+ # Cleanup functions
119
+ "cleanup_temp_dir",
120
+ "cleanup_test_context",
121
+ "register_for_cleanup",
122
+ ]