specsmd 0.1.44 → 0.1.45
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/lib/dashboard/tui/app.js +277 -8
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -253,6 +253,247 @@ function getCurrentRun(snapshot) {
|
|
|
253
253
|
return activeRuns[0] || null;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
function normalizeToken(value) {
|
|
257
|
+
if (typeof value !== 'string') {
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
return value.toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCurrentFireWorkItem(run) {
|
|
264
|
+
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
265
|
+
if (workItems.length === 0) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return workItems.find((item) => item.id === run.currentItem)
|
|
269
|
+
|| workItems.find((item) => normalizeToken(item?.status) === 'in_progress')
|
|
270
|
+
|| workItems[0]
|
|
271
|
+
|| null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readFileTextSafe(filePath) {
|
|
275
|
+
try {
|
|
276
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extractFrontmatterBlock(content) {
|
|
283
|
+
if (typeof content !== 'string') {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
287
|
+
return match ? match[1] : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function extractFrontmatterValue(frontmatterBlock, key) {
|
|
291
|
+
if (typeof frontmatterBlock !== 'string' || typeof key !== 'string' || key === '') {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
296
|
+
const expression = new RegExp(`^${escapedKey}\\s*:\\s*(.+)$`, 'mi');
|
|
297
|
+
const match = frontmatterBlock.match(expression);
|
|
298
|
+
if (!match) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const raw = String(match[1] || '').trim();
|
|
303
|
+
if (raw === '') {
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return raw
|
|
308
|
+
.replace(/^["']/, '')
|
|
309
|
+
.replace(/["']$/, '')
|
|
310
|
+
.trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseFirePlanApprovalState(run) {
|
|
314
|
+
if (!run || typeof run.folderPath !== 'string' || run.folderPath.trim() === '') {
|
|
315
|
+
return { hasPlan: false, approved: false, checkpoint: null };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const planPath = path.join(run.folderPath, 'plan.md');
|
|
319
|
+
if (!fileExists(planPath)) {
|
|
320
|
+
return { hasPlan: false, approved: false, checkpoint: null };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const content = readFileTextSafe(planPath);
|
|
324
|
+
const frontmatter = extractFrontmatterBlock(content);
|
|
325
|
+
if (!frontmatter) {
|
|
326
|
+
return { hasPlan: true, approved: false, checkpoint: null };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const approvedAt = extractFrontmatterValue(frontmatter, 'approved_at');
|
|
330
|
+
const checkpoint = extractFrontmatterValue(frontmatter, 'checkpoint');
|
|
331
|
+
const missingTokens = new Set([
|
|
332
|
+
'',
|
|
333
|
+
'null',
|
|
334
|
+
'none',
|
|
335
|
+
'pending',
|
|
336
|
+
'unknown',
|
|
337
|
+
'n/a',
|
|
338
|
+
'false',
|
|
339
|
+
'no',
|
|
340
|
+
'assumed-from-user-n'
|
|
341
|
+
]);
|
|
342
|
+
const normalizedApprovedAt = normalizeToken(approvedAt || '');
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
hasPlan: true,
|
|
346
|
+
approved: !missingTokens.has(normalizedApprovedAt),
|
|
347
|
+
checkpoint: checkpoint || null
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isFireRunAwaitingApproval(run, currentWorkItem) {
|
|
352
|
+
const mode = normalizeToken(currentWorkItem?.mode);
|
|
353
|
+
const status = normalizeToken(currentWorkItem?.status);
|
|
354
|
+
if (!['confirm', 'validate'].includes(mode) || status !== 'in_progress') {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const phase = normalizeToken(getCurrentPhaseLabel(run, currentWorkItem));
|
|
359
|
+
if (phase !== 'plan') {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const planState = parseFirePlanApprovalState(run);
|
|
364
|
+
if (!planState.hasPlan) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return !planState.approved;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function detectFireRunApprovalGate(snapshot) {
|
|
372
|
+
const run = getCurrentRun(snapshot);
|
|
373
|
+
if (!run) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
378
|
+
if (!currentWorkItem) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!isFireRunAwaitingApproval(run, currentWorkItem)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
387
|
+
const itemId = String(currentWorkItem?.id || run.currentItem || 'unknown-item');
|
|
388
|
+
return {
|
|
389
|
+
flow: 'fire',
|
|
390
|
+
title: 'Approval Needed',
|
|
391
|
+
message: `${run.id}: ${itemId} (${mode}) is waiting at plan checkpoint`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeStageName(stage) {
|
|
396
|
+
return normalizeToken(stage).replace(/_/g, '-');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function getAidlcCheckpointSignalFiles(boltType, stageName) {
|
|
400
|
+
const normalizedType = normalizeToken(boltType).replace(/_/g, '-');
|
|
401
|
+
const normalizedStage = normalizeStageName(stageName);
|
|
402
|
+
|
|
403
|
+
if (normalizedType === 'simple-construction-bolt') {
|
|
404
|
+
if (normalizedStage === 'plan') return ['implementation-plan.md'];
|
|
405
|
+
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
406
|
+
if (normalizedStage === 'test') return ['test-walkthrough.md'];
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (normalizedType === 'ddd-construction-bolt') {
|
|
411
|
+
if (normalizedStage === 'model') return ['ddd-01-domain-model.md'];
|
|
412
|
+
if (normalizedStage === 'design') return ['ddd-02-technical-design.md'];
|
|
413
|
+
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
414
|
+
if (normalizedStage === 'test') return ['ddd-03-test-report.md'];
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (normalizedType === 'spike-bolt') {
|
|
419
|
+
if (normalizedStage === 'explore') return ['spike-exploration.md'];
|
|
420
|
+
if (normalizedStage === 'document') return ['spike-report.md'];
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function hasAidlcCheckpointSignal(bolt, stageName) {
|
|
428
|
+
const fileNames = Array.isArray(bolt?.files) ? bolt.files : [];
|
|
429
|
+
const lowerNames = new Set(fileNames.map((name) => String(name || '').toLowerCase()));
|
|
430
|
+
const expectedFiles = getAidlcCheckpointSignalFiles(bolt?.type, stageName)
|
|
431
|
+
.map((name) => String(name).toLowerCase());
|
|
432
|
+
|
|
433
|
+
for (const expectedFile of expectedFiles) {
|
|
434
|
+
if (lowerNames.has(expectedFile)) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (normalizeStageName(stageName) === 'adr') {
|
|
440
|
+
for (const name of lowerNames) {
|
|
441
|
+
if (/^adr-[\w-]+\.md$/.test(name)) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isAidlcBoltAwaitingApproval(bolt) {
|
|
451
|
+
if (!bolt || normalizeToken(bolt.status) !== 'in_progress') {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const currentStage = normalizeStageName(bolt.currentStage);
|
|
456
|
+
if (!currentStage) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
461
|
+
const stageMeta = stages.find((stage) => normalizeStageName(stage?.name) === currentStage);
|
|
462
|
+
if (normalizeToken(stageMeta?.status) === 'completed') {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return hasAidlcCheckpointSignal(bolt, currentStage);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function detectAidlcBoltApprovalGate(snapshot) {
|
|
470
|
+
const bolt = getCurrentBolt(snapshot);
|
|
471
|
+
if (!bolt) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!isAidlcBoltAwaitingApproval(bolt)) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
flow: 'aidlc',
|
|
481
|
+
title: 'Approval Needed',
|
|
482
|
+
message: `${bolt.id}: ${bolt.currentStage || 'current'} stage is waiting for confirmation`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function detectDashboardApprovalGate(snapshot, flow) {
|
|
487
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
488
|
+
if (effectiveFlow === 'fire') {
|
|
489
|
+
return detectFireRunApprovalGate(snapshot);
|
|
490
|
+
}
|
|
491
|
+
if (effectiveFlow === 'aidlc') {
|
|
492
|
+
return detectAidlcBoltApprovalGate(snapshot);
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
256
497
|
function getCurrentPhaseLabel(run, currentWorkItem) {
|
|
257
498
|
const phase = currentWorkItem?.currentPhase || '';
|
|
258
499
|
if (typeof phase === 'string' && phase !== '') {
|
|
@@ -1185,10 +1426,8 @@ function buildFireCurrentRunGroups(snapshot) {
|
|
|
1185
1426
|
|
|
1186
1427
|
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
1187
1428
|
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
1188
|
-
const currentWorkItem =
|
|
1189
|
-
|
|
1190
|
-
|| workItems[0]
|
|
1191
|
-
|| null;
|
|
1429
|
+
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
1430
|
+
const awaitingApproval = isFireRunAwaitingApproval(run, currentWorkItem);
|
|
1192
1431
|
|
|
1193
1432
|
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
1194
1433
|
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
@@ -1232,7 +1471,7 @@ function buildFireCurrentRunGroups(snapshot) {
|
|
|
1232
1471
|
return [
|
|
1233
1472
|
{
|
|
1234
1473
|
key: `current:run:${run.id}:summary`,
|
|
1235
|
-
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items`,
|
|
1474
|
+
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1236
1475
|
files: []
|
|
1237
1476
|
},
|
|
1238
1477
|
{
|
|
@@ -1258,9 +1497,10 @@ function buildCurrentGroups(snapshot, flow) {
|
|
|
1258
1497
|
}
|
|
1259
1498
|
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
1260
1499
|
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
1500
|
+
const awaitingApproval = isAidlcBoltAwaitingApproval(bolt);
|
|
1261
1501
|
return [{
|
|
1262
1502
|
key: `current:bolt:${bolt.id}`,
|
|
1263
|
-
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages`,
|
|
1503
|
+
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1264
1504
|
files: filterExistingFiles([
|
|
1265
1505
|
...collectAidlcBoltFiles(bolt),
|
|
1266
1506
|
...collectAidlcIntentContextFiles(snapshot, bolt.intent)
|
|
@@ -2403,6 +2643,10 @@ function createDashboardApp(deps) {
|
|
|
2403
2643
|
}, [showErrorPanelForSections]);
|
|
2404
2644
|
|
|
2405
2645
|
const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
|
|
2646
|
+
const approvalGate = detectDashboardApprovalGate(snapshot, activeFlow);
|
|
2647
|
+
const approvalGateLine = approvalGate
|
|
2648
|
+
? `[APPROVAL NEEDED] ${approvalGate.message}`
|
|
2649
|
+
: '';
|
|
2406
2650
|
const currentGroups = buildCurrentGroups(snapshot, activeFlow);
|
|
2407
2651
|
const currentExpandedGroups = { ...expandedGroups };
|
|
2408
2652
|
for (const group of currentGroups) {
|
|
@@ -2411,11 +2655,24 @@ function createDashboardApp(deps) {
|
|
|
2411
2655
|
}
|
|
2412
2656
|
}
|
|
2413
2657
|
|
|
2414
|
-
const
|
|
2658
|
+
const currentRunRowsBase = toExpandableRows(
|
|
2415
2659
|
currentGroups,
|
|
2416
2660
|
getNoCurrentMessage(effectiveFlow),
|
|
2417
2661
|
currentExpandedGroups
|
|
2418
2662
|
);
|
|
2663
|
+
const currentRunRows = approvalGate
|
|
2664
|
+
? [
|
|
2665
|
+
{
|
|
2666
|
+
kind: 'info',
|
|
2667
|
+
key: 'approval-gate',
|
|
2668
|
+
label: approvalGateLine,
|
|
2669
|
+
color: 'yellow',
|
|
2670
|
+
bold: true,
|
|
2671
|
+
selectable: false
|
|
2672
|
+
},
|
|
2673
|
+
...currentRunRowsBase
|
|
2674
|
+
]
|
|
2675
|
+
: currentRunRowsBase;
|
|
2419
2676
|
const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
|
|
2420
2677
|
const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
|
|
2421
2678
|
includeBacklog: shouldHydrateSecondaryTabs
|
|
@@ -3027,12 +3284,14 @@ function createDashboardApp(deps) {
|
|
|
3027
3284
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
3028
3285
|
const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
|
|
3029
3286
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
3287
|
+
const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp;
|
|
3030
3288
|
const showStatusLine = statusLine !== '';
|
|
3031
3289
|
const densePanels = rows <= 28 || cols <= 120;
|
|
3032
3290
|
|
|
3033
3291
|
const reservedRows =
|
|
3034
3292
|
2 +
|
|
3035
3293
|
(showFlowBar ? 1 : 0) +
|
|
3294
|
+
(showApprovalBanner ? 1 : 0) +
|
|
3036
3295
|
(showFooterHelpLine ? 1 : 0) +
|
|
3037
3296
|
(showGlobalErrorPanel ? 5 : 0) +
|
|
3038
3297
|
(showErrorInline ? 1 : 0) +
|
|
@@ -3266,6 +3525,13 @@ function createDashboardApp(deps) {
|
|
|
3266
3525
|
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
|
|
3267
3526
|
React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
|
|
3268
3527
|
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons, flow: activeFlow }),
|
|
3528
|
+
showApprovalBanner
|
|
3529
|
+
? React.createElement(
|
|
3530
|
+
Text,
|
|
3531
|
+
{ color: 'black', backgroundColor: 'yellow', bold: true },
|
|
3532
|
+
truncate(approvalGateLine, fullWidth)
|
|
3533
|
+
)
|
|
3534
|
+
: null,
|
|
3269
3535
|
showErrorInline
|
|
3270
3536
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
3271
3537
|
: null,
|
|
@@ -3300,5 +3566,8 @@ module.exports = {
|
|
|
3300
3566
|
truncate,
|
|
3301
3567
|
fitLines,
|
|
3302
3568
|
safeJsonHash,
|
|
3303
|
-
allocateSingleColumnPanels
|
|
3569
|
+
allocateSingleColumnPanels,
|
|
3570
|
+
detectDashboardApprovalGate,
|
|
3571
|
+
detectFireRunApprovalGate,
|
|
3572
|
+
detectAidlcBoltApprovalGate
|
|
3304
3573
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
4
4
|
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
6
|
"bin": {
|