iikit-dashboard 1.0.0 → 1.1.0

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.
package/README.md CHANGED
@@ -2,27 +2,21 @@
2
2
 
3
3
  **Watch your AI agent develop features in real time.**
4
4
 
5
- A browser-based dashboard for [Intent Integrity Kit](https://github.com/intent-integrity-chain/kit) projects. Visualizes every phase of the IIKit workflow — from constitution principles through specification, planning, and implementation with live updates as artifacts change on disk.
5
+ A browser-based dashboard for [Intent Integrity Kit (IIKit)](https://github.com/intent-integrity-chain/kit) projects. IIKit is a specification-driven development framework that guides AI agents through a structured workflow — from governance constitution through specification, clarification, planning, testing, and implementation. The dashboard visualizes every phase of that workflow with live updates as artifacts change on disk.
6
6
 
7
7
  ## Usage
8
8
 
9
- ```bash
10
- # Run in your IIKit project directory
11
- npx iikit-dashboard
12
-
13
- # Or specify a project path
14
- npx iikit-dashboard /path/to/your/project
15
- ```
16
-
17
- The dashboard opens at `http://localhost:3000`.
9
+ The dashboard launches automatically early in the IIKit workflow — no manual setup needed.
18
10
 
19
- ## Setup
11
+ You can also start it standalone to browse historical data for any project that has feature specs:
20
12
 
21
13
  ```bash
22
- npm install
23
- tessl install # installs tile dependencies (like npm install for tiles)
14
+ npx iikit-dashboard # current directory
15
+ npx iikit-dashboard /path/to/project # specific project
24
16
  ```
25
17
 
18
+ The dashboard opens at `http://localhost:3000`.
19
+
26
20
  ## Views
27
21
 
28
22
  The pipeline bar at the top shows all nine IIKit workflow phases. Click any phase to see its visualization:
@@ -33,7 +27,7 @@ The pipeline bar at the top shows all nine IIKit workflow phases. Click any phas
33
27
  | **Spec** | Story map with swim lanes by priority + interactive requirements graph (US / FR / SC nodes and edges) |
34
28
  | **Clarify** | Q&A trail from clarification sessions, with clickable spec-item references that navigate back to the Spec view |
35
29
  | **Plan** | Tech stack badge wall, interactive file-structure tree (existing vs. planned files), rendered architecture diagram, and Tessl tile cards |
36
- | **Checklist** | *Coming soon* |
30
+ | **Checklist** | Progress rings per checklist file with color coding (red/yellow/green), gate traffic light (OPEN/BLOCKED), and accordion detail view with CHK IDs and tag badges |
37
31
  | **Testify** | *Coming soon* |
38
32
  | **Tasks** | *Coming soon* |
39
33
  | **Analyze** | *Coming soon* |
@@ -58,19 +52,16 @@ The server reads directly from your project's `specs/` directory:
58
52
  | `spec.md` | User stories, requirements, success criteria, and clarification Q&A |
59
53
  | `plan.md` | Tech stack, file structure, and architecture diagram |
60
54
  | `tasks.md` | Task checkboxes grouped by `[US1]`, `[US2]` tags |
55
+ | `checklists/*.md` | Checklist items with completion status, CHK IDs, and category groupings |
61
56
  | `CONSTITUTION.md` | Governance principles and obligation levels |
62
57
  | `tessl.json` | Installed Tessl tiles for the dependency panel |
63
58
 
64
59
  A file watcher (chokidar) detects changes and pushes updates to the browser via WebSocket with 300 ms debounce.
65
60
 
66
- ## Integration with IIKit
67
-
68
- When you run `/iikit-08-implement`, the implement skill automatically launches the dashboard in the background. No manual setup needed.
69
-
70
61
  ## Requirements
71
62
 
72
63
  - Node.js 18+
73
- - An IIKit project with a `specs/` directory
64
+ - A project with a `specs/` directory containing IIKit feature artifacts
74
65
 
75
66
  ## License
76
67
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iikit-dashboard",
3
- "version": "1.0.0",
4
- "description": "IIKit Dashboard — real-time visualization for Intent Integrity Kit projects",
3
+ "version": "1.1.0",
4
+ "description": "Real-time dashboard for Intent Integrity Kit (IIKit) — visualizes every phase of specification-driven AI development",
5
5
  "main": "src/server.js",
6
6
  "bin": {
7
7
  "iikit-dashboard": "bin/iikit-dashboard.js"
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { parseChecklistsDetailed } = require('./parser');
5
+
6
+ /**
7
+ * Map a percentage (0-100) to a color bracket.
8
+ * @param {number} percentage
9
+ * @returns {string} "red" | "yellow" | "green"
10
+ */
11
+ function percentageToColor(percentage) {
12
+ if (percentage <= 33) return 'red';
13
+ if (percentage <= 66) return 'yellow';
14
+ return 'green';
15
+ }
16
+
17
+ /**
18
+ * Compute gate status from an array of file objects with percentage fields.
19
+ * Uses worst-case precedence: red if any at 0%, yellow if all 1-99%, green if all 100%.
20
+ *
21
+ * @param {Array<{percentage: number}>} files
22
+ * @returns {{status: string, level: string, label: string}}
23
+ */
24
+ function computeGateStatus(files) {
25
+ if (files.length === 0) {
26
+ return { status: 'blocked', level: 'red', label: 'GATE: BLOCKED' };
27
+ }
28
+
29
+ const anyAtZero = files.some(f => f.percentage === 0);
30
+ if (anyAtZero) {
31
+ return { status: 'blocked', level: 'red', label: 'GATE: BLOCKED' };
32
+ }
33
+
34
+ const allComplete = files.every(f => f.percentage === 100);
35
+ if (allComplete) {
36
+ return { status: 'open', level: 'green', label: 'GATE: OPEN' };
37
+ }
38
+
39
+ return { status: 'blocked', level: 'yellow', label: 'GATE: BLOCKED' };
40
+ }
41
+
42
+ /**
43
+ * Compute checklist view state for a feature.
44
+ * Returns per-file detail with items, percentage, color, and aggregate gate status.
45
+ *
46
+ * @param {string} projectPath - Path to the project root
47
+ * @param {string} featureId - Feature directory name (e.g., "001-kanban-board")
48
+ * @returns {{files: Array, gate: {status: string, level: string, label: string}}}
49
+ */
50
+ function computeChecklistViewState(projectPath, featureId) {
51
+ const checklistDir = path.join(projectPath, 'specs', featureId, 'checklists');
52
+ const parsed = parseChecklistsDetailed(checklistDir);
53
+
54
+ const files = parsed.map(file => {
55
+ const percentage = file.total > 0 ? Math.round((file.checked / file.total) * 100) : 0;
56
+ return {
57
+ ...file,
58
+ percentage,
59
+ color: percentageToColor(percentage)
60
+ };
61
+ });
62
+
63
+ const gate = computeGateStatus(files);
64
+
65
+ return { files, gate };
66
+ }
67
+
68
+ module.exports = { computeChecklistViewState };
package/src/parser.js CHANGED
@@ -115,6 +115,96 @@ function parseChecklists(checklistDir) {
115
115
  return result;
116
116
  }
117
117
 
118
+ /**
119
+ * Parse all checklist files in a directory and return detailed per-file data
120
+ * with individual items, categories, CHK IDs, and tags.
121
+ *
122
+ * Applies same requirements.md-only filter as parseChecklists:
123
+ * if requirements.md is the only file, returns empty array.
124
+ *
125
+ * @param {string} checklistDir - Path to checklists/ directory
126
+ * @returns {Array<{name: string, filename: string, total: number, checked: number, items: Array}>}
127
+ */
128
+ function parseChecklistsDetailed(checklistDir) {
129
+ if (!fs.existsSync(checklistDir)) return [];
130
+
131
+ const files = fs.readdirSync(checklistDir).filter(f => f.endsWith('.md'));
132
+
133
+ // Same filter as parseChecklists: skip if requirements.md is the only file
134
+ const hasDomainChecklists = files.some(f => f !== 'requirements.md');
135
+ if (!hasDomainChecklists) return [];
136
+
137
+ const result = [];
138
+
139
+ for (const file of files) {
140
+ const content = fs.readFileSync(path.join(checklistDir, file), 'utf-8');
141
+ const lines = content.split('\n');
142
+
143
+ // Derive human-readable name from filename
144
+ const baseName = file.replace(/\.md$/, '');
145
+ const name = baseName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
146
+
147
+ const items = [];
148
+ let currentCategory = null;
149
+ let totalCount = 0;
150
+ let checkedCount = 0;
151
+
152
+ for (const line of lines) {
153
+ // Track category headings (## or ###)
154
+ const headingMatch = line.match(/^#{2,3}\s+(.+)/);
155
+ if (headingMatch) {
156
+ currentCategory = headingMatch[1].trim();
157
+ continue;
158
+ }
159
+
160
+ // Parse checkbox items
161
+ const checkboxMatch = line.match(/^- \[([ x])\]\s+(.*)/i);
162
+ if (!checkboxMatch) continue;
163
+
164
+ const isChecked = checkboxMatch[1].toLowerCase() === 'x';
165
+ let itemText = checkboxMatch[2].trim();
166
+ totalCount++;
167
+ if (isChecked) checkedCount++;
168
+
169
+ // Extract CHK-xxx ID
170
+ let chkId = null;
171
+ const chkMatch = itemText.match(/^(CHK-\d{3})\s+/);
172
+ if (chkMatch) {
173
+ chkId = chkMatch[1];
174
+ itemText = itemText.substring(chkMatch[0].length);
175
+ }
176
+
177
+ // Extract trailing tags [tag1] [tag2] — but not the checkbox itself
178
+ const tags = [];
179
+ const tagRegex = /\[([^\]]+)\]\s*$/;
180
+ let tagMatch;
181
+ while ((tagMatch = itemText.match(tagRegex))) {
182
+ // Don't treat spec references like [Completeness, FR-004] as simple tags
183
+ tags.unshift(tagMatch[1]);
184
+ itemText = itemText.substring(0, tagMatch.index).trim();
185
+ }
186
+
187
+ items.push({
188
+ text: itemText,
189
+ checked: isChecked,
190
+ chkId,
191
+ category: currentCategory,
192
+ tags
193
+ });
194
+ }
195
+
196
+ result.push({
197
+ name,
198
+ filename: file,
199
+ total: totalCount,
200
+ checked: checkedCount,
201
+ items
202
+ });
203
+ }
204
+
205
+ return result;
206
+ }
207
+
118
208
  /**
119
209
  * Parse CONSTITUTION.md to determine if TDD is required.
120
210
  * Looks for strong TDD indicators combined with MUST/NON-NEGOTIABLE.
@@ -765,4 +855,4 @@ function parseResearchDecisions(content) {
765
855
  return decisions;
766
856
  }
767
857
 
768
- module.exports = { parseSpecStories, parseTasks, parseChecklists, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions };
858
+ module.exports = { parseSpecStories, parseTasks, parseChecklists, parseChecklistsDetailed, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions };
@@ -854,6 +854,234 @@
854
854
  max-width: 360px;
855
855
  }
856
856
 
857
+ /* ====== Checklist Quality Gates Tab ====== */
858
+ .checklist-view {
859
+ padding: 24px 28px;
860
+ max-width: 900px;
861
+ margin: 0 auto;
862
+ }
863
+
864
+ .gate-indicator {
865
+ display: flex;
866
+ align-items: center;
867
+ gap: 12px;
868
+ padding: 16px 20px;
869
+ border-radius: var(--radius-lg);
870
+ background: var(--color-surface-elevated);
871
+ border: 1px solid var(--color-border);
872
+ margin-bottom: 28px;
873
+ }
874
+
875
+ .gate-dot {
876
+ width: 18px;
877
+ height: 18px;
878
+ border-radius: 50%;
879
+ flex-shrink: 0;
880
+ transition: background-color 0.4s ease;
881
+ }
882
+
883
+ .gate-dot.green { background-color: var(--color-green, #22c55e); }
884
+ .gate-dot.yellow { background-color: var(--color-yellow, #eab308); }
885
+ .gate-dot.red { background-color: var(--color-red, #ef4444); }
886
+
887
+ .gate-label {
888
+ font-size: 15px;
889
+ font-weight: 700;
890
+ letter-spacing: 0.5px;
891
+ }
892
+
893
+ .gate-label.open { color: var(--color-green, #22c55e); }
894
+ .gate-label.blocked-yellow { color: var(--color-yellow, #eab308); }
895
+ .gate-label.blocked-red { color: var(--color-red, #ef4444); }
896
+
897
+ .checklist-rings {
898
+ display: flex;
899
+ flex-wrap: wrap;
900
+ gap: 24px;
901
+ justify-content: center;
902
+ margin-bottom: 24px;
903
+ }
904
+
905
+ .checklist-ring-wrapper {
906
+ display: flex;
907
+ flex-direction: column;
908
+ align-items: center;
909
+ cursor: pointer;
910
+ padding: 12px;
911
+ border-radius: var(--radius-lg);
912
+ transition: background-color 0.15s ease;
913
+ border: 2px solid transparent;
914
+ }
915
+
916
+ .checklist-ring-wrapper:hover {
917
+ background: var(--color-surface-elevated);
918
+ }
919
+
920
+ .checklist-ring-wrapper:focus-visible {
921
+ outline: 2px solid var(--color-primary);
922
+ outline-offset: 2px;
923
+ }
924
+
925
+ .checklist-ring-wrapper.expanded {
926
+ border-color: var(--color-primary);
927
+ background: var(--color-surface-elevated);
928
+ }
929
+
930
+ .checklist-ring-svg {
931
+ width: 100px;
932
+ height: 100px;
933
+ }
934
+
935
+ .checklist-ring-track {
936
+ fill: none;
937
+ stroke: var(--color-border);
938
+ stroke-width: 8;
939
+ }
940
+
941
+ .checklist-ring-fill {
942
+ fill: none;
943
+ stroke-width: 8;
944
+ stroke-linecap: round;
945
+ transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s ease;
946
+ transform: rotate(-90deg);
947
+ transform-origin: 50% 50%;
948
+ }
949
+
950
+ .checklist-ring-fill.red { stroke: var(--color-red, #ef4444); }
951
+ .checklist-ring-fill.yellow { stroke: var(--color-yellow, #eab308); }
952
+ .checklist-ring-fill.green { stroke: var(--color-green, #22c55e); }
953
+
954
+ .checklist-ring-pct {
955
+ font-size: 18px;
956
+ font-weight: 700;
957
+ fill: var(--color-text);
958
+ text-anchor: middle;
959
+ dominant-baseline: central;
960
+ }
961
+
962
+ .checklist-ring-name {
963
+ margin-top: 8px;
964
+ font-size: 13px;
965
+ font-weight: 600;
966
+ color: var(--color-text);
967
+ text-align: center;
968
+ }
969
+
970
+ .checklist-ring-fraction {
971
+ font-size: 12px;
972
+ color: var(--color-text-muted);
973
+ }
974
+
975
+ .checklist-detail {
976
+ max-height: 0;
977
+ overflow: hidden;
978
+ transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.15s ease, opacity 0.15s ease;
979
+ border-radius: var(--radius-lg);
980
+ background: var(--color-surface-elevated);
981
+ border: 1px solid transparent;
982
+ margin-bottom: 0;
983
+ opacity: 0;
984
+ }
985
+
986
+ .checklist-detail.open {
987
+ max-height: 2000px;
988
+ border-color: var(--color-border);
989
+ margin-bottom: 16px;
990
+ opacity: 1;
991
+ }
992
+
993
+ .checklist-detail-inner {
994
+ padding: 16px 20px;
995
+ }
996
+
997
+ .checklist-category {
998
+ font-size: 12px;
999
+ font-weight: 700;
1000
+ text-transform: uppercase;
1001
+ letter-spacing: 0.5px;
1002
+ color: var(--color-text-muted);
1003
+ margin: 16px 0 8px;
1004
+ padding-bottom: 4px;
1005
+ border-bottom: 1px solid var(--color-border);
1006
+ }
1007
+
1008
+ .checklist-category:first-child {
1009
+ margin-top: 0;
1010
+ }
1011
+
1012
+ .checklist-item {
1013
+ display: flex;
1014
+ align-items: flex-start;
1015
+ gap: 8px;
1016
+ padding: 6px 0;
1017
+ font-size: 13px;
1018
+ color: var(--color-text);
1019
+ line-height: 1.4;
1020
+ }
1021
+
1022
+ .checklist-item-icon {
1023
+ flex-shrink: 0;
1024
+ width: 18px;
1025
+ font-size: 14px;
1026
+ }
1027
+
1028
+ .checklist-item-icon.checked { color: var(--color-green, #22c55e); }
1029
+ .checklist-item-icon.unchecked { color: var(--color-text-muted); }
1030
+
1031
+ .checklist-item-id {
1032
+ font-family: var(--font-mono, monospace);
1033
+ font-size: 11px;
1034
+ color: var(--color-text-muted);
1035
+ background: var(--color-surface);
1036
+ padding: 1px 5px;
1037
+ border-radius: var(--radius-sm, 4px);
1038
+ flex-shrink: 0;
1039
+ }
1040
+
1041
+ .checklist-item-tag {
1042
+ font-size: 10px;
1043
+ font-weight: 600;
1044
+ padding: 1px 6px;
1045
+ border-radius: 9999px;
1046
+ background: var(--color-primary-muted, rgba(99, 102, 241, 0.15));
1047
+ color: var(--color-primary, #6366f1);
1048
+ flex-shrink: 0;
1049
+ }
1050
+
1051
+ .checklist-empty {
1052
+ display: flex;
1053
+ flex-direction: column;
1054
+ align-items: center;
1055
+ justify-content: center;
1056
+ padding: 80px 20px;
1057
+ text-align: center;
1058
+ }
1059
+
1060
+ .checklist-empty-icon {
1061
+ width: 56px;
1062
+ height: 56px;
1063
+ background: var(--color-surface-elevated);
1064
+ border-radius: var(--radius-lg);
1065
+ display: flex;
1066
+ align-items: center;
1067
+ justify-content: center;
1068
+ font-size: 24px;
1069
+ margin-bottom: 16px;
1070
+ }
1071
+
1072
+ .checklist-empty-title {
1073
+ font-size: 16px;
1074
+ font-weight: 600;
1075
+ color: var(--color-text);
1076
+ margin-bottom: 6px;
1077
+ }
1078
+
1079
+ .checklist-empty-text {
1080
+ font-size: 13px;
1081
+ color: var(--color-text-muted);
1082
+ max-width: 360px;
1083
+ }
1084
+
857
1085
  /* ====== Spec Story Map Tab ====== */
858
1086
  .storymap-view {
859
1087
  padding: 24px 28px;
@@ -1707,6 +1935,8 @@
1707
1935
  let currentFeature = null;
1708
1936
  let currentBoard = null;
1709
1937
  let currentPipeline = null;
1938
+ let currentChecklist = null;
1939
+ let expandedChecklist = null;
1710
1940
  let activeTab = null;
1711
1941
  let ws = null;
1712
1942
  let reconnectTimer = null;
@@ -1783,6 +2013,8 @@
1783
2013
  renderPlanView();
1784
2014
  } else if (phaseId === 'clarify') {
1785
2015
  renderClarifyView();
2016
+ } else if (phaseId === 'checklist') {
2017
+ renderChecklistView();
1786
2018
  } else {
1787
2019
  renderPlaceholderView(phaseId);
1788
2020
  }
@@ -1814,6 +2046,150 @@
1814
2046
  </div>`;
1815
2047
  }
1816
2048
 
2049
+ // ====== Checklist Quality Gates View ======
2050
+
2051
+ async function renderChecklistView() {
2052
+ if (!currentFeature) return;
2053
+
2054
+ try {
2055
+ const res = await fetch(`/api/checklist/${currentFeature}`);
2056
+ if (!res.ok) throw new Error('Failed to load');
2057
+ currentChecklist = await res.json();
2058
+ expandedChecklist = null;
2059
+ renderChecklistContent(currentChecklist);
2060
+ } catch {
2061
+ contentArea.innerHTML = '<div class="checklist-empty"><div class="checklist-empty-title">Failed to load checklist data.</div></div>';
2062
+ }
2063
+ }
2064
+
2065
+ function renderChecklistContent(data) {
2066
+ if (!data) return;
2067
+
2068
+ // Empty state
2069
+ if (!data.files || data.files.length === 0) {
2070
+ contentArea.innerHTML = `
2071
+ <div class="checklist-empty">
2072
+ <div class="checklist-empty-icon">&#9744;</div>
2073
+ <div class="checklist-empty-title">No checklists generated for this feature</div>
2074
+ <div class="checklist-empty-text">Run <code>/iikit-04-checklist</code> to generate domain-specific quality checklists for requirements validation.</div>
2075
+ </div>`;
2076
+ return;
2077
+ }
2078
+
2079
+ const CIRCUMFERENCE = 2 * Math.PI * 42; // radius=42 for viewBox 100x100
2080
+
2081
+ let html = '<div class="checklist-view">';
2082
+
2083
+ // Gate indicator
2084
+ html += renderGateIndicator(data.gate);
2085
+
2086
+ // Progress rings
2087
+ html += '<div class="checklist-rings">';
2088
+ data.files.forEach((file) => {
2089
+ const isExpanded = expandedChecklist === file.filename;
2090
+ const offset = CIRCUMFERENCE - (file.percentage / 100) * CIRCUMFERENCE;
2091
+
2092
+ html += `<div class="checklist-ring-wrapper${isExpanded ? ' expanded' : ''}"
2093
+ tabindex="0" role="button"
2094
+ aria-expanded="${isExpanded}"
2095
+ aria-controls="checklist-detail-${escapeHtml(file.filename)}"
2096
+ aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%"
2097
+ data-filename="${escapeHtml(file.filename)}">
2098
+ <svg class="checklist-ring-svg" viewBox="0 0 100 100" role="img"
2099
+ aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%">
2100
+ <circle class="checklist-ring-track" cx="50" cy="50" r="42" />
2101
+ <circle class="checklist-ring-fill ${file.color}" cx="50" cy="50" r="42"
2102
+ stroke-dasharray="${CIRCUMFERENCE}"
2103
+ stroke-dashoffset="${offset}" />
2104
+ <text class="checklist-ring-pct" x="50" y="50">${file.percentage}%</text>
2105
+ </svg>
2106
+ <div class="checklist-ring-name">${escapeHtml(file.name)}</div>
2107
+ <div class="checklist-ring-fraction">${file.checked}/${file.total}</div>
2108
+ </div>`;
2109
+ });
2110
+ html += '</div>';
2111
+
2112
+ // Accordion detail panels
2113
+ data.files.forEach((file) => {
2114
+ const isExpanded = expandedChecklist === file.filename;
2115
+ html += `<div id="checklist-detail-${escapeHtml(file.filename)}" class="checklist-detail${isExpanded ? ' open' : ''}">`;
2116
+ html += '<div class="checklist-detail-inner">';
2117
+ html += renderChecklistDetail(file);
2118
+ html += '</div></div>';
2119
+ });
2120
+
2121
+ html += '</div>';
2122
+ contentArea.innerHTML = html;
2123
+
2124
+ // Attach click and keyboard handlers to rings
2125
+ document.querySelectorAll('.checklist-ring-wrapper').forEach(wrapper => {
2126
+ const filename = wrapper.getAttribute('data-filename');
2127
+ wrapper.addEventListener('click', () => toggleChecklistExpand(filename, data));
2128
+ wrapper.addEventListener('keydown', (e) => {
2129
+ if (e.key === 'Enter' || e.key === ' ') {
2130
+ e.preventDefault();
2131
+ toggleChecklistExpand(filename, data);
2132
+ }
2133
+ });
2134
+ });
2135
+ }
2136
+
2137
+ function renderGateIndicator(gate) {
2138
+ if (!gate) return '';
2139
+ const levelClass = gate.level === 'yellow' ? 'blocked-yellow' : (gate.level === 'red' ? 'blocked-red' : 'open');
2140
+ return `<div class="gate-indicator" role="status" aria-live="polite">
2141
+ <div class="gate-dot ${gate.level}"></div>
2142
+ <span class="gate-label ${levelClass}">${escapeHtml(gate.label)}</span>
2143
+ </div>`;
2144
+ }
2145
+
2146
+ function renderChecklistDetail(file) {
2147
+ if (!file || !file.items || file.items.length === 0) {
2148
+ return '<div style="color:var(--color-text-muted);font-size:13px;">No items detected in this checklist.</div>';
2149
+ }
2150
+
2151
+ let html = '';
2152
+ let currentCategory = null;
2153
+
2154
+ file.items.forEach(item => {
2155
+ if (item.category !== currentCategory) {
2156
+ currentCategory = item.category;
2157
+ if (currentCategory) {
2158
+ html += `<h4 class="checklist-category">${escapeHtml(currentCategory)}</h4>`;
2159
+ }
2160
+ }
2161
+
2162
+ const icon = item.checked
2163
+ ? '<span class="checklist-item-icon checked">&#10003;</span>'
2164
+ : '<span class="checklist-item-icon unchecked">&#9744;</span>';
2165
+
2166
+ const chkId = item.chkId
2167
+ ? `<span class="checklist-item-id">${escapeHtml(item.chkId)}</span>`
2168
+ : '';
2169
+
2170
+ const tags = (item.tags || []).map(t =>
2171
+ `<span class="checklist-item-tag">${escapeHtml(t)}</span>`
2172
+ ).join('');
2173
+
2174
+ html += `<div class="checklist-item">
2175
+ ${icon}${chkId}
2176
+ <span style="flex:1">${escapeHtml(item.text)}</span>
2177
+ ${tags}
2178
+ </div>`;
2179
+ });
2180
+
2181
+ return html;
2182
+ }
2183
+
2184
+ function toggleChecklistExpand(filename, data) {
2185
+ if (expandedChecklist === filename) {
2186
+ expandedChecklist = null;
2187
+ } else {
2188
+ expandedChecklist = filename;
2189
+ }
2190
+ renderChecklistContent(data);
2191
+ }
2192
+
1817
2193
  // ====== Spec Story Map View ======
1818
2194
  let currentStoryMap = null;
1819
2195
 
@@ -3212,6 +3588,15 @@
3212
3588
  }
3213
3589
  break;
3214
3590
 
3591
+ case 'checklist_update':
3592
+ if (msg.feature === currentFeature && msg.checklist) {
3593
+ currentChecklist = msg.checklist;
3594
+ if (activeTab === 'checklist') {
3595
+ renderChecklistContent(msg.checklist);
3596
+ }
3597
+ }
3598
+ break;
3599
+
3215
3600
  case 'constitution_update':
3216
3601
  if (msg.constitution) {
3217
3602
  currentConstitution = msg.constitution;
package/src/server.js CHANGED
@@ -12,6 +12,7 @@ const { computeAssertionHash, checkIntegrity } = require('./integrity');
12
12
  const { computePipelineState } = require('./pipeline');
13
13
  const { computeStoryMapState } = require('./storymap');
14
14
  const { computePlanViewState } = require('./planview');
15
+ const { computeChecklistViewState } = require('./checklist');
15
16
 
16
17
  /**
17
18
  * List features from specs/ directory.
@@ -167,6 +168,20 @@ function createServer({ projectPath, port = 3000 }) {
167
168
  }
168
169
  });
169
170
 
171
+ // API: checklist view state for a feature
172
+ app.get('/api/checklist/:feature', (req, res) => {
173
+ try {
174
+ const featureDir = path.join(projectPath, 'specs', req.params.feature);
175
+ if (!fs.existsSync(featureDir)) {
176
+ return res.status(404).json({ error: 'Feature not found' });
177
+ }
178
+ const checklist = computeChecklistViewState(projectPath, req.params.feature);
179
+ res.json(checklist);
180
+ } catch (err) {
181
+ res.status(500).json({ error: err.message });
182
+ }
183
+ });
184
+
170
185
  // API: board state for a feature
171
186
  app.get('/api/board/:feature', (req, res) => {
172
187
  try {
@@ -256,6 +271,14 @@ function createServer({ projectPath, port = 3000 }) {
256
271
  planview
257
272
  }));
258
273
  }
274
+ const checklist = computeChecklistViewState(projectPath, ws.currentFeature);
275
+ if (checklist) {
276
+ ws.send(JSON.stringify({
277
+ type: 'checklist_update',
278
+ feature: ws.currentFeature,
279
+ checklist
280
+ }));
281
+ }
259
282
  } catch {
260
283
  // ignore errors during push
261
284
  }