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,218 @@
1
+ """Test path selection based on agent availability.
2
+
3
+ This module determines which test execution path to use based on the number
4
+ of authenticated agents available. The path affects how tests are structured:
5
+
6
+ - 1-agent: Same agent handles both implementation and review
7
+ - 2-agent: Different agents for implementation vs review
8
+ - 3+-agent: Third agent available for fallback scenarios
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING, Literal
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+ # Module-level cache for test path
21
+ _test_path_cache: TestPath | None = None
22
+
23
+
24
+ @dataclass
25
+ class TestPath:
26
+ """Selected test path based on runtime agent availability."""
27
+
28
+ path_type: Literal["1-agent", "2-agent", "3+-agent"]
29
+ """The test path variant to execute."""
30
+
31
+ available_agents: list[str]
32
+ """List of authenticated agent IDs available for this run."""
33
+
34
+ implementation_agent: str
35
+ """Agent to use for implementation phase."""
36
+
37
+ review_agent: str
38
+ """Agent to use for review phase."""
39
+
40
+ fallback_agent: str | None
41
+ """Third agent for fallback scenarios (None for 1/2-agent paths)."""
42
+
43
+ @property
44
+ def is_cross_agent(self) -> bool:
45
+ """True if implementation and review use different agents."""
46
+ return self.implementation_agent != self.review_agent
47
+
48
+ @property
49
+ def has_fallback(self) -> bool:
50
+ """True if a fallback agent is available."""
51
+ return self.fallback_agent is not None
52
+
53
+ @property
54
+ def agent_count(self) -> int:
55
+ """Number of available agents."""
56
+ return len(self.available_agents)
57
+
58
+
59
+ def determine_path_type(agent_count: int) -> Literal["1-agent", "2-agent", "3+-agent"]:
60
+ """Determine test path type based on available agent count.
61
+
62
+ Args:
63
+ agent_count: Number of available (authenticated) agents
64
+
65
+ Returns:
66
+ Path type string indicating which test variant to run
67
+
68
+ Raises:
69
+ ValueError: If no agents available
70
+ """
71
+ if agent_count == 0:
72
+ raise ValueError("No agents available for testing")
73
+ elif agent_count == 1:
74
+ return "1-agent"
75
+ elif agent_count == 2:
76
+ return "2-agent"
77
+ else:
78
+ return "3+-agent"
79
+
80
+
81
+ def assign_agents(
82
+ available_agents: list[str],
83
+ path_type: Literal["1-agent", "2-agent", "3+-agent"],
84
+ ) -> tuple[str, str, str | None]:
85
+ """Assign agents to roles based on path type.
86
+
87
+ Agents are sorted alphabetically for deterministic assignment:
88
+ - First agent: implementation role
89
+ - Second agent: review role
90
+ - Third agent: fallback role (if available)
91
+
92
+ Args:
93
+ available_agents: List of available agent IDs
94
+ path_type: The test path type
95
+
96
+ Returns:
97
+ Tuple of (implementation_agent, review_agent, fallback_agent)
98
+
99
+ Raises:
100
+ ValueError: If no agents available
101
+ """
102
+ if not available_agents:
103
+ raise ValueError("No agents available")
104
+
105
+ # Sort for deterministic assignment
106
+ agents = sorted(available_agents)
107
+
108
+ if path_type == "1-agent":
109
+ # Same agent for both roles
110
+ return agents[0], agents[0], None
111
+
112
+ elif path_type == "2-agent":
113
+ # Different agents, no fallback
114
+ return agents[0], agents[1], None
115
+
116
+ else: # 3+-agent
117
+ # Different agents with fallback
118
+ return agents[0], agents[1], agents[2]
119
+
120
+
121
+ def clear_test_path_cache() -> None:
122
+ """Clear the cached test path.
123
+
124
+ Call this when agent availability may have changed (e.g., during
125
+ test setup or teardown).
126
+ """
127
+ global _test_path_cache
128
+ _test_path_cache = None
129
+
130
+
131
+ async def select_test_path(force_path: str | None = None) -> TestPath:
132
+ """Select test path based on available agents.
133
+
134
+ This is the main entry point for test path selection. It:
135
+ 1. Detects available (authenticated) agents
136
+ 2. Determines the appropriate path type
137
+ 3. Assigns agents to roles
138
+ 4. Caches the result for session duration
139
+
140
+ Args:
141
+ force_path: Optional path type to force (for testing).
142
+ Valid values: "1-agent", "2-agent", "3+-agent"
143
+
144
+ Returns:
145
+ TestPath with agent assignments
146
+
147
+ Raises:
148
+ ValueError: If no agents available
149
+ """
150
+ global _test_path_cache
151
+
152
+ if _test_path_cache is not None and force_path is None:
153
+ return _test_path_cache
154
+
155
+ # Import here to avoid circular imports and allow WP01 to be merged first
156
+ from specify_cli.orchestrator.testing.availability import (
157
+ detect_all_agents,
158
+ get_available_agents,
159
+ )
160
+
161
+ # Detect agents
162
+ await detect_all_agents()
163
+ available = get_available_agents()
164
+
165
+ if not available:
166
+ raise ValueError(
167
+ "No agents available for testing. "
168
+ "Install and authenticate at least one agent."
169
+ )
170
+
171
+ # Determine path type
172
+ if force_path:
173
+ # Validate force_path
174
+ if force_path not in ("1-agent", "2-agent", "3+-agent"):
175
+ raise ValueError(
176
+ f"Invalid force_path: {force_path}. "
177
+ "Must be '1-agent', '2-agent', or '3+-agent'"
178
+ )
179
+ path_type: Literal["1-agent", "2-agent", "3+-agent"] = force_path # type: ignore[assignment]
180
+ else:
181
+ path_type = determine_path_type(len(available))
182
+
183
+ # Assign agents
184
+ impl_agent, review_agent, fallback = assign_agents(available, path_type)
185
+
186
+ test_path = TestPath(
187
+ path_type=path_type,
188
+ available_agents=available,
189
+ implementation_agent=impl_agent,
190
+ review_agent=review_agent,
191
+ fallback_agent=fallback,
192
+ )
193
+
194
+ if force_path is None:
195
+ _test_path_cache = test_path
196
+
197
+ return test_path
198
+
199
+
200
+ def select_test_path_sync(force_path: str | None = None) -> TestPath:
201
+ """Synchronous wrapper for select_test_path.
202
+
203
+ Useful for pytest fixtures and non-async contexts.
204
+
205
+ Args:
206
+ force_path: Optional path type to force (for testing)
207
+
208
+ Returns:
209
+ TestPath with agent assignments
210
+ """
211
+ try:
212
+ loop = asyncio.get_running_loop()
213
+ except RuntimeError:
214
+ # No running loop - create one
215
+ return asyncio.run(select_test_path(force_path))
216
+
217
+ # Running loop exists - use it
218
+ return loop.run_until_complete(select_test_path(force_path))
@@ -0,0 +1,107 @@
1
+ """Plan validation utilities for Spec Kitty CLI.
2
+
3
+ Detects whether a plan.md file has been meaningfully filled out or is still in template form.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+
12
+ # Template markers that indicate an unfilled plan
13
+ TEMPLATE_MARKERS = [
14
+ "[FEATURE]",
15
+ "[###-feature-name]",
16
+ "[DATE]",
17
+ "[link]",
18
+ "[Extract from feature spec:",
19
+ "ACTION REQUIRED: Replace the content",
20
+ "[e.g., Python 3.11",
21
+ "or NEEDS CLARIFICATION",
22
+ "# [REMOVE IF UNUSED]",
23
+ "[Gates determined based on constitution file]",
24
+ "[Document the selected structure",
25
+ ]
26
+
27
+ # Minimum number of template markers that must be removed for plan to be considered filled
28
+ MIN_MARKERS_TO_REMOVE = 5
29
+
30
+
31
+ class PlanValidationError(Exception):
32
+ """Raised when plan.md validation fails."""
33
+
34
+ pass
35
+
36
+
37
+ def detect_unfilled_plan(plan_path: Path) -> tuple[bool, list[str]]:
38
+ """Check if plan.md is still in template form.
39
+
40
+ Args:
41
+ plan_path: Path to the plan.md file
42
+
43
+ Returns:
44
+ Tuple of (is_unfilled, list of markers found)
45
+ - is_unfilled: True if the plan appears to be unfilled template
46
+ - markers: List of template markers still present in the file
47
+ """
48
+ if not plan_path.exists():
49
+ return False, []
50
+
51
+ try:
52
+ content = plan_path.read_text(encoding="utf-8-sig")
53
+ except Exception:
54
+ # If we can't read it, assume it's filled (don't block progress)
55
+ return False, []
56
+
57
+ found_markers = []
58
+ for marker in TEMPLATE_MARKERS:
59
+ if marker in content:
60
+ found_markers.append(marker)
61
+
62
+ # Consider unfilled if multiple key markers are still present
63
+ is_unfilled = len(found_markers) >= MIN_MARKERS_TO_REMOVE
64
+
65
+ return is_unfilled, found_markers
66
+
67
+
68
+ def validate_plan_filled(
69
+ plan_path: Path,
70
+ *,
71
+ feature_slug: Optional[str] = None,
72
+ strict: bool = True,
73
+ ) -> None:
74
+ """Validate that plan.md has been filled out.
75
+
76
+ Args:
77
+ plan_path: Path to the plan.md file
78
+ feature_slug: Optional feature slug for error messages
79
+ strict: If True, raise error on unfilled plan. If False, just warn.
80
+
81
+ Raises:
82
+ PlanValidationError: If plan is unfilled and strict=True
83
+ """
84
+ is_unfilled, markers = detect_unfilled_plan(plan_path)
85
+
86
+ if not is_unfilled:
87
+ return
88
+
89
+ feature_display = f" for feature '{feature_slug}'" if feature_slug else ""
90
+ marker_list = "\n - ".join(markers[:5]) # Show first 5 markers
91
+ more_markers = f"\n ... and {len(markers) - 5} more" if len(markers) > 5 else ""
92
+
93
+ error_msg = (
94
+ f"plan.md{feature_display} appears to be unfilled (template form).\n"
95
+ f"Found {len(markers)} template markers:\n - {marker_list}{more_markers}\n\n"
96
+ f"Please complete the /spec-kitty.plan workflow before proceeding to research or tasks.\n"
97
+ f"The plan.md file must have technical details filled in, not just template placeholders."
98
+ )
99
+
100
+ if strict:
101
+ raise PlanValidationError(error_msg)
102
+ else:
103
+ import sys
104
+ print(f"Warning: {error_msg}", file=sys.stderr)
105
+
106
+
107
+ __all__ = ["PlanValidationError", "detect_unfilled_plan", "validate_plan_filled"]
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ """Debug script to test dashboard feature scanning."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ # Add src to path
8
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
9
+
10
+ from specify_cli.dashboard.scanner import scan_all_features, gather_feature_paths
11
+
12
+ def main():
13
+ if len(sys.argv) > 1:
14
+ project_dir = Path(sys.argv[1]).resolve()
15
+ else:
16
+ project_dir = Path.cwd()
17
+
18
+ print(f"Scanning project directory: {project_dir}")
19
+ print()
20
+
21
+ # Test gather_feature_paths
22
+ print("=== Feature Paths ===")
23
+ feature_paths = gather_feature_paths(project_dir)
24
+ if not feature_paths:
25
+ print(" No features found!")
26
+ print()
27
+ print("Checking directories:")
28
+ print(f" Main specs: {project_dir / 'kitty-specs'} exists: {(project_dir / 'kitty-specs').exists()}")
29
+ print(f" Worktrees: {project_dir / '.worktrees'} exists: {(project_dir / '.worktrees').exists()}")
30
+
31
+ if (project_dir / '.worktrees').exists():
32
+ for wt_dir in (project_dir / '.worktrees').iterdir():
33
+ if wt_dir.is_dir():
34
+ wt_specs = wt_dir / 'kitty-specs'
35
+ print(f" {wt_dir.name}/kitty-specs exists: {wt_specs.exists()}")
36
+ if wt_specs.exists():
37
+ for feat_dir in wt_specs.iterdir():
38
+ if feat_dir.is_dir():
39
+ print(f" Feature: {feat_dir.name}")
40
+ else:
41
+ for feature_id, feature_path in feature_paths.items():
42
+ print(f" {feature_id}: {feature_path}")
43
+ print()
44
+
45
+ # Test scan_all_features
46
+ print("=== Scanned Features ===")
47
+ features = scan_all_features(project_dir)
48
+ if not features:
49
+ print(" No features scanned!")
50
+ else:
51
+ for feature in features:
52
+ print(f" ID: {feature['id']}")
53
+ print(f" Name: {feature['name']}")
54
+ print(f" Path: {feature['path']}")
55
+ print(f" Artifacts: {feature['artifacts']}")
56
+ print(f" Workflow: {feature['workflow']}")
57
+ print(f" Kanban: {feature['kanban_stats']}")
58
+ print()
59
+
60
+ if __name__ == "__main__":
61
+ main()