opencastle 0.3.1 → 0.4.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.
Files changed (32) hide show
  1. package/bin/cli.mjs +0 -0
  2. package/dist/cli/dashboard.d.ts.map +1 -1
  3. package/dist/cli/dashboard.js +2 -1
  4. package/dist/cli/dashboard.js.map +1 -1
  5. package/dist/cli/init.d.ts.map +1 -1
  6. package/dist/cli/init.js +6 -2
  7. package/dist/cli/init.js.map +1 -1
  8. package/dist/cli/mcp.d.ts.map +1 -1
  9. package/dist/cli/mcp.js +21 -0
  10. package/dist/cli/mcp.js.map +1 -1
  11. package/dist/cli/stack-config.js +4 -4
  12. package/dist/cli/stack-config.js.map +1 -1
  13. package/package.json +2 -2
  14. package/src/cli/dashboard.ts +2 -1
  15. package/src/cli/init.ts +7 -2
  16. package/src/cli/mcp.ts +30 -1
  17. package/src/cli/stack-config.ts +4 -4
  18. package/src/dashboard/package.json +1 -0
  19. package/src/dashboard/src/pages/index.astro +163 -59
  20. package/src/dashboard/src/styles/dashboard.css +261 -0
  21. package/src/dashboard/tsconfig.json +1 -1
  22. package/src/orchestrator/agents/release-manager.agent.md +1 -1
  23. package/src/orchestrator/agents/team-lead.agent.md +13 -1
  24. package/src/orchestrator/customizations/stack/notifications-config.md +57 -0
  25. package/src/orchestrator/instructions/general.instructions.md +31 -2
  26. package/src/orchestrator/mcp.json +12 -6
  27. package/src/orchestrator/skills/agent-hooks/SKILL.md +13 -7
  28. package/src/orchestrator/skills/session-checkpoints/SKILL.md +12 -0
  29. package/src/orchestrator/skills/slack-notifications/SKILL.md +139 -39
  30. /package/src/dashboard/{public/data → seed-data}/delegations.ndjson +0 -0
  31. /package/src/dashboard/{public/data → seed-data}/panels.ndjson +0 -0
  32. /package/src/dashboard/{public/data → seed-data}/sessions.ndjson +0 -0
@@ -246,10 +246,84 @@ const base = import.meta.env.BASE_URL;
246
246
  return div.innerHTML;
247
247
  }
248
248
 
249
+ // ── SVG Icon Library (empty states) ────────────────────
250
+
251
+ const EMPTY_ICONS = {
252
+ welcome: '<svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M24 4L6 14v20l18 10 18-10V14L24 4z" opacity="0.3"/><path d="M24 4L6 14v20l18 10 18-10V14L24 4z"/><path d="M24 24v20"/><path d="M6 14l18 10 18-10"/><rect x="18" y="18" width="12" height="14" rx="1" opacity="0.5"/><path d="M21 32v-6h6v6"/></svg>',
253
+ agents: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="20" cy="14" r="6"/><path d="M8 34c0-6.627 5.373-12 12-12s12 5.373 12 12"/><circle cx="32" cy="12" r="4" opacity="0.4"/><path d="M36 26c0-3.5-2-6-4-7" opacity="0.4"/></svg>',
254
+ tiers: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="20" cy="10" rx="14" ry="5"/><path d="M6 10v8c0 2.761 6.268 5 14 5s14-2.239 14-5v-8"/><path d="M6 18v8c0 2.761 6.268 5 14 5s14-2.239 14-5v-8" opacity="0.5"/></svg>',
255
+ mechanism: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="20" cy="20" r="6"/><path d="M20 6v6M20 28v6M6 20h6M28 20h6"/><path d="M10.1 10.1l4.2 4.2M25.7 25.7l4.2 4.2M10.1 29.9l4.2-4.2M25.7 14.3l4.2-4.2" opacity="0.5"/></svg>',
256
+ outcomes: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="28" height="28" rx="4"/><polyline points="12 22 18 28 28 14" opacity="0.5"/><line x1="12" y1="16" x2="18" y2="16" opacity="0.3"/><line x1="12" y1="12" x2="24" y2="12" opacity="0.3"/></svg>',
257
+ timeline: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="8" width="28" height="26" rx="3"/><line x1="6" y1="16" x2="34" y2="16"/><line x1="14" y1="8" x2="14" y2="12"/><line x1="26" y1="8" x2="26" y2="12"/><circle cx="14" cy="24" r="2" opacity="0.4"/><circle cx="20" cy="28" r="2" opacity="0.4"/><circle cx="26" cy="22" r="2" opacity="0.4"/></svg>',
258
+ models: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="24" height="24" rx="4"/><circle cx="20" cy="20" r="4"/><path d="M20 12v4M20 24v4M12 20h4M24 20h4" opacity="0.5"/><circle cx="14" cy="14" r="1.5" opacity="0.3"/><circle cx="26" cy="14" r="1.5" opacity="0.3"/><circle cx="14" cy="26" r="1.5" opacity="0.3"/><circle cx="26" cy="26" r="1.5" opacity="0.3"/></svg>',
259
+ execLog: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 8h18a2 2 0 0 1 2 2v20a2 2 0 0 1-2 2H14"/><circle cx="10" cy="14" r="3"/><circle cx="10" cy="24" r="3" opacity="0.4"/><line x1="10" y1="17" x2="10" y2="21" opacity="0.3"/><line x1="18" y1="14" x2="28" y2="14" opacity="0.5"/><line x1="18" y1="24" x2="26" y2="24" opacity="0.3"/><line x1="18" y1="19" x2="24" y2="19" opacity="0.2"/></svg>',
260
+ panels: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6l2 4h4l-3 3 1 5-4-2.5L16 18l1-5-3-3h4l2-4z"/><rect x="8" y="22" width="24" height="12" rx="3" opacity="0.4"/><line x1="14" y1="28" x2="26" y2="28" opacity="0.3"/><line x1="14" y1="31" x2="22" y2="31" opacity="0.2"/></svg>',
261
+ sessions: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="28" height="28" rx="3"/><line x1="6" y1="14" x2="34" y2="14"/><line x1="14" y1="6" x2="14" y2="34" opacity="0.3"/><line x1="6" y1="22" x2="34" y2="22" opacity="0.2"/><line x1="6" y1="30" x2="34" y2="30" opacity="0.2"/></svg>',
262
+ pipeline: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="14" width="8" height="12" rx="2" opacity="0.3"/><rect x="16" y="14" width="8" height="12" rx="2" opacity="0.3"/><rect x="28" y="14" width="8" height="12" rx="2" opacity="0.3"/><path d="M12 20h4M24 20h4" opacity="0.4"/></svg>',
263
+ };
264
+
265
+ function emptyStateHtml(iconKey, title, description) {
266
+ return '<div class="empty-state empty-state--enhanced">' +
267
+ '<div class="empty-state__icon-wrap">' + EMPTY_ICONS[iconKey] + '</div>' +
268
+ '<p class="empty-state__title">' + title + '</p>' +
269
+ '<p class="empty-state__desc">' + description + '</p>' +
270
+ '</div>';
271
+ }
272
+
273
+ // ── Welcome Banner (all data empty) ────────────────────
274
+
275
+ function renderWelcomeBanner() {
276
+ const existing = document.getElementById('welcome-banner');
277
+ if (existing) existing.remove();
278
+
279
+ const banner = document.createElement('section');
280
+ banner.id = 'welcome-banner';
281
+ banner.className = 'welcome-banner';
282
+ banner.innerHTML =
283
+ '<div class="welcome-banner__glow"></div>' +
284
+ '<div class="welcome-banner__content">' +
285
+ '<div class="welcome-banner__icon">' + EMPTY_ICONS.welcome + '</div>' +
286
+ '<h2 class="welcome-banner__title">Welcome to your Observability Dashboard</h2>' +
287
+ '<p class="welcome-banner__subtitle">Agent sessions, delegations, and quality reviews will appear here as your team completes work.</p>' +
288
+ '<div class="welcome-banner__steps">' +
289
+ '<div class="welcome-step">' +
290
+ '<span class="welcome-step__num">1</span>' +
291
+ '<div class="welcome-step__text">' +
292
+ '<strong>Configure agents</strong>' +
293
+ '<span>Set up your orchestrator and specialist agents</span>' +
294
+ '</div>' +
295
+ '</div>' +
296
+ '<div class="welcome-step">' +
297
+ '<span class="welcome-step__num">2</span>' +
298
+ '<div class="welcome-step__text">' +
299
+ '<strong>Run a session</strong>' +
300
+ '<span>Delegate tasks — session logs are captured automatically</span>' +
301
+ '</div>' +
302
+ '</div>' +
303
+ '<div class="welcome-step">' +
304
+ '<span class="welcome-step__num">3</span>' +
305
+ '<div class="welcome-step__text">' +
306
+ '<strong>Watch insights flow in</strong>' +
307
+ '<span>Charts, metrics, and trends populate in real time</span>' +
308
+ '</div>' +
309
+ '</div>' +
310
+ '</div>' +
311
+ '</div>';
312
+
313
+ const main = document.querySelector('.dash-main');
314
+ if (main) main.prepend(banner);
315
+ }
316
+
317
+ function removeWelcomeBanner() {
318
+ const existing = document.getElementById('welcome-banner');
319
+ if (existing) existing.remove();
320
+ }
321
+
249
322
  // ── KPI Rendering ────────────────────────────────────────
250
323
 
251
324
  function renderKpis(sessions, delegations) {
252
325
  const total = sessions.length;
326
+ const isEmpty = total === 0;
253
327
  const successCount = sessions.filter((s) => s.outcome === 'success').length;
254
328
  const rate = total > 0 ? Math.round((successCount / total) * 100) : 0;
255
329
  const durSessions = sessions.filter((s) => s.duration_min != null);
@@ -262,50 +336,61 @@ const base = import.meta.env.BASE_URL;
262
336
  : 0;
263
337
  const uniqueAgents = new Set(delegations.map((d) => d.agent)).size;
264
338
 
339
+ // Toggle ghost class on KPI row
340
+ const kpiRow = document.querySelector('.kpi-row');
341
+ if (kpiRow) kpiRow.classList.toggle('kpi-row--empty', isEmpty);
342
+
265
343
  const kpiSessions = document.getElementById('kpi-sessions');
266
344
  const kpiSuccess = document.getElementById('kpi-success');
267
345
  const kpiDelegations = document.getElementById('kpi-delegations');
268
346
  const kpiDuration = document.getElementById('kpi-duration');
269
347
 
270
348
  if (kpiSessions) {
271
- kpiSessions.querySelector('.kpi-card__value').textContent = total;
272
- kpiSessions.querySelector('.kpi-card__sub').innerHTML =
273
- '<span class="kpi-trend kpi-trend--up">\u2191</span> ' +
274
- successCount +
275
- ' successful';
349
+ kpiSessions.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : total;
350
+ kpiSessions.querySelector('.kpi-card__sub').innerHTML = isEmpty
351
+ ? '<span class="kpi-card__hint">No sessions yet</span>'
352
+ : '<span class="kpi-trend kpi-trend--up">\u2191</span> ' + successCount + ' successful';
276
353
  }
277
354
  if (kpiSuccess) {
278
- const trendClass =
279
- rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
280
- kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
281
- kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
282
- '<span class="kpi-trend kpi-trend--' +
283
- trendClass +
284
- '">' +
285
- (trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
286
- '</span> across all sessions';
355
+ if (isEmpty) {
356
+ kpiSuccess.querySelector('.kpi-card__value').textContent = '\u2014';
357
+ kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
358
+ '<span class="kpi-card__hint">No sessions yet</span>';
359
+ } else {
360
+ const trendClass =
361
+ rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
362
+ kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
363
+ kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
364
+ '<span class="kpi-trend kpi-trend--' +
365
+ trendClass +
366
+ '">' +
367
+ (trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
368
+ '</span> across all sessions';
369
+ }
287
370
  }
288
371
  if (kpiDelegations) {
289
372
  kpiDelegations.querySelector('.kpi-card__value').textContent =
290
- delegations.length;
291
- kpiDelegations.querySelector('.kpi-card__sub').textContent =
292
- uniqueAgents + ' unique agents';
373
+ delegations.length === 0 ? '0' : delegations.length;
374
+ kpiDelegations.querySelector('.kpi-card__sub').innerHTML = isEmpty
375
+ ? '<span class="kpi-card__hint">No delegations yet</span>'
376
+ : uniqueAgents + ' unique agents';
293
377
  }
294
378
  if (kpiDuration) {
295
- kpiDuration.querySelector('.kpi-card__value').textContent =
296
- avgDur + 'm';
297
- kpiDuration.querySelector('.kpi-card__sub').innerHTML =
298
- '<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
379
+ kpiDuration.querySelector('.kpi-card__value').textContent = isEmpty ? '\u2014' : avgDur + 'm';
380
+ kpiDuration.querySelector('.kpi-card__sub').innerHTML = isEmpty
381
+ ? '<span class="kpi-card__hint">No duration yet</span>'
382
+ : '<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
299
383
  }
300
384
 
301
385
  // Retries KPI
302
386
  const totalRetries = sessions.reduce((sum, s) => sum + (s.retries || 0), 0);
303
387
  const kpiRetries = document.getElementById('kpi-retries');
304
388
  if (kpiRetries) {
305
- kpiRetries.querySelector('.kpi-card__value').textContent = totalRetries;
389
+ kpiRetries.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalRetries;
306
390
  const retriedSessions = sessions.filter((s) => (s.retries || 0) > 0).length;
307
- kpiRetries.querySelector('.kpi-card__sub').textContent =
308
- retriedSessions + ' sessions with retries';
391
+ kpiRetries.querySelector('.kpi-card__sub').innerHTML = isEmpty
392
+ ? '<span class="kpi-card__hint">No retries yet</span>'
393
+ : retriedSessions + ' sessions with retries';
309
394
  }
310
395
 
311
396
  // Lessons KPI
@@ -315,13 +400,14 @@ const base = import.meta.env.BASE_URL;
315
400
  );
316
401
  const kpiLessons = document.getElementById('kpi-lessons');
317
402
  if (kpiLessons) {
318
- kpiLessons.querySelector('.kpi-card__value').textContent = totalLessons;
403
+ kpiLessons.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalLessons;
319
404
  const discoveryCount = sessions.reduce(
320
405
  (sum, s) => sum + (s.discoveries ? s.discoveries.length : 0),
321
406
  0
322
407
  );
323
- kpiLessons.querySelector('.kpi-card__sub').textContent =
324
- discoveryCount + ' issues discovered';
408
+ kpiLessons.querySelector('.kpi-card__sub').innerHTML = isEmpty
409
+ ? '<span class="kpi-card__hint">No lessons yet</span>'
410
+ : discoveryCount + ' issues discovered';
325
411
  }
326
412
  }
327
413
 
@@ -331,6 +417,11 @@ const base = import.meta.env.BASE_URL;
331
417
  const el = document.getElementById('pipeline-view');
332
418
  if (!el) return;
333
419
 
420
+ if (delegations.length === 0) {
421
+ el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Delegation phases appear here as tasks flow through Foundation, Integration, Validation, and QA stages.');
422
+ return;
423
+ }
424
+
334
425
  const phases = { 1: 0, 2: 0, 3: 0, 4: 0 };
335
426
  delegations.forEach((d) => {
336
427
  const p = d.phase || 1;
@@ -395,8 +486,7 @@ const base = import.meta.env.BASE_URL;
395
486
  if (!el) return;
396
487
 
397
488
  if (sessions.length === 0) {
398
- el.innerHTML =
399
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCA</div><p class="empty-state__text">No session data available</p></div>';
489
+ el.innerHTML = emptyStateHtml('agents', 'No agent sessions yet', 'A breakdown of sessions per agent will appear here — stacked by outcome (success, partial, failed).');
400
490
  return;
401
491
  }
402
492
 
@@ -421,15 +511,18 @@ const base = import.meta.env.BASE_URL;
421
511
  escapeHtml(name) +
422
512
  '</span>' +
423
513
  '<div class="bar-track">' +
424
- '<div class="bar-segment bar--success" style="width: ' +
425
- ((data.success / maxTotal) * 100).toFixed(1) +
426
- '%"></div>' +
427
- '<div class="bar-segment bar--partial" style="width: ' +
428
- ((data.partial / maxTotal) * 100).toFixed(1) +
429
- '%"></div>' +
430
- '<div class="bar-segment bar--failed" style="width: ' +
431
- ((data.failed / maxTotal) * 100).toFixed(1) +
432
- '%"></div>' +
514
+ (data.success > 0
515
+ ? '<div class="bar-segment bar--success" style="width: ' +
516
+ ((data.success / maxTotal) * 100).toFixed(1) + '%"></div>'
517
+ : '') +
518
+ (data.partial > 0
519
+ ? '<div class="bar-segment bar--partial" style="width: ' +
520
+ ((data.partial / maxTotal) * 100).toFixed(1) + '%"></div>'
521
+ : '') +
522
+ (data.failed > 0
523
+ ? '<div class="bar-segment bar--failed" style="width: ' +
524
+ ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>'
525
+ : '') +
433
526
  '</div>' +
434
527
  '<span class="bar-value">' +
435
528
  data.total +
@@ -446,8 +539,7 @@ const base = import.meta.env.BASE_URL;
446
539
  if (!el) return;
447
540
 
448
541
  if (delegations.length === 0) {
449
- el.innerHTML =
450
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDFE3</div><p class="empty-state__text">No delegation data available</p></div>';
542
+ el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution (Premium, Standard, Utility, Economy) will be visualized as a donut chart.');
451
543
  return;
452
544
  }
453
545
 
@@ -469,6 +561,8 @@ const base = import.meta.env.BASE_URL;
469
561
  const circles = tiers.map((t) => {
470
562
  const pct = t.count / total;
471
563
  const dashLen = pct * circumference;
564
+ // Skip round linecap for single-segment donuts to avoid overlap artifact
565
+ const linecap = tiers.length === 1 ? 'butt' : 'round';
472
566
  const segment =
473
567
  '<circle cx="90" cy="90" r="' +
474
568
  r +
@@ -485,7 +579,7 @@ const base = import.meta.env.BASE_URL;
485
579
  (-cumOffset).toFixed(2) +
486
580
  '" ' +
487
581
  'transform="rotate(-90 90 90)" ' +
488
- 'stroke-linecap="round"/>';
582
+ 'stroke-linecap="' + linecap + '"/>';
489
583
  cumOffset += dashLen;
490
584
  return segment;
491
585
  });
@@ -535,8 +629,7 @@ const base = import.meta.env.BASE_URL;
535
629
  if (!el) return;
536
630
 
537
631
  if (delegations.length === 0) {
538
- el.innerHTML =
539
- '<div class="empty-state"><div class="empty-state__icon">\u2699\uFE0F</div><p class="empty-state__text">No delegation data available</p></div>';
632
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent (inline) and background (worktree) delegations will be shown here.');
540
633
  return;
541
634
  }
542
635
 
@@ -571,12 +664,14 @@ const base = import.meta.env.BASE_URL;
571
664
  var circles = mechs.map(function (m) {
572
665
  var pct = m.count / total;
573
666
  var dashLen = pct * circumference;
667
+ // Skip round linecap for single-segment donuts to avoid overlap artifact
668
+ var linecap = mechs.length === 1 ? 'butt' : 'round';
574
669
  var segment =
575
670
  '<circle cx="90" cy="90" r="' + r + '" fill="none" ' +
576
671
  'stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" ' +
577
672
  'stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" ' +
578
673
  'stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" ' +
579
- 'transform="rotate(-90 90 90)" stroke-linecap="round"/>';
674
+ 'transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
580
675
  cumOffset += dashLen;
581
676
  return segment;
582
677
  });
@@ -615,8 +710,7 @@ const base = import.meta.env.BASE_URL;
615
710
  if (!el) return;
616
711
 
617
712
  if (delegations.length === 0) {
618
- el.innerHTML =
619
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCA</div><p class="empty-state__text">No delegation data available</p></div>';
713
+ el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Delegation results — success, partial, failed, redirected — will be tracked and compared here.');
620
714
  return;
621
715
  }
622
716
 
@@ -674,8 +768,7 @@ const base = import.meta.env.BASE_URL;
674
768
  );
675
769
 
676
770
  if (dates.length === 0) {
677
- el.innerHTML =
678
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCC5</div><p class="empty-state__text">No timeline data</p></div>';
771
+ el.innerHTML = emptyStateHtml('timeline', 'No timeline data yet', 'A daily activity chart will build here as sessions and delegations accumulate over time.');
679
772
  return;
680
773
  }
681
774
 
@@ -687,14 +780,21 @@ const base = import.meta.env.BASE_URL;
687
780
  const pad = { top: 10, right: 10, bottom: 28, left: 10 };
688
781
  const plotW = w - pad.left - pad.right;
689
782
  const plotH = h - pad.top - pad.bottom;
690
- const groupWidth = plotW / dates.length;
691
- const barWidth = Math.min(16, groupWidth * 0.35);
783
+ // Prevent sparse layout when there are very few dates
784
+ const groupWidth = dates.length <= 3
785
+ ? Math.min(100, plotW / dates.length)
786
+ : plotW / dates.length;
787
+ const barWidth = Math.min(dates.length <= 3 ? 24 : 16, groupWidth * 0.35);
788
+ // Center the bars when there are few dates
789
+ const timelineStartX = dates.length <= 3
790
+ ? pad.left + (plotW - dates.length * groupWidth) / 2
791
+ : pad.left;
692
792
 
693
793
  let rects = '';
694
794
  let labels = '';
695
795
 
696
796
  dates.forEach(([date, data], i) => {
697
- const x = pad.left + i * groupWidth + groupWidth / 2;
797
+ const x = timelineStartX + i * groupWidth + groupWidth / 2;
698
798
  const sH = maxVal > 0 ? (data.sessions / maxVal) * plotH : 0;
699
799
  const dH = maxVal > 0 ? (data.delegations / maxVal) * plotH : 0;
700
800
 
@@ -754,8 +854,7 @@ const base = import.meta.env.BASE_URL;
754
854
  if (!el) return;
755
855
 
756
856
  if (sessions.length === 0) {
757
- el.innerHTML =
758
- '<div class="empty-state"><div class="empty-state__icon">\uD83E\uDD16</div><p class="empty-state__text">No model data</p></div>';
857
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across sessions — Claude Opus, GPT-5, Gemini, etc. — will be compared here.');
759
858
  return;
760
859
  }
761
860
 
@@ -801,8 +900,7 @@ const base = import.meta.env.BASE_URL;
801
900
  .slice(0, 10);
802
901
 
803
902
  if (sorted.length === 0) {
804
- el.innerHTML =
805
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCDD</div><p class="empty-state__text">No sessions recorded yet</p></div>';
903
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of agent activity — with outcomes, durations, and metadata — will appear here.');
806
904
  return;
807
905
  }
808
906
 
@@ -885,8 +983,7 @@ const base = import.meta.env.BASE_URL;
885
983
  if (!el) return;
886
984
 
887
985
  if (panels.length === 0) {
888
- el.innerHTML =
889
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDEE1\uFE0F</div><p class="empty-state__text">No panel reviews yet</p></div>';
986
+ el.innerHTML = emptyStateHtml('panels', 'No panel reviews yet', 'Quality gate verdicts from majority-vote panels — with pass/block counts and must-fix items — will be shown here.');
890
987
  return;
891
988
  }
892
989
 
@@ -951,8 +1048,7 @@ const base = import.meta.env.BASE_URL;
951
1048
  .slice(0, 15);
952
1049
 
953
1050
  if (sorted.length === 0) {
954
- el.innerHTML =
955
- '<div class="empty-state"><div class="empty-state__icon">\uD83D\uDCCB</div><p class="empty-state__text">No sessions recorded</p></div>';
1051
+ el.innerHTML = emptyStateHtml('sessions', 'No session records yet', 'A detailed table of recent sessions — with timestamps, agents, tasks, outcomes, and linked issues — will populate here.');
956
1052
  return;
957
1053
  }
958
1054
 
@@ -1014,6 +1110,14 @@ const base = import.meta.env.BASE_URL;
1014
1110
  loadNdjson(base + 'data/panels.ndjson'),
1015
1111
  ]);
1016
1112
 
1113
+ // Show/hide welcome banner
1114
+ const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0;
1115
+ if (allEmpty) {
1116
+ renderWelcomeBanner();
1117
+ } else {
1118
+ removeWelcomeBanner();
1119
+ }
1120
+
1017
1121
  renderKpis(sessions, delegations);
1018
1122
  renderPipeline(delegations);
1019
1123
  renderAgentChart(sessions);
@@ -991,6 +991,267 @@ body {
991
991
  max-width: 320px;
992
992
  }
993
993
 
994
+ /* ---------- Enhanced Empty State ---------- */
995
+ .empty-state--enhanced {
996
+ padding: 56px 32px;
997
+ gap: 16px;
998
+ border: 1px dashed rgba(167, 139, 250, 0.15);
999
+ border-radius: 12px;
1000
+ background:
1001
+ radial-gradient(ellipse 300px 200px at 50% 30%, rgba(99, 102, 241, 0.04) 0%, transparent 70%),
1002
+ var(--bg-tertiary);
1003
+ position: relative;
1004
+ overflow: hidden;
1005
+ }
1006
+
1007
+ .empty-state--enhanced::before {
1008
+ content: '';
1009
+ position: absolute;
1010
+ inset: 0;
1011
+ background:
1012
+ repeating-linear-gradient(
1013
+ 0deg,
1014
+ transparent,
1015
+ transparent 23px,
1016
+ rgba(255, 255, 255, 0.015) 23px,
1017
+ rgba(255, 255, 255, 0.015) 24px
1018
+ );
1019
+ pointer-events: none;
1020
+ }
1021
+
1022
+ .empty-state__icon-wrap {
1023
+ width: 64px;
1024
+ height: 64px;
1025
+ display: flex;
1026
+ align-items: center;
1027
+ justify-content: center;
1028
+ border-radius: 16px;
1029
+ background: rgba(167, 139, 250, 0.06);
1030
+ border: 1px solid rgba(167, 139, 250, 0.12);
1031
+ color: var(--text-accent);
1032
+ animation: empty-breathe 4s ease-in-out infinite;
1033
+ }
1034
+
1035
+ @keyframes empty-breathe {
1036
+ 0%, 100% {
1037
+ box-shadow: 0 0 0 0 rgba(167, 139, 250, 0.08);
1038
+ transform: scale(1);
1039
+ }
1040
+ 50% {
1041
+ box-shadow: 0 0 20px 4px rgba(167, 139, 250, 0.06);
1042
+ transform: scale(1.03);
1043
+ }
1044
+ }
1045
+
1046
+ .empty-state__title {
1047
+ font-size: 0.9375rem;
1048
+ font-weight: 600;
1049
+ color: var(--text-secondary);
1050
+ letter-spacing: -0.01em;
1051
+ }
1052
+
1053
+ .empty-state__desc {
1054
+ font-size: 0.8125rem;
1055
+ color: var(--text-tertiary);
1056
+ max-width: 380px;
1057
+ line-height: 1.55;
1058
+ }
1059
+
1060
+ /* ---------- KPI Empty Hints ---------- */
1061
+ .kpi-card__hint {
1062
+ color: var(--text-tertiary);
1063
+ font-style: italic;
1064
+ font-size: 0.6875rem;
1065
+ }
1066
+
1067
+ .kpi-row--empty .kpi-card {
1068
+ border-style: dashed;
1069
+ border-color: rgba(255, 255, 255, 0.04);
1070
+ }
1071
+
1072
+ .kpi-row--empty .kpi-card__value {
1073
+ color: var(--text-tertiary);
1074
+ opacity: 0.5;
1075
+ }
1076
+
1077
+ /* ---------- Welcome Banner ---------- */
1078
+ .welcome-banner {
1079
+ position: relative;
1080
+ background: var(--bg-secondary);
1081
+ border: 1px solid transparent;
1082
+ border-radius: 16px;
1083
+ padding: 48px 40px;
1084
+ overflow: hidden;
1085
+ z-index: 1;
1086
+ }
1087
+
1088
+ .welcome-banner::before {
1089
+ content: '';
1090
+ position: absolute;
1091
+ inset: -1px;
1092
+ border-radius: 16px;
1093
+ padding: 1px;
1094
+ background: linear-gradient(
1095
+ 135deg,
1096
+ rgba(167, 139, 250, 0.3) 0%,
1097
+ rgba(99, 102, 241, 0.15) 30%,
1098
+ rgba(59, 130, 246, 0.1) 60%,
1099
+ rgba(167, 139, 250, 0.2) 100%
1100
+ );
1101
+ -webkit-mask:
1102
+ linear-gradient(#fff 0 0) content-box,
1103
+ linear-gradient(#fff 0 0);
1104
+ mask:
1105
+ linear-gradient(#fff 0 0) content-box,
1106
+ linear-gradient(#fff 0 0);
1107
+ -webkit-mask-composite: xor;
1108
+ mask-composite: exclude;
1109
+ pointer-events: none;
1110
+ z-index: 0;
1111
+ }
1112
+
1113
+ .welcome-banner__glow {
1114
+ position: absolute;
1115
+ top: -60px;
1116
+ left: 50%;
1117
+ transform: translateX(-50%);
1118
+ width: 500px;
1119
+ height: 300px;
1120
+ background: radial-gradient(
1121
+ ellipse at center,
1122
+ rgba(167, 139, 250, 0.08) 0%,
1123
+ rgba(99, 102, 241, 0.04) 40%,
1124
+ transparent 70%
1125
+ );
1126
+ pointer-events: none;
1127
+ z-index: 0;
1128
+ }
1129
+
1130
+ .welcome-banner__content {
1131
+ position: relative;
1132
+ z-index: 1;
1133
+ display: flex;
1134
+ flex-direction: column;
1135
+ align-items: center;
1136
+ text-align: center;
1137
+ gap: 20px;
1138
+ }
1139
+
1140
+ .welcome-banner__icon {
1141
+ width: 72px;
1142
+ height: 72px;
1143
+ display: flex;
1144
+ align-items: center;
1145
+ justify-content: center;
1146
+ border-radius: 20px;
1147
+ background: rgba(167, 139, 250, 0.08);
1148
+ border: 1px solid rgba(167, 139, 250, 0.15);
1149
+ color: var(--text-accent);
1150
+ animation: welcome-float 6s ease-in-out infinite;
1151
+ }
1152
+
1153
+ @keyframes welcome-float {
1154
+ 0%, 100% {
1155
+ transform: translateY(0);
1156
+ box-shadow: 0 8px 32px rgba(167, 139, 250, 0.08);
1157
+ }
1158
+ 50% {
1159
+ transform: translateY(-6px);
1160
+ box-shadow: 0 16px 48px rgba(167, 139, 250, 0.12);
1161
+ }
1162
+ }
1163
+
1164
+ .welcome-banner__title {
1165
+ font-size: 1.375rem;
1166
+ font-weight: 700;
1167
+ color: var(--text-primary);
1168
+ letter-spacing: -0.02em;
1169
+ line-height: 1.3;
1170
+ }
1171
+
1172
+ .welcome-banner__subtitle {
1173
+ font-size: 0.9375rem;
1174
+ color: var(--text-secondary);
1175
+ max-width: 480px;
1176
+ line-height: 1.6;
1177
+ }
1178
+
1179
+ .welcome-banner__steps {
1180
+ display: flex;
1181
+ gap: 20px;
1182
+ margin-top: 12px;
1183
+ flex-wrap: wrap;
1184
+ justify-content: center;
1185
+ }
1186
+
1187
+ .welcome-step {
1188
+ display: flex;
1189
+ align-items: flex-start;
1190
+ gap: 12px;
1191
+ text-align: left;
1192
+ padding: 16px 20px;
1193
+ background: rgba(255, 255, 255, 0.02);
1194
+ border: 1px solid rgba(255, 255, 255, 0.05);
1195
+ border-radius: 12px;
1196
+ min-width: 200px;
1197
+ max-width: 220px;
1198
+ transition: border-color var(--transition-fast), background var(--transition-fast);
1199
+ }
1200
+
1201
+ .welcome-step:hover {
1202
+ border-color: rgba(167, 139, 250, 0.15);
1203
+ background: rgba(255, 255, 255, 0.03);
1204
+ }
1205
+
1206
+ .welcome-step__num {
1207
+ width: 28px;
1208
+ height: 28px;
1209
+ border-radius: 8px;
1210
+ display: flex;
1211
+ align-items: center;
1212
+ justify-content: center;
1213
+ font-size: 0.75rem;
1214
+ font-weight: 700;
1215
+ color: var(--text-accent);
1216
+ background: rgba(167, 139, 250, 0.1);
1217
+ border: 1px solid rgba(167, 139, 250, 0.2);
1218
+ flex-shrink: 0;
1219
+ }
1220
+
1221
+ .welcome-step__text {
1222
+ display: flex;
1223
+ flex-direction: column;
1224
+ gap: 3px;
1225
+ }
1226
+
1227
+ .welcome-step__text strong {
1228
+ font-size: 0.8125rem;
1229
+ font-weight: 600;
1230
+ color: var(--text-primary);
1231
+ }
1232
+
1233
+ .welcome-step__text span {
1234
+ font-size: 0.75rem;
1235
+ color: var(--text-tertiary);
1236
+ line-height: 1.4;
1237
+ }
1238
+
1239
+ @media (max-width: 640px) {
1240
+ .welcome-banner {
1241
+ padding: 32px 24px;
1242
+ }
1243
+
1244
+ .welcome-banner__steps {
1245
+ flex-direction: column;
1246
+ align-items: center;
1247
+ }
1248
+
1249
+ .welcome-step {
1250
+ max-width: 100%;
1251
+ width: 100%;
1252
+ }
1253
+ }
1254
+
994
1255
  /* ---------- Animations ---------- */
995
1256
  @keyframes slide-up {
996
1257
  from {
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "astro/tsconfigs/strict",
2
+ "extends": "./node_modules/astro/tsconfigs/strict.json",
3
3
  "compilerOptions": {
4
4
  "strictNullChecks": true
5
5
  }