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,221 @@
1
+ """Migration: Add missing task-prompt-template.md and tasks.md to research mission.
2
+
3
+ This migration fixes a bug where research missions were missing:
4
+ 1. task-prompt-template.md - The YAML frontmatter template for WP files
5
+ 2. tasks.md command template - Instructions for generating WP files
6
+
7
+ Without these templates, LLMs generating research WP files created files
8
+ with **Status**: in the markdown body instead of lane: in YAML frontmatter,
9
+ causing the review command to not find WPs ready for review.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import shutil
15
+ from pathlib import Path
16
+
17
+ from ..registry import MigrationRegistry
18
+ from .base import BaseMigration, MigrationResult
19
+
20
+
21
+ @MigrationRegistry.register
22
+ class ResearchMissionTemplatesMigration(BaseMigration):
23
+ """Add missing templates to research mission.
24
+
25
+ This fixes the bug where:
26
+ - Research WP files used **Status**: for_review in markdown body
27
+ - Review command expected lane: "for_review" in YAML frontmatter
28
+
29
+ The fix adds:
30
+ - templates/task-prompt-template.md with proper YAML frontmatter
31
+ - command-templates/tasks.md with instructions to use the template
32
+ """
33
+
34
+ migration_id = "0.9.2_research_mission_templates"
35
+ description = "Add missing task-prompt-template.md and tasks.md to research mission"
36
+ target_version = "0.9.2"
37
+
38
+ # Files that should exist in the research mission
39
+ REQUIRED_FILES = [
40
+ ("templates", "task-prompt-template.md"),
41
+ ("command-templates", "tasks.md"),
42
+ ]
43
+
44
+ def detect(self, project_path: Path) -> bool:
45
+ """Check if research mission is missing required templates."""
46
+ research_mission = project_path / ".kittify" / "missions" / "research"
47
+
48
+ if not research_mission.exists():
49
+ return False # No research mission, nothing to fix
50
+
51
+ for subdir, filename in self.REQUIRED_FILES:
52
+ target = research_mission / subdir / filename
53
+ if not target.exists():
54
+ return True
55
+
56
+ return False
57
+
58
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
59
+ """Check if we can copy templates from the package."""
60
+ package_research = self._find_package_research_mission()
61
+ if package_research is None:
62
+ return (
63
+ False,
64
+ "Could not locate package research mission to copy templates from. "
65
+ "This is expected in test environments. "
66
+ "Run 'spec-kitty init --force' to repair missions manually.",
67
+ )
68
+
69
+ # Check that package has the required files
70
+ missing_in_pkg = []
71
+ for subdir, filename in self.REQUIRED_FILES:
72
+ src = package_research / subdir / filename
73
+ if not src.exists():
74
+ missing_in_pkg.append(f"{subdir}/{filename}")
75
+
76
+ if missing_in_pkg:
77
+ return (
78
+ False,
79
+ f"Package research mission is missing: {', '.join(missing_in_pkg)}. "
80
+ "Please upgrade spec-kitty-cli to the latest version.",
81
+ )
82
+
83
+ return True, ""
84
+
85
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
86
+ """Copy missing templates from the package."""
87
+ changes: list[str] = []
88
+ warnings: list[str] = []
89
+ errors: list[str] = []
90
+
91
+ research_mission = project_path / ".kittify" / "missions" / "research"
92
+ package_research = self._find_package_research_mission()
93
+
94
+ if package_research is None:
95
+ errors.append("Could not locate package research mission")
96
+ return MigrationResult(success=False, errors=errors)
97
+
98
+ if not research_mission.exists():
99
+ # No research mission in project, nothing to do
100
+ return MigrationResult(
101
+ success=True,
102
+ changes_made=["Research mission not present, skipping"],
103
+ )
104
+
105
+ # Copy missing files
106
+ for subdir, filename in self.REQUIRED_FILES:
107
+ src = package_research / subdir / filename
108
+ dest_dir = research_mission / subdir
109
+ dest = dest_dir / filename
110
+
111
+ if dest.exists():
112
+ continue # Already exists
113
+
114
+ if dry_run:
115
+ changes.append(f"Would add research/{subdir}/{filename}")
116
+ else:
117
+ try:
118
+ # Ensure directory exists
119
+ dest_dir.mkdir(parents=True, exist_ok=True)
120
+ shutil.copy2(src, dest)
121
+ changes.append(f"Added research/{subdir}/{filename}")
122
+ except OSError as e:
123
+ errors.append(f"Failed to copy {subdir}/{filename}: {e}")
124
+
125
+ # Also update worktrees
126
+ worktrees_dir = project_path / ".worktrees"
127
+ if worktrees_dir.exists():
128
+ for worktree in worktrees_dir.iterdir():
129
+ if not worktree.is_dir():
130
+ continue
131
+
132
+ wt_research = worktree / ".kittify" / "missions" / "research"
133
+ if not wt_research.exists():
134
+ continue
135
+
136
+ for subdir, filename in self.REQUIRED_FILES:
137
+ src = package_research / subdir / filename
138
+ dest_dir = wt_research / subdir
139
+ dest = dest_dir / filename
140
+
141
+ if dest.exists():
142
+ continue
143
+
144
+ if dry_run:
145
+ changes.append(
146
+ f"Would add research/{subdir}/{filename} to worktree {worktree.name}"
147
+ )
148
+ else:
149
+ try:
150
+ dest_dir.mkdir(parents=True, exist_ok=True)
151
+ shutil.copy2(src, dest)
152
+ changes.append(
153
+ f"Added research/{subdir}/{filename} to worktree {worktree.name}"
154
+ )
155
+ except OSError as e:
156
+ warnings.append(
157
+ f"Could not copy to worktree {worktree.name}: {e}"
158
+ )
159
+
160
+ success = len(errors) == 0
161
+ return MigrationResult(
162
+ success=success,
163
+ changes_made=changes,
164
+ errors=errors,
165
+ warnings=warnings,
166
+ )
167
+
168
+ def _find_package_research_mission(self) -> Path | None:
169
+ """Find the research mission directory in the installed package or local repo."""
170
+ # First try from installed package
171
+ try:
172
+ from importlib.resources import files
173
+
174
+ pkg_files = files("specify_cli")
175
+ missions_path = pkg_files.joinpath("missions", "research")
176
+
177
+ # Convert to Path and check if it exists
178
+ missions_str = str(missions_path)
179
+ if Path(missions_str).exists():
180
+ return Path(missions_str)
181
+
182
+ except (ImportError, TypeError, AttributeError):
183
+ pass
184
+
185
+ # Try from package __file__ location
186
+ try:
187
+ import specify_cli
188
+
189
+ pkg_dir = Path(specify_cli.__file__).parent
190
+ research_dir = pkg_dir / "missions" / "research"
191
+ if research_dir.exists():
192
+ return research_dir
193
+ except (ImportError, AttributeError):
194
+ pass
195
+
196
+ # Fallback for development: Check SPEC_KITTY_TEMPLATE_ROOT env var
197
+ import os
198
+
199
+ template_root = os.environ.get("SPEC_KITTY_TEMPLATE_ROOT")
200
+ if template_root:
201
+ research_dir = Path(template_root) / ".kittify" / "missions" / "research"
202
+ if research_dir.exists():
203
+ return research_dir
204
+
205
+ # Fallback: Try to find the spec-kitty repo root
206
+ try:
207
+ cwd = Path.cwd()
208
+ for parent in [cwd] + list(cwd.parents):
209
+ research_dir = parent / "src" / "specify_cli" / "missions" / "research"
210
+ pyproject = parent / "pyproject.toml"
211
+ if research_dir.exists() and pyproject.exists():
212
+ try:
213
+ content = pyproject.read_text(encoding="utf-8-sig")
214
+ if "spec-kitty-cli" in content:
215
+ return research_dir
216
+ except OSError:
217
+ pass
218
+ except OSError:
219
+ pass
220
+
221
+ return None
@@ -0,0 +1,121 @@
1
+ """Migration registry for Spec Kitty upgrade system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Dict, List, Type
6
+
7
+ from packaging.version import Version
8
+
9
+ if TYPE_CHECKING:
10
+ from .migrations.base import BaseMigration
11
+
12
+
13
+ class MigrationRegistry:
14
+ """Registry of all available migrations, ordered by target version."""
15
+
16
+ _migrations: Dict[str, Type["BaseMigration"]] = {}
17
+
18
+ # Required fields for all migrations
19
+ REQUIRED_FIELDS = ['migration_id', 'description', 'target_version']
20
+
21
+ @classmethod
22
+ def register(
23
+ cls, migration_class: Type["BaseMigration"]
24
+ ) -> Type["BaseMigration"]:
25
+ """Decorator to register a migration class.
26
+
27
+ Args:
28
+ migration_class: The migration class to register
29
+
30
+ Returns:
31
+ The same migration class (for decorator use)
32
+
33
+ Raises:
34
+ ValueError: If migration_id is not set, required fields are missing,
35
+ or a migration with this ID is already registered
36
+ """
37
+ # Validate required fields
38
+ for field in cls.REQUIRED_FIELDS:
39
+ value = getattr(migration_class, field, None)
40
+ if not value:
41
+ raise ValueError(
42
+ f"Migration {migration_class.__name__} is missing required field '{field}'"
43
+ )
44
+
45
+ migration_id = migration_class.migration_id
46
+
47
+ # Check for duplicate registration
48
+ if migration_id in cls._migrations:
49
+ existing = cls._migrations[migration_id]
50
+ raise ValueError(
51
+ f"Duplicate migration ID '{migration_id}'. "
52
+ f"Already registered by {existing.__name__}, "
53
+ f"cannot register {migration_class.__name__}"
54
+ )
55
+
56
+ cls._migrations[migration_id] = migration_class
57
+ return migration_class
58
+
59
+ @classmethod
60
+ def get_all(cls) -> List["BaseMigration"]:
61
+ """Get all migrations as instances, ordered by target version.
62
+
63
+ Returns:
64
+ List of migration instances sorted by target version
65
+ """
66
+ instances = [m() for m in cls._migrations.values()]
67
+ return sorted(instances, key=lambda m: Version(m.target_version))
68
+
69
+ @classmethod
70
+ def get_applicable(
71
+ cls, from_version: str, to_version: str, project_path: "Path | None" = None
72
+ ) -> List["BaseMigration"]:
73
+ """Get migrations needed to go from one version to another.
74
+
75
+ Args:
76
+ from_version: Current version
77
+ to_version: Target version
78
+ project_path: Optional project path for detect() check
79
+
80
+ Returns:
81
+ List of applicable migrations in order
82
+ """
83
+ from pathlib import Path
84
+ from_v = Version(from_version)
85
+ to_v = Version(to_version)
86
+
87
+ applicable = []
88
+ for migration in cls.get_all():
89
+ target = Version(migration.target_version)
90
+ # Include if target is > from_version AND <= to_version
91
+ if from_v < target <= to_v:
92
+ applicable.append(migration)
93
+ # ALSO include migrations at current version if detect() returns True
94
+ elif target == from_v and project_path is not None:
95
+ if migration.detect(Path(project_path) if isinstance(project_path, str) else project_path):
96
+ applicable.append(migration)
97
+
98
+ return applicable
99
+
100
+ @classmethod
101
+ def get_by_id(cls, migration_id: str) -> "BaseMigration | None":
102
+ """Get a specific migration by ID.
103
+
104
+ Args:
105
+ migration_id: The migration ID to look up
106
+
107
+ Returns:
108
+ Migration instance if found, None otherwise
109
+ """
110
+ migration_class = cls._migrations.get(migration_id)
111
+ return migration_class() if migration_class else None
112
+
113
+ @classmethod
114
+ def clear(cls) -> None:
115
+ """Clear all registered migrations (for testing)."""
116
+ cls._migrations.clear()
117
+
118
+
119
+ # Export standalone decorator for convenience
120
+ # This allows: from specify_cli.upgrade.registry import register
121
+ register = MigrationRegistry.register
@@ -0,0 +1,284 @@
1
+ """Migration runner for Spec Kitty upgrade system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from rich.console import Console
13
+
14
+ from .detector import VersionDetector
15
+ from .metadata import ProjectMetadata
16
+ from .migrations.base import BaseMigration, MigrationResult
17
+ from .registry import MigrationRegistry
18
+
19
+
20
+ @dataclass
21
+ class UpgradeResult:
22
+ """Result of an upgrade operation."""
23
+
24
+ success: bool
25
+ from_version: str
26
+ to_version: str
27
+ migrations_applied: List[str] = field(default_factory=list)
28
+ migrations_skipped: List[str] = field(default_factory=list)
29
+ errors: List[str] = field(default_factory=list)
30
+ warnings: List[str] = field(default_factory=list)
31
+ dry_run: bool = False
32
+
33
+
34
+ class MigrationRunner:
35
+ """Orchestrates the migration process."""
36
+
37
+ def __init__(self, project_path: Path, console: Optional[Console] = None):
38
+ """Initialize the runner.
39
+
40
+ Args:
41
+ project_path: Root of the project
42
+ console: Optional Rich console for output
43
+ """
44
+ self.project_path = project_path
45
+ self.kittify_dir = project_path / ".kittify"
46
+ self.console = console or Console()
47
+ self.detector = VersionDetector(project_path)
48
+
49
+ def upgrade(
50
+ self,
51
+ target_version: str,
52
+ dry_run: bool = False,
53
+ force: bool = False,
54
+ include_worktrees: bool = True,
55
+ ) -> UpgradeResult:
56
+ """Run all needed migrations to reach target version.
57
+
58
+ Args:
59
+ target_version: Version to upgrade to
60
+ dry_run: If True, simulate but don't apply
61
+ force: If True, skip confirmation prompts
62
+ include_worktrees: If True, also upgrade worktrees
63
+
64
+ Returns:
65
+ UpgradeResult with details of the upgrade
66
+ """
67
+ from_version = self.detector.detect_version()
68
+
69
+ result = UpgradeResult(
70
+ success=True,
71
+ from_version=from_version,
72
+ to_version=target_version,
73
+ dry_run=dry_run,
74
+ )
75
+
76
+ # Get applicable migrations
77
+ migrations = MigrationRegistry.get_applicable(from_version, target_version, project_path=self.project_path)
78
+
79
+ if not migrations:
80
+ result.warnings.append(
81
+ f"No migrations needed from {from_version} to {target_version}"
82
+ )
83
+ return result
84
+
85
+ # Load or create metadata
86
+ metadata = ProjectMetadata.load(self.kittify_dir)
87
+ if metadata is None:
88
+ metadata = self._create_initial_metadata(from_version)
89
+
90
+ # Apply each migration to main project
91
+ for migration in migrations:
92
+ migration_result = self._apply_migration(migration, metadata, dry_run)
93
+
94
+ if migration_result.success:
95
+ result.migrations_applied.append(migration.migration_id)
96
+ result.warnings.extend(migration_result.warnings)
97
+ else:
98
+ # Check if it was skipped (already applied)
99
+ if metadata.has_migration(migration.migration_id):
100
+ result.migrations_skipped.append(migration.migration_id)
101
+ else:
102
+ result.success = False
103
+ result.errors.extend(migration_result.errors)
104
+ # Stop on first failure
105
+ break
106
+
107
+ # Update and save metadata for main project
108
+ if not dry_run and result.success:
109
+ metadata.version = target_version
110
+ metadata.last_upgraded_at = datetime.now()
111
+ metadata.save(self.kittify_dir)
112
+
113
+ # Handle worktrees
114
+ if include_worktrees:
115
+ worktrees_result = self._upgrade_worktrees(
116
+ target_version, migrations, dry_run
117
+ )
118
+ result.warnings.extend(worktrees_result.get("warnings", []))
119
+ if worktrees_result.get("errors"):
120
+ result.errors.extend(worktrees_result["errors"])
121
+ # Don't fail the whole upgrade for worktree issues
122
+ result.warnings.append(
123
+ "Some worktrees had issues - check errors above"
124
+ )
125
+
126
+ return result
127
+
128
+ def _apply_migration(
129
+ self,
130
+ migration: BaseMigration,
131
+ metadata: ProjectMetadata,
132
+ dry_run: bool,
133
+ ) -> MigrationResult:
134
+ """Apply a single migration.
135
+
136
+ Args:
137
+ migration: The migration to apply
138
+ metadata: Project metadata
139
+ dry_run: Whether to simulate only
140
+
141
+ Returns:
142
+ MigrationResult with details
143
+ """
144
+ # Skip if already applied
145
+ if metadata.has_migration(migration.migration_id):
146
+ return MigrationResult(
147
+ success=True,
148
+ warnings=[f"Migration {migration.migration_id} already applied, skipping"],
149
+ )
150
+
151
+ # Check if migration is needed via detection
152
+ if not migration.detect(self.project_path):
153
+ # Migration not needed - project doesn't have old state
154
+ if not dry_run:
155
+ metadata.record_migration(
156
+ migration.migration_id, "skipped", "Not applicable"
157
+ )
158
+ return MigrationResult(
159
+ success=True,
160
+ warnings=[
161
+ f"Migration {migration.migration_id} not needed (project already in target state)"
162
+ ],
163
+ )
164
+
165
+ # Check if safe to apply
166
+ can_apply, reason = migration.can_apply(self.project_path)
167
+ if not can_apply:
168
+ return MigrationResult(
169
+ success=False,
170
+ errors=[f"Cannot apply {migration.migration_id}: {reason}"],
171
+ )
172
+
173
+ # Apply the migration
174
+ result = migration.apply(self.project_path, dry_run=dry_run)
175
+
176
+ # Record in metadata
177
+ if not dry_run:
178
+ metadata.record_migration(
179
+ migration.migration_id,
180
+ "success" if result.success else "failed",
181
+ "; ".join(result.changes_made) if result.changes_made else None,
182
+ )
183
+
184
+ return result
185
+
186
+ def _upgrade_worktrees(
187
+ self,
188
+ target_version: str,
189
+ migrations: List[BaseMigration],
190
+ dry_run: bool,
191
+ ) -> dict:
192
+ """Upgrade all worktrees in .worktrees/ directory.
193
+
194
+ Args:
195
+ target_version: Target version
196
+ migrations: List of migrations to apply
197
+ dry_run: Whether to simulate only
198
+
199
+ Returns:
200
+ Dict with warnings and errors lists
201
+ """
202
+ result: dict = {"warnings": [], "errors": []}
203
+
204
+ worktrees_dir = self.project_path / ".worktrees"
205
+ if not worktrees_dir.exists():
206
+ return result
207
+
208
+ for worktree in worktrees_dir.iterdir():
209
+ if not worktree.is_dir():
210
+ continue
211
+
212
+ wt_kittify = worktree / ".kittify"
213
+ if not wt_kittify.exists():
214
+ continue
215
+
216
+ # Load or create worktree metadata
217
+ wt_metadata = ProjectMetadata.load(wt_kittify)
218
+ if wt_metadata is None:
219
+ wt_detector = VersionDetector(worktree)
220
+ wt_version = wt_detector.detect_version()
221
+ wt_metadata = self._create_initial_metadata(wt_version)
222
+
223
+ # Apply migrations to worktree
224
+ for migration in migrations:
225
+ if wt_metadata.has_migration(migration.migration_id):
226
+ continue
227
+
228
+ if not migration.detect(worktree):
229
+ if not dry_run:
230
+ wt_metadata.record_migration(
231
+ migration.migration_id, "skipped", "Not applicable"
232
+ )
233
+ continue
234
+
235
+ can_apply, reason = migration.can_apply(worktree)
236
+ if not can_apply:
237
+ result["warnings"].append(
238
+ f"Worktree {worktree.name}: Cannot apply {migration.migration_id}: {reason}"
239
+ )
240
+ continue
241
+
242
+ migration_result = migration.apply(worktree, dry_run=dry_run)
243
+
244
+ if migration_result.success:
245
+ if not dry_run:
246
+ wt_metadata.record_migration(
247
+ migration.migration_id,
248
+ "success",
249
+ "; ".join(migration_result.changes_made)
250
+ if migration_result.changes_made
251
+ else None,
252
+ )
253
+ result["warnings"].extend(
254
+ [f"Worktree {worktree.name}: {w}" for w in migration_result.warnings]
255
+ )
256
+ else:
257
+ result["errors"].extend(
258
+ [f"Worktree {worktree.name}: {e}" for e in migration_result.errors]
259
+ )
260
+
261
+ # Save worktree metadata
262
+ if not dry_run:
263
+ wt_metadata.version = target_version
264
+ wt_metadata.last_upgraded_at = datetime.now()
265
+ wt_metadata.save(wt_kittify)
266
+
267
+ return result
268
+
269
+ def _create_initial_metadata(self, detected_version: str) -> ProjectMetadata:
270
+ """Create initial metadata for a project without it.
271
+
272
+ Args:
273
+ detected_version: Version detected from heuristics
274
+
275
+ Returns:
276
+ New ProjectMetadata instance
277
+ """
278
+ return ProjectMetadata(
279
+ version=detected_version,
280
+ initialized_at=datetime.now(),
281
+ python_version=platform.python_version(),
282
+ platform=sys.platform,
283
+ platform_version=platform.platform(),
284
+ )
@@ -0,0 +1,14 @@
1
+ """Validation helpers for Spec Kitty missions.
2
+
3
+ This package hosts mission-specific validators that keep artifacts such
4
+ as CSV trackers and path conventions consistent. Modules included:
5
+
6
+ - ``research`` – citation + bibliography validation for research mission
7
+ - ``paths`` – (placeholder) path convention validation shared by missions
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from . import paths, research
13
+
14
+ __all__ = ["paths", "research"]