opencastle 0.22.0 → 0.23.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 (57) hide show
  1. package/dist/cli/convoy/engine.d.ts +1 -0
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +1 -0
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/export.d.ts +1 -0
  6. package/dist/cli/convoy/export.d.ts.map +1 -1
  7. package/dist/cli/convoy/export.js +34 -0
  8. package/dist/cli/convoy/export.js.map +1 -1
  9. package/dist/cli/convoy/pipeline.d.ts +35 -0
  10. package/dist/cli/convoy/pipeline.d.ts.map +1 -0
  11. package/dist/cli/convoy/pipeline.js +353 -0
  12. package/dist/cli/convoy/pipeline.js.map +1 -0
  13. package/dist/cli/convoy/pipeline.test.d.ts +2 -0
  14. package/dist/cli/convoy/pipeline.test.d.ts.map +1 -0
  15. package/dist/cli/convoy/pipeline.test.js +778 -0
  16. package/dist/cli/convoy/pipeline.test.js.map +1 -0
  17. package/dist/cli/convoy/store.d.ts +14 -2
  18. package/dist/cli/convoy/store.d.ts.map +1 -1
  19. package/dist/cli/convoy/store.js +84 -5
  20. package/dist/cli/convoy/store.js.map +1 -1
  21. package/dist/cli/convoy/store.test.js +216 -7
  22. package/dist/cli/convoy/store.test.js.map +1 -1
  23. package/dist/cli/convoy/types.d.ts +15 -0
  24. package/dist/cli/convoy/types.d.ts.map +1 -1
  25. package/dist/cli/dashboard.d.ts.map +1 -1
  26. package/dist/cli/dashboard.js +1 -0
  27. package/dist/cli/dashboard.js.map +1 -1
  28. package/dist/cli/run/schema.d.ts +5 -1
  29. package/dist/cli/run/schema.d.ts.map +1 -1
  30. package/dist/cli/run/schema.js +41 -8
  31. package/dist/cli/run/schema.js.map +1 -1
  32. package/dist/cli/run/schema.test.js +194 -5
  33. package/dist/cli/run/schema.test.js.map +1 -1
  34. package/dist/cli/run.d.ts.map +1 -1
  35. package/dist/cli/run.js +141 -2
  36. package/dist/cli/run.js.map +1 -1
  37. package/dist/cli/types.d.ts +3 -1
  38. package/dist/cli/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli/convoy/engine.ts +2 -0
  41. package/src/cli/convoy/export.ts +41 -0
  42. package/src/cli/convoy/pipeline.test.ts +939 -0
  43. package/src/cli/convoy/pipeline.ts +430 -0
  44. package/src/cli/convoy/store.test.ts +239 -7
  45. package/src/cli/convoy/store.ts +110 -7
  46. package/src/cli/convoy/types.ts +17 -0
  47. package/src/cli/dashboard.ts +1 -0
  48. package/src/cli/run/schema.test.ts +244 -5
  49. package/src/cli/run/schema.ts +49 -8
  50. package/src/cli/run.ts +140 -2
  51. package/src/cli/types.ts +3 -1
  52. package/src/dashboard/dist/_astro/{index.DyyaCW8L.css → index.Cq68OHaZ.css} +1 -1
  53. package/src/dashboard/dist/index.html +214 -2
  54. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  55. package/src/dashboard/src/pages/index.astro +230 -1
  56. package/src/dashboard/src/styles/dashboard.css +116 -0
  57. package/src/orchestrator/customizations/KNOWN-ISSUES.md +1 -1
@@ -27,8 +27,9 @@ const base = import.meta.env.BASE_URL;
27
27
  <nav class="dash-sidebar" id="dash-sidebar">
28
28
  <ul class="dash-sidebar__list">
29
29
  <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section">Convoy</a></li>
30
+ <li><a class="dash-sidebar__link" href="#convoy-pipeline-section" data-section="convoy-pipeline-section">Convoy Chain</a></li>
30
31
  <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li>
31
- <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li>
32
+ <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Task Flow</a></li>
32
33
  <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li>
33
34
  <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li>
34
35
  <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li>
@@ -73,6 +74,12 @@ const base = import.meta.env.BASE_URL;
73
74
  <option value="">All convoys</option>
74
75
  </select>
75
76
  </div>
77
+ <div class="filter-group">
78
+ <label class="filter-label" for="filter-pipeline">Pipeline</label>
79
+ <select class="filter-select" id="filter-pipeline">
80
+ <option value="">All</option>
81
+ </select>
82
+ </div>
76
83
  <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button>
77
84
  </div>
78
85
 
@@ -86,6 +93,16 @@ const base = import.meta.env.BASE_URL;
86
93
  </div>
87
94
  </section>
88
95
 
96
+ <!-- Convoy Pipeline (Chaining) Section -->
97
+ <section class="chart-card" id="convoy-pipeline-section" data-nav-section style="display:none">
98
+ <div class="chart-card__header">
99
+ <h2 class="chart-card__title">Convoy Pipeline</h2>
100
+ <p class="chart-card__desc" id="convoy-pipeline-desc">Pipeline convoy chain progress</p>
101
+ </div>
102
+ <div class="chart-card__body" id="convoy-pipeline-body">
103
+ </div>
104
+ </section>
105
+
89
106
  <!-- KPI Row -->
90
107
  <section class="kpi-row" id="kpi-row" data-nav-section>
91
108
  <div class="kpi-card" id="kpi-sessions">
@@ -1178,6 +1195,7 @@ const base = import.meta.env.BASE_URL;
1178
1195
  let rawPanels = [];
1179
1196
  let rawReviews = [];
1180
1197
  let rawConvoys = [];
1198
+ let rawPipelines = [];
1181
1199
 
1182
1200
  function applyFilters() {
1183
1201
  const dateFrom = document.getElementById('filter-date-from').value;
@@ -1185,6 +1203,7 @@ const base = import.meta.env.BASE_URL;
1185
1203
  const agentFilter = document.getElementById('filter-agent').value;
1186
1204
  const outcomeFilter = document.getElementById('filter-outcome').value;
1187
1205
  const convoyFilter = document.getElementById('filter-convoy').value;
1206
+ const pipelineFilter = document.getElementById('filter-pipeline')?.value || '';
1188
1207
 
1189
1208
  function matchDate(ts) {
1190
1209
  const date = ts.slice(0, 10);
@@ -1214,6 +1233,18 @@ const base = import.meta.env.BASE_URL;
1214
1233
  return true;
1215
1234
  });
1216
1235
 
1236
+ // Pipeline filter: restrict events to convoy_ids within the selected pipeline
1237
+ if (pipelineFilter) {
1238
+ const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1239
+ const pipelineConvoyIds = new Set((activePipeline && activePipeline.convoy_ids) || []);
1240
+ if (pipelineConvoyIds.size > 0) {
1241
+ sessions = sessions.filter((s) => !s.convoy_id || pipelineConvoyIds.has(s.convoy_id));
1242
+ delegations = delegations.filter((d) => !d.convoy_id || pipelineConvoyIds.has(d.convoy_id));
1243
+ panels = panels.filter((p2) => !p2.convoy_id || pipelineConvoyIds.has(p2.convoy_id));
1244
+ reviews = reviews.filter((r) => !r.convoy_id || pipelineConvoyIds.has(r.convoy_id));
1245
+ }
1246
+ }
1247
+
1217
1248
  if (convoyFilter) {
1218
1249
  sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
1219
1250
  delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
@@ -1230,6 +1261,17 @@ const base = import.meta.env.BASE_URL;
1230
1261
  }
1231
1262
  }
1232
1263
 
1264
+ // Show/hide convoy pipeline section based on pipeline filter
1265
+ const pipelineSectionEl = document.getElementById('convoy-pipeline-section');
1266
+ if (pipelineSectionEl) {
1267
+ if (pipelineFilter) {
1268
+ const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1269
+ renderConvoyPipeline(activePipeline, rawConvoys);
1270
+ } else {
1271
+ pipelineSectionEl.style.display = 'none';
1272
+ }
1273
+ }
1274
+
1233
1275
  renderAll(sessions, delegations, panels, reviews);
1234
1276
  }
1235
1277
 
@@ -1416,9 +1458,186 @@ const base = import.meta.env.BASE_URL;
1416
1458
  bodyEl.innerHTML = html;
1417
1459
  }
1418
1460
 
1461
+ // ── Pipeline Filter Population ───────────────────────────
1462
+
1463
+ function populatePipelineFilter(pipelines) {
1464
+ const select = document.getElementById('filter-pipeline');
1465
+ if (!select) return;
1466
+ while (select.options.length > 1) select.remove(1);
1467
+ const sorted = pipelines.slice().sort((a, b) =>
1468
+ (b.created_at || '').localeCompare(a.created_at || '')
1469
+ );
1470
+ sorted.forEach((p) => {
1471
+ const opt = document.createElement('option');
1472
+ opt.value = p.id;
1473
+ opt.textContent = (p.name || p.id) + ' (' + (p.status || 'unknown') + ')';
1474
+ select.appendChild(opt);
1475
+ });
1476
+ }
1477
+
1478
+ // ── Convoy Pipeline (Chaining) Render ────────────────────
1479
+
1480
+ function renderConvoyPipeline(pipeline, convoys) {
1481
+ const sectionEl = document.getElementById('convoy-pipeline-section');
1482
+ const descEl = document.getElementById('convoy-pipeline-desc');
1483
+ const bodyEl = document.getElementById('convoy-pipeline-body');
1484
+ if (!sectionEl || !bodyEl) return;
1485
+
1486
+ if (!pipeline) {
1487
+ sectionEl.style.display = 'none';
1488
+ return;
1489
+ }
1490
+
1491
+ sectionEl.style.display = '';
1492
+ if (descEl) {
1493
+ descEl.textContent =
1494
+ (pipeline.name || pipeline.id) + ' \u2014 ' + (pipeline.branch || 'no branch');
1495
+ }
1496
+
1497
+ const convoyIds = pipeline.convoy_ids || [];
1498
+ const pipelineConvoys = convoyIds
1499
+ .map((id) => convoys.find((c) => c.id === id))
1500
+ .filter(Boolean);
1501
+
1502
+ const total = pipelineConvoys.length;
1503
+ const done = pipelineConvoys.filter((c) => c.status === 'done').length;
1504
+ const failed = pipelineConvoys.filter(
1505
+ (c) => c.status === 'failed' || c.status === 'gate-failed'
1506
+ ).length;
1507
+ const totalTasks = pipelineConvoys.reduce((sum, c) => {
1508
+ const s = c.summary || {};
1509
+ return sum + (s.total || (c.tasks ? c.tasks.length : 0));
1510
+ }, 0);
1511
+ const doneTasks = pipelineConvoys.reduce((sum, c) => {
1512
+ const s = c.summary || {};
1513
+ return sum + (s.done || 0);
1514
+ }, 0);
1515
+ const totalTokens = pipelineConvoys.reduce((sum, c) => sum + (c.total_tokens || 0), 0);
1516
+
1517
+ const pct =
1518
+ totalTasks > 0
1519
+ ? Math.round((doneTasks / totalTasks) * 100)
1520
+ : total > 0
1521
+ ? Math.round((done / total) * 100)
1522
+ : 0;
1523
+
1524
+ const statusClass =
1525
+ pipeline.status === 'done'
1526
+ ? 'success'
1527
+ : pipeline.status === 'failed' || pipeline.status === 'gate-failed'
1528
+ ? 'failed'
1529
+ : pipeline.status === 'running'
1530
+ ? 'partial'
1531
+ : '';
1532
+
1533
+ let html = '<div class="convoy-overview">';
1534
+ html +=
1535
+ '<div class="convoy-stat"><span class="convoy-stat__label">Status</span>' +
1536
+ '<span class="outcome-badge outcome-badge--' + statusClass + '">' +
1537
+ escapeHtml(pipeline.status || 'unknown') + '</span></div>';
1538
+ html +=
1539
+ '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span>' +
1540
+ '<span class="convoy-stat__value">' + escapeHtml(pipeline.branch || '\u2014') + '</span></div>';
1541
+ html +=
1542
+ '<div class="convoy-stat"><span class="convoy-stat__label">Convoys</span>' +
1543
+ '<span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1544
+ if (totalTasks > 0) {
1545
+ html +=
1546
+ '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span>' +
1547
+ '<span class="convoy-stat__value">' + doneTasks + '/' + totalTasks + '</span></div>';
1548
+ }
1549
+ if (totalTokens > 0) {
1550
+ html +=
1551
+ '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span>' +
1552
+ '<span class="convoy-stat__value">' + formatTokens(totalTokens) + '</span></div>';
1553
+ }
1554
+ html +=
1555
+ '<div class="convoy-stat"><span class="convoy-stat__label">Started</span>' +
1556
+ '<span class="convoy-stat__value">' +
1557
+ (pipeline.started_at ? formatTime(pipeline.started_at) : '\u2014') + '</span></div>';
1558
+ if (pipeline.finished_at) {
1559
+ html +=
1560
+ '<div class="convoy-stat"><span class="convoy-stat__label">Finished</span>' +
1561
+ '<span class="convoy-stat__value">' + formatTime(pipeline.finished_at) + '</span></div>';
1562
+ }
1563
+ html += '</div>';
1564
+
1565
+ // Convoy chain visualization
1566
+ html += '<div class="convoy-chain">';
1567
+ pipelineConvoys.forEach((convoy, i) => {
1568
+ const cs = convoy.summary || {};
1569
+ const cDone = cs.done || 0;
1570
+ const cTotal = cs.total || (convoy.tasks ? convoy.tasks.length : 0);
1571
+ const cTokens = convoy.total_tokens || 0;
1572
+ const isActive =
1573
+ (pipeline.current_convoy_id && pipeline.current_convoy_id === convoy.id) ||
1574
+ convoy.status === 'running';
1575
+ const nodeStatusClass =
1576
+ convoy.status === 'done'
1577
+ ? 'done'
1578
+ : convoy.status === 'failed' || convoy.status === 'gate-failed'
1579
+ ? 'failed'
1580
+ : isActive
1581
+ ? 'active'
1582
+ : 'pending';
1583
+ const badgeClass =
1584
+ convoy.status === 'done'
1585
+ ? 'success'
1586
+ : convoy.status === 'failed' || convoy.status === 'gate-failed'
1587
+ ? 'failed'
1588
+ : convoy.status === 'running'
1589
+ ? 'partial'
1590
+ : '';
1591
+
1592
+ if (i > 0) {
1593
+ html += '<div class="convoy-chain__connector">\u2192</div>';
1594
+ }
1595
+ html +=
1596
+ '<div class="convoy-chain__node convoy-chain__node--' + nodeStatusClass +
1597
+ '" data-convoy-id="' + escapeHtml(convoy.id) + '" title="Click to filter to this convoy">';
1598
+ html += '<div class="convoy-chain__node-name">' + escapeHtml(convoy.name || convoy.id) + '</div>';
1599
+ html +=
1600
+ '<span class="outcome-badge outcome-badge--' + badgeClass + '">' +
1601
+ escapeHtml(convoy.status) + '</span>';
1602
+ if (cTotal > 0) {
1603
+ html += '<div class="convoy-chain__node-meta">' + cDone + '/' + cTotal + ' tasks</div>';
1604
+ }
1605
+ if (cTokens > 0) {
1606
+ html += '<div class="convoy-chain__node-meta">' + formatTokens(cTokens) + ' tokens</div>';
1607
+ }
1608
+ html += '</div>';
1609
+ });
1610
+ html += '</div>';
1611
+
1612
+ // Progress bar
1613
+ html += '<div class="convoy-progress">';
1614
+ html +=
1615
+ '<div class="convoy-progress__bar">' +
1616
+ '<div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1617
+ html +=
1618
+ '<span class="convoy-progress__label">' + pct + '% complete' +
1619
+ (failed > 0 ? ' \u00B7 ' + failed + ' failed' : '') + '</span>';
1620
+ html += '</div>';
1621
+
1622
+ bodyEl.innerHTML = html;
1623
+
1624
+ // Click handlers for convoy drill-down
1625
+ bodyEl.querySelectorAll('.convoy-chain__node').forEach((node) => {
1626
+ node.addEventListener('click', () => {
1627
+ const convoyId = node.dataset.convoyId;
1628
+ const sel = document.getElementById('filter-convoy');
1629
+ if (sel && convoyId) {
1630
+ sel.value = convoyId;
1631
+ applyFilters();
1632
+ }
1633
+ });
1634
+ });
1635
+ }
1636
+
1419
1637
  async function main() {
1420
1638
  const events = await loadNdjson(base + 'data/events.ndjson');
1421
1639
  const convoys = await loadNdjson(base + 'data/convoys.ndjson');
1640
+ const pipelines = await loadNdjson(base + 'data/pipelines.ndjson');
1422
1641
 
1423
1642
  const sessions = events.filter((e) => e.type === 'session');
1424
1643
  const delegations = events.filter((e) => e.type === 'delegation');
@@ -1430,9 +1649,11 @@ const base = import.meta.env.BASE_URL;
1430
1649
  rawPanels = panels;
1431
1650
  rawReviews = reviews;
1432
1651
  rawConvoys = convoys;
1652
+ rawPipelines = pipelines;
1433
1653
 
1434
1654
  populateAgentFilter(sessions, delegations, reviews);
1435
1655
  populateConvoyFilter(convoys);
1656
+ populatePipelineFilter(pipelines);
1436
1657
 
1437
1658
  // ── Read URL params ───────────────────────────────────
1438
1659
  const urlParams = new URLSearchParams(window.location.search);
@@ -1461,12 +1682,14 @@ const base = import.meta.env.BASE_URL;
1461
1682
  document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1462
1683
  document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1463
1684
  document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
1685
+ document.getElementById('filter-pipeline')?.addEventListener('change', applyFilters);
1464
1686
  document.getElementById('filter-reset')?.addEventListener('click', () => {
1465
1687
  document.getElementById('filter-date-from').value = '';
1466
1688
  document.getElementById('filter-date-to').value = '';
1467
1689
  document.getElementById('filter-agent').value = '';
1468
1690
  document.getElementById('filter-outcome').value = '';
1469
1691
  document.getElementById('filter-convoy').value = '';
1692
+ document.getElementById('filter-pipeline').value = '';
1470
1693
  applyFilters();
1471
1694
  });
1472
1695
 
@@ -1477,15 +1700,21 @@ const base = import.meta.env.BASE_URL;
1477
1700
  refreshInterval = setInterval(async () => {
1478
1701
  const freshEvents = await loadNdjson(base + 'data/events.ndjson');
1479
1702
  const freshConvoys = await loadNdjson(base + 'data/convoys.ndjson');
1703
+ const freshPipelines = await loadNdjson(base + 'data/pipelines.ndjson');
1480
1704
  rawSessions = freshEvents.filter((e) => e.type === 'session');
1481
1705
  rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
1482
1706
  rawPanels = freshEvents.filter((e) => e.type === 'panel');
1483
1707
  rawReviews = freshEvents.filter((e) => e.type === 'review');
1484
1708
  rawConvoys = freshConvoys;
1709
+ rawPipelines = freshPipelines;
1485
1710
  const currentValue = document.getElementById('filter-convoy')?.value;
1711
+ const currentPipelineValue = document.getElementById('filter-pipeline')?.value;
1486
1712
  populateConvoyFilter(freshConvoys);
1713
+ populatePipelineFilter(freshPipelines);
1487
1714
  const sel = document.getElementById('filter-convoy');
1488
1715
  if (sel && currentValue) sel.value = currentValue;
1716
+ const pSel = document.getElementById('filter-pipeline');
1717
+ if (pSel && currentPipelineValue) pSel.value = currentPipelineValue;
1489
1718
  applyFilters();
1490
1719
  }, 5000);
1491
1720
  }
@@ -1593,3 +1593,119 @@ body {
1593
1593
  .convoy-tasks {
1594
1594
  margin-top: 8px;
1595
1595
  }
1596
+
1597
+ /* ---------- Convoy Chain / Pipeline Section ---------- */
1598
+ .convoy-chain {
1599
+ display: flex;
1600
+ align-items: stretch;
1601
+ gap: 0;
1602
+ overflow-x: auto;
1603
+ padding: 1rem 0 1.5rem;
1604
+ scrollbar-width: thin;
1605
+ scrollbar-color: var(--border-color) transparent;
1606
+ }
1607
+
1608
+ .convoy-chain::-webkit-scrollbar {
1609
+ height: 4px;
1610
+ }
1611
+
1612
+ .convoy-chain::-webkit-scrollbar-track {
1613
+ background: transparent;
1614
+ }
1615
+
1616
+ .convoy-chain::-webkit-scrollbar-thumb {
1617
+ background: var(--border-color);
1618
+ border-radius: 2px;
1619
+ }
1620
+
1621
+ .convoy-chain__connector {
1622
+ display: flex;
1623
+ align-items: center;
1624
+ padding: 0 0.5rem;
1625
+ color: var(--text-tertiary);
1626
+ font-size: 1.1rem;
1627
+ flex-shrink: 0;
1628
+ }
1629
+
1630
+ .convoy-chain__node {
1631
+ display: flex;
1632
+ flex-direction: column;
1633
+ align-items: center;
1634
+ gap: 6px;
1635
+ padding: 12px 16px;
1636
+ background: var(--bg-tertiary);
1637
+ border: 1px solid var(--border-color);
1638
+ border-radius: 10px;
1639
+ min-width: 140px;
1640
+ cursor: pointer;
1641
+ transition:
1642
+ background var(--transition-fast),
1643
+ border-color var(--transition-fast),
1644
+ transform var(--transition-fast),
1645
+ box-shadow var(--transition-fast);
1646
+ flex-shrink: 0;
1647
+ }
1648
+
1649
+ .convoy-chain__node:hover {
1650
+ background: var(--bg-card-hover);
1651
+ transform: translateY(-2px);
1652
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1653
+ }
1654
+
1655
+ .convoy-chain__node-name {
1656
+ font-size: 0.8rem;
1657
+ font-weight: 600;
1658
+ color: var(--text-primary);
1659
+ text-align: center;
1660
+ word-break: break-word;
1661
+ max-width: 120px;
1662
+ }
1663
+
1664
+ .convoy-chain__node-meta {
1665
+ font-size: 0.72rem;
1666
+ color: var(--text-tertiary);
1667
+ text-align: center;
1668
+ }
1669
+
1670
+ .convoy-chain__node--active {
1671
+ border-color: var(--accent-purple);
1672
+ box-shadow: 0 0 0 1px var(--accent-purple), 0 0 12px rgba(167, 139, 250, 0.2);
1673
+ animation: convoy-pulse 2s ease-in-out infinite;
1674
+ }
1675
+
1676
+ .convoy-chain__node--done {
1677
+ border-color: rgba(34, 197, 94, 0.3);
1678
+ }
1679
+
1680
+ .convoy-chain__node--failed {
1681
+ border-color: rgba(239, 68, 68, 0.3);
1682
+ }
1683
+
1684
+ .convoy-chain__node--pending {
1685
+ opacity: 0.6;
1686
+ }
1687
+
1688
+ @keyframes convoy-pulse {
1689
+ 0%, 100% {
1690
+ box-shadow: 0 0 0 1px var(--accent-purple), 0 0 8px rgba(167, 139, 250, 0.15);
1691
+ }
1692
+ 50% {
1693
+ box-shadow: 0 0 0 1px var(--accent-purple), 0 0 18px rgba(167, 139, 250, 0.35);
1694
+ }
1695
+ }
1696
+
1697
+ @media (max-width: 768px) {
1698
+ .convoy-chain {
1699
+ flex-wrap: wrap;
1700
+ gap: 8px;
1701
+ }
1702
+
1703
+ .convoy-chain__connector {
1704
+ display: none;
1705
+ }
1706
+
1707
+ .convoy-chain__node {
1708
+ flex: 1 1 calc(50% - 4px);
1709
+ min-width: 120px;
1710
+ }
1711
+ }
@@ -14,7 +14,7 @@ Tracked issues, limitations, and accepted risks discovered during agent sessions
14
14
 
15
15
  | Issue ID | Status | Severity | Summary | Evidence | Root Cause | Solution Options |
16
16
  |----------|--------|----------|---------|----------|------------|------------------|
17
- | KI-001 | Open | Medium | _Example: Description of the issue_ | _Link or description of where/how the issue manifests_ | _Why it happens_ | _Possible fixes or workarounds_ |
17
+ | KI-001 | Open | Medium | Convoy engine run()/resume() don't catch unexpected errors from runConvoy() — convoy DB records can get stuck in 'running' status | `src/cli/convoy/engine.ts` lines 452-510 (run) and 520-570 (resume): if `runConvoy()` throws, the convoy record is never updated to 'failed' | The try/finally block exports and closes the store but doesn't catch to update convoy status | Add a catch block before finally that calls `store.updateConvoyStatus(convoyId, 'failed', ...)` before rethrowing |
18
18
 
19
19
  ### Status Values
20
20