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,175 @@
1
+ """Repair command to fix broken templates and diagnose worktrees."""
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+ from typing import Optional
7
+
8
+ from specify_cli.upgrade.migrations.m_0_10_9_repair_templates import RepairTemplatesMigration
9
+ from specify_cli.core.paths import locate_project_root, get_main_repo_root, is_worktree_context
10
+
11
+ app = typer.Typer()
12
+ console = Console()
13
+
14
+
15
+ @app.command()
16
+ def repair(
17
+ project_path: Path = typer.Option(
18
+ Path.cwd(),
19
+ "--project-path",
20
+ "-p",
21
+ help="Path to project to repair"
22
+ ),
23
+ dry_run: bool = typer.Option(
24
+ False,
25
+ "--dry-run",
26
+ help="Show what would be changed without making changes"
27
+ )
28
+ ):
29
+ """Repair broken templates caused by v0.10.0-0.10.8 bundling bug.
30
+
31
+ This command fixes templates that reference non-existent bash scripts
32
+ by regenerating them from the correct source. Run this if you see errors
33
+ like "scripts/bash/check-prerequisites.sh: No such file or directory".
34
+ """
35
+ console.print("[bold]Spec Kitty Template Repair[/bold]")
36
+ console.print()
37
+
38
+ migration = RepairTemplatesMigration()
39
+
40
+ # Detect if repair needed
41
+ needs_repair = migration.detect(project_path)
42
+
43
+ if not needs_repair:
44
+ console.print("[green]✓ No broken templates detected - project is healthy![/green]")
45
+ return
46
+
47
+ console.print("[yellow]⚠ Broken templates detected[/yellow]")
48
+ console.print("Found bash script references in slash commands")
49
+ console.print()
50
+
51
+ if dry_run:
52
+ console.print("[cyan]Dry run mode - showing what would be changed:[/cyan]")
53
+
54
+ # Apply repair
55
+ result = migration.apply(project_path, dry_run=dry_run)
56
+
57
+ if result.success:
58
+ console.print()
59
+ console.print("[green]✓ Repair completed successfully[/green]")
60
+ for change in result.changes_made:
61
+ console.print(f" • {change}")
62
+ else:
63
+ console.print()
64
+ console.print("[red]✗ Repair failed[/red]")
65
+ for error in result.errors:
66
+ console.print(f" • [red]{error}[/red]")
67
+
68
+ if result.warnings:
69
+ console.print()
70
+ console.print("[yellow]Warnings:[/yellow]")
71
+ for warning in result.warnings:
72
+ console.print(f" • {warning}")
73
+
74
+
75
+ @app.command(name="worktree")
76
+ def repair_worktree(
77
+ all_worktrees: bool = typer.Option(
78
+ False,
79
+ "--all",
80
+ help="Check all worktrees in .worktrees/ directory"
81
+ ),
82
+ worktree_path: Optional[Path] = typer.Argument(
83
+ None,
84
+ help="Specific worktree path to check (defaults to current directory if in a worktree)"
85
+ ),
86
+ ):
87
+ """Diagnose worktree kitty-specs/ status.
88
+
89
+ This command checks if worktrees have kitty-specs/ directories and explains
90
+ how WP operations work:
91
+
92
+ - WP lane changes (move-task) ALWAYS use main repo's kitty-specs/
93
+ - Research artifacts can be added to worktree's kitty-specs/
94
+ - Stale WP files in worktrees don't affect lane operations
95
+
96
+ Examples:
97
+ spec-kitty repair worktree # Check current worktree
98
+ spec-kitty repair worktree --all # Check all worktrees
99
+ """
100
+ console.print("[bold]Spec Kitty Worktree Diagnostics[/bold]")
101
+ console.print()
102
+
103
+ # Find project root
104
+ cwd = Path.cwd().resolve()
105
+ repo_root = locate_project_root(cwd)
106
+
107
+ if repo_root is None:
108
+ console.print("[red]Error:[/red] Could not locate project root")
109
+ raise typer.Exit(1)
110
+
111
+ main_root = get_main_repo_root(repo_root)
112
+
113
+ worktrees_to_check: list[Path] = []
114
+
115
+ if all_worktrees:
116
+ # Check all worktrees in .worktrees/
117
+ worktrees_dir = main_root / ".worktrees"
118
+ if worktrees_dir.exists():
119
+ for item in worktrees_dir.iterdir():
120
+ if item.is_dir() and (item / ".git").exists():
121
+ worktrees_to_check.append(item)
122
+ if not worktrees_to_check:
123
+ console.print("[yellow]No worktrees found in .worktrees/[/yellow]")
124
+ return
125
+ elif worktree_path:
126
+ # Use specified path
127
+ worktree_path = worktree_path.resolve()
128
+ if not worktree_path.exists():
129
+ console.print(f"[red]Error:[/red] Worktree path does not exist: {worktree_path}")
130
+ raise typer.Exit(1)
131
+ worktrees_to_check.append(worktree_path)
132
+ else:
133
+ # Try current directory
134
+ if is_worktree_context(cwd):
135
+ # Find the worktree root
136
+ current = cwd
137
+ while current != current.parent:
138
+ if (current / ".git").exists():
139
+ worktrees_to_check.append(current)
140
+ break
141
+ current = current.parent
142
+ if not worktrees_to_check:
143
+ console.print("[red]Error:[/red] Could not find worktree root")
144
+ raise typer.Exit(1)
145
+ else:
146
+ console.print("[yellow]Not in a worktree. Use --all to check all worktrees.[/yellow]")
147
+ return
148
+
149
+ console.print(f"Found {len(worktrees_to_check)} worktree(s) to check")
150
+ console.print()
151
+
152
+ for wt_path in worktrees_to_check:
153
+ has_kitty_specs = (wt_path / "kitty-specs").exists()
154
+ has_tasks = (wt_path / "kitty-specs").exists() and any(
155
+ (wt_path / "kitty-specs").rglob("tasks/*.md")
156
+ )
157
+
158
+ console.print(f"[bold]{wt_path.name}[/bold]")
159
+
160
+ if not has_kitty_specs:
161
+ console.print(" [dim]No kitty-specs/ directory[/dim]")
162
+ else:
163
+ console.print(f" kitty-specs/: [green]present[/green]")
164
+ if has_tasks:
165
+ console.print(f" tasks/*.md: [yellow]present (stale copies)[/yellow]")
166
+ else:
167
+ console.print(f" tasks/*.md: [dim]none[/dim]")
168
+
169
+ console.print()
170
+ console.print("[bold cyan]How WP operations work:[/bold cyan]")
171
+ console.print(" • [green]move-task[/green] always updates [bold]main repo's[/bold] kitty-specs/")
172
+ console.print(" • Research artifacts can be added to worktree's kitty-specs/")
173
+ console.print(" • Stale WP files in worktrees are [dim]ignored[/dim] by lane operations")
174
+ console.print()
175
+ console.print("[dim]No repair needed - this is informational only.[/dim]")
@@ -0,0 +1,165 @@
1
+ """Research command implementation for Spec Kitty CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.panel import Panel
11
+
12
+ from specify_cli.acceptance import AcceptanceError, detect_feature_slug
13
+ from specify_cli.cli import StepTracker
14
+ from specify_cli.cli.helpers import check_version_compatibility, console, get_project_root_or_exit, show_banner
15
+ from specify_cli.core import MISSION_CHOICES
16
+ from specify_cli.core.project_resolver import resolve_template_path, resolve_worktree_aware_feature_dir
17
+ from specify_cli.mission import get_feature_mission_key
18
+ from specify_cli.plan_validation import PlanValidationError, validate_plan_filled
19
+ from specify_cli.tasks_support import TaskCliError, find_repo_root
20
+
21
+
22
+ def research(
23
+ feature: Optional[str] = typer.Option(None, "--feature", help="Feature slug to target (auto-detected when omitted)"),
24
+ force: bool = typer.Option(False, "--force", help="Overwrite existing research artifacts"),
25
+ ) -> None:
26
+ """Execute Phase 0 research workflow to scaffold artifacts."""
27
+
28
+ show_banner()
29
+
30
+ try:
31
+ repo_root = find_repo_root()
32
+ except TaskCliError as exc:
33
+ console.print(f"[red]Error:[/red] {exc}")
34
+ raise typer.Exit(1)
35
+
36
+ project_root = get_project_root_or_exit(repo_root)
37
+ check_version_compatibility(project_root, "research")
38
+
39
+ tracker = StepTracker("Research Phase Setup")
40
+ tracker.add("project", "Locate project root")
41
+ tracker.add("feature", "Resolve feature directory")
42
+ tracker.add("research-md", "Ensure research.md")
43
+ tracker.add("data-model", "Ensure data-model.md")
44
+ tracker.add("research-csv", "Ensure research CSV stubs")
45
+ tracker.add("summary", "Summarize outputs")
46
+ console.print()
47
+
48
+ tracker.start("project")
49
+ tracker.complete("project", str(project_root))
50
+
51
+ tracker.start("feature")
52
+ try:
53
+ feature_slug = (feature or detect_feature_slug(repo_root, cwd=Path.cwd())).strip()
54
+ except AcceptanceError as exc:
55
+ tracker.error("feature", str(exc))
56
+ console.print(tracker.render())
57
+ console.print(f"[red]Error:[/red] {exc}")
58
+ raise typer.Exit(1)
59
+
60
+ feature_dir = resolve_worktree_aware_feature_dir(repo_root, feature_slug, Path.cwd(), console)
61
+ feature_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ # Get mission from feature's meta.json (not project-level default)
64
+ mission_key = get_feature_mission_key(feature_dir)
65
+ mission_display = MISSION_CHOICES.get(mission_key, mission_key)
66
+ tracker.complete("feature", f"{feature_dir} ({mission_display})")
67
+
68
+ # Validate that plan.md has been filled out before proceeding
69
+ plan_path = feature_dir / "plan.md"
70
+ try:
71
+ validate_plan_filled(plan_path, feature_slug=feature_slug, strict=True)
72
+ except PlanValidationError as exc:
73
+ console.print(tracker.render())
74
+ console.print()
75
+ console.print(f"[red]Error:[/red] {exc}")
76
+ console.print()
77
+ console.print("[yellow]Next steps:[/yellow]")
78
+ console.print(" 1. Run [cyan]/spec-kitty.plan[/cyan] to fill in the technical architecture")
79
+ console.print(" 2. Complete all [FEATURE], [DATE], and technical context placeholders")
80
+ console.print(" 3. Remove [REMOVE IF UNUSED] sections and choose your project structure")
81
+ console.print(" 4. Then run [cyan]/spec-kitty.research[/cyan] again")
82
+ raise typer.Exit(1)
83
+
84
+ created_paths: list[Path] = []
85
+
86
+ def _copy_asset(step_key: str, label: str, relative_path: Path, template_name: Path) -> None:
87
+ tracker.start(step_key)
88
+ dest_path = feature_dir / relative_path
89
+ template_path = resolve_template_path(project_root, mission_key, template_name)
90
+
91
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
92
+ try:
93
+ if dest_path.exists() and not force:
94
+ created_paths.append(dest_path)
95
+ return
96
+ if template_path and template_path.is_file():
97
+ shutil.copy2(template_path, dest_path)
98
+ else:
99
+ if dest_path.exists():
100
+ dest_path.unlink()
101
+ dest_path.touch()
102
+ created_paths.append(dest_path)
103
+ tracker.complete(step_key, label)
104
+ except Exception as exc: # pragma: no cover - surfaces filesystem errors
105
+ tracker.error(step_key, str(exc))
106
+ console.print(tracker.render())
107
+ raise typer.Exit(1)
108
+
109
+ _copy_asset("research-md", "research.md ready", Path("research.md"), Path("research.md"))
110
+ _copy_asset("data-model", "data-model.md ready", Path("data-model.md"), Path("data-model.md"))
111
+
112
+ tracker.start("research-csv")
113
+ csv_targets = [
114
+ (Path("research") / "evidence-log.csv", Path("research") / "evidence-log.csv"),
115
+ (Path("research") / "source-register.csv", Path("research") / "source-register.csv"),
116
+ ]
117
+ csv_errors: list[str] = []
118
+ for dest_rel, template_rel in csv_targets:
119
+ dest_path = feature_dir / dest_rel
120
+ template_path = resolve_template_path(project_root, mission_key, template_rel)
121
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
122
+ try:
123
+ if dest_path.exists() and not force:
124
+ created_paths.append(dest_path)
125
+ continue
126
+ if template_path and template_path.is_file():
127
+ shutil.copy2(template_path, dest_path)
128
+ else:
129
+ if dest_path.exists():
130
+ dest_path.unlink()
131
+ dest_path.touch()
132
+ created_paths.append(dest_path)
133
+ except Exception as exc: # pragma: no cover
134
+ csv_errors.append(f"{dest_rel}: {exc}")
135
+
136
+ if csv_errors:
137
+ tracker.error("research-csv", "; ".join(csv_errors))
138
+ console.print(tracker.render())
139
+ raise typer.Exit(1)
140
+ else:
141
+ tracker.complete("research-csv", "CSV templates ready")
142
+
143
+ tracker.start("summary")
144
+ tracker.complete("summary", f"{len(created_paths)} artifacts ready")
145
+
146
+ console.print(tracker.render())
147
+
148
+ relative_paths = [
149
+ str(path.relative_to(feature_dir)) if path.is_relative_to(feature_dir) else str(path)
150
+ for path in created_paths
151
+ ]
152
+ summary_lines = "\n".join(f"- [cyan]{rel}[/cyan]" for rel in sorted(set(relative_paths)))
153
+ console.print()
154
+ console.print(
155
+ Panel(
156
+ summary_lines or "No artifacts were created (existing files kept).",
157
+ title="Research Artifacts",
158
+ border_style="cyan",
159
+ padding=(1, 2),
160
+ )
161
+ )
162
+ console.print()
163
+
164
+
165
+ __all__ = ["research"]
@@ -0,0 +1,364 @@
1
+ """Sync command - synchronize workspace with upstream changes.
2
+
3
+ This command updates a workspace with changes from its base branch or parent.
4
+ For git workspaces, this performs a rebase. For jj workspaces, this updates
5
+ the workspace revision.
6
+
7
+ Key difference:
8
+ - git: Sync may fail on conflicts (must be resolved before continuing)
9
+ - jj: Sync always succeeds (conflicts are stored and can be resolved later)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ import typer
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.table import Table
22
+
23
+ from specify_cli.core.vcs import (
24
+ ChangeInfo,
25
+ ConflictInfo,
26
+ SyncResult,
27
+ SyncStatus,
28
+ VCSBackend,
29
+ get_vcs,
30
+ )
31
+
32
+ console = Console()
33
+
34
+
35
+ def _detect_workspace_context() -> tuple[Path, str | None]:
36
+ """Detect current workspace and feature context.
37
+
38
+ Returns:
39
+ Tuple of (workspace_path, feature_slug)
40
+ If not in a workspace, returns (cwd, None)
41
+ """
42
+ cwd = Path.cwd()
43
+
44
+ # Check if we're in a .worktrees directory
45
+ parts = cwd.parts
46
+ for i, part in enumerate(parts):
47
+ if part == ".worktrees" and i + 1 < len(parts):
48
+ # Found a worktree path like: /repo/.worktrees/010-feature-WP01
49
+ workspace_name = parts[i + 1]
50
+ # Extract feature slug from workspace name (###-feature-WP##)
51
+ match = re.match(r"^(\d{3}-[a-zA-Z0-9-]+)-WP\d+$", workspace_name)
52
+ if match:
53
+ return cwd, match.group(1)
54
+
55
+ # Try to detect from git branch
56
+ try:
57
+ result = subprocess.run(
58
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
59
+ capture_output=True,
60
+ text=True,
61
+ check=False,
62
+ cwd=cwd,
63
+ )
64
+ if result.returncode == 0:
65
+ branch_name = result.stdout.strip()
66
+ # Check if branch matches WP pattern (###-feature-WP##)
67
+ match = re.match(r"^(\d{3}-[a-zA-Z0-9-]+)-WP\d+$", branch_name)
68
+ if match:
69
+ return cwd, match.group(1)
70
+ except (FileNotFoundError, OSError):
71
+ pass
72
+
73
+ # Not in a recognized workspace
74
+ return cwd, None
75
+
76
+
77
+ def _display_changes_integrated(changes: list[ChangeInfo]) -> None:
78
+ """Display changes that were integrated during sync."""
79
+ if not changes:
80
+ return
81
+
82
+ console.print(f"\n[cyan]Changes integrated ({len(changes)}):[/cyan]")
83
+ for change in changes[:5]: # Show first 5 changes
84
+ short_id = change.commit_id[:7] if change.commit_id else "unknown"
85
+ # Truncate message to 50 chars
86
+ msg = change.message[:50] + "..." if len(change.message) > 50 else change.message
87
+ console.print(f" • [dim]{short_id}[/dim] {msg}")
88
+
89
+ if len(changes) > 5:
90
+ console.print(f" [dim]... and {len(changes) - 5} more[/dim]")
91
+
92
+
93
+ def _display_conflicts(conflicts: list[ConflictInfo]) -> None:
94
+ """Display conflicts with actionable details.
95
+
96
+ Shows:
97
+ - File path
98
+ - Line ranges (if available)
99
+ - Conflict type
100
+ - Resolution hints
101
+ """
102
+ if not conflicts:
103
+ return
104
+
105
+ console.print(f"\n[yellow]Conflicts ({len(conflicts)} files):[/yellow]")
106
+
107
+ # Create a table for better formatting
108
+ table = Table(show_header=True, header_style="bold yellow", show_lines=False)
109
+ table.add_column("File", style="cyan")
110
+ table.add_column("Type", style="dim")
111
+ table.add_column("Lines", style="dim")
112
+
113
+ for conflict in conflicts:
114
+ # Format line ranges
115
+ if conflict.line_ranges:
116
+ lines = ", ".join(f"{start}-{end}" for start, end in conflict.line_ranges)
117
+ else:
118
+ lines = "entire file"
119
+
120
+ table.add_row(
121
+ str(conflict.file_path),
122
+ conflict.conflict_type.value,
123
+ lines,
124
+ )
125
+
126
+ console.print(table)
127
+
128
+ # Show resolution hints
129
+ console.print("\n[dim]To resolve conflicts:[/dim]")
130
+ console.print("[dim] 1. Edit the conflicted files to resolve markers[/dim]")
131
+ console.print("[dim] 2. Continue your work (jj) or commit resolution (git)[/dim]")
132
+
133
+
134
+ def _git_repair(workspace_path: Path) -> bool:
135
+ """Attempt git workspace recovery.
136
+
137
+ This is a best-effort recovery that tries:
138
+ 1. Abort any in-progress rebase/merge
139
+ 2. Reset to HEAD
140
+
141
+ Returns:
142
+ True if recovery succeeded, False otherwise
143
+
144
+ Note: This may lose uncommitted work.
145
+ """
146
+ try:
147
+ # First, try to abort any in-progress operations
148
+ for abort_cmd in [
149
+ ["git", "rebase", "--abort"],
150
+ ["git", "merge", "--abort"],
151
+ ["git", "cherry-pick", "--abort"],
152
+ ]:
153
+ subprocess.run(
154
+ abort_cmd,
155
+ cwd=workspace_path,
156
+ capture_output=True,
157
+ check=False,
158
+ timeout=10,
159
+ )
160
+
161
+ # Reset to HEAD (keeping changes in working tree)
162
+ result = subprocess.run(
163
+ ["git", "reset", "--mixed", "HEAD"],
164
+ cwd=workspace_path,
165
+ capture_output=True,
166
+ text=True,
167
+ check=False,
168
+ timeout=30,
169
+ )
170
+
171
+ return result.returncode == 0
172
+
173
+ except (subprocess.TimeoutExpired, OSError):
174
+ return False
175
+
176
+
177
+ def _jj_repair(workspace_path: Path) -> bool:
178
+ """Attempt jj workspace recovery via operation undo.
179
+
180
+ Jujutsu has much better recovery capabilities via the operation log.
181
+ This function tries to undo the last operation.
182
+
183
+ Returns:
184
+ True if recovery succeeded, False otherwise
185
+ """
186
+ try:
187
+ # Try to undo the last operation
188
+ result = subprocess.run(
189
+ ["jj", "undo"],
190
+ cwd=workspace_path,
191
+ capture_output=True,
192
+ text=True,
193
+ check=False,
194
+ timeout=30,
195
+ )
196
+
197
+ if result.returncode == 0:
198
+ return True
199
+
200
+ # If undo fails, try to update the workspace
201
+ result = subprocess.run(
202
+ ["jj", "workspace", "update-stale"],
203
+ cwd=workspace_path,
204
+ capture_output=True,
205
+ text=True,
206
+ check=False,
207
+ timeout=30,
208
+ )
209
+
210
+ return result.returncode == 0
211
+
212
+ except (subprocess.TimeoutExpired, OSError):
213
+ return False
214
+
215
+
216
+ def sync(
217
+ repair: bool = typer.Option(
218
+ False,
219
+ "--repair",
220
+ "-r",
221
+ help="Attempt workspace recovery (may lose uncommitted work)",
222
+ ),
223
+ verbose: bool = typer.Option(
224
+ False,
225
+ "--verbose",
226
+ "-v",
227
+ help="Show detailed sync output",
228
+ ),
229
+ ) -> None:
230
+ """Synchronize workspace with upstream changes.
231
+
232
+ Updates the current workspace with changes from its base branch or parent.
233
+ This is equivalent to:
234
+ - git: `git rebase <base-branch>`
235
+ - jj: `jj workspace update-stale` + auto-rebase
236
+
237
+ Key difference between VCS backends:
238
+ - git: Sync may FAIL on conflicts (must resolve before continuing)
239
+ - jj: Sync always SUCCEEDS (conflicts stored, resolve later)
240
+
241
+ Examples:
242
+ # Sync current workspace
243
+ spec-kitty sync
244
+
245
+ # Sync with verbose output
246
+ spec-kitty sync --verbose
247
+
248
+ # Attempt recovery from broken state
249
+ spec-kitty sync --repair
250
+ """
251
+ console.print()
252
+
253
+ # Detect workspace context
254
+ workspace_path, feature_slug = _detect_workspace_context()
255
+
256
+ if feature_slug is None:
257
+ console.print("[yellow]⚠ Not in a recognized workspace[/yellow]")
258
+ console.print("Run this command from a worktree directory:")
259
+ console.print(" cd .worktrees/<feature>-WP##/")
260
+ raise typer.Exit(1)
261
+
262
+ console.print(f"[cyan]Workspace:[/cyan] {workspace_path.name}")
263
+
264
+ # Get VCS implementation
265
+ try:
266
+ vcs = get_vcs(workspace_path)
267
+ except Exception as e:
268
+ console.print(f"[red]Error:[/red] Failed to detect VCS: {e}")
269
+ raise typer.Exit(1)
270
+
271
+ console.print(f"[cyan]Backend:[/cyan] git")
272
+ console.print()
273
+
274
+ # Handle repair mode
275
+ if repair:
276
+ console.print("[yellow]Attempting workspace recovery...[/yellow]")
277
+ console.print("[dim]Note: This may lose uncommitted work[/dim]")
278
+ console.print()
279
+
280
+ if vcs.backend == VCSBackend.JUJUTSU:
281
+ success = _jj_repair(workspace_path)
282
+ else:
283
+ success = _git_repair(workspace_path)
284
+
285
+ if success:
286
+ console.print("[green]✓ Recovery successful[/green]")
287
+ console.print("Workspace state has been reset.")
288
+ else:
289
+ console.print("[red]✗ Recovery failed[/red]")
290
+ console.print("Manual intervention may be required.")
291
+ console.print()
292
+ if vcs.backend == VCSBackend.GIT:
293
+ console.print("[dim]Try these commands manually:[/dim]")
294
+ console.print(" git status")
295
+ console.print(" git rebase --abort")
296
+ console.print(" git reset --hard HEAD")
297
+ else:
298
+ console.print("[dim]Try these commands manually:[/dim]")
299
+ console.print(" jj status")
300
+ console.print(" jj op log")
301
+ console.print(" jj undo")
302
+ raise typer.Exit(1)
303
+
304
+ return
305
+
306
+ # Perform sync
307
+ console.print("[cyan]Syncing workspace...[/cyan]")
308
+
309
+ result: SyncResult = vcs.sync_workspace(workspace_path)
310
+
311
+ # Display result based on status
312
+ if result.status == SyncStatus.UP_TO_DATE:
313
+ console.print("\n[green]✓ Already up to date[/green]")
314
+ if result.message:
315
+ console.print(f"[dim]{result.message}[/dim]")
316
+
317
+ elif result.status == SyncStatus.SYNCED:
318
+ stats_parts = []
319
+ if result.files_updated > 0:
320
+ stats_parts.append(f"{result.files_updated} updated")
321
+ if result.files_added > 0:
322
+ stats_parts.append(f"{result.files_added} added")
323
+ if result.files_deleted > 0:
324
+ stats_parts.append(f"{result.files_deleted} deleted")
325
+
326
+ stats = ", ".join(stats_parts) if stats_parts else "no file changes"
327
+ console.print(f"\n[green]✓ Synced[/green] - {stats}")
328
+
329
+ if verbose:
330
+ _display_changes_integrated(result.changes_integrated)
331
+
332
+ if result.message:
333
+ console.print(f"[dim]{result.message}[/dim]")
334
+
335
+ elif result.status == SyncStatus.CONFLICTS:
336
+ # jj: This means sync succeeded but there are conflicts to resolve
337
+ console.print("\n[yellow]⚠ Synced with conflicts[/yellow]")
338
+
339
+ if vcs.backend == VCSBackend.JUJUTSU:
340
+ console.print("[dim]Conflicts are stored in the commit.[/dim]")
341
+ console.print("[dim]You can continue working and resolve later.[/dim]")
342
+ else:
343
+ console.print("[dim]You must resolve conflicts before continuing.[/dim]")
344
+
345
+ _display_conflicts(result.conflicts)
346
+
347
+ if verbose:
348
+ _display_changes_integrated(result.changes_integrated)
349
+
350
+ elif result.status == SyncStatus.FAILED:
351
+ console.print(f"\n[red]✗ Sync failed[/red]")
352
+ if result.message:
353
+ console.print(f"[dim]{result.message}[/dim]")
354
+
355
+ # Show conflicts if any
356
+ if result.conflicts:
357
+ _display_conflicts(result.conflicts)
358
+
359
+ console.print()
360
+ console.print("[dim]Try:[/dim]")
361
+ console.print(" spec-kitty sync --repair")
362
+ raise typer.Exit(1)
363
+
364
+ console.print()