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,1424 @@
1
+ let currentFeature = null;
2
+ let currentPage = 'overview';
3
+ let allFeatures = [];
4
+ let isConstitutionView = false;
5
+ let lastNonConstitutionPage = 'overview';
6
+ let projectPathDisplay = 'Loading…';
7
+ let activeWorktreeDisplay = 'detecting…';
8
+ let featureSelectActive = false;
9
+ let featureSelectIdleTimer = null;
10
+ let activeMission = {
11
+ name: 'Loading…',
12
+ domain: '',
13
+ version: '',
14
+ slug: '',
15
+ path: '',
16
+ description: ''
17
+ };
18
+
19
+ if (typeof window !== 'undefined' && window.__INITIAL_MISSION__) {
20
+ activeMission = window.__INITIAL_MISSION__;
21
+ }
22
+
23
+ /**
24
+ * Intercept clicks on links within rendered markdown content.
25
+ * Routes artifact links (spec.md, plan.md, etc.) through the dashboard API
26
+ * instead of navigating to broken URLs.
27
+ *
28
+ * @param {HTMLElement} container - The container element with rendered markdown
29
+ * @param {string} basePath - Base path for relative links (e.g., 'research/' for research artifacts)
30
+ */
31
+ function interceptMarkdownLinks(container, basePath = '') {
32
+ if (!container) return;
33
+
34
+ // Map of artifact names to their dashboard page keys
35
+ const artifactMap = {
36
+ 'spec.md': 'spec',
37
+ 'plan.md': 'plan',
38
+ 'tasks.md': 'tasks',
39
+ 'research.md': 'research',
40
+ 'quickstart.md': 'quickstart',
41
+ 'data-model.md': 'data_model',
42
+ 'data_model.md': 'data_model',
43
+ };
44
+
45
+ container.querySelectorAll('a').forEach(link => {
46
+ const href = link.getAttribute('href');
47
+ if (!href) return;
48
+
49
+ // Skip external links and anchor links
50
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#')) {
51
+ return;
52
+ }
53
+
54
+ // Normalize the path (remove leading ./ or /)
55
+ let normalizedPath = href.replace(/^\.?\//, '');
56
+
57
+ // Check if it's a known artifact (top-level .md file)
58
+ if (artifactMap[normalizedPath]) {
59
+ link.addEventListener('click', (e) => {
60
+ e.preventDefault();
61
+ switchPage(artifactMap[normalizedPath]);
62
+ });
63
+ link.style.cursor = 'pointer';
64
+ link.title = `View ${normalizedPath} in dashboard`;
65
+ return;
66
+ }
67
+
68
+ // Check if it's a research/, contracts/, or checklists/ subdirectory file
69
+ if (normalizedPath.startsWith('research/') || normalizedPath.startsWith('contracts/') || normalizedPath.startsWith('checklists/')) {
70
+ link.addEventListener('click', (e) => {
71
+ e.preventDefault();
72
+ const fileName = normalizedPath.split('/').pop();
73
+ if (normalizedPath.startsWith('research/')) {
74
+ loadResearchFile(normalizedPath, fileName);
75
+ } else if (normalizedPath.startsWith('contracts/')) {
76
+ loadContractFile(normalizedPath, fileName);
77
+ } else if (normalizedPath.startsWith('checklists/')) {
78
+ loadChecklistFile(normalizedPath, fileName);
79
+ }
80
+ });
81
+ link.style.cursor = 'pointer';
82
+ link.title = `View ${normalizedPath} in dashboard`;
83
+ return;
84
+ }
85
+
86
+ // Handle relative paths within the current context (e.g., evidence-log.csv from research.md)
87
+ if (basePath && !normalizedPath.includes('/')) {
88
+ const fullPath = basePath + normalizedPath;
89
+ link.addEventListener('click', (e) => {
90
+ e.preventDefault();
91
+ if (basePath === 'research/') {
92
+ loadResearchFile(fullPath, normalizedPath);
93
+ } else if (basePath === 'contracts/') {
94
+ loadContractFile(fullPath, normalizedPath);
95
+ } else if (basePath === 'checklists/') {
96
+ loadChecklistFile(fullPath, normalizedPath);
97
+ }
98
+ });
99
+ link.style.cursor = 'pointer';
100
+ link.title = `View ${fullPath} in dashboard`;
101
+ }
102
+ });
103
+ }
104
+
105
+ function updateMissionDisplay(mission) {
106
+ const nameEl = document.getElementById('mission-name');
107
+ if (!nameEl) return;
108
+
109
+ if (mission) {
110
+ activeMission = mission;
111
+ }
112
+
113
+ nameEl.textContent = activeMission.name || 'Unknown mission';
114
+ }
115
+
116
+ // Cookie-based state persistence
117
+ function restoreState() {
118
+ const cookies = document.cookie.split(';').reduce((acc, cookie) => {
119
+ const parts = cookie.trim().split('=');
120
+ if (parts.length === 2) {
121
+ acc[parts[0]] = decodeURIComponent(parts[1]);
122
+ }
123
+ return acc;
124
+ }, {});
125
+
126
+ return {
127
+ feature: cookies.lastFeature || null,
128
+ page: cookies.lastPage || 'overview'
129
+ };
130
+ }
131
+
132
+ function saveState(feature, page) {
133
+ const expires = new Date();
134
+ expires.setFullYear(expires.getFullYear() + 1); // 1 year
135
+
136
+ if (feature) {
137
+ document.cookie = `lastFeature=${encodeURIComponent(feature)}; expires=${expires.toUTCString()}; path=/; SameSite=Strict`;
138
+ }
139
+ if (page) {
140
+ document.cookie = `lastPage=${encodeURIComponent(page)}; expires=${expires.toUTCString()}; path=/; SameSite=Strict`;
141
+ }
142
+ }
143
+
144
+ function setFeatureSelectActive(isActive) {
145
+ if (isActive) {
146
+ featureSelectActive = true;
147
+ if (featureSelectIdleTimer) {
148
+ clearTimeout(featureSelectIdleTimer);
149
+ }
150
+ featureSelectIdleTimer = setTimeout(() => {
151
+ featureSelectActive = false;
152
+ featureSelectIdleTimer = null;
153
+ }, 5000);
154
+ } else {
155
+ featureSelectActive = false;
156
+ if (featureSelectIdleTimer) {
157
+ clearTimeout(featureSelectIdleTimer);
158
+ featureSelectIdleTimer = null;
159
+ }
160
+ }
161
+ }
162
+
163
+ function updateTreeInfo() {
164
+ const treeElement = document.getElementById('tree-info');
165
+ if (!treeElement) {
166
+ return;
167
+ }
168
+ const lines = [`└─ ${projectPathDisplay}`];
169
+ if (activeWorktreeDisplay) {
170
+ lines.push(` └─ Active worktree: ${activeWorktreeDisplay}`);
171
+ }
172
+ // Note: In 0.11.0+, worktrees are per-WP, not per-feature
173
+ // Feature-level worktree display removed (obsolete 0.10.x concept)
174
+ treeElement.textContent = lines.join('\n');
175
+ }
176
+
177
+ function computeFeatureWorktreeStatus(feature) {
178
+ // No-op: Feature-level worktrees are obsolete in 0.11.0+
179
+ // In workspace-per-WP model, worktrees are per-WP, not per-feature
180
+ // This function is kept for compatibility but does nothing
181
+ }
182
+
183
+ function switchFeature(featureId) {
184
+ const isSameFeature = featureId === currentFeature;
185
+ if (isConstitutionView) {
186
+ if (isSameFeature) {
187
+ return;
188
+ }
189
+ isConstitutionView = false;
190
+ if (lastNonConstitutionPage && lastNonConstitutionPage !== 'constitution') {
191
+ currentPage = lastNonConstitutionPage;
192
+ } else {
193
+ currentPage = 'overview';
194
+ }
195
+ document.querySelectorAll('.page').forEach(page => page.classList.remove('active'));
196
+ const currentPageEl = document.getElementById(`page-${currentPage}`);
197
+ if (currentPageEl) {
198
+ currentPageEl.classList.add('active');
199
+ }
200
+ document.querySelectorAll('.sidebar-item').forEach(item => {
201
+ if (item.dataset.page === currentPage) {
202
+ item.classList.add('active');
203
+ } else {
204
+ item.classList.remove('active');
205
+ }
206
+ });
207
+ }
208
+ currentFeature = featureId;
209
+ saveState(currentFeature, currentPage);
210
+ loadCurrentPage();
211
+ updateSidebarState();
212
+ const feature = allFeatures.find(f => f.id === currentFeature);
213
+ computeFeatureWorktreeStatus(feature);
214
+ updateTreeInfo();
215
+ }
216
+
217
+ function switchPage(pageName) {
218
+ if (pageName === 'constitution') {
219
+ showConstitution();
220
+ return;
221
+ }
222
+ if (pageName === 'diagnostics') {
223
+ showDiagnostics();
224
+ return;
225
+ }
226
+ isConstitutionView = false;
227
+ currentPage = pageName;
228
+ lastNonConstitutionPage = pageName;
229
+ saveState(currentFeature, currentPage);
230
+
231
+ // Update sidebar
232
+ document.querySelectorAll('.sidebar-item').forEach(item => {
233
+ if (item.dataset.page === pageName) {
234
+ item.classList.add('active');
235
+ } else {
236
+ item.classList.remove('active');
237
+ }
238
+ });
239
+
240
+ // Update pages
241
+ document.querySelectorAll('.page').forEach(page => {
242
+ page.classList.remove('active');
243
+ });
244
+ const activePageEl = document.getElementById(`page-${pageName}`);
245
+ if (activePageEl) {
246
+ activePageEl.classList.add('active');
247
+ }
248
+
249
+ loadCurrentPage();
250
+ }
251
+
252
+ function updateSidebarState() {
253
+ const feature = allFeatures.find(f => f.id === currentFeature);
254
+ if (!feature) return;
255
+
256
+ const artifacts = feature.artifacts;
257
+
258
+ document.querySelectorAll('.sidebar-item').forEach(item => {
259
+ const page = item.dataset.page;
260
+ // System pages (constitution, diagnostics) should never be disabled
261
+ if (!page || page === 'constitution' || page === 'diagnostics') {
262
+ item.classList.remove('disabled');
263
+ return;
264
+ }
265
+
266
+ const hasArtifact = page === 'overview' || artifacts[page.replace('-', '_')]?.exists;
267
+
268
+ if (hasArtifact) {
269
+ item.classList.remove('disabled');
270
+ } else {
271
+ item.classList.add('disabled');
272
+ }
273
+ });
274
+ }
275
+
276
+ function loadCurrentPage() {
277
+ if (isConstitutionView || currentPage === 'constitution') {
278
+ return;
279
+ }
280
+ if (!currentFeature) return;
281
+
282
+ if (currentPage === 'overview') {
283
+ loadOverview();
284
+ } else if (currentPage === 'kanban') {
285
+ loadKanban();
286
+ } else if (currentPage === 'contracts') {
287
+ loadContracts();
288
+ } else if (currentPage === 'checklists') {
289
+ loadChecklists();
290
+ } else if (currentPage === 'research') {
291
+ loadResearch();
292
+ } else {
293
+ loadArtifact(currentPage);
294
+ }
295
+ }
296
+
297
+ function loadOverview() {
298
+ const feature = allFeatures.find(f => f.id === currentFeature);
299
+ if (!feature) return;
300
+
301
+ const mergeBadge = (() => {
302
+ const meta = feature.meta || {};
303
+ const mergedAt = meta.merged_at || meta.merge_at;
304
+ const mergedInto = meta.merged_into || meta.merge_into || meta.merged_target;
305
+ if (!mergedAt || !mergedInto) {
306
+ return '';
307
+ }
308
+ const date = new Date(mergedAt);
309
+ const dateStr = isNaN(date.valueOf()) ? mergedAt : date.toLocaleDateString();
310
+ return `
311
+ <span class="merge-badge" title="Merged into ${escapeHtml(mergedInto)} on ${escapeHtml(dateStr)}">
312
+ <span class="icon">✅</span>
313
+ <span>merged → ${escapeHtml(mergedInto)}</span>
314
+ </span>
315
+ `;
316
+ })();
317
+
318
+ const stats = feature.kanban_stats;
319
+ const total = stats.total;
320
+ const completed = stats.done;
321
+ const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
322
+
323
+ const artifacts = feature.artifacts;
324
+ const artifactList = [
325
+ {name: 'Constitution', key: 'constitution', icon: '⚖️'},
326
+ {name: 'Specification', key: 'spec', icon: '📄'},
327
+ {name: 'Plan', key: 'plan', icon: '🏗️'},
328
+ {name: 'Tasks', key: 'tasks', icon: '📋'},
329
+ {name: 'Kanban Board', key: 'kanban', icon: '🎯'},
330
+ {name: 'Research', key: 'research', icon: '🔬'},
331
+ {name: 'Quickstart', key: 'quickstart', icon: '🚀'},
332
+ {name: 'Data Model', key: 'data_model', icon: '💾'},
333
+ {name: 'Contracts', key: 'contracts', icon: '📜'},
334
+ {name: 'Checklists', key: 'checklists', icon: '✅'},
335
+ ].map(a => `
336
+ <div style="padding: 10px; background: ${artifacts[a.key]?.exists ? '#ecfdf5' : '#fef2f2'};
337
+ border-radius: 6px; border-left: 3px solid ${artifacts[a.key]?.exists ? '#10b981' : '#ef4444'};">
338
+ ${a.icon} ${a.name}: ${artifacts[a.key]?.exists ? '✅ Available' : '❌ Not created'}
339
+ </div>
340
+ `).join('');
341
+
342
+ document.getElementById('overview-content').innerHTML = `
343
+ <div style="margin-bottom: 30px;">
344
+ <h3>Feature: ${feature.name} ${mergeBadge}</h3>
345
+ <p style="color: #6b7280;">View and track all artifacts for this feature</p>
346
+ </div>
347
+
348
+ <div class="status-summary">
349
+ <div class="status-card total">
350
+ <div class="status-label">Total Tasks</div>
351
+ <div class="status-value">${total}</div>
352
+ <div class="status-detail">${stats.planned} planned</div>
353
+ </div>
354
+ <div class="status-card progress">
355
+ <div class="status-label">In Progress</div>
356
+ <div class="status-value">${stats.doing}</div>
357
+ </div>
358
+ <div class="status-card review">
359
+ <div class="status-label">Review</div>
360
+ <div class="status-value">${stats.for_review}</div>
361
+ </div>
362
+ <div class="status-card completed">
363
+ <div class="status-label">Completed</div>
364
+ <div class="status-value">${completed}</div>
365
+ <div class="status-detail">${completionRate}% done</div>
366
+ <div class="progress-bar">
367
+ <div class="progress-fill" style="width: ${completionRate}%"></div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+
372
+ <h3 style="margin-top: 30px; margin-bottom: 15px; color: #1f2937;">Available Artifacts</h3>
373
+ <div style="display: grid; gap: 10px;">
374
+ ${artifactList}
375
+ </div>
376
+ `;
377
+ }
378
+
379
+ function loadKanban() {
380
+ fetch(`/api/kanban/${currentFeature}`)
381
+ .then(response => response.json())
382
+ .then(data => {
383
+ renderKanban(data && data.lanes ? data.lanes : data);
384
+ })
385
+ .catch(error => {
386
+ document.getElementById('kanban-board').innerHTML =
387
+ '<div class="empty-state">Error loading kanban board</div>';
388
+ });
389
+ }
390
+
391
+ function renderKanban(lanes) {
392
+ const total = lanes.planned.length + lanes.doing.length + lanes.for_review.length + lanes.done.length;
393
+ const completed = lanes.done.length;
394
+ const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
395
+
396
+ const agents = new Set();
397
+ Object.values(lanes).forEach(tasks => {
398
+ tasks.forEach(task => {
399
+ if (task.agent && task.agent !== 'system') agents.add(task.agent);
400
+ });
401
+ });
402
+
403
+ document.getElementById('kanban-status').innerHTML = `
404
+ <div class="status-card total">
405
+ <div class="status-label">Total Work Packages</div>
406
+ <div class="status-value">${total}</div>
407
+ <div class="status-detail">${lanes.planned.length} planned</div>
408
+ </div>
409
+ <div class="status-card progress">
410
+ <div class="status-label">In Progress</div>
411
+ <div class="status-value">${lanes.doing.length}</div>
412
+ </div>
413
+ <div class="status-card review">
414
+ <div class="status-label">Review</div>
415
+ <div class="status-value">${lanes.for_review.length}</div>
416
+ </div>
417
+ <div class="status-card completed">
418
+ <div class="status-label">Completed</div>
419
+ <div class="status-value">${completed}</div>
420
+ <div class="status-detail">${completionRate}% done</div>
421
+ <div class="progress-bar">
422
+ <div class="progress-fill" style="width: ${completionRate}%"></div>
423
+ </div>
424
+ </div>
425
+ <div class="status-card agents">
426
+ <div class="status-label">Active Agents</div>
427
+ <div class="status-value">${agents.size}</div>
428
+ <div class="status-detail">${agents.size > 0 ? Array.from(agents).join(', ') : 'none'}</div>
429
+ </div>
430
+ `;
431
+
432
+ const createCard = (task) => `
433
+ <div class="card" role="button">
434
+ <div class="card-id">${task.id}</div>
435
+ <div class="card-title">${task.title}</div>
436
+ <div class="card-meta">
437
+ ${task.agent ? `<span class="badge agent">${task.agent}</span>` : ''}
438
+ ${task.subtasks && task.subtasks.length > 0 ?
439
+ `<span class="badge subtasks">${task.subtasks.length} subtask${task.subtasks.length !== 1 ? 's' : ''}</span>` : ''}
440
+ </div>
441
+ </div>
442
+ `;
443
+
444
+ document.getElementById('kanban-board').innerHTML = `
445
+ <div class="lane planned">
446
+ <div class="lane-header">
447
+ <span>📋 Planned</span>
448
+ <span class="count">${lanes.planned.length}</span>
449
+ </div>
450
+ <div>${lanes.planned.length === 0 ? '<div class="empty-state">No tasks</div>' : lanes.planned.map(createCard).join('')}</div>
451
+ </div>
452
+ <div class="lane doing">
453
+ <div class="lane-header">
454
+ <span>🚀 Doing</span>
455
+ <span class="count">${lanes.doing.length}</span>
456
+ </div>
457
+ <div>${lanes.doing.length === 0 ? '<div class="empty-state">No tasks</div>' : lanes.doing.map(createCard).join('')}</div>
458
+ </div>
459
+ <div class="lane for_review">
460
+ <div class="lane-header">
461
+ <span>👀 For Review</span>
462
+ <span class="count">${lanes.for_review.length}</span>
463
+ </div>
464
+ <div>${lanes.for_review.length === 0 ? '<div class="empty-state">No tasks</div>' : lanes.for_review.map(createCard).join('')}</div>
465
+ </div>
466
+ <div class="lane done">
467
+ <div class="lane-header">
468
+ <span>✅ Done</span>
469
+ <span class="count">${lanes.done.length}</span>
470
+ </div>
471
+ <div>${lanes.done.length === 0 ? '<div class="empty-state">No tasks</div>' : lanes.done.map(createCard).join('')}</div>
472
+ </div>
473
+ `;
474
+
475
+ ['planned', 'doing', 'for_review', 'done'].forEach(laneName => {
476
+ const laneCards = document.querySelectorAll(`.lane.${laneName} .card`);
477
+ laneCards.forEach((card, index) => {
478
+ const task = lanes[laneName][index];
479
+ if (!task) return;
480
+ if (!card.hasAttribute('tabindex')) {
481
+ card.setAttribute('tabindex', '0');
482
+ }
483
+ card.addEventListener('click', () => showPromptModal(task));
484
+ card.addEventListener('keydown', (event) => {
485
+ if (event.key === 'Enter' || event.key === ' ') {
486
+ event.preventDefault();
487
+ showPromptModal(task);
488
+ }
489
+ });
490
+ });
491
+ });
492
+ }
493
+
494
+ function formatLaneName(lane) {
495
+ if (!lane) return '';
496
+ return lane.split('_').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
497
+ }
498
+
499
+ function showPromptModal(task) {
500
+ const modal = document.getElementById('prompt-modal');
501
+ if (!modal) return;
502
+
503
+ const titleEl = document.getElementById('modal-title');
504
+ const subtitleEl = document.getElementById('modal-subtitle');
505
+ const metaEl = document.getElementById('modal-prompt-meta');
506
+ const contentEl = document.getElementById('modal-prompt-content');
507
+ const modalBody = document.getElementById('modal-body');
508
+
509
+ if (titleEl) {
510
+ titleEl.textContent = task.title || 'Work Package Prompt';
511
+ }
512
+ if (subtitleEl) {
513
+ if (task.id) {
514
+ subtitleEl.textContent = task.id;
515
+ subtitleEl.style.display = 'block';
516
+ } else {
517
+ subtitleEl.textContent = '';
518
+ subtitleEl.style.display = 'none';
519
+ }
520
+ }
521
+
522
+ if (metaEl) {
523
+ const metaItems = [];
524
+ if (task.lane) metaItems.push(`<span>Lane: ${escapeHtml(formatLaneName(task.lane))}</span>`);
525
+ if (task.agent) metaItems.push(`<span>Agent: ${escapeHtml(task.agent)}</span>`);
526
+ if (task.subtasks && task.subtasks.length) {
527
+ metaItems.push(`<span>${task.subtasks.length} subtask${task.subtasks.length !== 1 ? 's' : ''}</span>`);
528
+ }
529
+ if (task.phase) metaItems.push(`<span>Phase: ${escapeHtml(task.phase)}</span>`);
530
+ if (task.prompt_path) metaItems.push(`<span>Source: ${escapeHtml(task.prompt_path)}</span>`);
531
+
532
+ if (metaItems.length > 0) {
533
+ metaEl.innerHTML = metaItems.join('');
534
+ metaEl.style.display = 'flex';
535
+ } else {
536
+ metaEl.innerHTML = '';
537
+ metaEl.style.display = 'none';
538
+ }
539
+ }
540
+
541
+ if (contentEl) {
542
+ if (task.prompt_markdown) {
543
+ contentEl.innerHTML = marked.parse(task.prompt_markdown);
544
+ } else {
545
+ contentEl.innerHTML = '<div class="empty-state">Prompt content unavailable.</div>';
546
+ }
547
+ }
548
+
549
+ if (modalBody) {
550
+ modalBody.scrollTop = 0;
551
+ }
552
+
553
+ modal.classList.remove('hidden');
554
+ modal.classList.add('show');
555
+ modal.setAttribute('aria-hidden', 'false');
556
+ document.body.classList.add('modal-open');
557
+ }
558
+
559
+ function hidePromptModal() {
560
+ const modal = document.getElementById('prompt-modal');
561
+ if (!modal) return;
562
+
563
+ modal.classList.remove('show');
564
+ modal.classList.add('hidden');
565
+ modal.setAttribute('aria-hidden', 'true');
566
+ document.body.classList.remove('modal-open');
567
+ }
568
+
569
+ const modalOverlay = document.querySelector('#prompt-modal .modal-overlay');
570
+ if (modalOverlay) {
571
+ modalOverlay.addEventListener('click', hidePromptModal);
572
+ }
573
+ const modalCloseButton = document.getElementById('modal-close-btn');
574
+ if (modalCloseButton) {
575
+ modalCloseButton.addEventListener('click', hidePromptModal);
576
+ }
577
+ document.addEventListener('keydown', (event) => {
578
+ if (event.key === 'Escape') {
579
+ const modal = document.getElementById('prompt-modal');
580
+ if (modal && modal.classList.contains('show')) {
581
+ hidePromptModal();
582
+ }
583
+ }
584
+ });
585
+
586
+ function loadArtifact(artifactName) {
587
+ const artifactKey = artifactName.replace('-', '_');
588
+ fetch(`/api/artifact/${currentFeature}/${artifactName}`)
589
+ .then(response => response.ok ? response.text() : Promise.reject('Not found'))
590
+ .then(content => {
591
+ // Render markdown to HTML
592
+ const htmlContent = marked.parse(content);
593
+ const container = document.getElementById(`${artifactName}-content`);
594
+ container.innerHTML = htmlContent;
595
+ // Intercept markdown links to route through dashboard
596
+ interceptMarkdownLinks(container);
597
+ })
598
+ .catch(error => {
599
+ document.getElementById(`${artifactName}-content`).innerHTML =
600
+ '<div class="empty-state">Artifact not available</div>';
601
+ });
602
+ }
603
+
604
+ function loadContracts() {
605
+ fetch(`/api/contracts/${currentFeature}`)
606
+ .then(response => response.ok ? response.json() : Promise.reject('Not found'))
607
+ .then(data => {
608
+ if (data.files && data.files.length > 0) {
609
+ renderContractsList(data.files);
610
+ } else {
611
+ document.getElementById('contracts-content').innerHTML =
612
+ '<div class="empty-state">No contracts available. Run /spec-kitty.plan to generate contracts.</div>';
613
+ }
614
+ })
615
+ .catch(error => {
616
+ document.getElementById('contracts-content').innerHTML =
617
+ '<div class="empty-state">Contracts directory not found</div>';
618
+ });
619
+ }
620
+
621
+ function renderContractsList(files) {
622
+ const contractsHtml = files.map((file, idx) => {
623
+ const fileNameEscaped = escapeHtml(file.name);
624
+ const filePathEscaped = escapeHtml(file.path);
625
+ return `
626
+ <div style="margin-bottom: 20px; padding: 15px; background: white; border-radius: 8px; border-left: 4px solid var(--lavender); cursor: pointer;"
627
+ data-filepath="${filePathEscaped}" data-filename="${fileNameEscaped}" class="contract-item">
628
+ <div style="font-weight: 600; color: var(--dark-text); margin-bottom: 5px;">
629
+ ${file.icon} ${fileNameEscaped}
630
+ </div>
631
+ <div style="font-size: 0.85em; color: var(--medium-text);">
632
+ Click to view contract
633
+ </div>
634
+ </div>
635
+ `;
636
+ }).join('');
637
+
638
+ document.getElementById('contracts-content').innerHTML = `
639
+ <p style="margin-bottom: 20px; color: var(--medium-text);">
640
+ API specifications and interface definitions for this feature.
641
+ </p>
642
+ ${contractsHtml}
643
+ `;
644
+
645
+ // Add click handlers
646
+ document.querySelectorAll('.contract-item').forEach(item => {
647
+ item.addEventListener('click', () => {
648
+ loadContractFile(item.dataset.filepath, item.dataset.filename);
649
+ });
650
+ });
651
+ }
652
+
653
+ function loadContractFile(filePath, fileName) {
654
+ fetch(`/api/contracts/${currentFeature}/${encodeURIComponent(filePath)}`)
655
+ .then(response => response.ok ? response.text() : Promise.reject('Not found'))
656
+ .then(content => {
657
+ let htmlContent;
658
+
659
+ // Format JSON files nicely
660
+ if (fileName.endsWith('.json')) {
661
+ try {
662
+ const jsonData = JSON.parse(content);
663
+ const prettyJson = JSON.stringify(jsonData, null, 2);
664
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #dee2e6;"><code style="font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9em; line-height: 1.5; color: #212529;">${escapeHtml(prettyJson)}</code></pre>`;
665
+ } catch (e) {
666
+ // If JSON parsing fails, show as plain text
667
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto;"><code>${escapeHtml(content)}</code></pre>`;
668
+ }
669
+ } else if (fileName.endsWith('.md')) {
670
+ // Render markdown files with proper styling
671
+ const renderedMarkdown = marked.parse(content);
672
+ htmlContent = `<div class="markdown-content" style="line-height: 1.6; font-size: 0.95em;">${renderedMarkdown}</div>`;
673
+ } else if (fileName.endsWith('.csv')) {
674
+ // Render CSV as a table
675
+ htmlContent = renderCSV(content);
676
+ } else if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) {
677
+ // Show YAML files as code blocks
678
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #dee2e6;"><code style="font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9em; line-height: 1.5;">${escapeHtml(content)}</code></pre>`;
679
+ } else {
680
+ // Default: show as code block
681
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto;"><code>${escapeHtml(content)}</code></pre>`;
682
+ }
683
+
684
+ const container = document.getElementById('contracts-content');
685
+ container.innerHTML = `
686
+ <div style="margin-bottom: 20px;">
687
+ <button onclick="loadContracts()"
688
+ style="padding: 8px 16px; background: var(--baby-blue); border: none; border-radius: 6px; cursor: pointer; color: var(--dark-text); font-weight: 500;">
689
+ ← Back to Contracts List
690
+ </button>
691
+ </div>
692
+ <h3 style="color: var(--grassy-green); margin-bottom: 15px;">${escapeHtml(fileName)}</h3>
693
+ ${htmlContent}
694
+ `;
695
+ // Intercept markdown links to route through dashboard
696
+ interceptMarkdownLinks(container, 'contracts/');
697
+ })
698
+ .catch(error => {
699
+ document.getElementById('contracts-content').innerHTML =
700
+ '<div class="empty-state">Error loading contract file</div>';
701
+ });
702
+ }
703
+
704
+ function loadChecklists() {
705
+ fetch(`/api/checklists/${currentFeature}`)
706
+ .then(response => response.ok ? response.json() : Promise.reject('Not found'))
707
+ .then(data => {
708
+ if (data.files && data.files.length > 0) {
709
+ renderChecklistsList(data.files);
710
+ } else {
711
+ document.getElementById('checklists-content').innerHTML =
712
+ '<div class="empty-state">No checklists available.</div>';
713
+ }
714
+ })
715
+ .catch(error => {
716
+ document.getElementById('checklists-content').innerHTML =
717
+ '<div class="empty-state">Checklists directory not found</div>';
718
+ });
719
+ }
720
+
721
+ function renderChecklistsList(files) {
722
+ const checklistsHtml = files.map((file, idx) => {
723
+ const fileNameEscaped = escapeHtml(file.name);
724
+ const filePathEscaped = escapeHtml(file.path);
725
+ return `
726
+ <div style="margin-bottom: 20px; padding: 15px; background: white; border-radius: 8px; border-left: 4px solid var(--lavender); cursor: pointer;"
727
+ data-filepath="${filePathEscaped}" data-filename="${fileNameEscaped}" class="checklist-item">
728
+ <div style="font-weight: 600; color: var(--dark-text); margin-bottom: 5px;">
729
+ ${file.icon} ${fileNameEscaped}
730
+ </div>
731
+ <div style="font-size: 0.85em; color: var(--medium-text);">
732
+ Click to view checklist
733
+ </div>
734
+ </div>
735
+ `;
736
+ }).join('');
737
+
738
+ document.getElementById('checklists-content').innerHTML = `
739
+ <p style="margin-bottom: 20px; color: var(--medium-text);">
740
+ Quality control and validation checklists for this feature.
741
+ </p>
742
+ ${checklistsHtml}
743
+ `;
744
+
745
+ // Add click handlers
746
+ document.querySelectorAll('.checklist-item').forEach(item => {
747
+ item.addEventListener('click', () => {
748
+ loadChecklistFile(item.dataset.filepath, item.dataset.filename);
749
+ });
750
+ });
751
+ }
752
+
753
+ function loadChecklistFile(filePath, fileName) {
754
+ fetch(`/api/checklists/${currentFeature}/${encodeURIComponent(filePath)}`)
755
+ .then(response => response.ok ? response.text() : Promise.reject('Not found'))
756
+ .then(content => {
757
+ let htmlContent;
758
+
759
+ // Format JSON files nicely
760
+ if (fileName.endsWith('.json')) {
761
+ try {
762
+ const jsonData = JSON.parse(content);
763
+ const prettyJson = JSON.stringify(jsonData, null, 2);
764
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #dee2e6;"><code style="font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9em; line-height: 1.5; color: #212529;">${escapeHtml(prettyJson)}</code></pre>`;
765
+ } catch (e) {
766
+ // If JSON parsing fails, show as plain text
767
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto;"><code>${escapeHtml(content)}</code></pre>`;
768
+ }
769
+ } else if (fileName.endsWith('.md')) {
770
+ // Render markdown files with proper styling
771
+ const renderedMarkdown = marked.parse(content);
772
+ htmlContent = `<div class="markdown-content" style="line-height: 1.6; font-size: 0.95em;">${renderedMarkdown}</div>`;
773
+ } else if (fileName.endsWith('.csv')) {
774
+ // Render CSV as a table
775
+ htmlContent = renderCSV(content);
776
+ } else if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) {
777
+ // Show YAML files as code blocks
778
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #dee2e6;"><code style="font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9em; line-height: 1.5;">${escapeHtml(content)}</code></pre>`;
779
+ } else {
780
+ // Default: show as code block
781
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto;"><code>${escapeHtml(content)}</code></pre>`;
782
+ }
783
+
784
+ const container = document.getElementById('checklists-content');
785
+ container.innerHTML = `
786
+ <div style="margin-bottom: 20px;">
787
+ <button onclick="loadChecklists()"
788
+ style="padding: 8px 16px; background: var(--baby-blue); border: none; border-radius: 6px; cursor: pointer; color: var(--dark-text); font-weight: 500;">
789
+ ← Back to Checklists List
790
+ </button>
791
+ </div>
792
+ <h3 style="color: var(--grassy-green); margin-bottom: 15px;">${escapeHtml(fileName)}</h3>
793
+ ${htmlContent}
794
+ `;
795
+ // Intercept markdown links to route through dashboard
796
+ interceptMarkdownLinks(container, 'checklists/');
797
+ })
798
+ .catch(error => {
799
+ document.getElementById('checklists-content').innerHTML =
800
+ '<div class="empty-state">Error loading checklist file</div>';
801
+ });
802
+ }
803
+
804
+ function loadResearch() {
805
+ fetch(`/api/research/${currentFeature}`)
806
+ .then(response => response.ok ? response.json() : Promise.reject('Not found'))
807
+ .then(data => {
808
+ if (data.main_file || (data.artifacts && data.artifacts.length > 0)) {
809
+ renderResearchContent(data);
810
+ } else {
811
+ document.getElementById('research-content').innerHTML =
812
+ '<div class="empty-state">No research artifacts available. Run /spec-kitty.research to create them.</div>';
813
+ }
814
+ })
815
+ .catch(error => {
816
+ document.getElementById('research-content').innerHTML =
817
+ '<div class="empty-state">Research artifacts not found</div>';
818
+ });
819
+ }
820
+
821
+ function renderResearchContent(data) {
822
+ let mainContent = '';
823
+ if (data.main_file) {
824
+ mainContent = `
825
+ <h3 style="color: var(--grassy-green); margin-bottom: 15px;">research.md</h3>
826
+ ${marked.parse(data.main_file)}
827
+ `;
828
+ }
829
+
830
+ let artifactsHtml = '';
831
+ if (data.artifacts && data.artifacts.length > 0) {
832
+ const artifactItems = data.artifacts.map(file => {
833
+ const nameEscaped = escapeHtml(file.name);
834
+ const pathEscaped = escapeHtml(file.path);
835
+ return `
836
+ <div style="padding: 12px; background: white; border-radius: 8px; border-left: 4px solid var(--soft-peach); cursor: pointer;"
837
+ data-filepath="${pathEscaped}" data-filename="${nameEscaped}" class="research-artifact-item">
838
+ <div style="font-weight: 600; color: var(--dark-text); margin-bottom: 3px;">
839
+ ${file.icon} ${nameEscaped}
840
+ </div>
841
+ <div style="font-size: 0.75em; color: var(--medium-text); font-family: monospace;">
842
+ ${pathEscaped}
843
+ </div>
844
+ </div>
845
+ `;
846
+ }).join('');
847
+
848
+ artifactsHtml = `
849
+ <h3 style="color: var(--grassy-green); margin-top: 30px; margin-bottom: 15px;">
850
+ Research Artifacts
851
+ </h3>
852
+ <div style="display: grid; gap: 10px;">
853
+ ${artifactItems}
854
+ </div>
855
+ `;
856
+ }
857
+
858
+ document.getElementById('research-content').innerHTML = mainContent + artifactsHtml;
859
+
860
+ // Intercept markdown links to route through dashboard
861
+ interceptMarkdownLinks(document.getElementById('research-content'));
862
+
863
+ // Add click handlers
864
+ document.querySelectorAll('.research-artifact-item').forEach(item => {
865
+ item.addEventListener('click', () => {
866
+ loadResearchFile(item.dataset.filepath, item.dataset.filename);
867
+ });
868
+ });
869
+ }
870
+
871
+ function loadResearchFile(filePath, fileName) {
872
+ fetch(`/api/research/${currentFeature}/${encodeURIComponent(filePath)}`)
873
+ .then(response => response.ok ? response.text() : Promise.reject('Not found'))
874
+ .then(content => {
875
+ let htmlContent;
876
+
877
+ if (filePath.endsWith('.md')) {
878
+ // Render markdown files with proper styling
879
+ const renderedMarkdown = marked.parse(content);
880
+ htmlContent = `<div class="markdown-content" style="line-height: 1.6; font-size: 0.95em;">${renderedMarkdown}</div>`;
881
+ } else if (filePath.endsWith('.csv')) {
882
+ // Render CSV as a table
883
+ htmlContent = renderCSV(content);
884
+ } else if (filePath.endsWith('.json')) {
885
+ // Format JSON files nicely
886
+ try {
887
+ const jsonData = JSON.parse(content);
888
+ const prettyJson = JSON.stringify(jsonData, null, 2);
889
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #dee2e6;"><code style="font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9em; line-height: 1.5; color: #212529;">${escapeHtml(prettyJson)}</code></pre>`;
890
+ } catch (e) {
891
+ // If JSON parsing fails, show as plain text
892
+ htmlContent = `<pre style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto;"><code>${escapeHtml(content)}</code></pre>`;
893
+ }
894
+ } else {
895
+ // Default: show as code block
896
+ htmlContent = `<pre style="background: white; padding: 20px; border-radius: 8px; overflow-x: auto;">${escapeHtml(content)}</pre>`;
897
+ }
898
+
899
+ const container = document.getElementById('research-content');
900
+ container.innerHTML = `
901
+ <div style="margin-bottom: 20px;">
902
+ <button onclick="loadResearch()"
903
+ style="padding: 8px 16px; background: var(--baby-blue); border: none; border-radius: 6px; cursor: pointer; color: var(--dark-text); font-weight: 500;">
904
+ ← Back to Research
905
+ </button>
906
+ </div>
907
+ <h3 style="color: var(--grassy-green); margin-bottom: 15px;">${escapeHtml(fileName)}</h3>
908
+ ${htmlContent}
909
+ `;
910
+ // Intercept markdown links to route through dashboard
911
+ interceptMarkdownLinks(container, 'research/');
912
+ })
913
+ .catch(error => {
914
+ document.getElementById('research-content').innerHTML =
915
+ '<div class="empty-state">Error loading research file</div>';
916
+ });
917
+ }
918
+
919
+ function renderCSV(csvContent) {
920
+ const lines = csvContent.trim().split('\n');
921
+ if (lines.length === 0) return '<div class="empty-state">Empty CSV file</div>';
922
+
923
+ const rows = lines.map(line => {
924
+ // Improved CSV parsing that handles quoted fields
925
+ const cells = [];
926
+ let current = '';
927
+ let inQuotes = false;
928
+
929
+ for (let i = 0; i < line.length; i++) {
930
+ const char = line[i];
931
+ const nextChar = line[i + 1];
932
+
933
+ if (char === '"') {
934
+ if (inQuotes && nextChar === '"') {
935
+ // Escaped quote
936
+ current += '"';
937
+ i++; // Skip next quote
938
+ } else {
939
+ // Toggle quote mode
940
+ inQuotes = !inQuotes;
941
+ }
942
+ } else if (char === ',' && !inQuotes) {
943
+ // End of field
944
+ cells.push(current.trim());
945
+ current = '';
946
+ } else {
947
+ current += char;
948
+ }
949
+ }
950
+ // Don't forget the last field
951
+ cells.push(current.trim());
952
+
953
+ return cells;
954
+ });
955
+
956
+ const headerRow = rows[0];
957
+ const dataRows = rows.slice(1);
958
+
959
+ return `
960
+ <div style="overflow-x: auto; margin: 20px 0;">
961
+ <table style="width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
962
+ <thead>
963
+ <tr style="background: var(--baby-blue);">
964
+ ${headerRow.map(header => `<th style="padding: 12px; text-align: left; font-weight: 600; color: var(--dark-text); border-bottom: 2px solid var(--lavender);">${escapeHtml(header)}</th>`).join('')}
965
+ </tr>
966
+ </thead>
967
+ <tbody>
968
+ ${dataRows.map((row, idx) => `
969
+ <tr style="border-top: 1px solid #e5e7eb; ${idx % 2 === 0 ? 'background: #fafbfc;' : 'background: white;'} transition: background 0.2s;"
970
+ onmouseover="this.style.background='#f0f4f8'"
971
+ onmouseout="this.style.background='${idx % 2 === 0 ? '#fafbfc' : 'white'}'">
972
+ ${row.map(cell => `<td style="padding: 10px; color: var(--medium-text);">${escapeHtml(cell)}</td>`).join('')}
973
+ </tr>
974
+ `).join('')}
975
+ </tbody>
976
+ </table>
977
+ ${dataRows.length === 0 ? '<div style="text-align: center; padding: 20px; color: var(--medium-text);">No data rows in CSV</div>' : ''}
978
+ </div>
979
+ `;
980
+ }
981
+
982
+ function escapeHtml(text) {
983
+ const div = document.createElement('div');
984
+ div.textContent = text;
985
+ return div.innerHTML;
986
+ }
987
+
988
+ function showConstitution() {
989
+ if (!isConstitutionView && currentPage !== 'constitution') {
990
+ lastNonConstitutionPage = currentPage;
991
+ }
992
+ // Switch to constitution page
993
+ currentPage = 'constitution';
994
+ isConstitutionView = true;
995
+ saveState(currentFeature, 'constitution');
996
+ document.querySelectorAll('.sidebar-item').forEach(item => item.classList.remove('active'));
997
+ const constitutionItem = document.querySelector('.sidebar-item[data-page="constitution"]');
998
+ if (constitutionItem) {
999
+ constitutionItem.classList.remove('disabled');
1000
+ constitutionItem.classList.add('active');
1001
+ }
1002
+ document.querySelectorAll('.page').forEach(page => page.classList.remove('active'));
1003
+ document.getElementById('page-constitution').classList.add('active');
1004
+
1005
+ // Load constitution
1006
+ fetch('/api/constitution')
1007
+ .then(response => response.ok ? response.text() : Promise.reject('Not found'))
1008
+ .then(content => {
1009
+ const htmlContent = marked.parse(content);
1010
+ const container = document.getElementById('constitution-content');
1011
+ container.innerHTML = htmlContent;
1012
+ // Intercept markdown links to route through dashboard
1013
+ interceptMarkdownLinks(container);
1014
+ })
1015
+ .catch(error => {
1016
+ document.getElementById('constitution-content').innerHTML =
1017
+ '<div class="empty-state">Constitution not found. Run /spec-kitty.constitution to create it.</div>';
1018
+ });
1019
+ }
1020
+
1021
+ function updateWorkflowIcons(workflow) {
1022
+ const iconMap = {
1023
+ 'complete': '✅',
1024
+ 'in_progress': '🔄',
1025
+ 'pending': '⏳'
1026
+ };
1027
+
1028
+ document.getElementById('icon-specify').textContent = iconMap[workflow.specify] || '⏳';
1029
+ document.getElementById('icon-plan').textContent = iconMap[workflow.plan] || '⏳';
1030
+ document.getElementById('icon-tasks').textContent = iconMap[workflow.tasks] || '⏳';
1031
+ document.getElementById('icon-implement').textContent = iconMap[workflow.implement] || '⏳';
1032
+ }
1033
+
1034
+ function updateFeatureList(features) {
1035
+ allFeatures = features;
1036
+ const selectContainer = document.getElementById('feature-selector-container');
1037
+ const select = document.getElementById('feature-select');
1038
+ const singleFeatureName = document.getElementById('single-feature-name');
1039
+ const sidebar = document.querySelector('.sidebar');
1040
+ const mainContent = document.querySelector('.main-content');
1041
+
1042
+ // Restore saved state from cookies on initial load
1043
+ const savedState = restoreState();
1044
+
1045
+ if (select && !select.dataset.pauseHandlersAttached) {
1046
+ const activate = () => setFeatureSelectActive(true);
1047
+ const deactivate = () => setFeatureSelectActive(false);
1048
+ ['focus', 'mousedown', 'keydown', 'click', 'input'].forEach(evt => {
1049
+ select.addEventListener(evt, activate);
1050
+ });
1051
+ ['change', 'blur'].forEach(evt => {
1052
+ select.addEventListener(evt, deactivate);
1053
+ });
1054
+ select.dataset.pauseHandlersAttached = 'true';
1055
+ }
1056
+
1057
+ // Handle 0 features - show welcome page
1058
+ if (features.length === 0) {
1059
+ selectContainer.style.display = 'none';
1060
+ singleFeatureName.style.display = 'none';
1061
+ sidebar.style.display = 'block';
1062
+ mainContent.style.display = 'block';
1063
+ isConstitutionView = false;
1064
+ currentFeature = null;
1065
+ computeFeatureWorktreeStatus(null);
1066
+ setFeatureSelectActive(false);
1067
+
1068
+ // Show welcome page
1069
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
1070
+ document.getElementById('page-welcome').classList.add('active');
1071
+ currentPage = 'welcome';
1072
+
1073
+ // Disable all sidebar items except constitution link
1074
+ document.querySelectorAll('.sidebar-item').forEach(item => {
1075
+ if (item.dataset.page === 'constitution') {
1076
+ item.classList.remove('disabled');
1077
+ } else {
1078
+ item.classList.add('disabled');
1079
+ }
1080
+ });
1081
+ return;
1082
+ }
1083
+
1084
+ // Handle 1 feature - show name directly (no dropdown)
1085
+ if (features.length === 1) {
1086
+ selectContainer.style.display = 'none';
1087
+ singleFeatureName.style.display = 'block';
1088
+ singleFeatureName.textContent = `Feature: ${features[0].name}`;
1089
+ currentFeature = features[0].id;
1090
+ setFeatureSelectActive(false);
1091
+ } else {
1092
+ // Handle multiple features - show dropdown
1093
+ selectContainer.style.display = 'block';
1094
+ singleFeatureName.style.display = 'none';
1095
+
1096
+ // Try to restore saved feature, fall back to first feature
1097
+ const savedFeatureExists = savedState.feature && features.find(f => f.id === savedState.feature);
1098
+ if (!currentFeature || !features.find(f => f.id === currentFeature)) {
1099
+ currentFeature = savedFeatureExists ? savedState.feature : features[0].id;
1100
+ }
1101
+
1102
+ select.innerHTML = features.map(f =>
1103
+ `<option value="${f.id}" ${f.id === currentFeature ? 'selected' : ''}>${f.name}</option>`
1104
+ ).join('');
1105
+ select.value = currentFeature;
1106
+ }
1107
+
1108
+ // Restore saved page if it's valid for the current feature
1109
+ const feature = features.find(f => f.id === currentFeature);
1110
+ if (savedState.page && savedState.page !== 'overview') {
1111
+ if (savedState.page === 'constitution') {
1112
+ // Will be handled by showConstitution() call below
1113
+ currentPage = savedState.page;
1114
+ } else if (savedState.page === 'kanban' && feature && feature.artifacts && feature.artifacts.kanban?.exists) {
1115
+ currentPage = savedState.page;
1116
+ } else if (feature && feature.artifacts) {
1117
+ const artifactKey = savedState.page.replace('-', '_');
1118
+ if (feature.artifacts[artifactKey]?.exists || savedState.page === 'overview') {
1119
+ currentPage = savedState.page;
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ sidebar.style.display = 'block';
1125
+ mainContent.style.display = 'block';
1126
+
1127
+ // Update workflow icons based on current feature
1128
+ if (feature && feature.workflow) {
1129
+ updateWorkflowIcons(feature.workflow);
1130
+ computeFeatureWorktreeStatus(feature);
1131
+ } else {
1132
+ computeFeatureWorktreeStatus(null);
1133
+ }
1134
+
1135
+ updateSidebarState();
1136
+
1137
+ // Restore the page view
1138
+ if (currentPage === 'constitution') {
1139
+ showConstitution();
1140
+ } else {
1141
+ isConstitutionView = false;
1142
+ // Update sidebar highlighting
1143
+ document.querySelectorAll('.sidebar-item').forEach(item => {
1144
+ if (item.dataset.page === currentPage) {
1145
+ item.classList.add('active');
1146
+ } else {
1147
+ item.classList.remove('active');
1148
+ }
1149
+ });
1150
+ // Update page visibility
1151
+ document.querySelectorAll('.page').forEach(page => page.classList.remove('active'));
1152
+ const activePageEl = document.getElementById(`page-${currentPage}`);
1153
+ if (activePageEl) {
1154
+ activePageEl.classList.add('active');
1155
+ }
1156
+ loadCurrentPage();
1157
+ }
1158
+ }
1159
+
1160
+ function updateFeatureListSilent(features) {
1161
+ // Same as updateFeatureList but doesn't reload the current page
1162
+ // Used during polling to avoid resetting user's view
1163
+ const oldFeature = allFeatures.find(f => f.id === currentFeature);
1164
+ allFeatures = features;
1165
+ const feature = features.find(f => f.id === currentFeature);
1166
+
1167
+ if (feature && feature.workflow) {
1168
+ updateWorkflowIcons(feature.workflow);
1169
+ computeFeatureWorktreeStatus(feature);
1170
+ } else {
1171
+ computeFeatureWorktreeStatus(null);
1172
+ }
1173
+ updateSidebarState();
1174
+
1175
+ // Detect artifact changes and reload overview if artifacts changed
1176
+ if (currentPage === 'overview' && oldFeature && feature) {
1177
+ const oldArtifacts = JSON.stringify(oldFeature.artifacts);
1178
+ const newArtifacts = JSON.stringify(feature.artifacts);
1179
+ if (oldArtifacts !== newArtifacts) {
1180
+ loadOverview();
1181
+ }
1182
+ }
1183
+ }
1184
+
1185
+ function fetchData(isInitialLoad = false) {
1186
+ if (featureSelectActive && !isInitialLoad) {
1187
+ return;
1188
+ }
1189
+ fetch('/api/features')
1190
+ .then(response => response.json())
1191
+ .then(data => {
1192
+ // Use full update on initial load, silent update on polls
1193
+ if (isInitialLoad) {
1194
+ updateFeatureList(data.features);
1195
+ } else {
1196
+ updateFeatureListSilent(data.features);
1197
+
1198
+ // Refresh kanban board if currently viewing it
1199
+ if (currentPage === 'kanban' && !isConstitutionView && currentFeature) {
1200
+ loadKanban();
1201
+ }
1202
+ }
1203
+
1204
+ document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
1205
+
1206
+ if (data.project_path) {
1207
+ projectPathDisplay = data.project_path;
1208
+ }
1209
+
1210
+ if (data.active_worktree) {
1211
+ activeWorktreeDisplay = data.active_worktree;
1212
+ } else {
1213
+ activeWorktreeDisplay = '';
1214
+ }
1215
+
1216
+ if (data.active_mission) {
1217
+ updateMissionDisplay(data.active_mission);
1218
+ }
1219
+
1220
+ const currentFeatureObj = allFeatures.find(f => f.id === currentFeature);
1221
+ computeFeatureWorktreeStatus(currentFeatureObj || null);
1222
+ updateTreeInfo();
1223
+ })
1224
+ .catch(error => console.error('Error fetching data:', error));
1225
+ }
1226
+
1227
+ // Initial fetch
1228
+ // Diagnostics functions
1229
+ function showDiagnostics() {
1230
+ if (!isConstitutionView && currentPage !== 'diagnostics') {
1231
+ lastNonConstitutionPage = currentPage;
1232
+ }
1233
+ // Switch to diagnostics page
1234
+ currentPage = 'diagnostics';
1235
+ isConstitutionView = false;
1236
+ saveState(currentFeature, 'diagnostics');
1237
+
1238
+ // Update sidebar - consistent with other pages
1239
+ document.querySelectorAll('.sidebar-item').forEach(item => {
1240
+ if (item.dataset.page === 'diagnostics') {
1241
+ item.classList.add('active');
1242
+ } else {
1243
+ item.classList.remove('active');
1244
+ }
1245
+ });
1246
+
1247
+ // Update pages
1248
+ document.querySelectorAll('.page').forEach(page => page.classList.remove('active'));
1249
+ const diagnosticsPage = document.getElementById('page-diagnostics');
1250
+ if (diagnosticsPage) {
1251
+ diagnosticsPage.classList.add('active');
1252
+ }
1253
+
1254
+ loadDiagnostics();
1255
+ }
1256
+
1257
+ function loadDiagnostics() {
1258
+ // Show loading state
1259
+ document.getElementById('diagnostics-loading').style.display = 'block';
1260
+ document.getElementById('diagnostics-content').style.display = 'none';
1261
+ document.getElementById('diagnostics-error').style.display = 'none';
1262
+
1263
+ fetch('/api/diagnostics')
1264
+ .then(response => response.json())
1265
+ .then(data => {
1266
+ displayDiagnostics(data);
1267
+ })
1268
+ .catch(error => {
1269
+ document.getElementById('diagnostics-loading').style.display = 'none';
1270
+ document.getElementById('diagnostics-error').style.display = 'block';
1271
+ document.getElementById('diagnostics-error-message').textContent = error.toString();
1272
+ });
1273
+ }
1274
+
1275
+ function displayDiagnostics(data) {
1276
+ document.getElementById('diagnostics-loading').style.display = 'none';
1277
+ document.getElementById('diagnostics-content').style.display = 'block';
1278
+
1279
+ // Display environment status
1280
+ const statusHtml = `
1281
+ <h3>Environment</h3>
1282
+ <div><strong>Working Directory:</strong> ${data.current_working_directory || '(not available)'}</div>
1283
+ <div><strong>Repository Root:</strong> ${data.project_path || '(not available)'}</div>
1284
+ <div><strong>Git Branch:</strong> ${data.git_branch || 'Not detected'}</div>
1285
+ <div><strong>In Worktree:</strong> ${data.in_worktree ? '✅ Yes' : '❌ No'}</div>
1286
+ <div><strong>Active Mission:</strong> ${data.active_mission || 'software-dev'}</div>
1287
+ `;
1288
+ document.getElementById('diagnostics-status').innerHTML = statusHtml;
1289
+
1290
+ // Display file integrity
1291
+ if (data.file_integrity) {
1292
+ const integrityHtml = `
1293
+ <h3>Mission File Integrity</h3>
1294
+ <div><strong>Expected Files:</strong> ${data.file_integrity.total_expected}</div>
1295
+ <div><strong>Present Files:</strong> ${data.file_integrity.total_present}</div>
1296
+ <div><strong>Missing Files:</strong> ${data.file_integrity.total_missing}</div>
1297
+ ${data.file_integrity.missing_files && data.file_integrity.missing_files.length > 0 ?
1298
+ `<div style="margin-top: 10px;"><strong>Missing:</strong><br>${data.file_integrity.missing_files.slice(0, 5).map(f => `• ${f}`).join('<br>')}</div>` : ''}
1299
+ `;
1300
+ const integrityDiv = document.createElement('div');
1301
+ integrityDiv.innerHTML = integrityHtml;
1302
+ integrityDiv.style.marginTop = '20px';
1303
+ document.getElementById('diagnostics-status').appendChild(integrityDiv);
1304
+ }
1305
+
1306
+ // Display worktree overview
1307
+ if (data.worktree_overview) {
1308
+ const overviewHtml = `
1309
+ <h3>Worktree Overview</h3>
1310
+ <div><strong>Total Features:</strong> ${data.worktree_overview.total_features}</div>
1311
+ <div><strong>Active Worktrees:</strong> ${data.worktree_overview.active_worktrees}</div>
1312
+ <div><strong>Merged Features:</strong> ${data.worktree_overview.merged_features}</div>
1313
+ <div><strong>In Development:</strong> ${data.worktree_overview.in_development}</div>
1314
+ <div><strong>Not Started:</strong> ${data.worktree_overview.not_started}</div>
1315
+ `;
1316
+ const overviewDiv = document.createElement('div');
1317
+ overviewDiv.innerHTML = overviewHtml;
1318
+ overviewDiv.style.marginTop = '20px';
1319
+ document.getElementById('diagnostics-status').appendChild(overviewDiv);
1320
+ }
1321
+
1322
+ // Display current feature
1323
+ if (data.current_feature && data.current_feature.detected) {
1324
+ const stateMap = {
1325
+ 'merged': '✅ MERGED',
1326
+ 'in_development': '🔄 IN DEVELOPMENT',
1327
+ 'ready_to_merge': '🔵 READY TO MERGE',
1328
+ 'not_started': '⏳ NOT STARTED',
1329
+ 'unknown': '❓ UNKNOWN'
1330
+ };
1331
+ const currentHtml = `
1332
+ <h3>Current Feature</h3>
1333
+ <div><strong>Feature:</strong> ${data.current_feature.name}</div>
1334
+ <div><strong>State:</strong> ${stateMap[data.current_feature.state] || data.current_feature.state}</div>
1335
+ <div><strong>Branch Exists:</strong> ${data.current_feature.branch_exists ? '✅' : '❌'}</div>
1336
+ <div><strong>Worktree Exists:</strong> ${data.current_feature.worktree_exists ? '✅' : '❌'}</div>
1337
+ ${data.current_feature.worktree_path ? `<div><strong>Worktree Path:</strong> ${data.current_feature.worktree_path}</div>` : ''}
1338
+ ${data.current_feature.artifacts_in_main && data.current_feature.artifacts_in_main.length > 0 ?
1339
+ `<div><strong>Artifacts in Main:</strong> ${data.current_feature.artifacts_in_main.join(', ')}</div>` : ''}
1340
+ ${data.current_feature.artifacts_in_worktree && data.current_feature.artifacts_in_worktree.length > 0 ?
1341
+ `<div><strong>Artifacts in Worktree:</strong> ${data.current_feature.artifacts_in_worktree.join(', ')}</div>` : ''}
1342
+ `;
1343
+ const currentDiv = document.createElement('div');
1344
+ currentDiv.innerHTML = currentHtml;
1345
+ currentDiv.style.marginTop = '20px';
1346
+ document.getElementById('diagnostics-status').appendChild(currentDiv);
1347
+ }
1348
+
1349
+ // Display all features table
1350
+ if (data.all_features && data.all_features.length > 0) {
1351
+ const tableHtml = `
1352
+ <h3>All Features Status</h3>
1353
+ <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
1354
+ <thead>
1355
+ <tr style="background: #f0f0f0;">
1356
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Feature</th>
1357
+ <th style="padding: 8px; text-align: left; border: 1px solid #ddd;">State</th>
1358
+ <th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Branch</th>
1359
+ <th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Worktree</th>
1360
+ <th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Artifacts</th>
1361
+ </tr>
1362
+ </thead>
1363
+ <tbody>
1364
+ ${data.all_features.slice(0, 10).map(feature => {
1365
+ const stateDisplay = {
1366
+ 'merged': '<span style="color: green;">MERGED</span>',
1367
+ 'in_development': '<span style="color: orange;">ACTIVE</span>',
1368
+ 'ready_to_merge': '<span style="color: blue;">READY</span>',
1369
+ 'not_started': '<span style="color: gray;">NOT STARTED</span>',
1370
+ 'unknown': '<span style="color: gray;">?</span>'
1371
+ }[feature.state] || feature.state;
1372
+
1373
+ const branchDisplay = feature.branch_merged ? 'merged' : (feature.branch_exists ? '✓' : '-');
1374
+ const worktreeDisplay = feature.worktree_exists ? '✓' : '-';
1375
+ const artifactCount = (feature.artifacts_in_main || []).length + (feature.artifacts_in_worktree || []).length;
1376
+ const artifactsDisplay = artifactCount > 0 ? artifactCount : '-';
1377
+
1378
+ return `
1379
+ <tr>
1380
+ <td style="padding: 8px; border: 1px solid #ddd;">${feature.name}</td>
1381
+ <td style="padding: 8px; border: 1px solid #ddd;">${stateDisplay}</td>
1382
+ <td style="padding: 8px; text-align: center; border: 1px solid #ddd;">${branchDisplay}</td>
1383
+ <td style="padding: 8px; text-align: center; border: 1px solid #ddd;">${worktreeDisplay}</td>
1384
+ <td style="padding: 8px; text-align: center; border: 1px solid #ddd;">${artifactsDisplay}</td>
1385
+ </tr>
1386
+ `;
1387
+ }).join('')}
1388
+ </tbody>
1389
+ </table>
1390
+ ${data.all_features.length > 10 ? `<div style="margin-top: 10px; color: #666;">... and ${data.all_features.length - 10} more features</div>` : ''}
1391
+ `;
1392
+ const tableDiv = document.createElement('div');
1393
+ tableDiv.innerHTML = tableHtml;
1394
+ tableDiv.style.marginTop = '20px';
1395
+ document.getElementById('diagnostics-status').appendChild(tableDiv);
1396
+ }
1397
+
1398
+ // Display observations (not prescriptive recommendations)
1399
+ if (data.observations && data.observations.length > 0) {
1400
+ document.getElementById('diagnostics-issues').style.display = 'block';
1401
+ document.querySelector('#diagnostics-issues h3').textContent = 'Observations';
1402
+ const obsHtml = data.observations.map(obs => `<div>• ${obs}</div>`).join('');
1403
+ document.getElementById('diagnostics-issues-content').innerHTML = obsHtml;
1404
+ } else {
1405
+ document.getElementById('diagnostics-issues').style.display = 'none';
1406
+ }
1407
+
1408
+ // Hide recommendations section since we're being observational
1409
+ const recsSection = document.getElementById('diagnostics-recommendations');
1410
+ if (recsSection) {
1411
+ recsSection.style.display = 'none';
1412
+ }
1413
+ }
1414
+
1415
+ function refreshDiagnostics() {
1416
+ loadDiagnostics();
1417
+ }
1418
+
1419
+ updateMissionDisplay();
1420
+ updateTreeInfo();
1421
+ fetchData(true); // Pass true for initial load
1422
+
1423
+ // Poll every second
1424
+ setInterval(fetchData, 1000);