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,1057 @@
1
+ """Feature lifecycle commands for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ from importlib.resources import files
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import typer
16
+ from rich.console import Console
17
+ from typing_extensions import Annotated
18
+
19
+ from specify_cli.core.paths import locate_project_root, is_worktree_context
20
+ from specify_cli.core.worktree import (
21
+ get_next_feature_number,
22
+ validate_feature_structure,
23
+ setup_feature_directory,
24
+ )
25
+ from specify_cli.core.git_ops import run_command, is_git_repo, get_current_branch
26
+ from specify_cli.core.dependency_graph import (
27
+ parse_wp_dependencies,
28
+ detect_cycles,
29
+ validate_dependencies,
30
+ )
31
+ from specify_cli.frontmatter import read_frontmatter, write_frontmatter
32
+
33
+ app = typer.Typer(
34
+ name="feature",
35
+ help="Feature lifecycle commands for AI agents",
36
+ no_args_is_help=True
37
+ )
38
+
39
+ console = Console()
40
+
41
+
42
+ def _commit_to_main(
43
+ file_path: Path,
44
+ feature_slug: str,
45
+ artifact_type: str,
46
+ repo_root: Path,
47
+ json_output: bool = False
48
+ ) -> None:
49
+ """Commit planning artifact to main branch.
50
+
51
+ Args:
52
+ file_path: Path to file being committed
53
+ feature_slug: Feature slug (e.g., "001-my-feature")
54
+ artifact_type: Type of artifact ("spec", "plan", "tasks")
55
+ repo_root: Repository root path (ensures commits go to main repo, not worktree)
56
+ json_output: If True, suppress Rich console output
57
+
58
+ Raises:
59
+ subprocess.CalledProcessError: If commit fails unexpectedly
60
+ typer.Exit: If not on main/master branch
61
+ """
62
+ try:
63
+ # Verify we're on main branch (check from repo root)
64
+ current_branch = get_current_branch(repo_root)
65
+ if current_branch not in ["main", "master"]:
66
+ error_msg = f"Planning artifacts must be committed to main branch (currently on: {current_branch})"
67
+ if not json_output:
68
+ console.print(f"[red]Error:[/red] {error_msg}")
69
+ console.print("[yellow]Switch to main branch:[/yellow] cd {repo_root} && git checkout main")
70
+ raise RuntimeError(error_msg)
71
+
72
+ # Add file to staging (run from repo root to ensure main repo, not worktree)
73
+ run_command(["git", "add", str(file_path)], check_return=True, capture=True, cwd=repo_root)
74
+
75
+ # Commit with descriptive message
76
+ commit_msg = f"Add {artifact_type} for feature {feature_slug}"
77
+ run_command(
78
+ ["git", "commit", "-m", commit_msg],
79
+ check_return=True,
80
+ capture=True,
81
+ cwd=repo_root
82
+ )
83
+
84
+ if not json_output:
85
+ console.print(f"[green]✓[/green] {artifact_type.capitalize()} committed to main")
86
+
87
+ except subprocess.CalledProcessError as e:
88
+ # Check if it's just "nothing to commit" (benign)
89
+ stderr = e.stderr if hasattr(e, 'stderr') and e.stderr else ""
90
+ if "nothing to commit" in stderr or "nothing added to commit" in stderr:
91
+ # Benign - file unchanged
92
+ if not json_output:
93
+ console.print(f"[dim]{artifact_type.capitalize()} unchanged, no commit needed[/dim]")
94
+ else:
95
+ # Actual error
96
+ if not json_output:
97
+ console.print(f"[yellow]Warning:[/yellow] Failed to commit {artifact_type}: {e}")
98
+ console.print(f"[yellow]You may need to commit manually:[/yellow] git add {file_path} && git commit")
99
+ raise
100
+
101
+
102
+ def _find_feature_directory(repo_root: Path, cwd: Path) -> Path:
103
+ """Find the current feature directory.
104
+
105
+ Handles three contexts:
106
+ 1. Worktree root (cwd contains kitty-specs/)
107
+ 2. Inside feature directory (walk up to find kitty-specs/)
108
+ 3. Main repo (find latest feature in kitty-specs/)
109
+
110
+ Args:
111
+ repo_root: Repository root path
112
+ cwd: Current working directory
113
+
114
+ Returns:
115
+ Path to feature directory
116
+
117
+ Raises:
118
+ ValueError: If feature directory cannot be determined
119
+ """
120
+ # Check if we're in a worktree
121
+ if is_worktree_context(cwd):
122
+ # Get the current git branch name to match feature directory
123
+ try:
124
+ result = subprocess.run(
125
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
126
+ cwd=cwd,
127
+ capture_output=True,
128
+ text=True,
129
+ check=True
130
+ )
131
+ branch_name = result.stdout.strip()
132
+ except subprocess.CalledProcessError:
133
+ branch_name = None
134
+
135
+ # Strategy 1: Check if cwd contains kitty-specs/ (we're at worktree root)
136
+ kitty_specs_candidate = cwd / "kitty-specs"
137
+ if kitty_specs_candidate.exists() and kitty_specs_candidate.is_dir():
138
+ kitty_specs = kitty_specs_candidate
139
+ else:
140
+ # Strategy 2: Walk up to find kitty-specs directory
141
+ kitty_specs = cwd
142
+ while kitty_specs != kitty_specs.parent:
143
+ if kitty_specs.name == "kitty-specs":
144
+ break
145
+ kitty_specs = kitty_specs.parent
146
+
147
+ if kitty_specs.name != "kitty-specs":
148
+ raise ValueError("Could not locate kitty-specs directory in worktree")
149
+
150
+ # Find the ###-* feature directory that matches the branch name
151
+ if branch_name:
152
+ # First try exact match with branch name
153
+ branch_feature_dir = kitty_specs / branch_name
154
+ if branch_feature_dir.exists() and branch_feature_dir.is_dir():
155
+ return branch_feature_dir
156
+
157
+ # Fallback: Find any ###-* feature directory (for older worktrees)
158
+ for item in kitty_specs.iterdir():
159
+ if item.is_dir() and len(item.name) >= 3 and item.name[:3].isdigit():
160
+ return item
161
+
162
+ raise ValueError("Could not find feature directory in worktree")
163
+ else:
164
+ # We're in main repo - find latest feature
165
+ specs_dir = repo_root / "kitty-specs"
166
+ if not specs_dir.exists():
167
+ raise ValueError("No kitty-specs directory found in repository")
168
+
169
+ # Find the highest numbered feature
170
+ max_num = 0
171
+ feature_dir = None
172
+ for item in specs_dir.iterdir():
173
+ if item.is_dir() and len(item.name) >= 3 and item.name[:3].isdigit():
174
+ try:
175
+ num = int(item.name[:3])
176
+ if num > max_num:
177
+ max_num = num
178
+ feature_dir = item
179
+ except ValueError:
180
+ continue
181
+
182
+ if feature_dir is None:
183
+ raise ValueError("No feature directories found in kitty-specs/")
184
+
185
+ return feature_dir
186
+
187
+
188
+ @app.command(name="create-feature")
189
+ def create_feature(
190
+ feature_slug: Annotated[str, typer.Argument(help="Feature slug (e.g., 'user-auth')")],
191
+ json_output: Annotated[bool, typer.Option("--json", help="Output JSON format")] = False,
192
+ ) -> None:
193
+ """Create new feature directory structure in main repository.
194
+
195
+ This command is designed for AI agents to call programmatically.
196
+ Creates feature directory in kitty-specs/ and commits to main branch.
197
+
198
+ Examples:
199
+ spec-kitty agent create-feature "new-dashboard" --json
200
+ """
201
+ try:
202
+ # GUARD: Refuse to run from inside a worktree (must be on main branch in main repo)
203
+ cwd = Path.cwd().resolve()
204
+ if is_worktree_context(cwd):
205
+ error_msg = "Cannot create features from inside a worktree. Must be on main branch in main repository."
206
+ if json_output:
207
+ print(json.dumps({"error": error_msg}))
208
+ else:
209
+ console.print(f"[bold red]Error:[/bold red] {error_msg}")
210
+ # Find and suggest the main repo path
211
+ for i, part in enumerate(cwd.parts):
212
+ if part == ".worktrees":
213
+ main_repo = Path(*cwd.parts[:i])
214
+ console.print(f"\n[cyan]Run from the main repository instead:[/cyan]")
215
+ console.print(f" cd {main_repo}")
216
+ console.print(f" spec-kitty agent create-feature {feature_slug}")
217
+ break
218
+ raise typer.Exit(1)
219
+
220
+ repo_root = locate_project_root()
221
+ if repo_root is None:
222
+ error_msg = "Could not locate project root. Run from within spec-kitty repository."
223
+ if json_output:
224
+ print(json.dumps({"error": error_msg}))
225
+ else:
226
+ console.print(f"[red]Error:[/red] {error_msg}")
227
+ raise typer.Exit(1)
228
+
229
+ # Verify we're in a git repository
230
+ if not is_git_repo(repo_root):
231
+ error_msg = "Not in a git repository. Feature creation requires git."
232
+ if json_output:
233
+ print(json.dumps({"error": error_msg}))
234
+ else:
235
+ console.print(f"[red]Error:[/red] {error_msg}")
236
+ raise typer.Exit(1)
237
+
238
+ # Verify we're on main branch (or acceptable branch)
239
+ current_branch = get_current_branch(repo_root)
240
+ if current_branch not in ["main", "master"]:
241
+ error_msg = f"Must be on main branch to create features (currently on: {current_branch})"
242
+ if json_output:
243
+ print(json.dumps({"error": error_msg}))
244
+ else:
245
+ console.print(f"[red]Error:[/red] {error_msg}")
246
+ raise typer.Exit(1)
247
+
248
+ # Get next feature number
249
+ feature_number = get_next_feature_number(repo_root)
250
+ feature_slug_formatted = f"{feature_number:03d}-{feature_slug}"
251
+
252
+ # Create feature directory in main repo
253
+ feature_dir = repo_root / "kitty-specs" / feature_slug_formatted
254
+ feature_dir.mkdir(parents=True, exist_ok=True)
255
+
256
+ # Create subdirectories
257
+ (feature_dir / "checklists").mkdir(exist_ok=True)
258
+ (feature_dir / "research").mkdir(exist_ok=True)
259
+ tasks_dir = feature_dir / "tasks"
260
+ tasks_dir.mkdir(exist_ok=True)
261
+
262
+ # Create tasks/.gitkeep and README.md
263
+ (tasks_dir / ".gitkeep").touch()
264
+
265
+ # Create tasks/README.md (using same content from setup_feature_directory)
266
+ tasks_readme_content = '''# Tasks Directory
267
+
268
+ This directory contains work package (WP) prompt files with lane status in frontmatter.
269
+
270
+ ## Directory Structure (v0.9.0+)
271
+
272
+ ```
273
+ tasks/
274
+ ├── WP01-setup-infrastructure.md
275
+ ├── WP02-user-authentication.md
276
+ ├── WP03-api-endpoints.md
277
+ └── README.md
278
+ ```
279
+
280
+ All WP files are stored flat in `tasks/`. The lane (planned, doing, for_review, done) is stored in the YAML frontmatter `lane:` field.
281
+
282
+ ## Work Package File Format
283
+
284
+ Each WP file **MUST** use YAML frontmatter:
285
+
286
+ ```yaml
287
+ ---
288
+ work_package_id: "WP01"
289
+ title: "Work Package Title"
290
+ lane: "planned"
291
+ subtasks:
292
+ - "T001"
293
+ - "T002"
294
+ phase: "Phase 1 - Setup"
295
+ assignee: ""
296
+ agent: ""
297
+ shell_pid: ""
298
+ review_status: ""
299
+ reviewed_by: ""
300
+ history:
301
+ - timestamp: "2025-01-01T00:00:00Z"
302
+ lane: "planned"
303
+ agent: "system"
304
+ action: "Prompt generated via /spec-kitty.tasks"
305
+ ---
306
+
307
+ # Work Package Prompt: WP01 – Work Package Title
308
+
309
+ [Content follows...]
310
+ ```
311
+
312
+ ## Valid Lane Values
313
+
314
+ - `planned` - Ready for implementation
315
+ - `doing` - Currently being worked on
316
+ - `for_review` - Awaiting review
317
+ - `done` - Completed
318
+
319
+ ## Moving Between Lanes
320
+
321
+ Use the CLI (updates frontmatter only, no file movement):
322
+ ```bash
323
+ spec-kitty agent tasks move-task <WPID> --to <lane>
324
+ ```
325
+
326
+ Example:
327
+ ```bash
328
+ spec-kitty agent tasks move-task WP01 --to doing
329
+ ```
330
+
331
+ ## File Naming
332
+
333
+ - Format: `WP01-kebab-case-slug.md`
334
+ - Examples: `WP01-setup-infrastructure.md`, `WP02-user-auth.md`
335
+ '''
336
+ (tasks_dir / "README.md").write_text(tasks_readme_content)
337
+
338
+ # Copy spec template if it exists
339
+ spec_file = feature_dir / "spec.md"
340
+ if not spec_file.exists():
341
+ spec_template_candidates = [
342
+ repo_root / ".kittify" / "templates" / "spec-template.md",
343
+ repo_root / "templates" / "spec-template.md",
344
+ ]
345
+
346
+ for template in spec_template_candidates:
347
+ if template.exists():
348
+ shutil.copy2(template, spec_file)
349
+ break
350
+ else:
351
+ # No template found, create empty spec.md
352
+ spec_file.touch()
353
+
354
+ # Commit spec.md to main
355
+ _commit_to_main(spec_file, feature_slug_formatted, "spec", repo_root, json_output)
356
+
357
+ if json_output:
358
+ print(json.dumps({
359
+ "result": "success",
360
+ "feature": feature_slug_formatted,
361
+ "feature_dir": str(feature_dir)
362
+ }))
363
+ else:
364
+ console.print(f"[green]✓[/green] Feature created: {feature_slug_formatted}")
365
+ console.print(f" Directory: {feature_dir}")
366
+ console.print(f" Spec committed to main")
367
+
368
+ except Exception as e:
369
+ if json_output:
370
+ print(json.dumps({"error": str(e)}))
371
+ else:
372
+ console.print(f"[red]Error:[/red] {e}")
373
+ raise typer.Exit(1)
374
+
375
+
376
+ @app.command(name="check-prerequisites")
377
+ def check_prerequisites(
378
+ json_output: Annotated[bool, typer.Option("--json", help="Output JSON format")] = False,
379
+ paths_only: Annotated[bool, typer.Option("--paths-only", help="Only output path variables")] = False,
380
+ include_tasks: Annotated[bool, typer.Option("--include-tasks", help="Include tasks.md in validation")] = False,
381
+ ) -> None:
382
+ """Validate feature structure and prerequisites.
383
+
384
+ This command is designed for AI agents to call programmatically.
385
+
386
+ Examples:
387
+ spec-kitty agent check-prerequisites --json
388
+ spec-kitty agent check-prerequisites --paths-only --json
389
+ """
390
+ try:
391
+ repo_root = locate_project_root()
392
+ if repo_root is None:
393
+ error_msg = "Could not locate project root. Run from within spec-kitty repository."
394
+ if json_output:
395
+ print(json.dumps({"error": error_msg}))
396
+ else:
397
+ console.print(f"[red]Error:[/red] {error_msg}")
398
+ raise typer.Exit(1)
399
+
400
+ # Determine feature directory (main repo or worktree)
401
+ cwd = Path.cwd().resolve()
402
+ feature_dir = _find_feature_directory(repo_root, cwd)
403
+
404
+ validation_result = validate_feature_structure(feature_dir, check_tasks=include_tasks)
405
+
406
+ if json_output:
407
+ if paths_only:
408
+ print(json.dumps(validation_result["paths"]))
409
+ else:
410
+ print(json.dumps(validation_result))
411
+ else:
412
+ if validation_result["valid"]:
413
+ console.print("[green]✓[/green] Prerequisites check passed")
414
+ console.print(f" Feature: {feature_dir.name}")
415
+ else:
416
+ console.print("[red]✗[/red] Prerequisites check failed")
417
+ for error in validation_result["errors"]:
418
+ console.print(f" • {error}")
419
+
420
+ if validation_result["warnings"]:
421
+ console.print("\n[yellow]Warnings:[/yellow]")
422
+ for warning in validation_result["warnings"]:
423
+ console.print(f" • {warning}")
424
+
425
+ except Exception as e:
426
+ if json_output:
427
+ print(json.dumps({"error": str(e)}))
428
+ else:
429
+ console.print(f"[red]Error:[/red] {e}")
430
+ raise typer.Exit(1)
431
+
432
+
433
+ @app.command(name="setup-plan")
434
+ def setup_plan(
435
+ json_output: Annotated[bool, typer.Option("--json", help="Output JSON format")] = False,
436
+ ) -> None:
437
+ """Scaffold implementation plan template in main repository.
438
+
439
+ This command is designed for AI agents to call programmatically.
440
+ Creates plan.md and commits to main branch.
441
+
442
+ Examples:
443
+ spec-kitty agent setup-plan --json
444
+ """
445
+ try:
446
+ repo_root = locate_project_root()
447
+ if repo_root is None:
448
+ error_msg = "Could not locate project root. Run from within spec-kitty repository."
449
+ if json_output:
450
+ print(json.dumps({"error": error_msg}))
451
+ else:
452
+ console.print(f"[red]Error:[/red] {error_msg}")
453
+ raise typer.Exit(1)
454
+
455
+ # Determine feature directory (main repo or worktree)
456
+ cwd = Path.cwd().resolve()
457
+ feature_dir = _find_feature_directory(repo_root, cwd)
458
+
459
+ plan_file = feature_dir / "plan.md"
460
+
461
+ # Find plan template
462
+ plan_template_candidates = [
463
+ repo_root / ".kittify" / "templates" / "plan-template.md",
464
+ repo_root / "src" / "specify_cli" / "templates" / "plan-template.md",
465
+ repo_root / "templates" / "plan-template.md",
466
+ ]
467
+
468
+ plan_template = None
469
+ for candidate in plan_template_candidates:
470
+ if candidate.exists():
471
+ plan_template = candidate
472
+ break
473
+
474
+ if plan_template is not None:
475
+ shutil.copy2(plan_template, plan_file)
476
+ else:
477
+ package_template = files("specify_cli").joinpath("templates", "plan-template.md")
478
+ if not package_template.exists():
479
+ raise FileNotFoundError("Plan template not found in repository or package")
480
+ with package_template.open("rb") as src, open(plan_file, "wb") as dst:
481
+ shutil.copyfileobj(src, dst)
482
+
483
+ # Commit plan.md to main
484
+ feature_slug = feature_dir.name
485
+ _commit_to_main(plan_file, feature_slug, "plan", repo_root, json_output)
486
+
487
+ if json_output:
488
+ print(json.dumps({
489
+ "result": "success",
490
+ "plan_file": str(plan_file),
491
+ "feature_dir": str(feature_dir)
492
+ }))
493
+ else:
494
+ console.print(f"[green]✓[/green] Plan scaffolded: {plan_file}")
495
+
496
+ except Exception as e:
497
+ if json_output:
498
+ print(json.dumps({"error": str(e)}))
499
+ else:
500
+ console.print(f"[red]Error:[/red] {e}")
501
+ raise typer.Exit(1)
502
+
503
+ def _find_latest_feature_worktree(repo_root: Path) -> Optional[Path]:
504
+ """Find the latest feature worktree by number.
505
+
506
+ Migrated from find_latest_feature_worktree() in common.sh
507
+
508
+ Args:
509
+ repo_root: Repository root directory
510
+
511
+ Returns:
512
+ Path to latest worktree, or None if no worktrees exist
513
+ """
514
+ worktrees_dir = repo_root / ".worktrees"
515
+ if not worktrees_dir.exists():
516
+ return None
517
+
518
+ latest_num = 0
519
+ latest_worktree = None
520
+
521
+ for worktree_dir in worktrees_dir.iterdir():
522
+ if not worktree_dir.is_dir():
523
+ continue
524
+
525
+ # Match pattern: 001-feature-name
526
+ match = re.match(r"^(\d{3})-", worktree_dir.name)
527
+ if match:
528
+ num = int(match.group(1))
529
+ if num > latest_num:
530
+ latest_num = num
531
+ latest_worktree = worktree_dir
532
+
533
+ return latest_worktree
534
+
535
+
536
+ def _get_current_branch(repo_root: Path) -> str:
537
+ """Get current git branch name.
538
+
539
+ Args:
540
+ repo_root: Repository root directory
541
+
542
+ Returns:
543
+ Current branch name, or 'main' if not in a git repo
544
+ """
545
+ result = subprocess.run(
546
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
547
+ cwd=repo_root,
548
+ capture_output=True,
549
+ text=True,
550
+ check=False
551
+ )
552
+ return result.stdout.strip() if result.returncode == 0 else "main"
553
+
554
+
555
+ @app.command(name="accept")
556
+ def accept_feature(
557
+ feature: Annotated[
558
+ Optional[str],
559
+ typer.Option(
560
+ "--feature",
561
+ help="Feature directory slug (auto-detected if not specified)"
562
+ )
563
+ ] = None,
564
+ mode: Annotated[
565
+ str,
566
+ typer.Option(
567
+ "--mode",
568
+ help="Acceptance mode: auto, pr, local, checklist"
569
+ )
570
+ ] = "auto",
571
+ json_output: Annotated[
572
+ bool,
573
+ typer.Option(
574
+ "--json",
575
+ help="Output results as JSON for agent parsing"
576
+ )
577
+ ] = False,
578
+ lenient: Annotated[
579
+ bool,
580
+ typer.Option(
581
+ "--lenient",
582
+ help="Skip strict metadata validation"
583
+ )
584
+ ] = False,
585
+ no_commit: Annotated[
586
+ bool,
587
+ typer.Option(
588
+ "--no-commit",
589
+ help="Skip auto-commit (report only)"
590
+ )
591
+ ] = False,
592
+ ) -> None:
593
+ """Perform feature acceptance workflow.
594
+
595
+ This command:
596
+ 1. Validates all tasks are in 'done' lane
597
+ 2. Runs acceptance checks from checklist files
598
+ 3. Creates acceptance report
599
+ 4. Marks feature as ready for merge
600
+
601
+ Delegates to existing tasks_cli.py accept implementation.
602
+
603
+ Examples:
604
+ # Run acceptance workflow
605
+ spec-kitty agent feature accept
606
+
607
+ # With JSON output for agents
608
+ spec-kitty agent feature accept --json
609
+
610
+ # Lenient mode (skip strict validation)
611
+ spec-kitty agent feature accept --lenient --json
612
+ """
613
+ try:
614
+ repo_root = locate_project_root()
615
+ if repo_root is None:
616
+ error = "Could not locate project root"
617
+ if json_output:
618
+ print(json.dumps({"error": error, "success": False}))
619
+ else:
620
+ console.print(f"[red]Error:[/red] {error}")
621
+ sys.exit(1)
622
+
623
+ # Build command to call tasks_cli.py
624
+ tasks_cli = repo_root / "scripts" / "tasks" / "tasks_cli.py"
625
+ if not tasks_cli.exists():
626
+ error = f"tasks_cli.py not found: {tasks_cli}"
627
+ if json_output:
628
+ print(json.dumps({"error": error, "success": False}))
629
+ else:
630
+ console.print(f"[red]Error:[/red] {error}")
631
+ sys.exit(1)
632
+
633
+ cmd = ["python3", str(tasks_cli), "accept"]
634
+ if feature:
635
+ cmd.extend(["--feature", feature])
636
+ cmd.extend(["--mode", mode])
637
+ if json_output:
638
+ cmd.append("--json")
639
+ if lenient:
640
+ cmd.append("--lenient")
641
+ if no_commit:
642
+ cmd.append("--no-commit")
643
+
644
+ # Execute accept command
645
+ result = subprocess.run(
646
+ cmd,
647
+ cwd=repo_root,
648
+ capture_output=True,
649
+ text=True,
650
+ )
651
+
652
+ # Pass through output
653
+ if result.stdout:
654
+ print(result.stdout, end="")
655
+ if result.stderr and not json_output:
656
+ print(result.stderr, end="", file=sys.stderr)
657
+
658
+ sys.exit(result.returncode)
659
+
660
+ except Exception as e:
661
+ if json_output:
662
+ print(json.dumps({"error": str(e), "success": False}))
663
+ else:
664
+ console.print(f"[red]Error:[/red] {e}")
665
+ sys.exit(1)
666
+
667
+
668
+ @app.command(name="merge")
669
+ def merge_feature(
670
+ feature: Annotated[
671
+ Optional[str],
672
+ typer.Option(
673
+ "--feature",
674
+ help="Feature directory slug (auto-detected if not specified)"
675
+ )
676
+ ] = None,
677
+ target: Annotated[
678
+ str,
679
+ typer.Option(
680
+ "--target",
681
+ help="Target branch to merge into"
682
+ )
683
+ ] = "main",
684
+ strategy: Annotated[
685
+ str,
686
+ typer.Option(
687
+ "--strategy",
688
+ help="Merge strategy: merge, squash, rebase"
689
+ )
690
+ ] = "merge",
691
+ push: Annotated[
692
+ bool,
693
+ typer.Option(
694
+ "--push",
695
+ help="Push to origin after merging"
696
+ )
697
+ ] = False,
698
+ dry_run: Annotated[
699
+ bool,
700
+ typer.Option(
701
+ "--dry-run",
702
+ help="Show actions without executing"
703
+ )
704
+ ] = False,
705
+ keep_branch: Annotated[
706
+ bool,
707
+ typer.Option(
708
+ "--keep-branch",
709
+ help="Keep feature branch after merge (default: delete)"
710
+ )
711
+ ] = False,
712
+ keep_worktree: Annotated[
713
+ bool,
714
+ typer.Option(
715
+ "--keep-worktree",
716
+ help="Keep worktree after merge (default: remove)"
717
+ )
718
+ ] = False,
719
+ auto_retry: Annotated[
720
+ bool,
721
+ typer.Option(
722
+ "--auto-retry/--no-auto-retry",
723
+ help="Auto-navigate to latest worktree if in wrong location"
724
+ )
725
+ ] = True,
726
+ ) -> None:
727
+ """Merge feature branch into target branch.
728
+
729
+ This command:
730
+ 1. Validates feature is accepted
731
+ 2. Merges feature branch into target (usually 'main')
732
+ 3. Cleans up worktree
733
+ 4. Deletes feature branch
734
+
735
+ Auto-retry logic (from merge-feature.sh):
736
+ If current branch doesn't match feature pattern (XXX-name) and auto-retry is enabled,
737
+ automatically finds and navigates to latest worktree.
738
+
739
+ Delegates to existing tasks_cli.py merge implementation.
740
+
741
+ Examples:
742
+ # Merge into main branch
743
+ spec-kitty agent feature merge
744
+
745
+ # Merge into specific branch with push
746
+ spec-kitty agent feature merge --target develop --push
747
+
748
+ # Dry-run mode
749
+ spec-kitty agent feature merge --dry-run
750
+
751
+ # Keep worktree and branch after merge
752
+ spec-kitty agent feature merge --keep-worktree --keep-branch
753
+ """
754
+ try:
755
+ repo_root = locate_project_root()
756
+ if repo_root is None:
757
+ error = "Could not locate project root"
758
+ print(json.dumps({"error": error, "success": False}))
759
+ sys.exit(1)
760
+
761
+ # Auto-retry logic: Check if we're on a feature branch
762
+ if auto_retry and not os.environ.get("SPEC_KITTY_AUTORETRY"):
763
+ current_branch = _get_current_branch(repo_root)
764
+ is_feature_branch = re.match(r"^\d{3}-", current_branch)
765
+
766
+ if not is_feature_branch:
767
+ # Try to find latest worktree and retry there
768
+ latest_worktree = _find_latest_feature_worktree(repo_root)
769
+ if latest_worktree:
770
+ console.print(
771
+ f"[yellow]Auto-retry:[/yellow] Not on feature branch ({current_branch}). "
772
+ f"Running merge in {latest_worktree.name}"
773
+ )
774
+
775
+ # Set env var to prevent infinite recursion
776
+ env = os.environ.copy()
777
+ env["SPEC_KITTY_AUTORETRY"] = "1"
778
+
779
+ # Re-run command in worktree
780
+ retry_cmd = ["spec-kitty", "agent", "feature", "merge"]
781
+ if feature:
782
+ retry_cmd.extend(["--feature", feature])
783
+ retry_cmd.extend(["--target", target, "--strategy", strategy])
784
+ if push:
785
+ retry_cmd.append("--push")
786
+ if dry_run:
787
+ retry_cmd.append("--dry-run")
788
+ if keep_branch:
789
+ retry_cmd.append("--keep-branch")
790
+ if keep_worktree:
791
+ retry_cmd.append("--keep-worktree")
792
+ retry_cmd.append("--no-auto-retry")
793
+
794
+ result = subprocess.run(
795
+ retry_cmd,
796
+ cwd=latest_worktree,
797
+ env=env,
798
+ )
799
+ sys.exit(result.returncode)
800
+
801
+ # Build command to call tasks_cli.py
802
+ tasks_cli = repo_root / "scripts" / "tasks" / "tasks_cli.py"
803
+ if not tasks_cli.exists():
804
+ error = f"tasks_cli.py not found: {tasks_cli}"
805
+ print(json.dumps({"error": error, "success": False}))
806
+ sys.exit(1)
807
+
808
+ cmd = ["python3", str(tasks_cli), "merge"]
809
+ if feature:
810
+ cmd.extend(["--feature", feature])
811
+ cmd.extend(["--target", target, "--strategy", strategy])
812
+ if push:
813
+ cmd.append("--push")
814
+ if dry_run:
815
+ cmd.append("--dry-run")
816
+ if keep_branch:
817
+ cmd.append("--keep-branch")
818
+ else:
819
+ cmd.append("--delete-branch")
820
+ if keep_worktree:
821
+ cmd.append("--keep-worktree")
822
+ else:
823
+ cmd.append("--remove-worktree")
824
+
825
+ # Execute merge command
826
+ result = subprocess.run(
827
+ cmd,
828
+ cwd=repo_root,
829
+ capture_output=True,
830
+ text=True,
831
+ )
832
+
833
+ # Pass through output
834
+ if result.stdout:
835
+ print(result.stdout, end="")
836
+ if result.stderr:
837
+ print(result.stderr, end="", file=sys.stderr)
838
+
839
+ sys.exit(result.returncode)
840
+
841
+ except Exception as e:
842
+ print(json.dumps({"error": str(e), "success": False}))
843
+ sys.exit(1)
844
+
845
+
846
+ @app.command(name="finalize-tasks")
847
+ def finalize_tasks(
848
+ json_output: Annotated[bool, typer.Option("--json", help="Output JSON format")] = False,
849
+ ) -> None:
850
+ """Parse dependencies from tasks.md and update WP frontmatter, then commit to main.
851
+
852
+ This command is designed to be called after LLM generates WP files via /spec-kitty.tasks.
853
+ It post-processes the generated files to add dependency information and commits everything.
854
+
855
+ Examples:
856
+ spec-kitty agent feature finalize-tasks --json
857
+ """
858
+ try:
859
+ repo_root = locate_project_root()
860
+ if repo_root is None:
861
+ error_msg = "Could not locate project root"
862
+ if json_output:
863
+ print(json.dumps({"error": error_msg}))
864
+ else:
865
+ console.print(f"[red]Error:[/red] {error_msg}")
866
+ raise typer.Exit(1)
867
+
868
+ # Determine feature directory
869
+ cwd = Path.cwd().resolve()
870
+ feature_dir = _find_feature_directory(repo_root, cwd)
871
+
872
+ tasks_dir = feature_dir / "tasks"
873
+ if not tasks_dir.exists():
874
+ error_msg = f"Tasks directory not found: {tasks_dir}"
875
+ if json_output:
876
+ print(json.dumps({"error": error_msg}))
877
+ else:
878
+ console.print(f"[red]Error:[/red] {error_msg}")
879
+ raise typer.Exit(1)
880
+
881
+ # Parse dependencies from tasks.md (if it exists)
882
+ tasks_md = feature_dir / "tasks.md"
883
+ wp_dependencies = {}
884
+ if tasks_md.exists():
885
+ # Read tasks.md and parse dependencies
886
+ tasks_content = tasks_md.read_text(encoding="utf-8")
887
+ wp_dependencies = _parse_dependencies_from_tasks_md(tasks_content)
888
+
889
+ # Validate dependencies (detect cycles, invalid references)
890
+ if wp_dependencies:
891
+ # Check for circular dependencies
892
+ cycles = detect_cycles(wp_dependencies)
893
+ if cycles:
894
+ error_msg = f"Circular dependencies detected: {cycles}"
895
+ if json_output:
896
+ print(json.dumps({"error": error_msg, "cycles": cycles}))
897
+ else:
898
+ console.print(f"[red]Error:[/red] Circular dependencies detected:")
899
+ for cycle in cycles:
900
+ console.print(f" {' → '.join(cycle)}")
901
+ raise typer.Exit(1)
902
+
903
+ # Validate each WP's dependencies
904
+ for wp_id, deps in wp_dependencies.items():
905
+ is_valid, errors = validate_dependencies(wp_id, deps, wp_dependencies)
906
+ if not is_valid:
907
+ error_msg = f"Invalid dependencies for {wp_id}: {errors}"
908
+ if json_output:
909
+ print(json.dumps({"error": error_msg, "wp_id": wp_id, "errors": errors}))
910
+ else:
911
+ console.print(f"[red]Error:[/red] Invalid dependencies for {wp_id}:")
912
+ for err in errors:
913
+ console.print(f" - {err}")
914
+ raise typer.Exit(1)
915
+
916
+ # Update each WP file's frontmatter with dependencies
917
+ wp_files = list(tasks_dir.glob("WP*.md"))
918
+ updated_count = 0
919
+
920
+ for wp_file in wp_files:
921
+ # Extract WP ID from filename
922
+ wp_id_match = re.match(r"(WP\d{2})", wp_file.name)
923
+ if not wp_id_match:
924
+ continue
925
+
926
+ wp_id = wp_id_match.group(1)
927
+
928
+ # Detect whether dependencies field exists in raw frontmatter
929
+ raw_content = wp_file.read_text(encoding="utf-8")
930
+ has_dependencies_line = False
931
+ if raw_content.startswith("---"):
932
+ parts = raw_content.split("---", 2)
933
+ if len(parts) >= 3:
934
+ frontmatter_text = parts[1]
935
+ has_dependencies_line = re.search(
936
+ r"^\s*dependencies\s*:", frontmatter_text, re.MULTILINE
937
+ ) is not None
938
+
939
+ # Read current frontmatter
940
+ try:
941
+ frontmatter, body = read_frontmatter(wp_file)
942
+ except Exception as e:
943
+ console.print(f"[yellow]Warning:[/yellow] Could not read {wp_file.name}: {e}")
944
+ continue
945
+
946
+ # Get dependencies for this WP (default to empty list)
947
+ deps = wp_dependencies.get(wp_id, [])
948
+
949
+ # Update frontmatter with dependencies
950
+ if not has_dependencies_line or frontmatter.get("dependencies") != deps:
951
+ frontmatter["dependencies"] = deps
952
+
953
+ # Write updated frontmatter
954
+ write_frontmatter(wp_file, frontmatter, body)
955
+ updated_count += 1
956
+
957
+ # Commit tasks.md and WP files to main
958
+ feature_slug = feature_dir.name
959
+ try:
960
+ # Add tasks.md (if present) and all WP files
961
+ if tasks_md.exists():
962
+ run_command(
963
+ ["git", "add", str(tasks_md)],
964
+ check_return=True,
965
+ capture=True,
966
+ cwd=repo_root
967
+ )
968
+ run_command(
969
+ ["git", "add", str(tasks_dir)],
970
+ check_return=True,
971
+ capture=True,
972
+ cwd=repo_root
973
+ )
974
+
975
+ # Commit with descriptive message
976
+ commit_msg = f"Add tasks for feature {feature_slug}"
977
+ run_command(
978
+ ["git", "commit", "-m", commit_msg],
979
+ check_return=True,
980
+ capture=True,
981
+ cwd=repo_root
982
+ )
983
+
984
+ if not json_output:
985
+ console.print(f"[green]✓[/green] Tasks committed to main")
986
+ console.print(f"[dim]Updated {updated_count} WP files with dependencies[/dim]")
987
+
988
+ except subprocess.CalledProcessError as e:
989
+ # Check if it's just "nothing to commit"
990
+ stderr = e.stderr if hasattr(e, 'stderr') and e.stderr else ""
991
+ if "nothing to commit" in stderr or "nothing added to commit" in stderr:
992
+ if not json_output:
993
+ console.print(f"[dim]Tasks unchanged, no commit needed[/dim]")
994
+ else:
995
+ raise
996
+
997
+ if json_output:
998
+ print(json.dumps({
999
+ "result": "success",
1000
+ "updated_wp_count": updated_count,
1001
+ "tasks_dir": str(tasks_dir)
1002
+ }))
1003
+
1004
+ except Exception as e:
1005
+ if json_output:
1006
+ print(json.dumps({"error": str(e)}))
1007
+ else:
1008
+ console.print(f"[red]Error:[/red] {e}")
1009
+ raise typer.Exit(1)
1010
+
1011
+
1012
+ def _parse_dependencies_from_tasks_md(tasks_content: str) -> dict[str, list[str]]:
1013
+ """Parse WP dependencies from tasks.md content.
1014
+
1015
+ Parsing strategy (priority order):
1016
+ 1. Explicit dependency markers ("Depends on WP01", "Dependencies: WP01, WP02")
1017
+ 2. Phase grouping (Phase 2 WPs depend on Phase 1 WPs)
1018
+ 3. Default to empty list if ambiguous
1019
+
1020
+ Returns:
1021
+ Dict mapping WP ID to list of dependencies
1022
+ Example: {"WP01": [], "WP02": ["WP01"], "WP03": ["WP01", "WP02"]}
1023
+ """
1024
+ dependencies = {}
1025
+
1026
+ # Split into WP sections
1027
+ wp_sections = re.split(r'##\s+Work Package (WP\d{2})', tasks_content)
1028
+
1029
+ # Process sections (they come in pairs: WP ID, then content)
1030
+ for i in range(1, len(wp_sections), 2):
1031
+ if i + 1 >= len(wp_sections):
1032
+ break
1033
+
1034
+ wp_id = wp_sections[i]
1035
+ section_content = wp_sections[i + 1]
1036
+
1037
+ # Method 1: Explicit "Depends on" or "Dependencies:"
1038
+ explicit_deps = []
1039
+
1040
+ # Pattern: "Depends on WP01" or "Depends on WP01, WP02"
1041
+ depends_matches = re.findall(r'Depends?\s+on\s+(WP\d{2}(?:\s*,\s*WP\d{2})*)', section_content, re.IGNORECASE)
1042
+ for match in depends_matches:
1043
+ explicit_deps.extend(re.findall(r'WP\d{2}', match))
1044
+
1045
+ # Pattern: "Dependencies: WP01, WP02"
1046
+ deps_line = re.search(r'Dependencies:\s*(.+)', section_content)
1047
+ if deps_line:
1048
+ explicit_deps.extend(re.findall(r'WP\d{2}', deps_line.group(1)))
1049
+
1050
+ if explicit_deps:
1051
+ # Remove duplicates and sort
1052
+ dependencies[wp_id] = sorted(list(set(explicit_deps)))
1053
+ else:
1054
+ # Default to empty
1055
+ dependencies[wp_id] = []
1056
+
1057
+ return dependencies