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,204 @@
1
+ """Project diagnostics helpers for the dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict
10
+
11
+ __all__ = ["run_diagnostics"]
12
+
13
+
14
+ def _ensure_specify_cli_on_path() -> None:
15
+ """Ensure the repository root (src directory) is on sys.path for fallback imports."""
16
+ candidate = Path(__file__).resolve().parents[2] # .../src
17
+ if str(candidate) not in sys.path:
18
+ sys.path.insert(0, str(candidate))
19
+
20
+
21
+ def run_diagnostics(project_dir: Path) -> Dict[str, Any]:
22
+ """Run comprehensive diagnostics on the project setup using enhanced verification."""
23
+ try:
24
+ from ..manifest import FileManifest, WorktreeStatus # type: ignore
25
+ from ..acceptance import detect_feature_slug, AcceptanceError
26
+ except (ImportError, ValueError):
27
+ try:
28
+ from specify_cli.manifest import FileManifest, WorktreeStatus # type: ignore
29
+ from specify_cli.acceptance import detect_feature_slug, AcceptanceError
30
+ except ImportError:
31
+ _ensure_specify_cli_on_path()
32
+ from specify_cli.manifest import FileManifest, WorktreeStatus # type: ignore
33
+ from specify_cli.acceptance import detect_feature_slug, AcceptanceError
34
+
35
+ kittify_dir = project_dir / ".kittify"
36
+ repo_root = project_dir
37
+
38
+ diagnostics: Dict[str, Any] = {
39
+ 'project_path': str(project_dir),
40
+ 'current_working_directory': str(Path.cwd()),
41
+ 'git_branch': None,
42
+ 'in_worktree': False,
43
+ 'worktrees_exist': False,
44
+ 'active_mission': None,
45
+ 'file_integrity': {},
46
+ 'worktree_overview': {},
47
+ 'current_feature': {},
48
+ 'all_features': [],
49
+ 'dashboard_health': {},
50
+ 'observations': [],
51
+ 'issues': [],
52
+ }
53
+
54
+ manifest = FileManifest(kittify_dir)
55
+ worktree_status = WorktreeStatus(repo_root)
56
+
57
+ try:
58
+ result = subprocess.run(
59
+ ['git', 'branch', '--show-current'],
60
+ cwd=project_dir,
61
+ capture_output=True,
62
+ text=True,
63
+ check=True,
64
+ )
65
+ diagnostics['git_branch'] = result.stdout.strip()
66
+ except subprocess.CalledProcessError:
67
+ diagnostics['issues'].append('Could not detect git branch')
68
+
69
+ diagnostics['in_worktree'] = '.worktrees' in str(Path.cwd())
70
+ worktrees_dir = project_dir / '.worktrees'
71
+ diagnostics['worktrees_exist'] = worktrees_dir.exists()
72
+ diagnostics['active_mission'] = manifest.active_mission
73
+
74
+ file_check = manifest.check_files()
75
+ expected_files = manifest.get_expected_files()
76
+
77
+ total_expected = sum(len(files) for files in expected_files.values())
78
+ total_present = len(file_check["present"])
79
+ total_missing = len(file_check["missing"])
80
+
81
+ diagnostics['file_integrity'] = {
82
+ "total_expected": total_expected,
83
+ "total_present": total_present,
84
+ "total_missing": total_missing,
85
+ "missing_files": list(file_check["missing"].keys()) if file_check["missing"] else [],
86
+ }
87
+
88
+ worktree_summary = worktree_status.get_worktree_summary()
89
+ diagnostics['worktree_overview'] = worktree_summary
90
+
91
+ diagnostics['all_features'] = []
92
+ for feature_slug in worktree_status.get_all_features():
93
+ feature_status = worktree_status.get_feature_status(feature_slug)
94
+ diagnostics['all_features'].append({
95
+ 'name': feature_slug,
96
+ 'state': feature_status['state'],
97
+ 'branch_exists': feature_status['branch_exists'],
98
+ 'branch_merged': feature_status['branch_merged'],
99
+ 'worktree_exists': feature_status['worktree_exists'],
100
+ 'worktree_path': feature_status['worktree_path'],
101
+ 'artifacts_in_main': feature_status['artifacts_in_main'],
102
+ 'artifacts_in_worktree': feature_status['artifacts_in_worktree'],
103
+ })
104
+
105
+ try:
106
+ feature_slug = detect_feature_slug(repo_root, cwd=Path.cwd())
107
+ if feature_slug:
108
+ feature_status = worktree_status.get_feature_status(feature_slug.strip())
109
+ diagnostics['current_feature'] = {
110
+ 'detected': True,
111
+ 'name': feature_slug.strip(),
112
+ 'state': feature_status['state'],
113
+ 'branch_exists': feature_status['branch_exists'],
114
+ 'branch_merged': feature_status['branch_merged'],
115
+ 'worktree_exists': feature_status['worktree_exists'],
116
+ 'worktree_path': feature_status['worktree_path'],
117
+ 'artifacts_in_main': feature_status['artifacts_in_main'],
118
+ 'artifacts_in_worktree': feature_status['artifacts_in_worktree'],
119
+ }
120
+ except (AcceptanceError, Exception) as exc: # type: ignore[misc]
121
+ diagnostics['current_feature'] = {
122
+ 'detected': False,
123
+ 'error': str(exc),
124
+ }
125
+
126
+ observations = []
127
+
128
+ if diagnostics['git_branch'] == 'main' and diagnostics['in_worktree']:
129
+ observations.append("Unusual: In worktree but on main branch")
130
+
131
+ current_feature = diagnostics.get('current_feature') or {}
132
+ if current_feature.get('detected') and current_feature.get('state') == 'in_development':
133
+ if not current_feature.get('worktree_exists'):
134
+ observations.append(
135
+ f"Feature {current_feature.get('name')} has no worktree but has development artifacts"
136
+ )
137
+
138
+ if total_missing > 0:
139
+ observations.append(f"Mission integrity: {total_missing} expected files not found")
140
+
141
+ if worktree_summary.get('active_worktrees', 0) > 5:
142
+ observations.append(f"Multiple worktrees active: {worktree_summary['active_worktrees']}")
143
+
144
+ # Check dashboard health
145
+ dashboard_file = kittify_dir / '.dashboard'
146
+ dashboard_health = {
147
+ 'metadata_exists': dashboard_file.exists(),
148
+ 'can_start': None,
149
+ 'startup_test': None,
150
+ }
151
+
152
+ if dashboard_file.exists():
153
+ try:
154
+ from ..dashboard.lifecycle import _parse_dashboard_file, _check_dashboard_health
155
+ url, port, token, pid = _parse_dashboard_file(dashboard_file)
156
+ dashboard_health['url'] = url
157
+ dashboard_health['port'] = port
158
+ dashboard_health['pid'] = pid
159
+ dashboard_health['has_pid'] = pid is not None
160
+
161
+ if port:
162
+ is_healthy = _check_dashboard_health(port, project_dir, token)
163
+ dashboard_health['responding'] = is_healthy
164
+ if not is_healthy:
165
+ diagnostics['issues'].append(f'Dashboard metadata exists but not responding on port {port}')
166
+ if pid:
167
+ # Check if process is alive
168
+ try:
169
+ from ..dashboard.lifecycle import _is_process_alive
170
+ if _is_process_alive(pid):
171
+ diagnostics['issues'].append(f'Dashboard process (PID {pid}) is alive but not responding')
172
+ else:
173
+ diagnostics['issues'].append(f'Dashboard process (PID {pid}) is dead - stale metadata file')
174
+ except Exception:
175
+ pass
176
+ except Exception as e:
177
+ dashboard_health['parse_error'] = str(e)
178
+ diagnostics['issues'].append(f'Dashboard metadata file corrupted: {e}')
179
+ else:
180
+ # No dashboard running - try to start one and see what happens
181
+ try:
182
+ from ..dashboard.lifecycle import ensure_dashboard_running
183
+ url, port, started = ensure_dashboard_running(project_dir, background_process=False)
184
+ dashboard_health['can_start'] = True
185
+ dashboard_health['startup_test'] = 'SUCCESS'
186
+ dashboard_health['test_url'] = url
187
+ dashboard_health['test_port'] = port
188
+
189
+ # Stop the test dashboard
190
+ try:
191
+ from ..dashboard.lifecycle import stop_dashboard
192
+ stop_dashboard(project_dir)
193
+ except Exception:
194
+ pass
195
+ except Exception as e:
196
+ dashboard_health['can_start'] = False
197
+ dashboard_health['startup_test'] = 'FAILED'
198
+ dashboard_health['startup_error'] = str(e)
199
+ diagnostics['issues'].append(f'Dashboard cannot start: {e}')
200
+
201
+ diagnostics['dashboard_health'] = dashboard_health
202
+ diagnostics['observations'] = observations
203
+
204
+ return diagnostics
@@ -0,0 +1,17 @@
1
+ """Dashboard HTTP handler subpackage."""
2
+
3
+ from .api import APIHandler
4
+ from .base import DashboardHandler
5
+ from .features import FeatureHandler
6
+ from .router import DashboardRouter
7
+ from .static import STATIC_DIR, STATIC_URL_PREFIX, StaticHandler
8
+
9
+ __all__ = [
10
+ "APIHandler",
11
+ "DashboardHandler",
12
+ "DashboardRouter",
13
+ "FeatureHandler",
14
+ "StaticHandler",
15
+ "STATIC_DIR",
16
+ "STATIC_URL_PREFIX",
17
+ ]
@@ -0,0 +1,143 @@
1
+ """API-focused dashboard HTTP handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from ..diagnostics import run_diagnostics
9
+ from ..scanner import format_path_for_display, scan_all_features
10
+ from ..templates import get_dashboard_html
11
+ from .base import DashboardHandler
12
+ from specify_cli.mission import MissionError, get_mission_by_name
13
+
14
+ __all__ = ["APIHandler"]
15
+
16
+
17
+ class APIHandler(DashboardHandler):
18
+ """Serve dashboard root, health, diagnostics, and shutdown endpoints."""
19
+
20
+ def handle_root(self) -> None:
21
+ """Return the rendered dashboard HTML shell."""
22
+ project_path = Path(self.project_dir).resolve()
23
+
24
+ # Derive active mission from the most active feature (per-feature mission model)
25
+ mission_context = {
26
+ 'name': 'No active feature',
27
+ 'domain': 'unknown',
28
+ 'version': '',
29
+ 'slug': '',
30
+ 'description': '',
31
+ 'path': '',
32
+ }
33
+
34
+ try:
35
+ features = scan_all_features(project_path)
36
+
37
+ # Find active feature: WPs in doing > for_review > most recent
38
+ active_feature = None
39
+ for feature in features:
40
+ stats = feature.get('kanban_stats', {})
41
+ if stats.get('doing', 0) > 0:
42
+ active_feature = feature
43
+ break
44
+ if stats.get('for_review', 0) > 0 and active_feature is None:
45
+ active_feature = feature
46
+
47
+ if active_feature is None and features:
48
+ active_feature = features[0] # Most recent
49
+
50
+ if active_feature:
51
+ feature_mission_key = active_feature.get('meta', {}).get('mission', 'software-dev')
52
+ kittify_dir = project_path / ".kittify"
53
+ mission = get_mission_by_name(feature_mission_key, kittify_dir)
54
+ mission_context = {
55
+ 'name': mission.name,
56
+ 'domain': mission.config.domain,
57
+ 'version': mission.config.version,
58
+ 'slug': mission.path.name,
59
+ 'description': mission.config.description or '',
60
+ 'path': format_path_for_display(str(mission.path)),
61
+ }
62
+ except (MissionError, Exception):
63
+ pass # Keep default "No active feature" context
64
+
65
+ self.send_response(200)
66
+ self.send_header('Content-type', 'text/html')
67
+ self.end_headers()
68
+ self.wfile.write(get_dashboard_html(mission_context=mission_context).encode())
69
+
70
+ def handle_health(self) -> None:
71
+ """Return project health metadata."""
72
+ self.send_response(200)
73
+ self.send_header('Content-type', 'application/json')
74
+ self.send_header('Cache-Control', 'no-cache')
75
+ self.end_headers()
76
+
77
+ try:
78
+ project_path = str(Path(self.project_dir).resolve())
79
+ except Exception:
80
+ project_path = str(self.project_dir)
81
+
82
+ response_data = {
83
+ 'status': 'ok',
84
+ 'project_path': project_path,
85
+ }
86
+
87
+ token = getattr(self, 'project_token', None)
88
+ if token:
89
+ response_data['token'] = token
90
+
91
+ self.wfile.write(json.dumps(response_data).encode())
92
+
93
+ def handle_shutdown(self) -> None:
94
+ """Delegate to the shared shutdown helper."""
95
+ self._handle_shutdown()
96
+
97
+ def handle_diagnostics(self) -> None:
98
+ """Run diagnostics and report JSON payloads (or errors)."""
99
+ try:
100
+ diagnostics = run_diagnostics(Path(self.project_dir))
101
+ self.send_response(200)
102
+ self.send_header('Content-type', 'application/json')
103
+ self.send_header('Cache-Control', 'no-cache')
104
+ self.end_headers()
105
+ self.wfile.write(json.dumps(diagnostics).encode())
106
+ except Exception as exc: # pragma: no cover - fallback safety
107
+ import traceback
108
+
109
+ error_msg = {
110
+ "error": str(exc),
111
+ "traceback": traceback.format_exc(),
112
+ }
113
+ self.send_response(500)
114
+ self.send_header('Content-type', 'application/json')
115
+ self.end_headers()
116
+ self.wfile.write(json.dumps(error_msg).encode())
117
+
118
+ def handle_constitution(self) -> None:
119
+ """Serve project-level constitution from .kittify/memory/constitution.md"""
120
+ try:
121
+ constitution_path = Path(self.project_dir) / ".kittify" / "memory" / "constitution.md"
122
+
123
+ if not constitution_path.exists():
124
+ self.send_response(404)
125
+ self.send_header('Content-type', 'text/plain')
126
+ self.end_headers()
127
+ self.wfile.write(b'Constitution not found')
128
+ return
129
+
130
+ content = constitution_path.read_text(encoding='utf-8')
131
+ self.send_response(200)
132
+ self.send_header('Content-type', 'text/plain; charset=utf-8')
133
+ self.send_header('Cache-Control', 'no-cache')
134
+ self.end_headers()
135
+ self.wfile.write(content.encode('utf-8'))
136
+ except Exception as exc: # pragma: no cover - fallback safety
137
+ import traceback
138
+
139
+ error_msg = f"Error loading constitution: {exc}\n{traceback.format_exc()}"
140
+ self.send_response(500)
141
+ self.send_header('Content-type', 'text/plain')
142
+ self.end_headers()
143
+ self.wfile.write(error_msg.encode())
@@ -0,0 +1,65 @@
1
+ """Shared helpers for dashboard HTTP handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ import time
8
+ import urllib.parse
9
+ from http.server import BaseHTTPRequestHandler
10
+ from typing import Any, Dict, Optional
11
+
12
+ __all__ = ["DashboardHandler"]
13
+
14
+
15
+ class DashboardHandler(BaseHTTPRequestHandler):
16
+ """Base class that provides shared helpers for router/endpoint handlers."""
17
+
18
+ project_dir: Optional[str] = None
19
+ project_token: Optional[str] = None
20
+
21
+ def log_message(self, format: str, *args: Any) -> None: # noqa: A003 - signature from BaseHTTPRequestHandler
22
+ """Suppress default HTTP handler logging noise."""
23
+ del format, args
24
+
25
+ def _send_json(self, status_code: int, payload: Dict[str, Any]) -> None:
26
+ """Write a JSON response with common headers."""
27
+ self.send_response(status_code)
28
+ self.send_header('Content-type', 'application/json')
29
+ self.send_header('Cache-Control', 'no-cache')
30
+ self.end_headers()
31
+ self.wfile.write(json.dumps(payload).encode())
32
+
33
+ def _handle_shutdown(self) -> None:
34
+ """Validate shutdown tokens and stop the server."""
35
+ expected_token = getattr(self, 'project_token', None)
36
+
37
+ token = None
38
+ if self.command == 'POST':
39
+ content_length = int(self.headers.get('Content-Length') or 0)
40
+ body = self.rfile.read(content_length) if content_length else b''
41
+ if body:
42
+ try:
43
+ payload = json.loads(body.decode('utf-8'))
44
+ token = payload.get('token')
45
+ except (UnicodeDecodeError, json.JSONDecodeError):
46
+ self._send_json(400, {'error': 'invalid_payload'})
47
+ return
48
+ else:
49
+ parsed_path = urllib.parse.urlparse(self.path)
50
+ params = urllib.parse.parse_qs(parsed_path.query)
51
+ token_values = params.get('token')
52
+ if token_values:
53
+ token = token_values[0]
54
+
55
+ if expected_token and token != expected_token:
56
+ self._send_json(403, {'error': 'invalid_token'})
57
+ return
58
+
59
+ self._send_json(200, {'status': 'stopping'})
60
+
61
+ def shutdown_server(server):
62
+ time.sleep(0.05) # allow response to flush
63
+ server.shutdown()
64
+
65
+ threading.Thread(target=shutdown_server, args=(self.server,), daemon=True).start()