specsmd 0.1.43 → 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 +374 -40
- 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)
|
|
@@ -2218,28 +2458,58 @@ function createDashboardApp(deps) {
|
|
|
2218
2458
|
const primaryLabel = effectiveFlow === 'aidlc' ? 'BOLTS' : (effectiveFlow === 'simple' ? 'SPECS' : 'RUNS');
|
|
2219
2459
|
const completedLabel = effectiveFlow === 'aidlc' ? 'COMPLETED BOLTS' : (effectiveFlow === 'simple' ? 'COMPLETED SPECS' : 'COMPLETED RUNS');
|
|
2220
2460
|
const tabs = [
|
|
2221
|
-
{ id: 'runs', label: `
|
|
2222
|
-
{ id: 'intents', label: `
|
|
2223
|
-
{ id: 'completed', label: `
|
|
2224
|
-
{ id: 'health', label: `
|
|
2461
|
+
{ id: 'runs', label: `1 ${icons.runs} ${primaryLabel}` },
|
|
2462
|
+
{ id: 'intents', label: `2 ${icons.overview} INTENTS` },
|
|
2463
|
+
{ id: 'completed', label: `3 ${icons.runs} ${completedLabel}` },
|
|
2464
|
+
{ id: 'health', label: `4 ${icons.health} STANDARDS/HEALTH` }
|
|
2225
2465
|
];
|
|
2466
|
+
const maxWidth = Math.max(8, Math.floor(width));
|
|
2467
|
+
const segments = [];
|
|
2468
|
+
let consumed = 0;
|
|
2469
|
+
|
|
2470
|
+
for (const tab of tabs) {
|
|
2471
|
+
const isActive = tab.id === view;
|
|
2472
|
+
const segmentText = isActive ? `[${tab.label}]` : tab.label;
|
|
2473
|
+
const separator = segments.length > 0 ? ' ' : '';
|
|
2474
|
+
const segmentWidth = stringWidth(separator) + stringWidth(segmentText);
|
|
2475
|
+
if (consumed + segmentWidth > maxWidth) {
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (separator !== '') {
|
|
2480
|
+
segments.push({
|
|
2481
|
+
key: `${tab.id}:sep`,
|
|
2482
|
+
text: separator,
|
|
2483
|
+
active: false
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
segments.push({
|
|
2487
|
+
key: tab.id,
|
|
2488
|
+
text: segmentText,
|
|
2489
|
+
active: isActive
|
|
2490
|
+
});
|
|
2491
|
+
consumed += segmentWidth;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
if (segments.length === 0) {
|
|
2495
|
+
const fallback = tabs.find((tab) => tab.id === view) || tabs[0];
|
|
2496
|
+
const fallbackText = truncate(`[${fallback.label}]`, maxWidth);
|
|
2497
|
+
return React.createElement(Text, { color: 'white', bold: true }, fallbackText);
|
|
2498
|
+
}
|
|
2226
2499
|
|
|
2227
2500
|
return React.createElement(
|
|
2228
2501
|
Box,
|
|
2229
|
-
{ width, flexWrap: 'nowrap' },
|
|
2230
|
-
...
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
tab.label
|
|
2241
|
-
);
|
|
2242
|
-
})
|
|
2502
|
+
{ width: maxWidth, flexWrap: 'nowrap' },
|
|
2503
|
+
...segments.map((segment) => React.createElement(
|
|
2504
|
+
Text,
|
|
2505
|
+
{
|
|
2506
|
+
key: segment.key,
|
|
2507
|
+
bold: segment.active,
|
|
2508
|
+
color: segment.active ? 'white' : 'gray',
|
|
2509
|
+
backgroundColor: segment.active ? 'blue' : undefined
|
|
2510
|
+
},
|
|
2511
|
+
segment.text
|
|
2512
|
+
))
|
|
2243
2513
|
);
|
|
2244
2514
|
}
|
|
2245
2515
|
|
|
@@ -2248,23 +2518,52 @@ function createDashboardApp(deps) {
|
|
|
2248
2518
|
if (!Array.isArray(flowIds) || flowIds.length <= 1) {
|
|
2249
2519
|
return null;
|
|
2250
2520
|
}
|
|
2521
|
+
const maxWidth = Math.max(8, Math.floor(width));
|
|
2522
|
+
const segments = [];
|
|
2523
|
+
let consumed = 0;
|
|
2524
|
+
|
|
2525
|
+
for (const flowId of flowIds) {
|
|
2526
|
+
const isActive = flowId === activeFlow;
|
|
2527
|
+
const segmentText = isActive ? `[${flowId.toUpperCase()}]` : flowId.toUpperCase();
|
|
2528
|
+
const separator = segments.length > 0 ? ' ' : '';
|
|
2529
|
+
const segmentWidth = stringWidth(separator) + stringWidth(segmentText);
|
|
2530
|
+
if (consumed + segmentWidth > maxWidth) {
|
|
2531
|
+
break;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
if (separator !== '') {
|
|
2535
|
+
segments.push({
|
|
2536
|
+
key: `${flowId}:sep`,
|
|
2537
|
+
text: separator,
|
|
2538
|
+
active: false
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
segments.push({
|
|
2542
|
+
key: flowId,
|
|
2543
|
+
text: segmentText,
|
|
2544
|
+
active: isActive
|
|
2545
|
+
});
|
|
2546
|
+
consumed += segmentWidth;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (segments.length === 0) {
|
|
2550
|
+
const fallback = (activeFlow || flowIds[0] || 'flow').toUpperCase();
|
|
2551
|
+
return React.createElement(Text, { color: 'black', backgroundColor: 'green', bold: true }, truncate(`[${fallback}]`, maxWidth));
|
|
2552
|
+
}
|
|
2251
2553
|
|
|
2252
2554
|
return React.createElement(
|
|
2253
2555
|
Box,
|
|
2254
|
-
{ width, flexWrap: 'nowrap' },
|
|
2255
|
-
...
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
` ${flowId.toUpperCase()} `
|
|
2266
|
-
);
|
|
2267
|
-
})
|
|
2556
|
+
{ width: maxWidth, flexWrap: 'nowrap' },
|
|
2557
|
+
...segments.map((segment) => React.createElement(
|
|
2558
|
+
Text,
|
|
2559
|
+
{
|
|
2560
|
+
key: segment.key,
|
|
2561
|
+
bold: segment.active,
|
|
2562
|
+
color: segment.active ? 'black' : 'gray',
|
|
2563
|
+
backgroundColor: segment.active ? 'green' : undefined
|
|
2564
|
+
},
|
|
2565
|
+
segment.text
|
|
2566
|
+
))
|
|
2268
2567
|
);
|
|
2269
2568
|
}
|
|
2270
2569
|
|
|
@@ -2344,6 +2643,10 @@ function createDashboardApp(deps) {
|
|
|
2344
2643
|
}, [showErrorPanelForSections]);
|
|
2345
2644
|
|
|
2346
2645
|
const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
|
|
2646
|
+
const approvalGate = detectDashboardApprovalGate(snapshot, activeFlow);
|
|
2647
|
+
const approvalGateLine = approvalGate
|
|
2648
|
+
? `[APPROVAL NEEDED] ${approvalGate.message}`
|
|
2649
|
+
: '';
|
|
2347
2650
|
const currentGroups = buildCurrentGroups(snapshot, activeFlow);
|
|
2348
2651
|
const currentExpandedGroups = { ...expandedGroups };
|
|
2349
2652
|
for (const group of currentGroups) {
|
|
@@ -2352,11 +2655,24 @@ function createDashboardApp(deps) {
|
|
|
2352
2655
|
}
|
|
2353
2656
|
}
|
|
2354
2657
|
|
|
2355
|
-
const
|
|
2658
|
+
const currentRunRowsBase = toExpandableRows(
|
|
2356
2659
|
currentGroups,
|
|
2357
2660
|
getNoCurrentMessage(effectiveFlow),
|
|
2358
2661
|
currentExpandedGroups
|
|
2359
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;
|
|
2360
2676
|
const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
|
|
2361
2677
|
const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
|
|
2362
2678
|
includeBacklog: shouldHydrateSecondaryTabs
|
|
@@ -2888,6 +3204,12 @@ function createDashboardApp(deps) {
|
|
|
2888
3204
|
columns: stdout.columns || process.stdout.columns || 120,
|
|
2889
3205
|
rows: stdout.rows || process.stdout.rows || 40
|
|
2890
3206
|
});
|
|
3207
|
+
|
|
3208
|
+
// Resize in some terminals can leave stale frame rows behind.
|
|
3209
|
+
// Force clear so next render paints from a clean origin.
|
|
3210
|
+
if (typeof stdout.write === 'function' && stdout.isTTY !== false) {
|
|
3211
|
+
stdout.write('\u001B[2J\u001B[3J\u001B[H');
|
|
3212
|
+
}
|
|
2891
3213
|
};
|
|
2892
3214
|
|
|
2893
3215
|
updateSize();
|
|
@@ -2962,12 +3284,14 @@ function createDashboardApp(deps) {
|
|
|
2962
3284
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
2963
3285
|
const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
|
|
2964
3286
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
3287
|
+
const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp;
|
|
2965
3288
|
const showStatusLine = statusLine !== '';
|
|
2966
3289
|
const densePanels = rows <= 28 || cols <= 120;
|
|
2967
3290
|
|
|
2968
3291
|
const reservedRows =
|
|
2969
3292
|
2 +
|
|
2970
3293
|
(showFlowBar ? 1 : 0) +
|
|
3294
|
+
(showApprovalBanner ? 1 : 0) +
|
|
2971
3295
|
(showFooterHelpLine ? 1 : 0) +
|
|
2972
3296
|
(showGlobalErrorPanel ? 5 : 0) +
|
|
2973
3297
|
(showErrorInline ? 1 : 0) +
|
|
@@ -3201,6 +3525,13 @@ function createDashboardApp(deps) {
|
|
|
3201
3525
|
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
|
|
3202
3526
|
React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
|
|
3203
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,
|
|
3204
3535
|
showErrorInline
|
|
3205
3536
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
3206
3537
|
: null,
|
|
@@ -3235,5 +3566,8 @@ module.exports = {
|
|
|
3235
3566
|
truncate,
|
|
3236
3567
|
fitLines,
|
|
3237
3568
|
safeJsonHash,
|
|
3238
|
-
allocateSingleColumnPanels
|
|
3569
|
+
allocateSingleColumnPanels,
|
|
3570
|
+
detectDashboardApprovalGate,
|
|
3571
|
+
detectFireRunApprovalGate,
|
|
3572
|
+
detectAidlcBoltApprovalGate
|
|
3239
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": {
|