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,114 @@
1
+ """Migration: Ensure workflow commands in agent prompts include --agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ from ..registry import MigrationRegistry
9
+ from .base import BaseMigration, MigrationResult
10
+ from .m_0_9_1_complete_lane_migration import get_agent_dirs_for_project
11
+
12
+
13
+ @MigrationRegistry.register
14
+ class WorkflowAgentFlagMigration(BaseMigration):
15
+ """Append --agent <name> to workflow commands in agent prompts."""
16
+
17
+ migration_id = "0.11.3_workflow_agent_flag"
18
+ description = "Ensure workflow commands in agent prompts include --agent"
19
+ target_version = "0.11.3"
20
+
21
+ AGENT_NAME_MAP = {
22
+ ".github": "copilot",
23
+ ".augment": "auggie",
24
+ ".amazonq": "q",
25
+ }
26
+
27
+ TARGET_FILES = ("spec-kitty.implement.md", "spec-kitty.review.md")
28
+
29
+ def _agent_name(self, agent_root: str) -> str:
30
+ return self.AGENT_NAME_MAP.get(agent_root, agent_root.lstrip("."))
31
+
32
+ def _update_workflow_lines(self, path: Path, agent_name: str, dry_run: bool) -> bool:
33
+ if not path.exists():
34
+ return False
35
+
36
+ text = path.read_text(encoding="utf-8")
37
+ lines = text.splitlines()
38
+ updated = False
39
+
40
+ def _patch_line(line: str) -> str:
41
+ nonlocal updated
42
+ # Replace __AGENT__ placeholder with actual agent name
43
+ if "__AGENT__" in line:
44
+ updated = True
45
+ line = line.replace("__AGENT__", agent_name)
46
+ # Add --agent flag if missing
47
+ if "spec-kitty agent workflow implement" in line and "--agent" not in line:
48
+ updated = True
49
+ return f"{line} --agent {agent_name}"
50
+ if "spec-kitty agent workflow review" in line and "--agent" not in line:
51
+ updated = True
52
+ return f"{line} --agent {agent_name}"
53
+ return line
54
+
55
+ lines = [_patch_line(line) for line in lines]
56
+ if updated and not dry_run:
57
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
58
+ return updated
59
+
60
+ def detect(self, project_path: Path) -> bool:
61
+ agent_dirs = get_agent_dirs_for_project(project_path)
62
+ for agent_root, subdir in agent_dirs:
63
+ agent_dir = project_path / agent_root / subdir
64
+ if not agent_dir.exists():
65
+ continue
66
+ agent_name = self._agent_name(agent_root)
67
+ for filename in self.TARGET_FILES:
68
+ path = agent_dir / filename
69
+ if not path.exists():
70
+ continue
71
+ text = path.read_text(encoding="utf-8")
72
+ for line in text.splitlines():
73
+ # Detect __AGENT__ placeholder that needs replacement
74
+ if "__AGENT__" in line:
75
+ return True
76
+ if "spec-kitty agent workflow implement" in line and "--agent" not in line:
77
+ return True
78
+ if "spec-kitty agent workflow review" in line and "--agent" not in line:
79
+ return True
80
+ return False
81
+
82
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
83
+ if not (project_path / ".kittify").exists():
84
+ return False, "No .kittify directory (not a spec-kitty project)"
85
+ return True, ""
86
+
87
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
88
+ changes: List[str] = []
89
+ warnings: List[str] = []
90
+ errors: List[str] = []
91
+
92
+ agent_dirs = get_agent_dirs_for_project(project_path)
93
+ for agent_root, subdir in agent_dirs:
94
+ agent_dir = project_path / agent_root / subdir
95
+ if not agent_dir.exists():
96
+ continue
97
+ agent_name = self._agent_name(agent_root)
98
+ updated_count = 0
99
+ for filename in self.TARGET_FILES:
100
+ path = agent_dir / filename
101
+ if self._update_workflow_lines(path, agent_name, dry_run):
102
+ updated_count += 1
103
+ if updated_count:
104
+ changes.append(f"Updated {updated_count} workflow prompts for {agent_name}")
105
+
106
+ if not changes:
107
+ warnings.append("No workflow prompts required updates")
108
+
109
+ return MigrationResult(
110
+ success=len(errors) == 0,
111
+ changes_made=changes,
112
+ errors=errors,
113
+ warnings=warnings,
114
+ )
@@ -0,0 +1,155 @@
1
+ """Migration: Install documentation mission to user projects (v0.12.0)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from ..registry import MigrationRegistry
10
+ from .base import BaseMigration, MigrationResult
11
+
12
+
13
+ @MigrationRegistry.register
14
+ class InstallDocumentationMission(BaseMigration):
15
+ """Install the documentation mission to user projects.
16
+
17
+ This migration copies the documentation mission from the spec-kitty
18
+ installation (src/specify_cli/missions/documentation/) to the user's
19
+ project (.kittify/missions/documentation/).
20
+
21
+ The documentation mission enables users to create and maintain software
22
+ documentation following Write the Docs and Divio principles.
23
+ """
24
+
25
+ migration_id = "0.12.0_documentation_mission"
26
+ description = "Install documentation mission to user projects"
27
+ target_version = "0.12.0"
28
+
29
+ def detect(self, project_path: Path) -> bool:
30
+ """Detect if documentation mission needs to be installed.
31
+
32
+ Args:
33
+ project_path: Root directory of user's spec-kitty project
34
+
35
+ Returns:
36
+ True if documentation mission is missing, False if already installed
37
+ """
38
+ kittify_dir = project_path / ".kittify"
39
+
40
+ if not kittify_dir.exists():
41
+ # Not a spec-kitty project, migration doesn't apply
42
+ return False
43
+
44
+ missions_dir = kittify_dir / "missions"
45
+
46
+ if not missions_dir.exists():
47
+ # Missions directory doesn't exist (very old project)
48
+ # Migration should run to create it
49
+ return True
50
+
51
+ doc_mission_dir = missions_dir / "documentation"
52
+
53
+ # Check if documentation mission already exists
54
+ if doc_mission_dir.exists() and (doc_mission_dir / "mission.yaml").exists():
55
+ # Already installed
56
+ return False
57
+
58
+ # Documentation mission is missing, migration should run
59
+ return True
60
+
61
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
62
+ """Check if migration can be safely applied.
63
+
64
+ Args:
65
+ project_path: Root of the project
66
+
67
+ Returns:
68
+ (can_apply, reason) - True if safe, False with explanation if not
69
+ """
70
+ # Check if source mission exists
71
+ source_mission = self._find_source_mission()
72
+ if source_mission is None:
73
+ return (
74
+ False,
75
+ "Could not locate documentation mission source in spec-kitty installation. "
76
+ "This may indicate an incomplete installation.",
77
+ )
78
+
79
+ return True, ""
80
+
81
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
82
+ """Copy documentation mission to user project.
83
+
84
+ Args:
85
+ project_path: Root directory of user's spec-kitty project
86
+ dry_run: If True, only simulate changes
87
+
88
+ Returns:
89
+ MigrationResult indicating success or failure
90
+ """
91
+ changes: list[str] = []
92
+ errors: list[str] = []
93
+
94
+ kittify_dir = project_path / ".kittify"
95
+ missions_dir = kittify_dir / "missions"
96
+
97
+ # Find source documentation mission
98
+ source_mission = self._find_source_mission()
99
+
100
+ if source_mission is None:
101
+ errors.append("Could not find documentation mission source in spec-kitty installation")
102
+ return MigrationResult(success=False, errors=errors)
103
+
104
+ # Destination
105
+ dest_mission = missions_dir / "documentation"
106
+
107
+ # Check if destination already exists
108
+ if dest_mission.exists() and (dest_mission / "mission.yaml").exists():
109
+ return MigrationResult(
110
+ success=True,
111
+ changes_made=["Documentation mission already installed (skipped)"]
112
+ )
113
+
114
+ # Ensure missions directory exists
115
+ if not missions_dir.exists():
116
+ if dry_run:
117
+ changes.append("Would create .kittify/missions/ directory")
118
+ else:
119
+ missions_dir.mkdir(parents=True, exist_ok=True)
120
+ changes.append("Created .kittify/missions/ directory")
121
+
122
+ # Copy mission directory
123
+ if dry_run:
124
+ changes.append("Would copy documentation mission to .kittify/missions/documentation/")
125
+ else:
126
+ try:
127
+ shutil.copytree(source_mission, dest_mission)
128
+
129
+ # Count copied files for reporting
130
+ copied_files = list(dest_mission.rglob("*"))
131
+ file_count = len([f for f in copied_files if f.is_file()])
132
+
133
+ changes.append(f"Copied documentation mission ({file_count} files)")
134
+
135
+ except Exception as e:
136
+ errors.append(f"Failed to copy documentation mission: {e}")
137
+ return MigrationResult(success=False, errors=errors)
138
+
139
+ return MigrationResult(success=True, changes_made=changes)
140
+
141
+ def _find_source_mission(self) -> Optional[Path]:
142
+ """Find the documentation mission in spec-kitty's installation.
143
+
144
+ Returns:
145
+ Path to source mission directory, or None if not found
146
+ """
147
+ # The source is relative to this migration file
148
+ migrations_dir = Path(__file__).parent
149
+ src_dir = migrations_dir.parent.parent # Up to src/specify_cli/
150
+ source_mission = src_dir / "missions" / "documentation"
151
+
152
+ if source_mission.exists() and (source_mission / "mission.yaml").exists():
153
+ return source_mission
154
+
155
+ return None
@@ -0,0 +1,183 @@
1
+ """Migration: Remove kitty-specs/ from main repo .gitignore.
2
+
3
+ Historical context:
4
+ - Earlier spec-kitty versions or user templates may have added `kitty-specs/` to .gitignore
5
+ - This prevents git from tracking feature specifications, causing failures in:
6
+ - `spec-kitty agent feature create-feature`
7
+ - `/spec-kitty.specify` (commit step)
8
+ - Other commands that commit to kitty-specs/
9
+
10
+ The fix:
11
+ - Remove `kitty-specs/` entries from main repo .gitignore
12
+ - Keep worktree-specific patterns like `kitty-specs/**/tasks/*.md` (those prevent merge conflicts)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from pathlib import Path
19
+ from typing import List, Tuple
20
+
21
+ from ..registry import MigrationRegistry
22
+ from .base import BaseMigration, MigrationResult
23
+
24
+ MIGRATION_ID = "0.12.1_remove_kitty_specs_from_gitignore"
25
+ MIGRATION_VERSION = "0.12.1"
26
+ MIGRATION_DESCRIPTION = "Remove kitty-specs/ from main repo .gitignore to allow tracking feature specs"
27
+
28
+ # Patterns to REMOVE (block entire kitty-specs directory)
29
+ PATTERNS_TO_REMOVE = [
30
+ r"^kitty-specs/?$", # kitty-specs or kitty-specs/
31
+ r"^/kitty-specs/?$", # /kitty-specs or /kitty-specs/
32
+ ]
33
+
34
+ # Patterns to KEEP (worktree-specific, prevent merge conflicts)
35
+ PATTERNS_TO_KEEP = [
36
+ r"kitty-specs/\*\*/tasks/", # kitty-specs/**/tasks/*.md
37
+ r"kitty-specs/.*/tasks/", # kitty-specs/*/tasks/*.md
38
+ ]
39
+
40
+
41
+ def is_blocking_pattern(line: str) -> bool:
42
+ """Check if a line blocks the entire kitty-specs directory.
43
+
44
+ Returns True for patterns like:
45
+ - kitty-specs
46
+ - kitty-specs/
47
+ - /kitty-specs
48
+ - /kitty-specs/
49
+
50
+ Returns False for specific subpath patterns like:
51
+ - kitty-specs/**/tasks/*.md (this is fine, used in worktrees)
52
+ """
53
+ stripped = line.strip()
54
+
55
+ # Skip comments and empty lines
56
+ if not stripped or stripped.startswith("#"):
57
+ return False
58
+
59
+ # Check if it's a specific subpath pattern (KEEP these)
60
+ for keep_pattern in PATTERNS_TO_KEEP:
61
+ if re.search(keep_pattern, stripped):
62
+ return False
63
+
64
+ # Check if it blocks the entire directory (REMOVE these)
65
+ for remove_pattern in PATTERNS_TO_REMOVE:
66
+ if re.match(remove_pattern, stripped):
67
+ return True
68
+
69
+ return False
70
+
71
+
72
+ def find_blocking_entries(gitignore_path: Path) -> List[Tuple[int, str]]:
73
+ """Find all lines that block kitty-specs/ entirely.
74
+
75
+ Returns list of (line_number, line_content) tuples.
76
+ Line numbers are 1-indexed for user display.
77
+ """
78
+ if not gitignore_path.exists():
79
+ return []
80
+
81
+ blocking_entries = []
82
+ content = gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
83
+
84
+ for i, line in enumerate(content.splitlines(), start=1):
85
+ if is_blocking_pattern(line):
86
+ blocking_entries.append((i, line.strip()))
87
+
88
+ return blocking_entries
89
+
90
+
91
+ def remove_blocking_entries(gitignore_path: Path, dry_run: bool = False) -> Tuple[List[str], List[str]]:
92
+ """Remove entries that block kitty-specs/ from .gitignore.
93
+
94
+ Returns (changes, errors) tuple.
95
+ """
96
+ changes: List[str] = []
97
+ errors: List[str] = []
98
+
99
+ if not gitignore_path.exists():
100
+ changes.append("No .gitignore file found")
101
+ return changes, errors
102
+
103
+ try:
104
+ content = gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
105
+ except OSError as e:
106
+ errors.append(f"Failed to read .gitignore: {e}")
107
+ return changes, errors
108
+
109
+ lines = content.splitlines(keepends=True)
110
+ new_lines = []
111
+ removed_count = 0
112
+
113
+ for i, line in enumerate(lines, start=1):
114
+ if is_blocking_pattern(line):
115
+ changes.append(f"Line {i}: Removed '{line.strip()}'")
116
+ removed_count += 1
117
+ # Skip this line (don't add to new_lines)
118
+ else:
119
+ new_lines.append(line)
120
+
121
+ if removed_count == 0:
122
+ changes.append("No blocking kitty-specs/ entries found in .gitignore")
123
+ return changes, errors
124
+
125
+ if dry_run:
126
+ changes.insert(0, f"Would remove {removed_count} blocking entries from .gitignore")
127
+ return changes, errors
128
+
129
+ # Write updated content
130
+ try:
131
+ new_content = "".join(new_lines)
132
+ # Clean up any resulting double blank lines
133
+ new_content = re.sub(r"\n{3,}", "\n\n", new_content)
134
+ gitignore_path.write_text(new_content, encoding="utf-8")
135
+ changes.insert(0, f"Removed {removed_count} blocking entries from .gitignore")
136
+ except OSError as e:
137
+ errors.append(f"Failed to write .gitignore: {e}")
138
+
139
+ return changes, errors
140
+
141
+
142
+ @MigrationRegistry.register
143
+ class RemoveKittySpecsFromGitignoreMigration(BaseMigration):
144
+ """Remove kitty-specs/ from main repo .gitignore.
145
+
146
+ Feature specifications must be tracked in git. If kitty-specs/ is in
147
+ .gitignore, git add operations fail during feature creation.
148
+ """
149
+
150
+ migration_id = MIGRATION_ID
151
+ description = MIGRATION_DESCRIPTION
152
+ target_version = MIGRATION_VERSION
153
+
154
+ def detect(self, project_path: Path) -> bool:
155
+ """Check if .gitignore contains blocking kitty-specs/ entries."""
156
+ gitignore_path = project_path / ".gitignore"
157
+ blocking_entries = find_blocking_entries(gitignore_path)
158
+ return len(blocking_entries) > 0
159
+
160
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
161
+ """Check if we can modify .gitignore."""
162
+ gitignore_path = project_path / ".gitignore"
163
+
164
+ if not gitignore_path.exists():
165
+ return True, ""
166
+
167
+ try:
168
+ gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
169
+ return True, ""
170
+ except OSError as e:
171
+ return False, f".gitignore is not readable: {e}"
172
+
173
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
174
+ """Remove blocking kitty-specs/ entries from .gitignore."""
175
+ gitignore_path = project_path / ".gitignore"
176
+ changes, errors = remove_blocking_entries(gitignore_path, dry_run=dry_run)
177
+
178
+ return MigrationResult(
179
+ success=len(errors) == 0,
180
+ changes_made=changes,
181
+ errors=errors,
182
+ warnings=[],
183
+ )
@@ -0,0 +1,80 @@
1
+ """Migration: Rename .specify/ to .kittify/ and /specs/ to /kitty-specs/."""
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 SpecifyToKittifyMigration(BaseMigration):
14
+ """Migrate from .specify/ to .kittify/ and /specs/ to /kitty-specs/.
15
+
16
+ This migration handles the rebranding from the original "specify"
17
+ naming to "kittify" naming introduced in v0.2.0.
18
+ """
19
+
20
+ migration_id = "0.2.0_specify_to_kittify"
21
+ description = "Rename .specify/ to .kittify/ and /specs/ to /kitty-specs/"
22
+ target_version = "0.2.0"
23
+
24
+ def detect(self, project_path: Path) -> bool:
25
+ """Check if project uses old .specify/ directory."""
26
+ return (project_path / ".specify").exists()
27
+
28
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
29
+ """Check if migration can be safely applied."""
30
+ specify_dir = project_path / ".specify"
31
+ kittify_dir = project_path / ".kittify"
32
+
33
+ if not specify_dir.exists():
34
+ return False, ".specify/ directory does not exist"
35
+
36
+ if kittify_dir.exists():
37
+ return False, ".kittify/ already exists - manual merge required"
38
+
39
+ specs_dir = project_path / "specs"
40
+ kitty_specs_dir = project_path / "kitty-specs"
41
+
42
+ if specs_dir.exists() and kitty_specs_dir.exists():
43
+ return False, "Both /specs/ and /kitty-specs/ exist - manual merge required"
44
+
45
+ return True, ""
46
+
47
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
48
+ """Apply the migration."""
49
+ changes: list[str] = []
50
+ errors: list[str] = []
51
+
52
+ specify_dir = project_path / ".specify"
53
+ kittify_dir = project_path / ".kittify"
54
+ specs_dir = project_path / "specs"
55
+ kitty_specs_dir = project_path / "kitty-specs"
56
+
57
+ # Rename .specify/ to .kittify/
58
+ if specify_dir.exists():
59
+ if dry_run:
60
+ changes.append(f"Would rename {specify_dir} to {kittify_dir}")
61
+ else:
62
+ try:
63
+ shutil.move(str(specify_dir), str(kittify_dir))
64
+ changes.append(f"Renamed {specify_dir} to {kittify_dir}")
65
+ except OSError as e:
66
+ errors.append(f"Failed to rename .specify/ to .kittify/: {e}")
67
+
68
+ # Rename /specs/ to /kitty-specs/
69
+ if specs_dir.exists() and not kitty_specs_dir.exists():
70
+ if dry_run:
71
+ changes.append(f"Would rename {specs_dir} to {kitty_specs_dir}")
72
+ else:
73
+ try:
74
+ shutil.move(str(specs_dir), str(kitty_specs_dir))
75
+ changes.append(f"Renamed {specs_dir} to {kitty_specs_dir}")
76
+ except OSError as e:
77
+ errors.append(f"Failed to rename /specs/ to /kitty-specs/: {e}")
78
+
79
+ success = len(errors) == 0
80
+ return MigrationResult(success=success, changes_made=changes, errors=errors)
@@ -0,0 +1,118 @@
1
+ """Migration: Ensure all 12 agent directories are in .gitignore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ..registry import MigrationRegistry
8
+ from .base import BaseMigration, MigrationResult
9
+
10
+
11
+ @MigrationRegistry.register
12
+ class GitignoreAgentsMigration(BaseMigration):
13
+ """Ensure all 12 agent directories are in .gitignore.
14
+
15
+ This migration adds protection for all known AI agent directories
16
+ that should never be committed to git (they contain auth tokens).
17
+ """
18
+
19
+ migration_id = "0.4.8_gitignore_agents"
20
+ description = "Add all 12 AI agent directories to .gitignore"
21
+ target_version = "0.4.8"
22
+
23
+ EXPECTED_AGENTS = [
24
+ ".claude/",
25
+ ".codex/",
26
+ ".opencode/",
27
+ ".windsurf/",
28
+ ".gemini/",
29
+ ".cursor/",
30
+ ".qwen/",
31
+ ".kilocode/",
32
+ ".augment/",
33
+ ".roo/",
34
+ ".amazonq/",
35
+ ".github/copilot/",
36
+ ]
37
+
38
+ def detect(self, project_path: Path) -> bool:
39
+ """Check if .gitignore is missing agent directories."""
40
+ gitignore = project_path / ".gitignore"
41
+ if not gitignore.exists():
42
+ return True
43
+
44
+ try:
45
+ content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
46
+ except OSError:
47
+ return True
48
+
49
+ missing = [d for d in self.EXPECTED_AGENTS if d not in content]
50
+ return len(missing) > 0
51
+
52
+ def can_apply(self, project_path: Path) -> tuple[bool, str]:
53
+ """Check if we can modify .gitignore."""
54
+ gitignore = project_path / ".gitignore"
55
+
56
+ if gitignore.exists():
57
+ try:
58
+ # Test read access; tolerate BOM and ignore invalid UTF-8 bytes
59
+ gitignore.read_text(encoding="utf-8-sig", errors="ignore")
60
+ except (OSError, UnicodeDecodeError):
61
+ return False, ".gitignore is not readable"
62
+
63
+ return True, ""
64
+
65
+ def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
66
+ """Apply gitignore updates."""
67
+ changes: list[str] = []
68
+ warnings: list[str] = []
69
+ errors: list[str] = []
70
+
71
+ gitignore = project_path / ".gitignore"
72
+
73
+ # Determine what needs to be added
74
+ existing_content = ""
75
+ if gitignore.exists():
76
+ try:
77
+ existing_content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
78
+ except OSError as e:
79
+ errors.append(f"Failed to read .gitignore: {e}")
80
+ return MigrationResult(success=False, errors=errors)
81
+
82
+ missing = [d for d in self.EXPECTED_AGENTS if d not in existing_content]
83
+
84
+ if not missing:
85
+ changes.append("All agent directories already in .gitignore")
86
+ return MigrationResult(success=True, changes_made=changes)
87
+
88
+ if dry_run:
89
+ changes.append(f"Would add {len(missing)} agent directories to .gitignore")
90
+ for d in missing:
91
+ changes.append(f" - {d}")
92
+ return MigrationResult(success=True, changes_made=changes)
93
+
94
+ # Build new content
95
+ new_entries = "\n# AI Agent directories (added by Spec Kitty CLI)\n"
96
+ new_entries += "# These contain auth tokens and should NEVER be committed\n"
97
+ for d in missing:
98
+ new_entries += f"{d}\n"
99
+
100
+ # Ensure existing content ends with newline
101
+ if existing_content and not existing_content.endswith("\n"):
102
+ existing_content += "\n"
103
+
104
+ new_content = existing_content + new_entries
105
+
106
+ try:
107
+ gitignore.write_text(new_content, encoding="utf-8")
108
+ changes.append(f"Added {len(missing)} agent directories to .gitignore")
109
+ except OSError as e:
110
+ errors.append(f"Failed to write .gitignore: {e}")
111
+
112
+ success = len(errors) == 0
113
+ return MigrationResult(
114
+ success=success,
115
+ changes_made=changes,
116
+ errors=errors,
117
+ warnings=warnings,
118
+ )