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,249 @@
1
+ """Upgrade command implementation for Spec Kitty CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ from specify_cli.cli.helpers import console, show_banner
14
+
15
+
16
+ def upgrade(
17
+ dry_run: bool = typer.Option(
18
+ False, "--dry-run", help="Preview changes without applying"
19
+ ),
20
+ force: bool = typer.Option(False, "--force", help="Skip confirmation prompts"),
21
+ target: Optional[str] = typer.Option(
22
+ None, "--target", help="Target version (defaults to current CLI version)"
23
+ ),
24
+ json_output: bool = typer.Option(False, "--json", help="Output results as JSON"),
25
+ verbose: bool = typer.Option(
26
+ False, "--verbose", "-v", help="Show detailed migration information"
27
+ ),
28
+ no_worktrees: bool = typer.Option(
29
+ False, "--no-worktrees", help="Skip upgrading worktrees"
30
+ ),
31
+ ) -> None:
32
+ """Upgrade a Spec Kitty project to the current version.
33
+
34
+ Detects the project's current version and applies all necessary migrations
35
+ to bring it up to date with the installed CLI version.
36
+
37
+ Examples:
38
+ spec-kitty upgrade # Upgrade to current version
39
+ spec-kitty upgrade --dry-run # Preview changes
40
+ spec-kitty upgrade --target 0.6.5 # Upgrade to specific version
41
+ """
42
+ if not json_output:
43
+ show_banner()
44
+
45
+ project_path = Path.cwd()
46
+ kittify_dir = project_path / ".kittify"
47
+ specify_dir = project_path / ".specify" # Old name
48
+
49
+ # Check if this is a Spec Kitty project
50
+ if not kittify_dir.exists() and not specify_dir.exists():
51
+ if json_output:
52
+ console.print(json.dumps({"error": "Not a Spec Kitty project"}))
53
+ else:
54
+ console.print("[red]Error:[/red] Not a Spec Kitty project.")
55
+ console.print(
56
+ "[dim]Run 'spec-kitty init' to initialize a project.[/dim]"
57
+ )
58
+ raise typer.Exit(1)
59
+
60
+ # Import upgrade system (lazy to avoid circular imports)
61
+ from specify_cli.upgrade.detector import VersionDetector
62
+ from specify_cli.upgrade.registry import MigrationRegistry
63
+ from specify_cli.upgrade.runner import MigrationRunner
64
+
65
+ # Import migrations to register them
66
+ from specify_cli.upgrade import migrations # noqa: F401
67
+
68
+ # Detect current version
69
+ detector = VersionDetector(project_path)
70
+ current_version = detector.detect_version()
71
+
72
+ # Determine target version
73
+ if target is None:
74
+ from specify_cli import __version__
75
+
76
+ target_version = __version__
77
+ else:
78
+ target_version = target
79
+
80
+ if not json_output:
81
+ console.print(f"[cyan]Current version:[/cyan] {current_version}")
82
+ console.print(f"[cyan]Target version:[/cyan] {target_version}")
83
+ console.print()
84
+
85
+ # Get needed migrations
86
+ # Handle "unknown" version by treating it as very old (0.0.0)
87
+ version_for_migration = "0.0.0" if current_version == "unknown" else current_version
88
+ migrations_needed = MigrationRegistry.get_applicable(version_for_migration, target_version, project_path=project_path)
89
+
90
+ if not migrations_needed:
91
+ if json_output:
92
+ console.print(
93
+ json.dumps(
94
+ {
95
+ "status": "up_to_date",
96
+ "current_version": current_version,
97
+ "target_version": target_version,
98
+ }
99
+ )
100
+ )
101
+ else:
102
+ console.print("[green]Project is already up to date![/green]")
103
+ return
104
+
105
+ # Show migration plan
106
+ if not json_output:
107
+ table = Table(
108
+ title="Migration Plan", show_lines=False, header_style="bold cyan"
109
+ )
110
+ table.add_column("Migration", style="bright_white")
111
+ table.add_column("Description", style="dim")
112
+ table.add_column("Target", style="cyan")
113
+
114
+ for migration in migrations_needed:
115
+ table.add_row(
116
+ migration.migration_id,
117
+ migration.description,
118
+ migration.target_version,
119
+ )
120
+
121
+ console.print(table)
122
+ console.print()
123
+
124
+ if verbose:
125
+ # Show detection results
126
+ console.print("[dim]Detection results:[/dim]")
127
+ for migration in migrations_needed:
128
+ detected = migration.detect(project_path)
129
+ can_apply, reason = migration.can_apply(project_path)
130
+ status = "[green]ready[/green]" if detected and can_apply else "[yellow]skipped[/yellow]"
131
+ console.print(f" {migration.migration_id}: {status}")
132
+ if not can_apply and reason:
133
+ console.print(f" [dim]{reason}[/dim]")
134
+ console.print()
135
+
136
+ # Confirm if not dry-run and not forced
137
+ if not dry_run and not force:
138
+ proceed = typer.confirm(
139
+ f"Apply {len(migrations_needed)} migration(s)?",
140
+ default=True,
141
+ )
142
+ if not proceed:
143
+ console.print("[yellow]Upgrade cancelled.[/yellow]")
144
+ raise typer.Exit(0)
145
+
146
+ # Run migrations
147
+ runner = MigrationRunner(project_path, console)
148
+ result = runner.upgrade(
149
+ target_version,
150
+ dry_run=dry_run,
151
+ force=force,
152
+ include_worktrees=not no_worktrees,
153
+ )
154
+
155
+ if json_output:
156
+ # Build detailed migrations array
157
+ migrations_detail = []
158
+ for migration in migrations_needed:
159
+ status = "applied" if migration.migration_id in result.migrations_applied else (
160
+ "skipped" if migration.migration_id in result.migrations_skipped else "pending"
161
+ )
162
+ migrations_detail.append({
163
+ "id": migration.migration_id,
164
+ "description": migration.description,
165
+ "target_version": migration.target_version,
166
+ "status": status,
167
+ })
168
+
169
+ output = {
170
+ "status": "success" if result.success else "failed",
171
+ "current_version": result.from_version,
172
+ "target_version": result.to_version,
173
+ "dry_run": result.dry_run,
174
+ "migrations": migrations_detail,
175
+ "migrations_applied": result.migrations_applied,
176
+ "migrations_skipped": result.migrations_skipped,
177
+ "success": result.success,
178
+ "errors": result.errors,
179
+ "warnings": result.warnings,
180
+ }
181
+ console.print(json.dumps(output, indent=2))
182
+ return
183
+
184
+ # Display results
185
+ console.print()
186
+
187
+ if result.dry_run:
188
+ console.print(
189
+ Panel(
190
+ "[yellow]DRY RUN[/yellow] - No changes were made",
191
+ border_style="yellow",
192
+ )
193
+ )
194
+
195
+ if result.migrations_applied:
196
+ console.print("[green]Migrations applied:[/green]")
197
+ for m in result.migrations_applied:
198
+ console.print(f" [green]✓[/green] {m}")
199
+
200
+ if result.migrations_skipped:
201
+ console.print("[dim]Migrations skipped (already applied or not needed):[/dim]")
202
+ for m in result.migrations_skipped:
203
+ console.print(f" [dim]○[/dim] {m}")
204
+
205
+ if result.warnings:
206
+ console.print("[yellow]Warnings:[/yellow]")
207
+ for w in result.warnings:
208
+ console.print(f" [yellow]![/yellow] {w}")
209
+
210
+ if result.errors:
211
+ console.print("[red]Errors:[/red]")
212
+ for e in result.errors:
213
+ console.print(f" [red]✗[/red] {e}")
214
+
215
+ console.print()
216
+ if result.success:
217
+ console.print(
218
+ f"[bold green]Upgrade complete![/bold green] {result.from_version} -> {result.to_version}"
219
+ )
220
+ else:
221
+ console.print("[bold red]Upgrade failed.[/bold red]")
222
+ raise typer.Exit(1)
223
+
224
+
225
+ def list_legacy_features() -> None:
226
+ """List legacy worktrees blocking 0.11.0 upgrade."""
227
+ from specify_cli.tasks_support import find_repo_root
228
+ from specify_cli.upgrade.migrations.m_0_11_0_workspace_per_wp import (
229
+ detect_legacy_worktrees,
230
+ )
231
+
232
+ repo_root = find_repo_root()
233
+ legacy = detect_legacy_worktrees(repo_root)
234
+
235
+ if not legacy:
236
+ console.print("[green]✓[/green] No legacy worktrees found")
237
+ console.print("Project is ready for 0.11.0 upgrade")
238
+ return
239
+
240
+ console.print(f"[yellow]Legacy worktrees found:[/yellow] {len(legacy)}\n")
241
+ for worktree in legacy:
242
+ console.print(f" - {worktree.name}")
243
+
244
+ console.print("\n[cyan]Action required:[/cyan]")
245
+ console.print(" Complete: spec-kitty merge <feature>")
246
+ console.print(" OR Delete: git worktree remove .worktrees/<feature>")
247
+
248
+
249
+ __all__ = ["upgrade", "list_legacy_features"]
@@ -0,0 +1,186 @@
1
+ """Encoding validation command for Spec Kitty CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from specify_cli.acceptance import AcceptanceError, detect_feature_slug
13
+ from specify_cli.cli.helpers import check_version_compatibility, console, get_project_root_or_exit
14
+ from specify_cli.core.project_resolver import resolve_worktree_aware_feature_dir
15
+ from specify_cli.tasks_support import TaskCliError, find_repo_root
16
+ from specify_cli.text_sanitization import detect_problematic_characters, sanitize_directory, sanitize_file
17
+
18
+
19
+ def validate_encoding(
20
+ feature: Optional[str] = typer.Option(None, "--feature", help="Feature slug to validate (auto-detected when omitted)"),
21
+ fix: bool = typer.Option(False, "--fix", help="Automatically fix encoding errors by sanitizing files"),
22
+ check_all: bool = typer.Option(False, "--all", help="Check all features, not just one"),
23
+ backup: bool = typer.Option(True, "--backup/--no-backup", help="Create .bak files before fixing"),
24
+ ) -> None:
25
+ """Validate and optionally fix file encoding in feature artifacts.
26
+
27
+ Scans markdown files for Windows-1252 smart quotes and other problematic
28
+ characters that cause UTF-8 encoding errors. Can automatically fix issues
29
+ by replacing problematic characters with safe alternatives.
30
+ """
31
+ try:
32
+ repo_root = find_repo_root()
33
+ except TaskCliError as exc:
34
+ console.print(f"[red]Error:[/red] {exc}")
35
+ raise typer.Exit(1)
36
+
37
+ project_root = get_project_root_or_exit(repo_root)
38
+ check_version_compatibility(project_root, "validate-encoding")
39
+
40
+ if check_all:
41
+ # Validate all features
42
+ kitty_specs = repo_root / "kitty-specs"
43
+ if not kitty_specs.exists():
44
+ console.print("[yellow]No kitty-specs directory found.[/yellow]")
45
+ raise typer.Exit(0)
46
+
47
+ feature_dirs = [d for d in kitty_specs.iterdir() if d.is_dir()]
48
+ if not feature_dirs:
49
+ console.print("[yellow]No feature directories found.[/yellow]")
50
+ raise typer.Exit(0)
51
+
52
+ console.print(f"[cyan]Checking encoding for {len(feature_dirs)} features...[/cyan]")
53
+ console.print()
54
+
55
+ total_issues = 0
56
+ total_fixed = 0
57
+
58
+ for feature_dir in sorted(feature_dirs):
59
+ issues, fixed = _validate_feature_dir(feature_dir, fix=fix, backup=backup)
60
+ total_issues += issues
61
+ total_fixed += fixed
62
+
63
+ console.print()
64
+ console.print(Panel(
65
+ f"[bold]Summary:[/bold]\n"
66
+ f"Total files with issues: [yellow]{total_issues}[/yellow]\n"
67
+ f"Total files fixed: [green]{total_fixed}[/green]",
68
+ title="Encoding Validation Complete",
69
+ border_style="cyan" if total_issues == 0 else "yellow",
70
+ ))
71
+
72
+ raise typer.Exit(0 if total_issues == 0 or fix else 1)
73
+
74
+ # Validate single feature
75
+ try:
76
+ feature_slug = (feature or detect_feature_slug(repo_root, cwd=Path.cwd())).strip()
77
+ except AcceptanceError as exc:
78
+ console.print(f"[red]Error:[/red] {exc}")
79
+ raise typer.Exit(1)
80
+
81
+ feature_dir = resolve_worktree_aware_feature_dir(repo_root, feature_slug, Path.cwd(), console)
82
+
83
+ if not feature_dir.exists():
84
+ console.print(f"[red]Error:[/red] Feature directory not found: {feature_dir}")
85
+ raise typer.Exit(1)
86
+
87
+ console.print(f"[cyan]Validating encoding for feature:[/cyan] {feature_slug}")
88
+ console.print()
89
+
90
+ issues, fixed = _validate_feature_dir(feature_dir, fix=fix, backup=backup)
91
+
92
+ if issues == 0:
93
+ console.print("[green]✓ All files are properly UTF-8 encoded![/green]")
94
+ raise typer.Exit(0)
95
+ elif fix and fixed > 0:
96
+ console.print()
97
+ console.print(f"[green]✓ Fixed {fixed} file(s) with encoding issues.[/green]")
98
+ if backup:
99
+ console.print("[dim]Backup files (.bak) were created.[/dim]")
100
+ raise typer.Exit(0)
101
+ else:
102
+ console.print()
103
+ console.print(f"[yellow]Found {issues} file(s) with encoding issues.[/yellow]")
104
+ console.print("[dim]Run with --fix to automatically repair these files.[/dim]")
105
+ raise typer.Exit(1)
106
+
107
+
108
+ def _validate_feature_dir(feature_dir: Path, *, fix: bool, backup: bool) -> tuple[int, int]:
109
+ """Validate encoding for a single feature directory.
110
+
111
+ Returns:
112
+ Tuple of (issues_found, files_fixed)
113
+ """
114
+ console.print(f"[cyan]Checking:[/cyan] {feature_dir.name}")
115
+
116
+ # Scan all markdown files
117
+ results = sanitize_directory(feature_dir, pattern="**/*.md", backup=backup, dry_run=not fix)
118
+
119
+ files_with_issues = []
120
+ files_fixed = []
121
+ file_errors = []
122
+
123
+ for file_path_str, (was_modified, error) in results.items():
124
+ file_path = Path(file_path_str)
125
+ relative_path = file_path.relative_to(feature_dir) if file_path.is_relative_to(feature_dir) else file_path
126
+
127
+ if error:
128
+ file_errors.append((relative_path, error))
129
+ elif was_modified:
130
+ files_with_issues.append(relative_path)
131
+ if fix:
132
+ files_fixed.append(relative_path)
133
+
134
+ # Display results
135
+ if files_with_issues:
136
+ table = Table(title=f"Files with Encoding Issues: {feature_dir.name}", show_header=True)
137
+ table.add_column("File", style="cyan")
138
+ table.add_column("Status", style="yellow")
139
+
140
+ for file_path in files_with_issues:
141
+ status = "[green]Fixed[/green]" if fix else "[yellow]Needs Fix[/yellow]"
142
+ table.add_row(str(file_path), status)
143
+
144
+ console.print(table)
145
+
146
+ # Show detailed character issues for first file
147
+ if files_with_issues and not fix:
148
+ first_file = feature_dir / files_with_issues[0]
149
+ try:
150
+ # Read with fallback encoding
151
+ try:
152
+ content = first_file.read_text(encoding="utf-8-sig")
153
+ except UnicodeDecodeError:
154
+ content_bytes = first_file.read_bytes()
155
+ for encoding in ("cp1252", "latin-1"):
156
+ try:
157
+ content = content_bytes.decode(encoding)
158
+ break
159
+ except UnicodeDecodeError:
160
+ continue
161
+ else:
162
+ content = content_bytes.decode("utf-8", errors="replace")
163
+
164
+ issues = detect_problematic_characters(content)
165
+ if issues:
166
+ console.print()
167
+ console.print(f"[yellow]Example issues in {files_with_issues[0]}:[/yellow]")
168
+ for line_num, col, char, replacement in issues[:5]: # Show first 5
169
+ console.print(
170
+ f" Line {line_num}, col {col}: '{char}' (U+{ord(char):04X}) → '{replacement}'"
171
+ )
172
+ if len(issues) > 5:
173
+ console.print(f" ... and {len(issues) - 5} more")
174
+ except Exception:
175
+ pass
176
+
177
+ if file_errors:
178
+ console.print()
179
+ console.print("[red]Errors encountered:[/red]")
180
+ for file_path, error in file_errors:
181
+ console.print(f" [red]✗[/red] {file_path}: {error}")
182
+
183
+ return len(files_with_issues), len(files_fixed)
184
+
185
+
186
+ __all__ = ["validate_encoding"]
@@ -0,0 +1,186 @@
1
+ """Task metadata validation command for Spec Kitty CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ from specify_cli.acceptance import AcceptanceError, detect_feature_slug
14
+ from specify_cli.cli.helpers import check_version_compatibility, console, get_project_root_or_exit
15
+ from specify_cli.core.project_resolver import resolve_worktree_aware_feature_dir
16
+ from specify_cli.task_metadata_validation import (
17
+ detect_lane_mismatch,
18
+ repair_lane_mismatch,
19
+ scan_all_tasks_for_mismatches,
20
+ validate_task_metadata,
21
+ )
22
+ from specify_cli.tasks_support import TaskCliError, find_repo_root
23
+
24
+
25
+ def validate_tasks(
26
+ feature: Optional[str] = typer.Option(
27
+ None, "--feature", help="Feature slug to validate (auto-detected when omitted)"
28
+ ),
29
+ fix: bool = typer.Option(False, "--fix", help="Automatically repair metadata inconsistencies"),
30
+ check_all: bool = typer.Option(False, "--all", help="Check all features, not just one"),
31
+ agent: Optional[str] = typer.Option(None, "--agent", help="Agent name for activity log"),
32
+ shell_pid: Optional[str] = typer.Option(None, "--shell-pid", help="Shell PID for activity log"),
33
+ ) -> None:
34
+ """Validate and optionally fix task metadata inconsistencies.
35
+
36
+ Detects when work package frontmatter doesn't match file location:
37
+ - File in tasks/for_review/ but lane: "planned" in frontmatter
38
+ - File in tasks/doing/ but lane: "done" in frontmatter
39
+ - etc.
40
+
41
+ Can automatically fix by updating frontmatter to match directory.
42
+ """
43
+ try:
44
+ repo_root = find_repo_root()
45
+ except TaskCliError as exc:
46
+ console.print(f"[red]Error:[/red] {exc}")
47
+ raise typer.Exit(1)
48
+
49
+ project_root = get_project_root_or_exit(repo_root)
50
+ check_version_compatibility(project_root, "validate-tasks")
51
+
52
+ # Get agent and shell_pid from environment if not provided
53
+ if not agent:
54
+ agent = os.environ.get("SPEC_KITTY_AGENT", "system")
55
+ if not shell_pid:
56
+ shell_pid = str(os.getpid())
57
+
58
+ if check_all:
59
+ # Validate all features
60
+ kitty_specs = repo_root / "kitty-specs"
61
+ worktrees = repo_root / ".worktrees"
62
+
63
+ feature_dirs = []
64
+ if kitty_specs.exists():
65
+ feature_dirs.extend([d for d in kitty_specs.iterdir() if d.is_dir()])
66
+ if worktrees.exists():
67
+ for wt_dir in worktrees.iterdir():
68
+ if wt_dir.is_dir():
69
+ wt_specs = wt_dir / "kitty-specs"
70
+ if wt_specs.exists():
71
+ feature_dirs.extend([d for d in wt_specs.iterdir() if d.is_dir()])
72
+
73
+ if not feature_dirs:
74
+ console.print("[yellow]No feature directories found.[/yellow]")
75
+ raise typer.Exit(0)
76
+
77
+ console.print(f"[cyan]Checking task metadata for {len(feature_dirs)} features...[/cyan]")
78
+ console.print()
79
+
80
+ total_mismatches = 0
81
+ total_fixed = 0
82
+
83
+ for feature_dir in sorted(feature_dirs, key=lambda d: d.name):
84
+ mismatches, fixed = _validate_feature_tasks(
85
+ feature_dir, fix=fix, agent=agent, shell_pid=shell_pid
86
+ )
87
+ total_mismatches += mismatches
88
+ total_fixed += fixed
89
+
90
+ console.print()
91
+ console.print(
92
+ Panel(
93
+ f"[bold]Summary:[/bold]\n"
94
+ f"Total mismatches found: [yellow]{total_mismatches}[/yellow]\n"
95
+ f"Total mismatches fixed: [green]{total_fixed}[/green]",
96
+ title="Task Metadata Validation Complete",
97
+ border_style="cyan" if total_mismatches == 0 else "yellow",
98
+ )
99
+ )
100
+
101
+ raise typer.Exit(0 if total_mismatches == 0 or fix else 1)
102
+
103
+ # Validate single feature
104
+ try:
105
+ feature_slug = (feature or detect_feature_slug(repo_root, cwd=Path.cwd())).strip()
106
+ except AcceptanceError as exc:
107
+ console.print(f"[red]Error:[/red] {exc}")
108
+ raise typer.Exit(1)
109
+
110
+ feature_dir = resolve_worktree_aware_feature_dir(repo_root, feature_slug, Path.cwd(), console)
111
+
112
+ if not feature_dir.exists():
113
+ console.print(f"[red]Error:[/red] Feature directory not found: {feature_dir}")
114
+ raise typer.Exit(1)
115
+
116
+ console.print(f"[cyan]Validating task metadata for feature:[/cyan] {feature_slug}")
117
+ console.print()
118
+
119
+ mismatches, fixed = _validate_feature_tasks(
120
+ feature_dir, fix=fix, agent=agent, shell_pid=shell_pid
121
+ )
122
+
123
+ if mismatches == 0:
124
+ console.print("[green]✓ All task metadata is consistent![/green]")
125
+ raise typer.Exit(0)
126
+ elif fix and fixed > 0:
127
+ console.print()
128
+ console.print(f"[green]✓ Fixed {fixed} metadata mismatch(es).[/green]")
129
+ raise typer.Exit(0)
130
+ else:
131
+ console.print()
132
+ console.print(f"[yellow]Found {mismatches} metadata mismatch(es).[/yellow]")
133
+ console.print("[dim]Run with --fix to automatically repair these mismatches.[/dim]")
134
+ raise typer.Exit(1)
135
+
136
+
137
+ def _validate_feature_tasks(
138
+ feature_dir: Path, *, fix: bool, agent: str, shell_pid: str
139
+ ) -> tuple[int, int]:
140
+ """Validate task metadata for a single feature directory.
141
+
142
+ Returns:
143
+ Tuple of (mismatches_found, mismatches_fixed)
144
+ """
145
+ console.print(f"[cyan]Checking:[/cyan] {feature_dir.name}")
146
+
147
+ mismatches_dict = scan_all_tasks_for_mismatches(feature_dir)
148
+
149
+ if not mismatches_dict:
150
+ console.print(f" [green]✓[/green] No metadata mismatches")
151
+ return 0, 0
152
+
153
+ # Display mismatches in a table
154
+ table = Table(title=f"Task Metadata Mismatches: {feature_dir.name}", show_header=True)
155
+ table.add_column("File", style="cyan")
156
+ table.add_column("Expected Lane", style="green")
157
+ table.add_column("Actual Lane", style="yellow")
158
+ table.add_column("Status", style="white")
159
+
160
+ fixed_count = 0
161
+ for file_path, (_, expected, actual) in mismatches_dict.items():
162
+ full_path = feature_dir / file_path
163
+
164
+ status = "[yellow]Needs Fix[/yellow]"
165
+ if fix:
166
+ was_repaired, error = repair_lane_mismatch(
167
+ full_path, agent=agent, shell_pid=shell_pid, add_history=True, dry_run=False
168
+ )
169
+ if was_repaired:
170
+ status = "[green]Fixed[/green]"
171
+ fixed_count += 1
172
+ elif error:
173
+ status = f"[red]Error: {error}[/red]"
174
+
175
+ table.add_row(file_path, expected or "?", actual or "?", status)
176
+
177
+ console.print(table)
178
+
179
+ if fix:
180
+ console.print()
181
+ console.print(f" [green]Fixed {fixed_count} of {len(mismatches_dict)} mismatches[/green]")
182
+
183
+ return len(mismatches_dict), fixed_count
184
+
185
+
186
+ __all__ = ["validate_tasks"]