@hanzlaa/rcode 3.5.0 → 3.6.1
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/package.json +7 -1
- package/rihal/bin/rihal-tools.cjs +274 -31
- package/server/dashboard.js +105 -3
- package/server/lib/html/client/agents-data.js +27 -0
- package/server/lib/html/client/app.js +15 -0
- package/server/lib/html/client/components/App.js +211 -0
- package/server/lib/html/client/components/OrchPanel.js +293 -0
- package/server/lib/html/client/components/Sidebar.js +73 -0
- package/server/lib/html/client/components/Topbar.js +53 -0
- package/server/lib/html/client/components/XtermPanel.js +220 -0
- package/server/lib/html/client/components/shared.js +330 -0
- package/server/lib/html/client/icons-client.js +85 -0
- package/server/lib/html/client/orchestrator.js +280 -0
- package/server/lib/html/client/preact.js +34 -0
- package/server/lib/html/client/store.js +91 -0
- package/server/lib/html/client/util.js +186 -0
- package/server/lib/html/client/views/AgentsView.js +83 -0
- package/server/lib/html/client/views/DecisionsView.js +102 -0
- package/server/lib/html/client/views/FilesView.js +223 -0
- package/server/lib/html/client/views/KanbanView.js +236 -0
- package/server/lib/html/client/views/MemoryView.js +157 -0
- package/server/lib/html/client/views/MilestonesView.js +136 -0
- package/server/lib/html/client/views/OrchestrationView.js +167 -0
- package/server/lib/html/client/views/OverviewView.js +221 -0
- package/server/lib/html/client/views/PhasesView.js +184 -0
- package/server/lib/html/client/views/RoadmapView.js +238 -0
- package/server/lib/html/client/views/SprintsView.js +178 -0
- package/server/lib/html/client/views/TasksView.js +148 -0
- package/server/lib/html/client.js +41 -1775
- package/server/lib/html/css.js +265 -56
- package/server/lib/html/icons.js +68 -0
- package/server/lib/html/shell.js +9 -296
- package/server/lib/scanner.js +89 -0
- package/server/orchestrator.js +252 -310
package/server/lib/html/shell.js
CHANGED
|
@@ -7,46 +7,10 @@ const { renderClientJs } = require('./client');
|
|
|
7
7
|
function esc(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
8
8
|
|
|
9
9
|
function renderHtml(state, orchToken) {
|
|
10
|
-
const projectName
|
|
11
|
-
const currentPhase = state.currentPhase || '—';
|
|
12
|
-
const currentSprint = state.currentSprint || null;
|
|
13
|
-
const phaseCount = (state.raw?.phases || []).length;
|
|
14
|
-
const decisionCount = (state.raw?.decisions || []).length;
|
|
15
|
-
const artifactCount = state.planningFiles.length;
|
|
10
|
+
const projectName = state.projectName || 'No project initialized';
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ name: 'Waleed Al Harthi', arabic: 'وليد', role: 'CTO', real: true, type: 'leadership' },
|
|
20
|
-
{ name: 'Ahmed Al Hassani', arabic: 'أحمد الحسني', role: 'Technology & Development Director', real: true, type: 'leadership' },
|
|
21
|
-
{ name: 'Nasser', arabic: 'ناصر', role: 'Engineering Manager', real: true, type: 'leadership' },
|
|
22
|
-
{ name: 'Hussain', arabic: 'حسين', role: 'PM + Scrum Master', type: 'product' },
|
|
23
|
-
{ name: 'Layla', arabic: 'ليلى', role: 'Lead UX Designer', type: 'design' },
|
|
24
|
-
{ name: 'Zahra', arabic: 'زهرة', role: 'Branding & Creative Director', type: 'design' },
|
|
25
|
-
{ name: 'Omar', arabic: 'عمر', role: 'Full-Stack Engineer', type: 'engineering' },
|
|
26
|
-
{ name: 'Haitham Al Khamiyasi', arabic: 'هيثم', role: 'Senior Frontend', real: true, type: 'engineering' },
|
|
27
|
-
{ name: 'Yousef', arabic: 'يوسف', role: 'Senior Backend', type: 'engineering' },
|
|
28
|
-
{ name: 'Zayd', arabic: 'زيد', role: 'ML Engineer', type: 'engineering' },
|
|
29
|
-
{ name: 'Fatima', arabic: 'فاطمة', role: 'QA Lead', type: 'quality' },
|
|
30
|
-
{ name: 'Khalid', arabic: 'خالد', role: 'DevOps', type: 'engineering' },
|
|
31
|
-
{ name: 'Noor', arabic: 'نور', role: 'Scribe', type: 'support' },
|
|
32
|
-
{ name: 'Mariam', arabic: 'مريم', role: 'Marketing Lead', type: 'product' },
|
|
33
|
-
{ name: 'Raees', arabic: 'رئيس', role: 'Orchestration Director', type: 'system' },
|
|
34
|
-
{ name: 'Majlis', arabic: 'مجلس', role: 'Consulting Council', type: 'system' },
|
|
35
|
-
{ name: 'Diwan', arabic: 'ديوان', role: 'Dashboard Registry', type: 'system' },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const realAgents = agents.filter(a => a.real);
|
|
39
|
-
const aiAgents = agents.filter(a => !a.real);
|
|
40
|
-
|
|
41
|
-
function agentCard(a) {
|
|
42
|
-
const filterText = (a.name + ' ' + a.role + ' ' + a.arabic + ' ' + a.type).toLowerCase();
|
|
43
|
-
const skillName = a.name.split(' ')[0].toLowerCase();
|
|
44
|
-
return `<div class="agent-card" data-filter-text="${filterText}" onclick="viewAgentSkill('${skillName}')" style="cursor:pointer;">
|
|
45
|
-
<div class="name">${esc(a.name)}${a.real ? ' <span class="real-badge">real</span>' : ''} <span class="type-badge">${esc(a.type)}</span></div>
|
|
46
|
-
<div class="arabic">${a.arabic}</div>
|
|
47
|
-
<div class="role">${esc(a.role)}</div>
|
|
48
|
-
</div>`;
|
|
49
|
-
}
|
|
12
|
+
// Agent roster moved to server/lib/html/client/agents-data.js (Sprint 31.3).
|
|
13
|
+
// AgentsView.js renders it client-side; shell.js no longer needs it.
|
|
50
14
|
|
|
51
15
|
return `<!DOCTYPE html>
|
|
52
16
|
<html lang="en" dir="ltr">
|
|
@@ -56,7 +20,7 @@ function renderHtml(state, orchToken) {
|
|
|
56
20
|
<title>Majlis — ${esc(projectName)}</title>
|
|
57
21
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
58
22
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
|
59
|
-
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"><\/script>
|
|
60
24
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"><\/script>
|
|
61
25
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"><\/script>
|
|
62
26
|
<script>window.__ORCH_TOKEN__ = ${JSON.stringify(orchToken || '')};<\/script>
|
|
@@ -64,266 +28,15 @@ ${renderCss()}
|
|
|
64
28
|
</head>
|
|
65
29
|
<body>
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<aside class="sidebar" id="sidebar">
|
|
71
|
-
<div class="sidebar-project">
|
|
72
|
-
<div class="project-label">Rihal</div>
|
|
73
|
-
<span>${esc(projectName)}</span>
|
|
74
|
-
</div>
|
|
75
|
-
<nav>
|
|
76
|
-
<div class="nav-section">Overview</div>
|
|
77
|
-
<button class="nav-link" data-view="overview">🏠 Overview</button>
|
|
78
|
-
<button class="nav-link" data-view="roadmap">🗺 Roadmap</button>
|
|
79
|
-
|
|
80
|
-
<div class="nav-section">Planning</div>
|
|
81
|
-
<button class="nav-link" data-view="milestones">🎯 Milestones</button>
|
|
82
|
-
<button class="nav-link" data-view="phases">📋 Phases</button>
|
|
83
|
-
<button class="nav-link" data-view="sprints">⚡ Sprints</button>
|
|
84
|
-
<button class="nav-link" data-view="tasks">✓ Tasks</button>
|
|
85
|
-
<button class="nav-link" data-view="kanban">🗂 Kanban</button>
|
|
86
|
-
|
|
87
|
-
<div class="nav-section">Workspace</div>
|
|
88
|
-
<button class="nav-link" data-view="files">📄 Files</button>
|
|
89
|
-
<button class="nav-link" data-view="agents">🤝 Agents</button>
|
|
90
|
-
<button class="nav-link" data-view="decisions">⚖ Decisions</button>
|
|
91
|
-
<button class="nav-link" data-view="memory">🧠 Memory</button>
|
|
92
|
-
</nav>
|
|
93
|
-
</aside>
|
|
94
|
-
|
|
95
|
-
<div id="sidebar-backdrop" onclick="closeSidebar()"></div>
|
|
96
|
-
|
|
97
|
-
<!-- ── Content area ────────────────────────────────────────── -->
|
|
98
|
-
<div class="content-area" id="main-content">
|
|
99
|
-
|
|
100
|
-
<!-- Topbar -->
|
|
101
|
-
<header>
|
|
102
|
-
<div style="display:flex;align-items:center;gap:12px;">
|
|
103
|
-
<button class="hamburger-btn" id="hamburger-btn" onclick="toggleSidebar()" aria-label="Toggle menu">
|
|
104
|
-
<span></span><span></span><span></span>
|
|
105
|
-
</button>
|
|
106
|
-
<div class="brand">
|
|
107
|
-
<div class="icon">🕌</div>
|
|
108
|
-
<div>
|
|
109
|
-
<h1>Majlis — The Council</h1>
|
|
110
|
-
<div class="arabic">مجلس · ${esc(projectName)}</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="header-actions">
|
|
115
|
-
<span class="live" id="live-dot" title="Live"></span>
|
|
116
|
-
<span id="updated-ago" style="font-size:11px;color:var(--text-muted);">just now</span>
|
|
117
|
-
<button class="header-btn" id="refresh-btn" onclick="manualRefresh()">↺ Refresh</button>
|
|
118
|
-
<button class="header-btn" id="theme-btn" onclick="toggleTheme()" title="Toggle theme">◑</button>
|
|
119
|
-
<button class="header-btn" onclick="copyUrl()" title="Copy URL">⎘ Link</button>
|
|
120
|
-
</div>
|
|
121
|
-
</header>
|
|
122
|
-
|
|
123
|
-
<!-- Main scroll container -->
|
|
124
|
-
<div class="main-scroll" id="main-scroll">
|
|
125
|
-
|
|
126
|
-
${state.rawParseError ? `<div id="parse-warning">⚠ <strong>state.json parse error:</strong> ${esc(state.rawParseError)}</div>` : ''}
|
|
127
|
-
|
|
128
|
-
${state.blockers.length > 0 ? `
|
|
129
|
-
<div id="blocker-banner">
|
|
130
|
-
<span class="banner-title">🚧 ${state.blockers.length} blocker${state.blockers.length > 1 ? 's' : ''}</span>
|
|
131
|
-
<span class="banner-list">${state.blockers.map(b => esc(typeof b === 'string' ? b : (b.title || ''))).join(' · ')}</span>
|
|
132
|
-
<button class="banner-dismiss" onclick="dismissBlockers()">Dismiss</button>
|
|
133
|
-
</div>` : ''}
|
|
134
|
-
|
|
135
|
-
<!-- ── Overview ─────────────────────────────────────── -->
|
|
136
|
-
<div id="view-overview" class="view active">
|
|
137
|
-
${!state.exists ? `
|
|
138
|
-
<div class="empty" style="padding:80px;background:var(--bg-elev-2);border:1px solid var(--border-subtle);border-radius:var(--radius-4);">
|
|
139
|
-
<h2 style="color:var(--accent-primary);margin-bottom:12px;font-size:20px;letter-spacing:-0.017em;">No .rihal/ directory found</h2>
|
|
140
|
-
<p style="color:var(--text-tertiary);">Run the kickoff workflow to initialize a project.</p>
|
|
141
|
-
<div class="empty-action">npx rcode install</div>
|
|
142
|
-
</div>
|
|
143
|
-
` : `
|
|
144
|
-
<div class="stats">
|
|
145
|
-
<div class="stat">
|
|
146
|
-
<div class="label">Current Phase</div>
|
|
147
|
-
<div class="value" style="font-size:22px;">${esc(currentPhase)}</div>
|
|
148
|
-
<div class="sub">${phaseCount} total${currentSprint ? ` · Sprint ${esc(currentSprint)}` : ''}</div>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="stat">
|
|
151
|
-
<div class="label">Milestone</div>
|
|
152
|
-
<div class="value" style="font-size:16px;padding-top:6px;" id="stat-milestone">${esc(state.milestone || '—')}</div>
|
|
153
|
-
<div class="sub"> </div>
|
|
154
|
-
</div>
|
|
155
|
-
<div class="stat">
|
|
156
|
-
<div class="label">Decisions</div>
|
|
157
|
-
<div class="value">${decisionCount}</div>
|
|
158
|
-
<div class="sub">Architecture records</div>
|
|
159
|
-
</div>
|
|
160
|
-
<div class="stat">
|
|
161
|
-
<div class="label">Planning Files</div>
|
|
162
|
-
<div class="value">${artifactCount}</div>
|
|
163
|
-
<div class="sub">SPRINT · CONTEXT · VERIFY</div>
|
|
164
|
-
</div>
|
|
165
|
-
${state.blockers.length > 0 ? `
|
|
166
|
-
<div class="stat" style="border-left-color:var(--red);">
|
|
167
|
-
<div class="label" style="color:var(--red);">Blockers</div>
|
|
168
|
-
<div class="value" style="color:var(--red);">${state.blockers.length}</div>
|
|
169
|
-
<div class="sub">Active</div>
|
|
170
|
-
</div>` : ''}
|
|
171
|
-
${state.councilSessions > 0 ? `
|
|
172
|
-
<div class="stat">
|
|
173
|
-
<div class="label">Council Sessions</div>
|
|
174
|
-
<div class="value">${state.councilSessions}</div>
|
|
175
|
-
<div class="sub">Recorded</div>
|
|
176
|
-
</div>` : ''}
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
<div id="view-overview-dynamic"></div>
|
|
180
|
-
|
|
181
|
-
<section>
|
|
182
|
-
<h2>🎯 Active Context</h2>
|
|
183
|
-
<div class="body">
|
|
184
|
-
${state.context
|
|
185
|
-
? `<pre class="ctx-pre">${esc(state.context.replace(/^#[^\n]*\n?/, '').trim())}</pre>`
|
|
186
|
-
: `<div class="empty">No active context.<div class="empty-action">Run /rihal-init to populate</div></div>`}
|
|
187
|
-
</div>
|
|
188
|
-
</section>
|
|
189
|
-
`}
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<!-- ── Dynamic views (rendered by JS) ─────────────────── -->
|
|
193
|
-
<div id="view-roadmap" class="view"></div>
|
|
194
|
-
<div id="view-milestones" class="view"></div>
|
|
195
|
-
<div id="view-phases" class="view"></div>
|
|
196
|
-
<div id="view-sprints" class="view"></div>
|
|
197
|
-
<div id="view-tasks" class="view"></div>
|
|
198
|
-
|
|
199
|
-
<!-- Kanban -->
|
|
200
|
-
<div id="view-kanban" class="view"></div>
|
|
201
|
-
|
|
202
|
-
<!-- Files -->
|
|
203
|
-
<div id="view-files" class="view">
|
|
204
|
-
<div class="view-title">Files</div>
|
|
205
|
-
<div id="file-list-inline"></div>
|
|
206
|
-
<div id="file-view"></div>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<!-- Agents -->
|
|
210
|
-
<div id="view-agents" class="view">
|
|
211
|
-
<div class="view-title">Agents</div>
|
|
212
|
-
<div class="body">
|
|
213
|
-
<div class="filter-bar">
|
|
214
|
-
<input class="filter-input" type="text" placeholder="Filter agents…" oninput="filterItems(this,'agents-list')">
|
|
215
|
-
</div>
|
|
216
|
-
<div id="agents-list" style="padding:12px;">
|
|
217
|
-
<div style="font-size:11px;font-weight:600;color:var(--amber);margin-bottom:10px;text-transform:uppercase;letter-spacing:0.06em;">Team</div>
|
|
218
|
-
<div class="agents" style="padding:0;margin-bottom:20px;">
|
|
219
|
-
${realAgents.map(agentCard).join('')}
|
|
220
|
-
</div>
|
|
221
|
-
<div style="font-size:11px;font-weight:600;color:var(--accent-primary);margin-bottom:10px;text-transform:uppercase;letter-spacing:0.06em;">AI Agents</div>
|
|
222
|
-
<div class="agents" style="padding:0;">
|
|
223
|
-
${aiAgents.map(agentCard).join('')}
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div id="view-decisions" class="view"></div>
|
|
230
|
-
|
|
231
|
-
<!-- Memory -->
|
|
232
|
-
<div id="view-memory" class="view">
|
|
233
|
-
<div id="view-memory-content">
|
|
234
|
-
<div class="empty" style="background:var(--bg-elev-2);border:1px solid var(--border-subtle);border-radius:var(--radius-4);">
|
|
235
|
-
<h2 style="color:var(--accent-primary);margin-bottom:12px;">Memory Bank</h2>
|
|
236
|
-
<p>Loading…</p>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
</div><!-- /main-scroll -->
|
|
242
|
-
</div><!-- /content-area -->
|
|
243
|
-
</div><!-- /app-shell -->
|
|
31
|
+
<!-- ── Preact app mount ────────────────────────────────────────────────── -->
|
|
32
|
+
<!-- App renders: sidebar, topbar, and all 12 Preact views (sprint 31.4). -->
|
|
33
|
+
<div id="app-root"></div>
|
|
244
34
|
|
|
245
35
|
<!-- ── Toast ──────────────────────────────────────────────── -->
|
|
246
36
|
<div class="toast" id="toast"></div>
|
|
247
37
|
|
|
248
|
-
<!--
|
|
249
|
-
|
|
250
|
-
<div class="orch-panel-header">
|
|
251
|
-
<div class="orch-panel-title">
|
|
252
|
-
<span id="orch-panel-orch-dot" class="orch-status-dot"></span>
|
|
253
|
-
Agent Sessions
|
|
254
|
-
</div>
|
|
255
|
-
<button class="orch-panel-close" onclick="closeOrchPanel()" title="Close">✕</button>
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
<!-- Tab strip -->
|
|
259
|
-
<div class="orch-tabs" id="orch-tabs">
|
|
260
|
-
<div class="orch-term-empty" style="padding:6px 8px;font-size:11px;color:var(--text-muted);">No active sessions</div>
|
|
261
|
-
</div>
|
|
262
|
-
|
|
263
|
-
<!-- Terminal body -->
|
|
264
|
-
<div class="orch-terminal">
|
|
265
|
-
<div class="orch-term-body" id="orch-term-body">
|
|
266
|
-
<div class="orch-term-empty">
|
|
267
|
-
<div>Select a session or run a story card</div>
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
270
|
-
<div class="orch-files" id="orch-files" style="display:none;">
|
|
271
|
-
<div class="orch-files-head">File changes</div>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
|
|
275
|
-
<!-- Footer controls -->
|
|
276
|
-
<div class="orch-panel-footer">
|
|
277
|
-
<button class="orch-footer-btn stop" id="orch-stop-btn" onclick="stopActiveSession()" style="display:none;">■ Stop</button>
|
|
278
|
-
<button class="orch-footer-btn" onclick="clearActiveTerminal()">Clear</button>
|
|
279
|
-
<button class="orch-footer-btn" onclick="openCleanSessions()">Clean sessions…</button>
|
|
280
|
-
<div style="flex:1;"></div>
|
|
281
|
-
<span id="orch-session-status" style="font-size:11px;color:var(--text-muted);"></span>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
<script>
|
|
286
|
-
function viewAgentSkill(name) {
|
|
287
|
-
navTo('files');
|
|
288
|
-
// Wait for inline file tree to render (fetched async), then find the agent's skill file
|
|
289
|
-
setTimeout(function() {
|
|
290
|
-
var items = document.querySelectorAll('.inline-file-entry');
|
|
291
|
-
for (var i = 0; i < items.length; i++) {
|
|
292
|
-
if ((items[i].dataset.path || '').toLowerCase().includes(name)) {
|
|
293
|
-
items[i].click();
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// No exact match — pre-fill the search so the user can see related files
|
|
298
|
-
var search = document.querySelector('#file-list-inline .filter-input');
|
|
299
|
-
if (search) {
|
|
300
|
-
search.value = name;
|
|
301
|
-
search.dispatchEvent(new Event('input'));
|
|
302
|
-
}
|
|
303
|
-
}, 400);
|
|
304
|
-
}
|
|
305
|
-
<\/script>
|
|
306
|
-
|
|
307
|
-
<!-- ── xterm Terminal Panel ───────────────────────────────────── -->
|
|
308
|
-
<div id="term-backdrop" class="term-backdrop"></div>
|
|
309
|
-
<div id="term-panel" class="term-panel">
|
|
310
|
-
<div class="term-header">
|
|
311
|
-
<div class="term-header-left">
|
|
312
|
-
<div class="term-status-dot" id="term-status-dot"></div>
|
|
313
|
-
<span class="term-title" id="term-title">Terminal</span>
|
|
314
|
-
</div>
|
|
315
|
-
<div class="term-header-right">
|
|
316
|
-
<button class="term-btn term-stop-btn" id="term-stop-btn" onclick="termStop()">■ Stop</button>
|
|
317
|
-
<button class="term-btn" onclick="closeTermPanel()">✕ Close</button>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
<div id="term-container"></div>
|
|
321
|
-
<div class="term-input-row">
|
|
322
|
-
<span class="term-prompt">❯</span>
|
|
323
|
-
<input type="text" id="term-input" class="term-input-field" placeholder="Send message to agent… (Enter)">
|
|
324
|
-
<button class="term-send-btn" onclick="termSend()">Send ↑</button>
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
38
|
+
<!-- Xterm and orchestrator panels are now rendered by Preact (Sprint 31.4).
|
|
39
|
+
Static panel markup removed — XtermPanel.js + OrchPanel.js own the DOM. -->
|
|
327
40
|
|
|
328
41
|
${renderClientJs(state)}
|
|
329
42
|
</body>
|
package/server/lib/scanner.js
CHANGED
|
@@ -31,6 +31,93 @@ function parseSimpleYaml(text) {
|
|
|
31
31
|
return out;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Derive the phase → sprint → story tree from the .planning/phases/ filesystem,
|
|
36
|
+
* which is the committed source of truth. state.json sprint/story records are
|
|
37
|
+
* often incomplete (planner agents write SPRINT.md files without registering
|
|
38
|
+
* sprint entries), so the dashboard derives counts from disk instead of trusting
|
|
39
|
+
* state.json. When a phase has a directory with *-SPRINT.md files, those win;
|
|
40
|
+
* otherwise the raw state.json sprints array is kept as-is.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} projectDir repo root
|
|
43
|
+
* @param {Array} rawPhases state.raw.phases
|
|
44
|
+
* @returns {Array|null} phases with a populated `sprints` array each
|
|
45
|
+
*/
|
|
46
|
+
function buildPhaseTree(projectDir, rawPhases) {
|
|
47
|
+
if (!Array.isArray(rawPhases)) return null;
|
|
48
|
+
const phasesDir = path.join(projectDir, '.planning', 'phases');
|
|
49
|
+
let dirs;
|
|
50
|
+
try {
|
|
51
|
+
dirs = fs.readdirSync(phasesDir, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
52
|
+
} catch { return rawPhases; }
|
|
53
|
+
|
|
54
|
+
return rawPhases.map(p => {
|
|
55
|
+
const intId = String(p.id || p.number || '').split('.')[0];
|
|
56
|
+
if (!intId) return p;
|
|
57
|
+
const dir = dirs.find(d => d.name.startsWith(intId + '-') ||
|
|
58
|
+
d.name.startsWith(intId.padStart(2, '0') + '-'));
|
|
59
|
+
if (!dir) return p;
|
|
60
|
+
|
|
61
|
+
let files;
|
|
62
|
+
try { files = fs.readdirSync(path.join(phasesDir, dir.name)); } catch { return p; }
|
|
63
|
+
const sprintFiles = files.filter(f => /-SPRINT\.md$/i.test(f)).sort();
|
|
64
|
+
if (!sprintFiles.length) return p;
|
|
65
|
+
|
|
66
|
+
const phaseComplete = /complete|done/i.test(p.status || '');
|
|
67
|
+
const sprints = sprintFiles.map(f => {
|
|
68
|
+
const m = f.match(/^(\d+)-(\d+)-SPRINT\.md$/i);
|
|
69
|
+
const num = m ? parseInt(m[2], 10) : 0;
|
|
70
|
+
const sid = m ? `${parseInt(m[1], 10)}.${num}` : f.replace(/-SPRINT\.md$/i, '');
|
|
71
|
+
const text = safeReadText(path.join(phasesDir, dir.name, f)) || '';
|
|
72
|
+
|
|
73
|
+
// Sprint goal: frontmatter `goal:`, else first line of <objective>.
|
|
74
|
+
const fm = parseSimpleYaml((text.match(/^---\n([\s\S]*?)\n---/) || [])[1] || '');
|
|
75
|
+
let goal = fm.goal || '';
|
|
76
|
+
if (!goal) {
|
|
77
|
+
const obj = (text.match(/<objective>\s*([\s\S]*?)<\/objective>/) || [])[1] || '';
|
|
78
|
+
goal = (obj.trim().split('\n').map(s => s.trim()).filter(Boolean)[0] || '').slice(0, 160);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Stories: one per <task> block; title from <title>.
|
|
82
|
+
const stories = [];
|
|
83
|
+
const taskRe = /<task\b([^>]*)>([\s\S]*?)<\/task>/g;
|
|
84
|
+
let tm;
|
|
85
|
+
while ((tm = taskRe.exec(text))) {
|
|
86
|
+
const idM = tm[1].match(/id="([^"]+)"/);
|
|
87
|
+
const titleM = tm[2].match(/<title>([\s\S]*?)<\/title>/);
|
|
88
|
+
stories.push({
|
|
89
|
+
id: idM ? idM[1] : `${sid}-task-${stories.length + 1}`,
|
|
90
|
+
title: titleM ? titleM[1].trim() : `Task ${stories.length + 1}`,
|
|
91
|
+
status: phaseComplete ? 'done' : 'todo',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Fallback for pre-<task> SPRINT.md format (phases 20-30 era):
|
|
95
|
+
// "### Story 20.01.01 — title" / "### Task X — title" headings.
|
|
96
|
+
if (!stories.length) {
|
|
97
|
+
const headRe = /^#{2,4}\s+(?:Story|Task)\s+([^\s—–-]+)\s*[—–-]\s*(.+?)\s*$/gm;
|
|
98
|
+
let hm;
|
|
99
|
+
while ((hm = headRe.exec(text))) {
|
|
100
|
+
stories.push({
|
|
101
|
+
id: hm[1].trim(),
|
|
102
|
+
title: hm[2].trim(),
|
|
103
|
+
status: phaseComplete ? 'done' : 'todo',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Status: a *-SUMMARY.md sibling means the sprint shipped.
|
|
109
|
+
const hasSummary = files.includes(f.replace(/-SPRINT\.md$/i, '-SUMMARY.md'));
|
|
110
|
+
const status = hasSummary ? 'complete'
|
|
111
|
+
: (p.status === 'active' || p.status === 'in_progress') ? 'in_progress'
|
|
112
|
+
: 'planned';
|
|
113
|
+
|
|
114
|
+
return { id: sid, number: num, goal: goal || `Sprint ${num}`, status, stories };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { ...p, sprints };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
34
121
|
function scanState(rihalDir) {
|
|
35
122
|
const projectDir = path.dirname(rihalDir);
|
|
36
123
|
const state = {
|
|
@@ -200,6 +287,8 @@ function scanState(rihalDir) {
|
|
|
200
287
|
} catch { /* ignore */ }
|
|
201
288
|
}
|
|
202
289
|
|
|
290
|
+
state.phaseTree = buildPhaseTree(projectDir, state.raw && state.raw.phases);
|
|
291
|
+
|
|
203
292
|
return state;
|
|
204
293
|
}
|
|
205
294
|
|