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,390 @@
1
+ """Feature-centric dashboard handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.parse
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from ..scanner import (
11
+ format_path_for_display,
12
+ resolve_feature_dir,
13
+ scan_all_features,
14
+ scan_feature_kanban,
15
+ )
16
+ from .base import DashboardHandler
17
+ from specify_cli.legacy_detector import is_legacy_format
18
+ from specify_cli.mission import MissionError, get_mission_by_name
19
+
20
+ __all__ = ["FeatureHandler"]
21
+
22
+
23
+ class FeatureHandler(DashboardHandler):
24
+ """Serve feature lists, kanban lanes, and artifact viewers."""
25
+
26
+ def handle_features_list(self) -> None:
27
+ """Return summary data for all features."""
28
+ self.send_response(200)
29
+ self.send_header('Content-type', 'application/json')
30
+ self.send_header('Cache-Control', 'no-cache')
31
+ self.end_headers()
32
+
33
+ project_path = Path(self.project_dir).resolve()
34
+ features = scan_all_features(project_path)
35
+
36
+ # Add legacy format indicator to each feature
37
+ for feature in features:
38
+ feature_dir = project_path / feature['path']
39
+ feature['is_legacy'] = is_legacy_format(feature_dir)
40
+
41
+ # Derive active mission from the most active feature (per-feature mission model)
42
+ # Priority: feature with WPs in doing > for_review > most recent feature
43
+ mission_context = {
44
+ 'name': 'No active feature',
45
+ 'domain': 'unknown',
46
+ 'version': '',
47
+ 'slug': '',
48
+ 'description': '',
49
+ 'path': '',
50
+ }
51
+
52
+ active_feature = None
53
+ for feature in features:
54
+ stats = feature.get('kanban_stats', {})
55
+ if stats.get('doing', 0) > 0:
56
+ active_feature = feature
57
+ break
58
+ if stats.get('for_review', 0) > 0 and active_feature is None:
59
+ active_feature = feature
60
+
61
+ # Fall back to most recent feature if none are active
62
+ if active_feature is None and features:
63
+ active_feature = features[0] # Already sorted by id descending (most recent first)
64
+
65
+ if active_feature:
66
+ feature_mission_key = active_feature.get('meta', {}).get('mission', 'software-dev')
67
+ try:
68
+ kittify_dir = project_path / ".kittify"
69
+ mission = get_mission_by_name(feature_mission_key, kittify_dir)
70
+ mission_context = {
71
+ 'name': mission.name,
72
+ 'domain': mission.config.domain,
73
+ 'version': mission.config.version,
74
+ 'slug': mission.path.name,
75
+ 'description': mission.config.description or '',
76
+ 'path': format_path_for_display(str(mission.path)),
77
+ 'feature': active_feature.get('name', ''),
78
+ }
79
+ except MissionError:
80
+ # Fallback: show feature name with unknown mission
81
+ mission_context = {
82
+ 'name': f"Unknown ({feature_mission_key})",
83
+ 'domain': 'unknown',
84
+ 'version': '',
85
+ 'slug': feature_mission_key,
86
+ 'description': '',
87
+ 'path': '',
88
+ 'feature': active_feature.get('name', ''),
89
+ }
90
+
91
+ worktrees_root_path = project_path / '.worktrees'
92
+ try:
93
+ worktrees_root_resolved = worktrees_root_path.resolve()
94
+ except Exception:
95
+ worktrees_root_resolved = worktrees_root_path
96
+
97
+ try:
98
+ current_path = Path.cwd().resolve()
99
+ except Exception:
100
+ current_path = Path.cwd()
101
+
102
+ worktrees_root_exists = worktrees_root_path.exists()
103
+ worktrees_root_display = (
104
+ format_path_for_display(str(worktrees_root_resolved))
105
+ if worktrees_root_exists
106
+ else None
107
+ )
108
+
109
+ active_worktree_display: Optional[str] = None
110
+ if worktrees_root_exists:
111
+ try:
112
+ current_path.relative_to(worktrees_root_resolved)
113
+ active_worktree_display = format_path_for_display(str(current_path))
114
+ except ValueError:
115
+ active_worktree_display = None
116
+
117
+ if not active_worktree_display and current_path != project_path:
118
+ active_worktree_display = format_path_for_display(str(current_path))
119
+
120
+ response = {
121
+ 'features': features,
122
+ 'project_path': format_path_for_display(str(project_path)),
123
+ 'worktrees_root': worktrees_root_display,
124
+ 'active_worktree': active_worktree_display,
125
+ 'active_mission': mission_context,
126
+ }
127
+ self.wfile.write(json.dumps(response).encode())
128
+
129
+ def handle_kanban(self, path: str) -> None:
130
+ """Return kanban data for a specific feature slug."""
131
+ parts = path.split('/')
132
+ if len(parts) >= 4:
133
+ feature_id = parts[3]
134
+ project_path = Path(self.project_dir).resolve()
135
+ kanban_data = scan_feature_kanban(project_path, feature_id)
136
+
137
+ # Check if feature uses legacy format
138
+ feature_dir = resolve_feature_dir(project_path, feature_id)
139
+ is_legacy = is_legacy_format(feature_dir) if feature_dir else False
140
+
141
+ response = {
142
+ 'lanes': kanban_data,
143
+ 'is_legacy': is_legacy,
144
+ 'upgrade_needed': is_legacy,
145
+ }
146
+
147
+ self.send_response(200)
148
+ self.send_header('Content-type', 'application/json')
149
+ self.send_header('Cache-Control', 'no-cache')
150
+ self.end_headers()
151
+ self.wfile.write(json.dumps(response).encode())
152
+ return
153
+
154
+ self.send_response(404)
155
+ self.end_headers()
156
+
157
+ def handle_research(self, path: str) -> None:
158
+ """Return research.md contents + artifacts, or serve a specific file."""
159
+ parts = path.split('/')
160
+ if len(parts) < 4:
161
+ self.send_response(404)
162
+ self.end_headers()
163
+ return
164
+
165
+ feature_id = parts[3]
166
+ project_path = Path(self.project_dir)
167
+ feature_dir = resolve_feature_dir(project_path, feature_id)
168
+
169
+ if len(parts) == 4:
170
+ response = {'main_file': None, 'artifacts': []}
171
+
172
+ if feature_dir:
173
+ research_md = feature_dir / 'research.md'
174
+ if research_md.exists():
175
+ try:
176
+ response['main_file'] = research_md.read_text(encoding='utf-8')
177
+ except UnicodeDecodeError as err:
178
+ error_msg = (
179
+ f'⚠️ **Encoding Error in research.md**\\n\\n'
180
+ f'This file contains non-UTF-8 characters at position {err.start}.\\n'
181
+ 'Please convert the file to UTF-8 encoding.\\n\\n'
182
+ 'Attempting to read with error recovery...\\n\\n---\\n\\n'
183
+ )
184
+ response['main_file'] = error_msg + research_md.read_text(
185
+ encoding='utf-8', errors='replace'
186
+ )
187
+
188
+ research_dir = feature_dir / 'research'
189
+ if research_dir.exists() and research_dir.is_dir():
190
+ for file_path in sorted(research_dir.rglob('*')):
191
+ if file_path.is_file():
192
+ relative_path = str(file_path.relative_to(feature_dir))
193
+ icon = '📄'
194
+ if file_path.suffix == '.csv':
195
+ icon = '📊'
196
+ elif file_path.suffix == '.md':
197
+ icon = '📝'
198
+ elif file_path.suffix in ['.xlsx', '.xls']:
199
+ icon = '📈'
200
+ elif file_path.suffix == '.json':
201
+ icon = '📋'
202
+ response['artifacts'].append({
203
+ 'name': file_path.name,
204
+ 'path': relative_path,
205
+ 'icon': icon,
206
+ })
207
+
208
+ self.send_response(200)
209
+ self.send_header('Content-type', 'application/json')
210
+ self.send_header('Cache-Control', 'no-cache')
211
+ self.end_headers()
212
+ self.wfile.write(json.dumps(response).encode())
213
+ return
214
+
215
+ if len(parts) >= 5 and feature_dir:
216
+ file_path_encoded = parts[4]
217
+ file_path_str = urllib.parse.unquote(file_path_encoded)
218
+ artifact_file = (feature_dir / file_path_str).resolve()
219
+
220
+ try:
221
+ artifact_file.relative_to(feature_dir.resolve())
222
+ except ValueError:
223
+ self.send_response(404)
224
+ self.end_headers()
225
+ return
226
+
227
+ if artifact_file.exists() and artifact_file.is_file():
228
+ self.send_response(200)
229
+ self.send_header('Content-type', 'text/plain')
230
+ self.send_header('Cache-Control', 'no-cache')
231
+ self.end_headers()
232
+ try:
233
+ content = artifact_file.read_text(encoding='utf-8')
234
+ self.wfile.write(content.encode('utf-8'))
235
+ except UnicodeDecodeError as err:
236
+ error_msg = (
237
+ f'⚠️ Encoding Error in {artifact_file.name}\\n\\n'
238
+ f'This file contains non-UTF-8 characters at position {err.start}.\\n'
239
+ 'Please convert the file to UTF-8 encoding.\\n\\n'
240
+ 'Attempting to read with error recovery...\\n\\n'
241
+ )
242
+ content = artifact_file.read_text(encoding='utf-8', errors='replace')
243
+ self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
244
+ except Exception as exc:
245
+ self.wfile.write(f'Error reading file: {exc}'.encode('utf-8'))
246
+ return
247
+
248
+ self.send_response(404)
249
+ self.end_headers()
250
+
251
+ def _handle_artifact_directory(self, path: str, directory_name: str, md_icon: str = '📝') -> None:
252
+ """Generic handler for artifact directories (contracts, checklists, etc).
253
+
254
+ Args:
255
+ path: The request path
256
+ directory_name: Name of the subdirectory (e.g., 'contracts', 'checklists')
257
+ md_icon: Icon to use for .md files (default: '📝')
258
+ """
259
+ parts = path.split('/')
260
+ if len(parts) < 4:
261
+ self.send_response(404)
262
+ self.end_headers()
263
+ return
264
+
265
+ feature_id = parts[3]
266
+ project_path = Path(self.project_dir)
267
+ feature_dir = resolve_feature_dir(project_path, feature_id)
268
+
269
+ if len(parts) == 4:
270
+ # Return directory listing
271
+ response = {'files': []}
272
+
273
+ if feature_dir:
274
+ artifact_dir = feature_dir / directory_name
275
+ if artifact_dir.exists() and artifact_dir.is_dir():
276
+ for file_path in sorted(artifact_dir.rglob('*')):
277
+ if file_path.is_file():
278
+ relative_path = str(file_path.relative_to(feature_dir))
279
+ icon = '📄'
280
+ if file_path.suffix == '.md':
281
+ icon = md_icon
282
+ elif file_path.suffix == '.json':
283
+ icon = '📋'
284
+ response['files'].append({
285
+ 'name': file_path.name,
286
+ 'path': relative_path,
287
+ 'icon': icon,
288
+ })
289
+
290
+ self.send_response(200)
291
+ self.send_header('Content-type', 'application/json')
292
+ self.send_header('Cache-Control', 'no-cache')
293
+ self.end_headers()
294
+ self.wfile.write(json.dumps(response).encode())
295
+ return
296
+
297
+ if len(parts) >= 5 and feature_dir:
298
+ # Serve specific file
299
+ file_path_encoded = parts[4]
300
+ file_path_str = urllib.parse.unquote(file_path_encoded)
301
+ artifact_file = (feature_dir / file_path_str).resolve()
302
+
303
+ try:
304
+ artifact_file.relative_to(feature_dir.resolve())
305
+ except ValueError:
306
+ self.send_response(404)
307
+ self.end_headers()
308
+ return
309
+
310
+ if artifact_file.exists() and artifact_file.is_file():
311
+ self.send_response(200)
312
+ self.send_header('Content-type', 'text/plain')
313
+ self.send_header('Cache-Control', 'no-cache')
314
+ self.end_headers()
315
+ try:
316
+ content = artifact_file.read_text(encoding='utf-8')
317
+ self.wfile.write(content.encode('utf-8'))
318
+ except UnicodeDecodeError as err:
319
+ error_msg = (
320
+ f'⚠️ Encoding Error in {artifact_file.name}\\n\\n'
321
+ f'This file contains non-UTF-8 characters at position {err.start}.\\n'
322
+ 'Please convert the file to UTF-8 encoding.\\n\\n'
323
+ 'Attempting to read with error recovery...\\n\\n'
324
+ )
325
+ content = artifact_file.read_text(encoding='utf-8', errors='replace')
326
+ self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
327
+ except Exception as exc:
328
+ self.wfile.write(f'Error reading file: {exc}'.encode('utf-8'))
329
+ return
330
+
331
+ self.send_response(404)
332
+ self.end_headers()
333
+
334
+ def handle_contracts(self, path: str) -> None:
335
+ """Return contracts directory listing or serve a specific file."""
336
+ self._handle_artifact_directory(path, 'contracts', md_icon='📝')
337
+
338
+ def handle_checklists(self, path: str) -> None:
339
+ """Return checklists directory listing or serve a specific file."""
340
+ self._handle_artifact_directory(path, 'checklists', md_icon='✅')
341
+
342
+ def handle_artifact(self, path: str) -> None:
343
+ """Serve primary artifacts like spec.md and plan.md."""
344
+ parts = path.split('/')
345
+ if len(parts) < 4:
346
+ self.send_response(404)
347
+ self.end_headers()
348
+ return
349
+
350
+ feature_id = parts[3]
351
+ artifact_name = parts[4] if len(parts) > 4 else ''
352
+
353
+ project_path = Path(self.project_dir)
354
+ feature_dir = resolve_feature_dir(project_path, feature_id)
355
+
356
+ artifact_map = {
357
+ 'spec': 'spec.md',
358
+ 'plan': 'plan.md',
359
+ 'tasks': 'tasks.md',
360
+ 'research': 'research.md',
361
+ 'quickstart': 'quickstart.md',
362
+ 'data-model': 'data-model.md',
363
+ }
364
+
365
+ filename = artifact_map.get(artifact_name)
366
+ if feature_dir and filename:
367
+ artifact_file = feature_dir / filename
368
+ if artifact_file.exists():
369
+ self.send_response(200)
370
+ self.send_header('Content-type', 'text/plain')
371
+ self.send_header('Cache-Control', 'no-cache')
372
+ self.end_headers()
373
+ try:
374
+ content = artifact_file.read_text(encoding='utf-8')
375
+ self.wfile.write(content.encode('utf-8'))
376
+ except UnicodeDecodeError as err:
377
+ error_msg = (
378
+ f'⚠️ **Encoding Error in {filename}**\\n\\n'
379
+ f'This file contains non-UTF-8 characters at position {err.start}.\\n'
380
+ 'Please convert the file to UTF-8 encoding.\\n\\n'
381
+ 'Attempting to read with error recovery...\\n\\n---\\n\\n'
382
+ )
383
+ content = artifact_file.read_text(encoding='utf-8', errors='replace')
384
+ self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
385
+ except Exception as exc:
386
+ self.wfile.write(f'Error reading {filename}: {exc}'.encode('utf-8'))
387
+ return
388
+
389
+ self.send_response(404)
390
+ self.end_headers()
@@ -0,0 +1,81 @@
1
+ """Router that dispatches HTTP requests to specialized handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.parse
6
+
7
+ from .api import APIHandler
8
+ from .features import FeatureHandler
9
+ from .static import STATIC_URL_PREFIX, StaticHandler
10
+
11
+ __all__ = ["DashboardRouter"]
12
+
13
+
14
+ class DashboardRouter(APIHandler, FeatureHandler, StaticHandler):
15
+ """Dispatch GET/POST requests to API, feature, or static handlers."""
16
+
17
+ def do_POST(self) -> None: # noqa: N802 (BaseHTTPRequestHandler signature)
18
+ parsed_path = urllib.parse.urlparse(self.path)
19
+ path = parsed_path.path
20
+
21
+ if path == '/api/shutdown':
22
+ self.handle_shutdown()
23
+ return
24
+
25
+ self.send_response(404)
26
+ self.end_headers()
27
+
28
+ def do_GET(self) -> None: # noqa: N802
29
+ parsed_path = urllib.parse.urlparse(self.path)
30
+ path = parsed_path.path
31
+
32
+ if path == '/':
33
+ self.handle_root()
34
+ return
35
+
36
+ if path == '/api/health':
37
+ self.handle_health()
38
+ return
39
+
40
+ if path == '/api/shutdown':
41
+ self.handle_shutdown()
42
+ return
43
+
44
+ if path == '/api/features':
45
+ self.handle_features_list()
46
+ return
47
+
48
+ if path.startswith('/api/kanban/'):
49
+ self.handle_kanban(path)
50
+ return
51
+
52
+ if path.startswith('/api/research/'):
53
+ self.handle_research(path)
54
+ return
55
+
56
+ if path.startswith('/api/contracts/'):
57
+ self.handle_contracts(path)
58
+ return
59
+
60
+ if path.startswith('/api/checklists/'):
61
+ self.handle_checklists(path)
62
+ return
63
+
64
+ if path.startswith('/api/artifact/'):
65
+ self.handle_artifact(path)
66
+ return
67
+
68
+ if path == '/api/diagnostics':
69
+ self.handle_diagnostics()
70
+ return
71
+
72
+ if path == '/api/constitution':
73
+ self.handle_constitution()
74
+ return
75
+
76
+ if path.startswith(STATIC_URL_PREFIX):
77
+ self.handle_static(path)
78
+ return
79
+
80
+ self.send_response(404)
81
+ self.end_headers()
@@ -0,0 +1,50 @@
1
+ """Static asset handler for the dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mimetypes
6
+ from pathlib import Path
7
+
8
+ from .base import DashboardHandler
9
+
10
+ STATIC_URL_PREFIX = '/static/'
11
+ STATIC_DIR = (Path(__file__).resolve().parents[1] / 'static').resolve()
12
+
13
+ __all__ = ["STATIC_DIR", "STATIC_URL_PREFIX", "StaticHandler"]
14
+
15
+
16
+ class StaticHandler(DashboardHandler):
17
+ """Serve files from the dashboard/static directory."""
18
+
19
+ def handle_static(self, path: str) -> None:
20
+ relative_path = path[len(STATIC_URL_PREFIX):]
21
+ static_root = STATIC_DIR
22
+ try:
23
+ safe_path = (STATIC_DIR / relative_path).resolve()
24
+ except (RuntimeError, ValueError):
25
+ safe_path = None
26
+
27
+ if not relative_path or not safe_path:
28
+ self.send_response(404)
29
+ self.end_headers()
30
+ return
31
+
32
+ try:
33
+ safe_path.relative_to(static_root)
34
+ except ValueError:
35
+ self.send_response(404)
36
+ self.end_headers()
37
+ return
38
+
39
+ if not safe_path.is_file():
40
+ self.send_response(404)
41
+ self.end_headers()
42
+ return
43
+
44
+ mime_type, _ = mimetypes.guess_type(safe_path.name)
45
+ self.send_response(200)
46
+ self.send_header('Content-type', mime_type or 'application/octet-stream')
47
+ self.send_header('Cache-Control', 'no-cache')
48
+ self.end_headers()
49
+ with safe_path.open('rb') as static_file:
50
+ self.wfile.write(static_file.read())