specsmd 0.1.23 → 0.1.25
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 +15 -0
- package/bin/cli.js +14 -1
- package/lib/dashboard/fire/model.js +333 -0
- package/lib/dashboard/fire/parser.js +387 -0
- package/lib/dashboard/flow-detect.js +86 -0
- package/lib/dashboard/index.js +134 -0
- package/lib/dashboard/runtime/watch-runtime.js +113 -0
- package/lib/dashboard/tui/app.js +567 -0
- package/lib/dashboard/tui/components/error-banner.js +35 -0
- package/lib/dashboard/tui/components/header.js +62 -0
- package/lib/dashboard/tui/components/help-footer.js +15 -0
- package/lib/dashboard/tui/components/stats-strip.js +35 -0
- package/lib/dashboard/tui/renderer.js +78 -0
- package/lib/dashboard/tui/store.js +30 -0
- package/lib/dashboard/tui/views/overview-view.js +61 -0
- package/lib/dashboard/tui/views/runs-view.js +98 -0
- package/lib/installers/CodexInstaller.js +0 -11
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -89,6 +89,21 @@ During installation, select your flow:
|
|
|
89
89
|
|
|
90
90
|
The installer detects your AI coding tools and sets up agent definitions, slash commands, and project structure for your selected flow.
|
|
91
91
|
|
|
92
|
+
### Live Dashboard (FIRE)
|
|
93
|
+
|
|
94
|
+
Track FIRE state continuously from terminal:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx specsmd@latest dashboard
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Useful options:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx specsmd@latest dashboard --flow fire --path . --refresh-ms 1000
|
|
104
|
+
npx specsmd@latest dashboard --no-watch
|
|
105
|
+
```
|
|
106
|
+
|
|
92
107
|
### Install VS Code Extension (Optional)
|
|
93
108
|
|
|
94
109
|
Track your progress visually with our sidebar extension:
|
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const installer = require('../lib/installer');
|
|
5
|
+
const dashboard = require('../lib/dashboard');
|
|
5
6
|
const packageJson = require('../package.json');
|
|
6
7
|
|
|
7
8
|
program
|
|
@@ -18,4 +19,16 @@ program
|
|
|
18
19
|
.description('Uninstall specsmd from the current project')
|
|
19
20
|
.action(installer.uninstall);
|
|
20
21
|
|
|
21
|
-
program
|
|
22
|
+
program
|
|
23
|
+
.command('dashboard')
|
|
24
|
+
.description('Live terminal dashboard for flow state (FIRE first)')
|
|
25
|
+
.option('--flow <flow>', 'Flow to inspect (fire|aidlc|simple), default auto-detect')
|
|
26
|
+
.option('--path <dir>', 'Workspace path', process.cwd())
|
|
27
|
+
.option('--refresh-ms <n>', 'Fallback refresh interval in milliseconds (default: 1000)', '1000')
|
|
28
|
+
.option('--no-watch', 'Render once and exit')
|
|
29
|
+
.action((options) => dashboard.run(options));
|
|
30
|
+
|
|
31
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
32
|
+
console.error(error.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
function normalizeStatus(status) {
|
|
2
|
+
if (typeof status !== 'string') {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const normalized = status.toLowerCase().trim().replace(/[-\s]+/g, '_');
|
|
7
|
+
const map = {
|
|
8
|
+
pending: 'pending',
|
|
9
|
+
todo: 'pending',
|
|
10
|
+
in_progress: 'in_progress',
|
|
11
|
+
inprogress: 'in_progress',
|
|
12
|
+
active: 'in_progress',
|
|
13
|
+
completed: 'completed',
|
|
14
|
+
complete: 'completed',
|
|
15
|
+
done: 'completed',
|
|
16
|
+
blocked: 'blocked'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return map[normalized];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeMode(mode) {
|
|
23
|
+
if (typeof mode !== 'string') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalized = mode.toLowerCase().trim();
|
|
28
|
+
if (normalized === 'autopilot' || normalized === 'confirm' || normalized === 'validate') {
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeScope(scope) {
|
|
36
|
+
if (typeof scope !== 'string') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalized = scope.toLowerCase().trim();
|
|
41
|
+
if (normalized === 'single' || normalized === 'batch' || normalized === 'wide') {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeComplexity(complexity) {
|
|
49
|
+
if (typeof complexity !== 'string') {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const normalized = complexity.toLowerCase().trim();
|
|
54
|
+
if (normalized === 'low' || normalized === 'medium' || normalized === 'high') {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeArray(value) {
|
|
62
|
+
return Array.isArray(value) ? value : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeTimestamp(value) {
|
|
66
|
+
if (value == null) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (value instanceof Date) {
|
|
71
|
+
return value.toISOString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
return value.trim() === '' ? undefined : value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseDependencies(raw) {
|
|
82
|
+
if (Array.isArray(raw)) {
|
|
83
|
+
return raw.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
87
|
+
return [raw.trim()];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeRunWorkItem(raw, fallbackIntentId = '') {
|
|
94
|
+
if (typeof raw === 'string') {
|
|
95
|
+
return {
|
|
96
|
+
id: raw,
|
|
97
|
+
intentId: fallbackIntentId,
|
|
98
|
+
mode: 'confirm',
|
|
99
|
+
status: 'pending',
|
|
100
|
+
currentPhase: undefined
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!raw || typeof raw !== 'object') {
|
|
105
|
+
return {
|
|
106
|
+
id: '',
|
|
107
|
+
intentId: fallbackIntentId,
|
|
108
|
+
mode: 'confirm',
|
|
109
|
+
status: 'pending',
|
|
110
|
+
currentPhase: undefined
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const id = typeof raw.id === 'string' ? raw.id : '';
|
|
115
|
+
const intentId = typeof raw.intent === 'string'
|
|
116
|
+
? raw.intent
|
|
117
|
+
: (typeof raw.intentId === 'string' ? raw.intentId : fallbackIntentId);
|
|
118
|
+
|
|
119
|
+
const status = normalizeStatus(raw.status) || 'pending';
|
|
120
|
+
const mode = normalizeMode(raw.mode) || 'confirm';
|
|
121
|
+
const currentPhase = typeof raw.current_phase === 'string'
|
|
122
|
+
? raw.current_phase
|
|
123
|
+
: (typeof raw.currentPhase === 'string' ? raw.currentPhase : undefined);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
intentId,
|
|
128
|
+
mode,
|
|
129
|
+
status,
|
|
130
|
+
currentPhase
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeState(rawState) {
|
|
135
|
+
const raw = rawState && typeof rawState === 'object' ? rawState : {};
|
|
136
|
+
|
|
137
|
+
const project = raw.project && typeof raw.project === 'object'
|
|
138
|
+
? {
|
|
139
|
+
name: typeof raw.project.name === 'string' ? raw.project.name : 'Unknown',
|
|
140
|
+
description: typeof raw.project.description === 'string' ? raw.project.description : undefined,
|
|
141
|
+
created: normalizeTimestamp(raw.project.created) || new Date().toISOString(),
|
|
142
|
+
fireVersion: typeof raw.project.fire_version === 'string'
|
|
143
|
+
? raw.project.fire_version
|
|
144
|
+
: (typeof raw.project.fireVersion === 'string'
|
|
145
|
+
? raw.project.fireVersion
|
|
146
|
+
: '0.0.0')
|
|
147
|
+
}
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
const workspace = raw.workspace && typeof raw.workspace === 'object'
|
|
151
|
+
? {
|
|
152
|
+
type: typeof raw.workspace.type === 'string' ? raw.workspace.type : 'greenfield',
|
|
153
|
+
structure: typeof raw.workspace.structure === 'string' ? raw.workspace.structure : 'monolith',
|
|
154
|
+
autonomyBias: typeof raw.workspace.autonomy_bias === 'string'
|
|
155
|
+
? raw.workspace.autonomy_bias
|
|
156
|
+
: (typeof raw.workspace.autonomyBias === 'string' ? raw.workspace.autonomyBias : 'balanced'),
|
|
157
|
+
runScopePreference: normalizeScope(raw.workspace.run_scope_preference)
|
|
158
|
+
|| normalizeScope(raw.workspace.runScopePreference)
|
|
159
|
+
|| 'single',
|
|
160
|
+
scannedAt: normalizeTimestamp(raw.workspace.scanned_at)
|
|
161
|
+
|| normalizeTimestamp(raw.workspace.scannedAt),
|
|
162
|
+
parts: normalizeArray(raw.workspace.parts)
|
|
163
|
+
}
|
|
164
|
+
: null;
|
|
165
|
+
|
|
166
|
+
const intents = normalizeArray(raw.intents).map((intent) => {
|
|
167
|
+
if (!intent || typeof intent !== 'object') {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const workItemsRaw = normalizeArray(intent.work_items).concat(normalizeArray(intent.workItems));
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
id: typeof intent.id === 'string' ? intent.id : '',
|
|
175
|
+
title: typeof intent.title === 'string' ? intent.title : '',
|
|
176
|
+
status: normalizeStatus(intent.status),
|
|
177
|
+
workItems: workItemsRaw
|
|
178
|
+
.map((item) => {
|
|
179
|
+
if (!item || typeof item !== 'object') {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
id: typeof item.id === 'string' ? item.id : '',
|
|
185
|
+
status: normalizeStatus(item.status) || 'pending',
|
|
186
|
+
mode: normalizeMode(item.mode)
|
|
187
|
+
};
|
|
188
|
+
})
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
};
|
|
191
|
+
}).filter(Boolean);
|
|
192
|
+
|
|
193
|
+
const rawRuns = raw.runs && typeof raw.runs === 'object' ? raw.runs : {};
|
|
194
|
+
|
|
195
|
+
const activeRuns = normalizeArray(rawRuns.active).map((run) => {
|
|
196
|
+
if (!run || typeof run !== 'object') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const workItemsRaw = normalizeArray(run.work_items).concat(normalizeArray(run.workItems));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
id: typeof run.id === 'string' ? run.id : '',
|
|
204
|
+
scope: normalizeScope(run.scope) || 'single',
|
|
205
|
+
workItems: workItemsRaw.map((item) => normalizeRunWorkItem(item)).filter((item) => item.id !== ''),
|
|
206
|
+
currentItem: typeof run.current_item === 'string'
|
|
207
|
+
? run.current_item
|
|
208
|
+
: (typeof run.currentItem === 'string' ? run.currentItem : ''),
|
|
209
|
+
started: normalizeTimestamp(run.started) || ''
|
|
210
|
+
};
|
|
211
|
+
}).filter(Boolean);
|
|
212
|
+
|
|
213
|
+
const completedRuns = normalizeArray(rawRuns.completed).map((run) => {
|
|
214
|
+
if (!run || typeof run !== 'object') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fallbackIntentId = typeof run.intent === 'string' ? run.intent : '';
|
|
219
|
+
const workItemsRaw = normalizeArray(run.work_items).concat(normalizeArray(run.workItems));
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: typeof run.id === 'string' ? run.id : '',
|
|
223
|
+
workItems: workItemsRaw.map((item) => normalizeRunWorkItem(item, fallbackIntentId)).filter((item) => item.id !== ''),
|
|
224
|
+
completed: normalizeTimestamp(run.completed) || ''
|
|
225
|
+
};
|
|
226
|
+
}).filter(Boolean);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
project,
|
|
230
|
+
workspace,
|
|
231
|
+
intents,
|
|
232
|
+
runs: {
|
|
233
|
+
active: activeRuns,
|
|
234
|
+
completed: completedRuns
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function deriveIntentStatus(stateStatus, workItems) {
|
|
240
|
+
const normalizedState = normalizeStatus(stateStatus);
|
|
241
|
+
if (normalizedState) {
|
|
242
|
+
return normalizedState;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!Array.isArray(workItems) || workItems.length === 0) {
|
|
246
|
+
return 'pending';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (workItems.every((item) => item.status === 'completed')) {
|
|
250
|
+
return 'completed';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (workItems.some((item) => item.status === 'in_progress')) {
|
|
254
|
+
return 'in_progress';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (workItems.some((item) => item.status === 'blocked')) {
|
|
258
|
+
return 'blocked';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return 'pending';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function calculateStats(intents, runs, activeRuns) {
|
|
265
|
+
const safeIntents = Array.isArray(intents) ? intents : [];
|
|
266
|
+
const safeRuns = Array.isArray(runs) ? runs : [];
|
|
267
|
+
const safeActiveRuns = Array.isArray(activeRuns) ? activeRuns : [];
|
|
268
|
+
const workItems = safeIntents.flatMap((intent) => intent.workItems || []);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
totalIntents: safeIntents.length,
|
|
272
|
+
completedIntents: safeIntents.filter((intent) => intent.status === 'completed').length,
|
|
273
|
+
inProgressIntents: safeIntents.filter((intent) => intent.status === 'in_progress').length,
|
|
274
|
+
pendingIntents: safeIntents.filter((intent) => intent.status === 'pending').length,
|
|
275
|
+
blockedIntents: safeIntents.filter((intent) => intent.status === 'blocked').length,
|
|
276
|
+
totalWorkItems: workItems.length,
|
|
277
|
+
completedWorkItems: workItems.filter((item) => item.status === 'completed').length,
|
|
278
|
+
inProgressWorkItems: workItems.filter((item) => item.status === 'in_progress').length,
|
|
279
|
+
pendingWorkItems: workItems.filter((item) => item.status === 'pending').length,
|
|
280
|
+
blockedWorkItems: workItems.filter((item) => item.status === 'blocked').length,
|
|
281
|
+
totalRuns: safeRuns.length,
|
|
282
|
+
completedRuns: safeRuns.filter((run) => run.completedAt != null).length,
|
|
283
|
+
activeRunsCount: safeActiveRuns.length
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildPendingItems(intents) {
|
|
288
|
+
const pendingItems = [];
|
|
289
|
+
|
|
290
|
+
for (const intent of intents || []) {
|
|
291
|
+
for (const item of intent.workItems || []) {
|
|
292
|
+
if (item.status !== 'pending') {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
pendingItems.push({
|
|
297
|
+
id: item.id,
|
|
298
|
+
title: item.title,
|
|
299
|
+
intentId: intent.id,
|
|
300
|
+
intentTitle: intent.title,
|
|
301
|
+
mode: item.mode,
|
|
302
|
+
complexity: item.complexity,
|
|
303
|
+
dependencies: item.dependencies || [],
|
|
304
|
+
filePath: item.filePath
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
pendingItems.sort((a, b) => {
|
|
310
|
+
const depDiff = (a.dependencies?.length || 0) - (b.dependencies?.length || 0);
|
|
311
|
+
if (depDiff !== 0) {
|
|
312
|
+
return depDiff;
|
|
313
|
+
}
|
|
314
|
+
return a.id.localeCompare(b.id);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return pendingItems;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
normalizeStatus,
|
|
322
|
+
normalizeMode,
|
|
323
|
+
normalizeScope,
|
|
324
|
+
normalizeComplexity,
|
|
325
|
+
normalizeArray,
|
|
326
|
+
normalizeTimestamp,
|
|
327
|
+
parseDependencies,
|
|
328
|
+
normalizeRunWorkItem,
|
|
329
|
+
normalizeState,
|
|
330
|
+
deriveIntentStatus,
|
|
331
|
+
calculateStats,
|
|
332
|
+
buildPendingItems
|
|
333
|
+
};
|