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,141 @@
1
+ """Migration: Install encoding validation pre-commit hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from ..registry import MigrationRegistry
9
+ from .base import BaseMigration, MigrationResult
10
+
11
+
12
+ @MigrationRegistry.register
13
+ class EncodingHooksMigration(BaseMigration):
14
+ """Install encoding validation pre-commit hooks.
15
+
16
+ This migration installs git hooks that validate file encoding
17
+ before commits, preventing encoding issues from being committed.
18
+ """
19
+
20
+ migration_id = "0.5.0_encoding_hooks"
21
+ description = "Install encoding validation pre-commit hooks"
22
+ target_version = "0.5.0"
23
+
24
+ HOOK_FILES = [
25
+ "pre-commit",
26
+ "pre-commit-encoding-check",
27
+ "pre-commit-agent-check",
28
+ ]
29
+
30
+ def detect(self, project_path: Path) -> bool:
31
+ """Check if encoding hooks are missing."""
32
+ git_dir = project_path / ".git"
33
+ if not git_dir.exists():
34
+ return False # Not a git repo, can't install hooks
35
+
36
+ pre_commit = git_dir / "hooks" / "pre-commit"
37
+ if not pre_commit.exists():
38
+ return True
39
+
40
+ try:
41
+ content = pre_commit.read_text(encoding="utf-8", errors="ignore")
42
+ except OSError:
43
+ return True
44
+
45
+ # Check if it's our hook or a custom one
46
+ return "spec-kitty" not in content.lower() and "encoding" not in content.lower()
47
+
48
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
49
+ """Check if we can install hooks."""
50
+ git_dir = project_path / ".git"
51
+ if not git_dir.exists():
52
+ return False, "Not a git repository"
53
+
54
+ hooks_dir = git_dir / "hooks"
55
+ if not hooks_dir.exists():
56
+ return True, ""
57
+
58
+ pre_commit = hooks_dir / "pre-commit"
59
+ if pre_commit.exists():
60
+ try:
61
+ content = pre_commit.read_text(encoding="utf-8", errors="ignore")
62
+ except OSError:
63
+ return False, "Cannot read existing pre-commit hook"
64
+
65
+ # Check if it's our hook
66
+ if "spec-kitty" not in content.lower() and "encoding" not in content.lower():
67
+ # It's a custom hook - warn but allow (will append)
68
+ pass
69
+
70
+ return True, ""
71
+
72
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
73
+ """Install or update pre-commit hooks."""
74
+ changes: list[str] = []
75
+ warnings: list[str] = []
76
+ errors: list[str] = []
77
+
78
+ git_dir = project_path / ".git"
79
+ if not git_dir.exists():
80
+ errors.append("Not a git repository")
81
+ return MigrationResult(success=False, errors=errors)
82
+
83
+ hooks_dir = git_dir / "hooks"
84
+
85
+ # Find hook templates - try .kittify/templates first, then package
86
+ template_hooks_dir = project_path / ".kittify" / "templates" / "git-hooks"
87
+
88
+ if not template_hooks_dir.exists():
89
+ # Try to find from package
90
+ try:
91
+ from importlib.resources import files
92
+
93
+ pkg_hooks = files("specify_cli").joinpath("templates", "git-hooks")
94
+ if hasattr(pkg_hooks, "is_dir") and pkg_hooks.is_dir():
95
+ template_hooks_dir = Path(str(pkg_hooks))
96
+ else:
97
+ warnings.append(
98
+ "Hook templates not found in .kittify/templates/ or package"
99
+ )
100
+ return MigrationResult(
101
+ success=True, changes_made=changes, warnings=warnings
102
+ )
103
+ except (ImportError, TypeError):
104
+ warnings.append("Could not locate hook templates")
105
+ return MigrationResult(
106
+ success=True, changes_made=changes, warnings=warnings
107
+ )
108
+
109
+ if dry_run:
110
+ changes.append("Would install pre-commit hooks from templates")
111
+ return MigrationResult(success=True, changes_made=changes)
112
+
113
+ # Create hooks directory if needed
114
+ try:
115
+ hooks_dir.mkdir(exist_ok=True)
116
+ except OSError as e:
117
+ errors.append(f"Failed to create hooks directory: {e}")
118
+ return MigrationResult(success=False, errors=errors)
119
+
120
+ # Copy hook files
121
+ for hook_name in self.HOOK_FILES:
122
+ template_hook = template_hooks_dir / hook_name
123
+ dest_hook = hooks_dir / hook_name
124
+
125
+ if template_hook.exists():
126
+ try:
127
+ shutil.copy2(template_hook, dest_hook)
128
+ dest_hook.chmod(0o755)
129
+ changes.append(f"Installed {hook_name} hook")
130
+ except OSError as e:
131
+ errors.append(f"Failed to install {hook_name}: {e}")
132
+ else:
133
+ warnings.append(f"Template for {hook_name} not found")
134
+
135
+ success = len(errors) == 0
136
+ return MigrationResult(
137
+ success=success,
138
+ changes_made=changes,
139
+ errors=errors,
140
+ warnings=warnings,
141
+ )
@@ -0,0 +1,169 @@
1
+ """Migration: Rename commands/ to command-templates/ directories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from ..registry import MigrationRegistry
9
+ from .base import BaseMigration, MigrationResult
10
+
11
+
12
+ @MigrationRegistry.register
13
+ class CommandsRenameMigration(BaseMigration):
14
+ """Rename commands/ to command-templates/ in templates and missions.
15
+
16
+ This migration fixes the issue where Claude Code discovers commands
17
+ from .kittify/templates/commands/ and .kittify/missions/*/commands/
18
+ causing duplicate slash commands.
19
+
20
+ The directories are renamed to command-templates/ which Claude Code
21
+ does not automatically discover.
22
+ """
23
+
24
+ migration_id = "0.6.5_commands_rename"
25
+ description = "Rename commands/ to command-templates/ directories"
26
+ target_version = "0.6.5"
27
+
28
+ def detect(self, project_path: Path) -> bool:
29
+ """Check if project uses old commands/ directories."""
30
+ kittify_dir = project_path / ".kittify"
31
+
32
+ # Check templates/commands/
33
+ if (kittify_dir / "templates" / "commands").exists():
34
+ return True
35
+
36
+ # Check missions/*/commands/
37
+ missions_dir = kittify_dir / "missions"
38
+ if missions_dir.exists():
39
+ for mission in missions_dir.iterdir():
40
+ if mission.is_dir() and (mission / "commands").exists():
41
+ return True
42
+
43
+ # Check worktrees
44
+ worktrees_dir = project_path / ".worktrees"
45
+ if worktrees_dir.exists():
46
+ for worktree in worktrees_dir.iterdir():
47
+ if worktree.is_dir():
48
+ wt_kittify = worktree / ".kittify"
49
+ if (wt_kittify / "templates" / "commands").exists():
50
+ return True
51
+ wt_missions = wt_kittify / "missions"
52
+ if wt_missions.exists():
53
+ for mission in wt_missions.iterdir():
54
+ if mission.is_dir() and (mission / "commands").exists():
55
+ return True
56
+
57
+ return False
58
+
59
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
60
+ """Check for conflicts."""
61
+ kittify_dir = project_path / ".kittify"
62
+
63
+ # Check if both old and new exist in templates
64
+ templates_dir = kittify_dir / "templates"
65
+ if templates_dir.exists():
66
+ old_exists = (templates_dir / "commands").exists()
67
+ new_exists = (templates_dir / "command-templates").exists()
68
+ if old_exists and new_exists:
69
+ return (
70
+ False,
71
+ "Both commands/ and command-templates/ exist in templates - manual merge required",
72
+ )
73
+
74
+ # Check missions
75
+ missions_dir = kittify_dir / "missions"
76
+ if missions_dir.exists():
77
+ for mission in missions_dir.iterdir():
78
+ if mission.is_dir():
79
+ old_exists = (mission / "commands").exists()
80
+ new_exists = (mission / "command-templates").exists()
81
+ if old_exists and new_exists:
82
+ return (
83
+ False,
84
+ f"Both directories exist in mission {mission.name} - manual merge required",
85
+ )
86
+
87
+ return True, ""
88
+
89
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
90
+ """Rename commands/ to command-templates/."""
91
+ changes: list[str] = []
92
+ warnings: list[str] = []
93
+ errors: list[str] = []
94
+
95
+ kittify_dir = project_path / ".kittify"
96
+
97
+ def rename_dir(old: Path, new: Path, context: str) -> None:
98
+ if old.exists() and not new.exists():
99
+ if dry_run:
100
+ changes.append(f"Would rename {context}: commands/ -> command-templates/")
101
+ else:
102
+ try:
103
+ shutil.move(str(old), str(new))
104
+ changes.append(f"Renamed {context}: commands/ -> command-templates/")
105
+ except OSError as e:
106
+ errors.append(f"Failed to rename {context}: {e}")
107
+
108
+ # Rename in templates/
109
+ templates_dir = kittify_dir / "templates"
110
+ if templates_dir.exists():
111
+ rename_dir(
112
+ templates_dir / "commands",
113
+ templates_dir / "command-templates",
114
+ ".kittify/templates",
115
+ )
116
+
117
+ # Rename in each mission
118
+ missions_dir = kittify_dir / "missions"
119
+ if missions_dir.exists():
120
+ for mission in missions_dir.iterdir():
121
+ if mission.is_dir():
122
+ rename_dir(
123
+ mission / "commands",
124
+ mission / "command-templates",
125
+ f".kittify/missions/{mission.name}",
126
+ )
127
+
128
+ # Handle worktrees - remove old commands/ directories
129
+ # (worktrees should use their own .claude/commands/ not templates)
130
+ worktrees_dir = project_path / ".worktrees"
131
+ if worktrees_dir.exists():
132
+ for worktree in worktrees_dir.iterdir():
133
+ if worktree.is_dir():
134
+ # Remove old templates/commands/
135
+ wt_templates_commands = worktree / ".kittify" / "templates" / "commands"
136
+ if wt_templates_commands.exists():
137
+ if dry_run:
138
+ changes.append(
139
+ f"Would remove old commands/ from worktree {worktree.name}"
140
+ )
141
+ else:
142
+ try:
143
+ shutil.rmtree(wt_templates_commands)
144
+ changes.append(
145
+ f"Removed old commands/ from worktree {worktree.name}"
146
+ )
147
+ except OSError as e:
148
+ warnings.append(
149
+ f"Could not remove old commands/ from worktree {worktree.name}: {e}"
150
+ )
151
+
152
+ # Rename missions/*/commands/ in worktree
153
+ wt_missions = worktree / ".kittify" / "missions"
154
+ if wt_missions.exists():
155
+ for mission in wt_missions.iterdir():
156
+ if mission.is_dir():
157
+ rename_dir(
158
+ mission / "commands",
159
+ mission / "command-templates",
160
+ f".worktrees/{worktree.name}/.kittify/missions/{mission.name}",
161
+ )
162
+
163
+ success = len(errors) == 0
164
+ return MigrationResult(
165
+ success=success,
166
+ changes_made=changes,
167
+ errors=errors,
168
+ warnings=warnings,
169
+ )
@@ -0,0 +1,228 @@
1
+ """Migration: Ensure all missions are present in the project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ from ..registry import MigrationRegistry
10
+ from .base import BaseMigration, MigrationResult
11
+
12
+
13
+ @MigrationRegistry.register
14
+ class EnsureMissionsMigration(BaseMigration):
15
+ """Ensure all required missions are present in the project.
16
+
17
+ This migration addresses the bug in v0.6.5-0.6.6 where the software-dev
18
+ mission was missing from PyPI packages due to symlink handling issues
19
+ during build.
20
+
21
+ It copies missing missions from the package to the project.
22
+ """
23
+
24
+ migration_id = "0.6.7_ensure_missions"
25
+ description = "Ensure all required missions (software-dev, research) are present"
26
+ target_version = "0.6.7"
27
+
28
+ # Required missions that should always be present
29
+ REQUIRED_MISSIONS = ["software-dev", "research"]
30
+
31
+ def detect(self, project_path: Path) -> bool:
32
+ """Check if any required missions are missing."""
33
+ missions_dir = project_path / ".kittify" / "missions"
34
+
35
+ if not missions_dir.exists():
36
+ return True # No missions directory at all
37
+
38
+ for mission_name in self.REQUIRED_MISSIONS:
39
+ mission_dir = missions_dir / mission_name
40
+ if not mission_dir.exists():
41
+ return True
42
+ # Check for essential files
43
+ if not (mission_dir / "mission.yaml").exists():
44
+ return True
45
+
46
+ return False
47
+
48
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
49
+ """Check if we can copy missions from the package."""
50
+ # Try to find package missions
51
+ package_missions = self._find_package_missions()
52
+ if package_missions is None:
53
+ # In test environments, package missions may not be available
54
+ # Skip gracefully rather than blocking all upgrades
55
+ return (
56
+ False,
57
+ "Could not locate package missions to copy from. "
58
+ "This is expected in test environments. "
59
+ "Run 'spec-kitty init --force' to repair missions manually.",
60
+ )
61
+
62
+ # Check we have all required missions in the package
63
+ missing_in_pkg = []
64
+ for mission_name in self.REQUIRED_MISSIONS:
65
+ pkg_mission = package_missions / mission_name
66
+ if not pkg_mission.exists():
67
+ missing_in_pkg.append(mission_name)
68
+
69
+ if missing_in_pkg:
70
+ return (
71
+ False,
72
+ f"Package is missing missions: {', '.join(missing_in_pkg)}. "
73
+ "Please upgrade spec-kitty-cli to the latest version.",
74
+ )
75
+
76
+ return True, ""
77
+
78
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
79
+ """Copy missing missions from the package."""
80
+ changes: list[str] = []
81
+ warnings: list[str] = []
82
+ errors: list[str] = []
83
+
84
+ missions_dir = project_path / ".kittify" / "missions"
85
+ package_missions = self._find_package_missions()
86
+
87
+ if package_missions is None:
88
+ errors.append("Could not locate package missions")
89
+ return MigrationResult(success=False, errors=errors)
90
+
91
+ # Ensure missions directory exists
92
+ if not missions_dir.exists():
93
+ if dry_run:
94
+ changes.append("Would create .kittify/missions/ directory")
95
+ else:
96
+ missions_dir.mkdir(parents=True, exist_ok=True)
97
+ changes.append("Created .kittify/missions/ directory")
98
+
99
+ # Copy missing missions
100
+ for mission_name in self.REQUIRED_MISSIONS:
101
+ dest_mission = missions_dir / mission_name
102
+ src_mission = package_missions / mission_name
103
+
104
+ if dest_mission.exists():
105
+ # Check if it has essential files
106
+ if (dest_mission / "mission.yaml").exists():
107
+ continue
108
+ else:
109
+ # Mission directory exists but is incomplete
110
+ if dry_run:
111
+ changes.append(
112
+ f"Would repair incomplete mission: {mission_name}"
113
+ )
114
+ else:
115
+ try:
116
+ # Remove incomplete and copy fresh
117
+ shutil.rmtree(dest_mission)
118
+ shutil.copytree(src_mission, dest_mission)
119
+ changes.append(f"Repaired incomplete mission: {mission_name}")
120
+ except OSError as e:
121
+ errors.append(f"Failed to repair mission {mission_name}: {e}")
122
+ else:
123
+ # Mission doesn't exist, copy it
124
+ if dry_run:
125
+ changes.append(f"Would copy missing mission: {mission_name}")
126
+ else:
127
+ try:
128
+ shutil.copytree(src_mission, dest_mission)
129
+ changes.append(f"Copied missing mission: {mission_name}")
130
+ except OSError as e:
131
+ errors.append(f"Failed to copy mission {mission_name}: {e}")
132
+
133
+ # Also fix worktrees
134
+ worktrees_dir = project_path / ".worktrees"
135
+ if worktrees_dir.exists():
136
+ for worktree in worktrees_dir.iterdir():
137
+ if not worktree.is_dir():
138
+ continue
139
+
140
+ wt_missions = worktree / ".kittify" / "missions"
141
+ if not wt_missions.exists():
142
+ continue
143
+
144
+ for mission_name in self.REQUIRED_MISSIONS:
145
+ wt_mission = wt_missions / mission_name
146
+ src_mission = package_missions / mission_name
147
+
148
+ if not wt_mission.exists():
149
+ if dry_run:
150
+ changes.append(
151
+ f"Would copy missing mission to worktree {worktree.name}: {mission_name}"
152
+ )
153
+ else:
154
+ try:
155
+ shutil.copytree(src_mission, wt_mission)
156
+ changes.append(
157
+ f"Copied missing mission to worktree {worktree.name}: {mission_name}"
158
+ )
159
+ except OSError as e:
160
+ warnings.append(
161
+ f"Could not copy mission to worktree {worktree.name}: {e}"
162
+ )
163
+
164
+ success = len(errors) == 0
165
+ return MigrationResult(
166
+ success=success,
167
+ changes_made=changes,
168
+ errors=errors,
169
+ warnings=warnings,
170
+ )
171
+
172
+ def _find_package_missions(self) -> Path | None:
173
+ """Find the missions directory in the installed package or local repo."""
174
+ # First try from installed package
175
+ try:
176
+ from importlib.resources import files
177
+
178
+ pkg_files = files("specify_cli")
179
+ missions_path = pkg_files.joinpath("missions")
180
+
181
+ # Convert to Path and check if it exists
182
+ missions_str = str(missions_path)
183
+ if Path(missions_str).exists():
184
+ return Path(missions_str)
185
+
186
+ except (ImportError, TypeError, AttributeError):
187
+ pass
188
+
189
+ # Try from package __file__ location
190
+ try:
191
+ import specify_cli
192
+
193
+ pkg_dir = Path(specify_cli.__file__).parent
194
+ missions_dir = pkg_dir / "missions"
195
+ if missions_dir.exists():
196
+ return missions_dir
197
+ except (ImportError, AttributeError):
198
+ pass
199
+
200
+ # Fallback for development: Check SPEC_KITTY_TEMPLATE_ROOT env var
201
+ import os
202
+
203
+ template_root = os.environ.get("SPEC_KITTY_TEMPLATE_ROOT")
204
+ if template_root:
205
+ missions_dir = Path(template_root) / ".kittify" / "missions"
206
+ if missions_dir.exists():
207
+ return missions_dir
208
+
209
+ # Fallback: Try to find the spec-kitty repo root from current working directory
210
+ # This handles cases where we're running from the repo in development
211
+ try:
212
+ cwd = Path.cwd()
213
+ # Check if we're in the spec-kitty repo
214
+ for parent in [cwd] + list(cwd.parents):
215
+ missions_dir = parent / ".kittify" / "missions"
216
+ pyproject = parent / "pyproject.toml"
217
+ if missions_dir.exists() and pyproject.exists():
218
+ # Verify it's the spec-kitty repo by checking pyproject.toml
219
+ try:
220
+ content = pyproject.read_text(encoding='utf-8-sig')
221
+ if "spec-kitty-cli" in content:
222
+ return missions_dir
223
+ except OSError:
224
+ pass
225
+ except OSError:
226
+ pass
227
+
228
+ return None
@@ -0,0 +1,89 @@
1
+ """Migration: Remove duplicate .claude/commands/ from worktrees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from ..registry import MigrationRegistry
9
+ from .base import BaseMigration, MigrationResult
10
+
11
+
12
+ @MigrationRegistry.register
13
+ class WorktreeCommandsDedupMigration(BaseMigration):
14
+ """Remove .claude/commands/ from worktrees - they inherit from main repo.
15
+
16
+ Claude Code traverses parent directories looking for .claude/commands/.
17
+ When a worktree is located inside the main repo (at .worktrees/),
18
+ the worktree can find commands by traversing up to the main repo.
19
+
20
+ This migration removes .claude/commands/ from worktrees since they
21
+ don't need their own copy - they inherit from the main repo.
22
+ """
23
+
24
+ migration_id = "0.7.2_worktree_commands_dedup"
25
+ description = "Remove duplicate .claude/commands/ from worktrees (inherit from main repo)"
26
+ target_version = "0.7.2"
27
+
28
+ def detect(self, project_path: Path) -> bool:
29
+ """Check if any worktrees have their own .claude/commands/."""
30
+ worktrees_dir = project_path / ".worktrees"
31
+
32
+ if not worktrees_dir.exists():
33
+ return False
34
+
35
+ for worktree in worktrees_dir.iterdir():
36
+ if worktree.is_dir():
37
+ wt_commands = worktree / ".claude" / "commands"
38
+ if wt_commands.exists():
39
+ return True
40
+
41
+ return False
42
+
43
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
44
+ """Check that main repo has commands before removing from worktrees."""
45
+ main_claude_commands = project_path / ".claude" / "commands"
46
+
47
+ if not main_claude_commands.exists():
48
+ return (
49
+ False,
50
+ "Main repo .claude/commands/ must exist before removing from worktrees"
51
+ )
52
+
53
+ return True, ""
54
+
55
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
56
+ """Remove .claude/commands/ from all worktrees."""
57
+ changes: list[str] = []
58
+ warnings: list[str] = []
59
+ errors: list[str] = []
60
+
61
+ worktrees_dir = project_path / ".worktrees"
62
+
63
+ if worktrees_dir.exists():
64
+ for worktree in worktrees_dir.iterdir():
65
+ if worktree.is_dir():
66
+ wt_commands = worktree / ".claude" / "commands"
67
+ if wt_commands.exists():
68
+ if dry_run:
69
+ changes.append(
70
+ f"Would remove .claude/commands/ from worktree {worktree.name}"
71
+ )
72
+ else:
73
+ try:
74
+ shutil.rmtree(wt_commands)
75
+ changes.append(
76
+ f"Removed .claude/commands/ from worktree {worktree.name} (inherits from main repo)"
77
+ )
78
+ except OSError as e:
79
+ errors.append(
80
+ f"Failed to remove .claude/commands/ from {worktree.name}: {e}"
81
+ )
82
+
83
+ success = len(errors) == 0
84
+ return MigrationResult(
85
+ success=success,
86
+ changes_made=changes,
87
+ errors=errors,
88
+ warnings=warnings,
89
+ )