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,684 @@
1
+ """Fixture data structures for orchestrator e2e testing.
2
+
3
+ This module defines the core data structures for managing test fixtures:
4
+ - FixtureCheckpoint: A restorable snapshot of orchestration state
5
+ - WorktreeMetadata: Information needed to recreate a git worktree
6
+ - TestContext: Complete runtime context for an e2e test
7
+
8
+ It also provides JSON schema validation for:
9
+ - worktrees.json: List of worktree metadata
10
+ - state.json: Serialized OrchestrationRun
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ if TYPE_CHECKING:
22
+ from specify_cli.orchestrator.state import OrchestrationRun
23
+
24
+
25
+ # =============================================================================
26
+ # Exceptions
27
+ # =============================================================================
28
+
29
+
30
+ class WorktreesFileError(Exception):
31
+ """Error loading or validating worktrees.json."""
32
+
33
+ pass
34
+
35
+
36
+ class StateFileError(Exception):
37
+ """Error loading or validating state.json."""
38
+
39
+ pass
40
+
41
+
42
+ class GitError(Exception):
43
+ """Error executing git command."""
44
+
45
+ pass
46
+
47
+
48
+ # =============================================================================
49
+ # FixtureCheckpoint Dataclass (T010)
50
+ # =============================================================================
51
+
52
+
53
+ @dataclass
54
+ class FixtureCheckpoint:
55
+ """A restorable snapshot of orchestration state.
56
+
57
+ Represents a checkpoint directory containing:
58
+ - state.json: Serialized OrchestrationRun
59
+ - feature/: Copy of the feature directory
60
+ - worktrees.json: Worktree metadata for recreation
61
+ """
62
+
63
+ name: str
64
+ """Checkpoint identifier (e.g., 'wp_created', 'review_pending')."""
65
+
66
+ path: Path
67
+ """Absolute path to the checkpoint directory."""
68
+
69
+ orchestrator_version: str
70
+ """Version of spec-kitty that created this checkpoint."""
71
+
72
+ created_at: datetime
73
+ """When this checkpoint was created."""
74
+
75
+ @property
76
+ def state_file(self) -> Path:
77
+ """Path to state.json within checkpoint."""
78
+ return self.path / "state.json"
79
+
80
+ @property
81
+ def feature_dir(self) -> Path:
82
+ """Path to feature/ directory within checkpoint."""
83
+ return self.path / "feature"
84
+
85
+ @property
86
+ def worktrees_file(self) -> Path:
87
+ """Path to worktrees.json within checkpoint."""
88
+ return self.path / "worktrees.json"
89
+
90
+ def exists(self) -> bool:
91
+ """Check if all required checkpoint files exist."""
92
+ return (
93
+ self.path.exists()
94
+ and self.state_file.exists()
95
+ and self.feature_dir.exists()
96
+ and self.worktrees_file.exists()
97
+ )
98
+
99
+ def to_dict(self) -> dict[str, Any]:
100
+ """Convert to JSON-serializable dict."""
101
+ return {
102
+ "name": self.name,
103
+ "path": str(self.path),
104
+ "orchestrator_version": self.orchestrator_version,
105
+ "created_at": self.created_at.isoformat(),
106
+ }
107
+
108
+ @classmethod
109
+ def from_dict(cls, data: dict[str, Any]) -> FixtureCheckpoint:
110
+ """Create from JSON dict."""
111
+ return cls(
112
+ name=data["name"],
113
+ path=Path(data["path"]),
114
+ orchestrator_version=data["orchestrator_version"],
115
+ created_at=datetime.fromisoformat(data["created_at"]),
116
+ )
117
+
118
+
119
+ # =============================================================================
120
+ # WorktreeMetadata Dataclass (T011)
121
+ # =============================================================================
122
+
123
+
124
+ @dataclass
125
+ class WorktreeMetadata:
126
+ """Information needed to recreate a git worktree.
127
+
128
+ Used in worktrees.json to track which worktrees exist in a fixture
129
+ and how to recreate them when restoring from checkpoint.
130
+ """
131
+
132
+ wp_id: str
133
+ """Work package identifier (e.g., 'WP01')."""
134
+
135
+ branch_name: str
136
+ """Git branch name for this worktree."""
137
+
138
+ relative_path: str
139
+ """Path relative to repo root (e.g., '.worktrees/test-feature-WP01')."""
140
+
141
+ commit_hash: str | None = None
142
+ """Optional commit hash to checkout (None = branch HEAD)."""
143
+
144
+ def to_dict(self) -> dict[str, Any]:
145
+ """Convert to JSON-serializable dict."""
146
+ return {
147
+ "wp_id": self.wp_id,
148
+ "branch_name": self.branch_name,
149
+ "relative_path": self.relative_path,
150
+ "commit_hash": self.commit_hash,
151
+ }
152
+
153
+ @classmethod
154
+ def from_dict(cls, data: dict[str, Any]) -> WorktreeMetadata:
155
+ """Create from JSON dict."""
156
+ return cls(
157
+ wp_id=data["wp_id"],
158
+ branch_name=data["branch_name"],
159
+ relative_path=data["relative_path"],
160
+ commit_hash=data.get("commit_hash"),
161
+ )
162
+
163
+
164
+ # =============================================================================
165
+ # TestContext Dataclass (T012)
166
+ # =============================================================================
167
+
168
+
169
+ @dataclass
170
+ class TestContext:
171
+ """Complete context for running an e2e orchestrator test.
172
+
173
+ Combines:
174
+ - Temporary test environment paths
175
+ - Test path selection (which agents to use)
176
+ - Loaded checkpoint state (if starting from snapshot)
177
+ - Worktree metadata
178
+ """
179
+
180
+ temp_dir: Path
181
+ """Temporary directory containing the test environment."""
182
+
183
+ repo_root: Path
184
+ """Root of the test git repository."""
185
+
186
+ feature_dir: Path
187
+ """Path to the test feature directory."""
188
+
189
+ test_path: Any # TestPath from paths.py - forward reference until WP02 merges
190
+ """Selected test path with agent assignments."""
191
+
192
+ checkpoint: FixtureCheckpoint | None = None
193
+ """Loaded checkpoint if test started from snapshot."""
194
+
195
+ orchestration_state: OrchestrationRun | None = None
196
+ """Loaded state from checkpoint (None if fresh start)."""
197
+
198
+ worktrees: list[WorktreeMetadata] = field(default_factory=list)
199
+ """Worktree metadata for this test context."""
200
+
201
+ @property
202
+ def kitty_specs_dir(self) -> Path:
203
+ """Path to kitty-specs directory in test repo."""
204
+ return self.repo_root / "kitty-specs"
205
+
206
+ @property
207
+ def worktrees_dir(self) -> Path:
208
+ """Path to .worktrees directory in test repo."""
209
+ return self.repo_root / ".worktrees"
210
+
211
+ @property
212
+ def state_file(self) -> Path:
213
+ """Path to orchestration state file."""
214
+ return self.feature_dir / ".orchestration-state.json"
215
+
216
+
217
+ # =============================================================================
218
+ # worktrees.json Schema Validation (T013)
219
+ # =============================================================================
220
+
221
+
222
+ def load_worktrees_file(path: Path) -> list[WorktreeMetadata]:
223
+ """Load and validate worktrees.json file.
224
+
225
+ Expected format:
226
+ {
227
+ "worktrees": [
228
+ {
229
+ "wp_id": "WP01",
230
+ "branch_name": "test-feature-WP01",
231
+ "relative_path": ".worktrees/test-feature-WP01",
232
+ "commit_hash": null
233
+ }
234
+ ]
235
+ }
236
+
237
+ Args:
238
+ path: Path to worktrees.json
239
+
240
+ Returns:
241
+ List of WorktreeMetadata objects
242
+
243
+ Raises:
244
+ WorktreesFileError: If file is invalid or missing required fields
245
+ """
246
+ if not path.exists():
247
+ raise WorktreesFileError(f"Worktrees file not found: {path}")
248
+
249
+ try:
250
+ with open(path) as f:
251
+ data = json.load(f)
252
+ except json.JSONDecodeError as e:
253
+ raise WorktreesFileError(f"Invalid JSON in {path}: {e}")
254
+
255
+ # Validate top-level structure
256
+ if not isinstance(data, dict):
257
+ raise WorktreesFileError(f"Expected object, got {type(data).__name__}")
258
+
259
+ if "worktrees" not in data:
260
+ raise WorktreesFileError("Missing 'worktrees' key")
261
+
262
+ worktrees_list = data["worktrees"]
263
+ if not isinstance(worktrees_list, list):
264
+ raise WorktreesFileError("'worktrees' must be an array")
265
+
266
+ # Parse and validate each worktree entry
267
+ result: list[WorktreeMetadata] = []
268
+ required_keys = {"wp_id", "branch_name", "relative_path"}
269
+
270
+ for i, item in enumerate(worktrees_list):
271
+ if not isinstance(item, dict):
272
+ raise WorktreesFileError(f"Worktree entry {i} must be an object")
273
+
274
+ missing = required_keys - set(item.keys())
275
+ if missing:
276
+ raise WorktreesFileError(f"Worktree entry {i} missing required keys: {missing}")
277
+
278
+ result.append(WorktreeMetadata.from_dict(item))
279
+
280
+ return result
281
+
282
+
283
+ def save_worktrees_file(path: Path, worktrees: list[WorktreeMetadata]) -> None:
284
+ """Save worktrees to JSON file.
285
+
286
+ Args:
287
+ path: Path to write to
288
+ worktrees: List of worktree metadata
289
+ """
290
+ data = {"worktrees": [w.to_dict() for w in worktrees]}
291
+ path.parent.mkdir(parents=True, exist_ok=True)
292
+ with open(path, "w") as f:
293
+ json.dump(data, f, indent=2)
294
+
295
+
296
+ # =============================================================================
297
+ # state.json Schema Validation (T014)
298
+ # =============================================================================
299
+
300
+
301
+ def load_state_file(path: Path) -> OrchestrationRun:
302
+ """Load and validate state.json file.
303
+
304
+ Args:
305
+ path: Path to state.json
306
+
307
+ Returns:
308
+ OrchestrationRun object
309
+
310
+ Raises:
311
+ StateFileError: If file is invalid or cannot be parsed
312
+ """
313
+ # Import here to avoid circular imports
314
+ from specify_cli.orchestrator.state import OrchestrationRun
315
+
316
+ if not path.exists():
317
+ raise StateFileError(f"State file not found: {path}")
318
+
319
+ try:
320
+ with open(path) as f:
321
+ data = json.load(f)
322
+ except json.JSONDecodeError as e:
323
+ raise StateFileError(f"Invalid JSON in {path}: {e}")
324
+
325
+ # Validate required fields per OrchestrationRun schema
326
+ required_fields = {
327
+ "run_id",
328
+ "feature_slug",
329
+ "started_at",
330
+ "status",
331
+ "wps_total",
332
+ "wps_completed",
333
+ "wps_failed",
334
+ "work_packages",
335
+ }
336
+ missing = required_fields - set(data.keys())
337
+ if missing:
338
+ raise StateFileError(f"Missing required fields: {missing}")
339
+
340
+ # Use OrchestrationRun's deserialization
341
+ try:
342
+ return OrchestrationRun.from_dict(data)
343
+ except Exception as e:
344
+ raise StateFileError(f"Failed to parse OrchestrationRun: {e}")
345
+
346
+
347
+ def save_state_file(path: Path, state: OrchestrationRun) -> None:
348
+ """Save OrchestrationRun to JSON file.
349
+
350
+ Args:
351
+ path: Path to write to
352
+ state: Orchestration state
353
+ """
354
+ path.parent.mkdir(parents=True, exist_ok=True)
355
+ with open(path, "w") as f:
356
+ json.dump(state.to_dict(), f, indent=2)
357
+
358
+
359
+ # =============================================================================
360
+ # Test Helper Functions
361
+ # =============================================================================
362
+
363
+
364
+ _cleanup_registry: list[Path] = []
365
+
366
+
367
+ def register_for_cleanup(path: Path) -> None:
368
+ """Register a path for cleanup.
369
+
370
+ Args:
371
+ path: Path to register
372
+ """
373
+ _cleanup_registry.append(path)
374
+
375
+
376
+ def cleanup_temp_dir(path: Path) -> None:
377
+ """Clean up a temporary directory.
378
+
379
+ Args:
380
+ path: Directory to remove
381
+ """
382
+ import shutil
383
+
384
+ if path.exists():
385
+ shutil.rmtree(path, ignore_errors=True)
386
+
387
+
388
+ def cleanup_test_context(ctx: TestContext) -> None:
389
+ """Clean up a test context.
390
+
391
+ Args:
392
+ ctx: Context to clean up
393
+ """
394
+ cleanup_temp_dir(ctx.temp_dir)
395
+
396
+
397
+ def copy_fixture_to_temp(checkpoint: FixtureCheckpoint) -> Path:
398
+ """Copy a fixture checkpoint to a temp directory.
399
+
400
+ Creates directory structure:
401
+ temp_dir/
402
+ kitty-specs/test-feature/ (copied from checkpoint.feature_dir)
403
+ .orchestration-state.json (copied from checkpoint.state_file)
404
+ worktrees.json
405
+
406
+ Args:
407
+ checkpoint: Checkpoint to copy
408
+
409
+ Returns:
410
+ Path to temporary directory
411
+
412
+ Raises:
413
+ FileNotFoundError: If checkpoint is incomplete (missing required files)
414
+ """
415
+ import shutil
416
+ import tempfile
417
+
418
+ # Validate checkpoint has required files
419
+ if not checkpoint.worktrees_file.exists():
420
+ raise FileNotFoundError(
421
+ f"Checkpoint not found or incomplete: missing {checkpoint.worktrees_file}"
422
+ )
423
+
424
+ temp_dir = Path(tempfile.mkdtemp(prefix=f"orchestrator_test_{checkpoint.name}_"))
425
+
426
+ # Create kitty-specs/test-feature directory structure
427
+ feature_dest = temp_dir / "kitty-specs" / "test-feature"
428
+ feature_dest.parent.mkdir(parents=True, exist_ok=True)
429
+
430
+ # Copy feature directory if it exists
431
+ if checkpoint.feature_dir.exists():
432
+ shutil.copytree(checkpoint.feature_dir, feature_dest)
433
+
434
+ # Copy state file into the feature directory as .orchestration-state.json
435
+ if checkpoint.state_file.exists():
436
+ shutil.copy(checkpoint.state_file, feature_dest / ".orchestration-state.json")
437
+
438
+ # Copy worktrees file to temp dir root
439
+ shutil.copy(checkpoint.worktrees_file, temp_dir / "worktrees.json")
440
+
441
+ register_for_cleanup(temp_dir)
442
+ return temp_dir
443
+
444
+
445
+ def init_git_repo(path: Path) -> None:
446
+ """Initialize a git repository at path with initial commit.
447
+
448
+ Creates a git repo, configures user, and makes an initial commit.
449
+ If no files exist, creates a .gitkeep file.
450
+
451
+ Args:
452
+ path: Directory to initialize
453
+
454
+ Raises:
455
+ GitError: If git command fails
456
+ """
457
+ import subprocess
458
+
459
+ path.mkdir(parents=True, exist_ok=True)
460
+
461
+ try:
462
+ subprocess.run(
463
+ ["git", "init"],
464
+ cwd=path,
465
+ capture_output=True,
466
+ check=True,
467
+ )
468
+ subprocess.run(
469
+ ["git", "config", "user.name", "Test User"],
470
+ cwd=path,
471
+ capture_output=True,
472
+ check=True,
473
+ )
474
+ subprocess.run(
475
+ ["git", "config", "user.email", "test@example.com"],
476
+ cwd=path,
477
+ capture_output=True,
478
+ check=True,
479
+ )
480
+
481
+ # Create a .gitkeep if no files exist (to ensure we can make a commit)
482
+ gitkeep = path / ".gitkeep"
483
+ if not gitkeep.exists():
484
+ gitkeep.write_text("")
485
+
486
+ # Add all files and make initial commit
487
+ subprocess.run(
488
+ ["git", "add", "."],
489
+ cwd=path,
490
+ capture_output=True,
491
+ check=True,
492
+ )
493
+ subprocess.run(
494
+ ["git", "commit", "-m", "Initial test fixture commit"],
495
+ cwd=path,
496
+ capture_output=True,
497
+ check=True,
498
+ )
499
+ except subprocess.CalledProcessError as e:
500
+ raise GitError(f"Failed to init git repo: {e}")
501
+
502
+
503
+ def create_worktrees_from_metadata(
504
+ repo_root: Path, worktrees: list[WorktreeMetadata]
505
+ ) -> None:
506
+ """Create git worktrees from metadata.
507
+
508
+ Args:
509
+ repo_root: Root of the main repository
510
+ worktrees: List of worktree metadata
511
+
512
+ Raises:
513
+ GitError: If worktree creation fails
514
+ """
515
+ import subprocess
516
+
517
+ for wt in worktrees:
518
+ # Use relative_path from metadata to determine worktree location
519
+ worktree_path = repo_root / wt.relative_path
520
+ worktree_path.parent.mkdir(parents=True, exist_ok=True)
521
+
522
+ try:
523
+ # First create the branch
524
+ subprocess.run(
525
+ ["git", "branch", wt.branch_name],
526
+ cwd=repo_root,
527
+ capture_output=True,
528
+ )
529
+ # Then create the worktree
530
+ subprocess.run(
531
+ ["git", "worktree", "add", str(worktree_path), wt.branch_name],
532
+ cwd=repo_root,
533
+ capture_output=True,
534
+ check=True,
535
+ )
536
+ except subprocess.CalledProcessError as e:
537
+ raise GitError(f"Failed to create worktree for {wt.wp_id}: {e}")
538
+
539
+
540
+ def load_orchestration_state(path: Path) -> "OrchestrationRun":
541
+ """Load orchestration state from a feature directory or state file.
542
+
543
+ Args:
544
+ path: Path to feature directory or state.json file.
545
+ If a directory, looks for .orchestration-state.json inside.
546
+
547
+ Returns:
548
+ OrchestrationRun instance
549
+
550
+ Raises:
551
+ StateFileError: If loading fails or state file not found
552
+ """
553
+ if path.is_dir():
554
+ # Assume it's a feature directory, look for state file inside
555
+ state_file = path / ".orchestration-state.json"
556
+ else:
557
+ state_file = path
558
+
559
+ return load_state_file(state_file)
560
+
561
+
562
+ def load_checkpoint(
563
+ checkpoint: FixtureCheckpoint,
564
+ test_path: Any | None = None,
565
+ ) -> TestContext:
566
+ """Load a checkpoint into a TestContext.
567
+
568
+ Args:
569
+ checkpoint: Checkpoint to load from
570
+ test_path: Optional TestPath to use (creates mock if None)
571
+
572
+ Returns:
573
+ TestContext with checkpoint loaded
574
+
575
+ Raises:
576
+ FileNotFoundError: If checkpoint files are missing
577
+ StateFileError: If state.json is invalid
578
+ """
579
+ import shutil
580
+ import tempfile
581
+
582
+ # Validate checkpoint has required files
583
+ if not checkpoint.feature_dir.exists():
584
+ raise FileNotFoundError(f"Checkpoint feature directory missing: {checkpoint.feature_dir}")
585
+
586
+ # Create temp directory for test
587
+ temp_dir = Path(tempfile.mkdtemp(prefix=f"orchestrator_test_{checkpoint.name}_"))
588
+ register_for_cleanup(temp_dir)
589
+
590
+ # Copy files to temp_dir
591
+ state_file = temp_dir / "state.json"
592
+ feature_dir = temp_dir / "feature"
593
+ repo_root = temp_dir / "repo"
594
+
595
+ if checkpoint.state_file.exists():
596
+ shutil.copy(checkpoint.state_file, state_file)
597
+
598
+ if checkpoint.feature_dir.exists():
599
+ shutil.copytree(checkpoint.feature_dir, feature_dir)
600
+
601
+ # Also copy state.json to where TestContext.state_file property expects it
602
+ expected_state_file = feature_dir / ".orchestration-state.json"
603
+ if checkpoint.state_file.exists():
604
+ shutil.copy(checkpoint.state_file, expected_state_file)
605
+
606
+ # Initialize git repo
607
+ init_git_repo(repo_root)
608
+
609
+ # Load worktrees metadata if present
610
+ worktrees: list[WorktreeMetadata] = []
611
+ if checkpoint.worktrees_file.exists():
612
+ worktrees = load_worktrees_file(checkpoint.worktrees_file)
613
+ # Create the worktrees in the repo
614
+ if worktrees:
615
+ create_worktrees_from_metadata(repo_root, worktrees)
616
+
617
+ # Load state if it exists
618
+ orchestration_state = None
619
+ if state_file.exists():
620
+ orchestration_state = load_state_file(state_file)
621
+
622
+ # Use provided test_path or create a mock one
623
+ if test_path is None:
624
+ from specify_cli.orchestrator.testing.paths import TestPath
625
+
626
+ test_path = TestPath(
627
+ path_type="1-agent",
628
+ implementation_agent="mock",
629
+ review_agent="mock",
630
+ available_agents=["mock"],
631
+ fallback_agent=None,
632
+ )
633
+
634
+ return TestContext(
635
+ temp_dir=temp_dir,
636
+ repo_root=repo_root,
637
+ feature_dir=feature_dir,
638
+ test_path=test_path,
639
+ checkpoint=checkpoint,
640
+ orchestration_state=orchestration_state,
641
+ worktrees=worktrees,
642
+ )
643
+
644
+
645
+ def setup_test_repo(tmp_path: Path, feature_slug: str = "test-feature") -> TestContext:
646
+ """Set up a test repository with basic structure.
647
+
648
+ Args:
649
+ tmp_path: pytest tmp_path
650
+ feature_slug: Name for the test feature
651
+
652
+ Returns:
653
+ TestContext for the test
654
+ """
655
+ repo_root = tmp_path / "repo"
656
+ init_git_repo(repo_root)
657
+
658
+ # Create feature directory
659
+ feature_dir = repo_root / "kitty-specs" / feature_slug
660
+ feature_dir.mkdir(parents=True)
661
+ tasks_dir = feature_dir / "tasks"
662
+ tasks_dir.mkdir()
663
+
664
+ # Create basic files
665
+ (feature_dir / "spec.md").write_text("# Test Spec\n")
666
+ (feature_dir / "plan.md").write_text("# Test Plan\n")
667
+
668
+ # Create a mock test_path
669
+ from specify_cli.orchestrator.testing.paths import TestPath
670
+
671
+ mock_path = TestPath(
672
+ path_type="1-agent",
673
+ implementation_agent="mock",
674
+ review_agent="mock",
675
+ available_agents=["mock"],
676
+ fallback_agent=None,
677
+ )
678
+
679
+ return TestContext(
680
+ temp_dir=tmp_path,
681
+ repo_root=repo_root,
682
+ feature_dir=feature_dir,
683
+ test_path=mock_path,
684
+ )