opencastle 0.29.0 → 0.30.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.
@@ -40,11 +40,18 @@ try {
40
40
  <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#overall-section" data-section="overall-section" data-view="home" aria-label="Overview section">Overview</a></li>
41
41
  <li><a class="dash-sidebar__link" href="#convoy-list-section" data-section="convoy-list-section" data-view="home" aria-label="Convoys section">Convoys</a></li>
42
42
  <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" data-view="detail" aria-label="Tasks section">Tasks</a></li>
43
+ <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section" data-view="detail" aria-label="Pipeline section">Pipeline</a></li>
44
+ <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section" data-view="detail" aria-label="Agents section">Agents</a></li>
45
+ <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section" data-view="detail" aria-label="Tiers section">Tiers</a></li>
46
+ <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section" data-view="detail" aria-label="Models section">Models</a></li>
43
47
  <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" data-view="detail" aria-label="Quality section">Quality</a></li>
44
48
  <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" data-view="detail" aria-label="Reliability section">Reliability</a></li>
45
49
  <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" data-view="detail" aria-label="Drift section">Drift</a></li>
46
50
  <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" data-view="detail" aria-label="Outputs section">Outputs</a></li>
51
+ <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section" data-view="detail" aria-label="Execution Log section">Execution Log</a></li>
47
52
  <li><a class="dash-sidebar__link" href="#event-timeline-section" data-section="event-timeline-section" data-view="detail" aria-label="Event Log section">Event Log</a></li>
53
+ <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section" data-view="detail" aria-label="Panel Reviews section">Panel Reviews</a></li>
54
+ <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section" data-view="detail" aria-label="Fast Reviews section">Fast Reviews</a></li>
48
55
  </ul>
49
56
  </nav>
50
57
 
@@ -152,6 +159,61 @@ try {
152
159
  </div>
153
160
  </section>
154
161
 
162
+ <!-- Pipeline View -->
163
+ <section class="chart-card" id="pipeline-section" data-nav-section>
164
+ <div class="chart-card__header">
165
+ <h2 class="chart-card__title">Task Pipeline</h2>
166
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Task flow across execution phases." data-tooltip="Task flow across execution phases."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
167
+ <p class="chart-card__desc">Task flow across execution phases</p>
168
+ </div>
169
+ <div class="chart-card__body" id="pipeline-view"></div>
170
+ </section>
171
+
172
+ <!-- Charts Row: Agent + Outcomes -->
173
+ <div class="charts-row" id="agent-section" data-nav-section>
174
+ <section class="chart-card">
175
+ <div class="chart-card__header">
176
+ <h2 class="chart-card__title">Sessions by Agent</h2>
177
+ <p class="chart-card__desc">Task count per agent, stacked by outcome</p>
178
+ </div>
179
+ <div class="chart-card__body" id="agent-chart"></div>
180
+ </section>
181
+ <section class="chart-card">
182
+ <div class="chart-card__header">
183
+ <h2 class="chart-card__title">Delegation Outcomes</h2>
184
+ <p class="chart-card__desc">Task outcome distribution</p>
185
+ </div>
186
+ <div class="chart-card__body" id="delegation-outcome-chart"></div>
187
+ </section>
188
+ </div>
189
+
190
+ <!-- Charts Row: Tiers + Mechanism -->
191
+ <div class="charts-row" id="tier-section" data-nav-section>
192
+ <section class="chart-card">
193
+ <div class="chart-card__header">
194
+ <h2 class="chart-card__title">Tier Distribution</h2>
195
+ <p class="chart-card__desc">Model tier breakdown</p>
196
+ </div>
197
+ <div class="chart-card__body" id="tier-chart"></div>
198
+ </section>
199
+ <section class="chart-card">
200
+ <div class="chart-card__header">
201
+ <h2 class="chart-card__title">Delegation Mechanism</h2>
202
+ <p class="chart-card__desc">Sub-agent vs background split</p>
203
+ </div>
204
+ <div class="chart-card__body" id="mechanism-chart"></div>
205
+ </section>
206
+ </div>
207
+
208
+ <!-- Model Usage -->
209
+ <section class="chart-card" id="model-section" data-nav-section>
210
+ <div class="chart-card__header">
211
+ <h2 class="chart-card__title">Model Usage</h2>
212
+ <p class="chart-card__desc">Tasks by model</p>
213
+ </div>
214
+ <div class="chart-card__body" id="model-chart"></div>
215
+ </section>
216
+
155
217
  <!-- Quality Section -->
156
218
  <section class="chart-card" id="quality-section" data-nav-section>
157
219
  <div class="chart-card__header">
@@ -223,6 +285,37 @@ try {
223
285
  </div>
224
286
  </div>
225
287
  </section>
288
+
289
+ <!-- Execution Log -->
290
+ <section class="chart-card" id="execution-section" data-nav-section>
291
+ <div class="chart-card__header">
292
+ <h2 class="chart-card__title">Execution Log</h2>
293
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Step-by-step trace of recent task activity." data-tooltip="Step-by-step trace of recent task activity."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
294
+ <p class="chart-card__desc">Recent agent activity, step by step</p>
295
+ </div>
296
+ <div class="chart-card__body" id="execution-log"></div>
297
+ </section>
298
+
299
+ <!-- Panel Reviews -->
300
+ <section class="chart-card" id="panel-section" data-nav-section>
301
+ <div class="chart-card__header">
302
+ <h2 class="chart-card__title">Panel Reviews</h2>
303
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Quality gate verdicts from majority-vote panels." data-tooltip="Quality gate verdicts from majority-vote panels."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
304
+ <p class="chart-card__desc">Quality gate verdicts and fix items</p>
305
+ </div>
306
+ <div class="chart-card__body" id="panel-chart"></div>
307
+ </section>
308
+
309
+ <!-- Fast Reviews -->
310
+ <section class="chart-card" id="reviews-section" data-nav-section>
311
+ <div class="chart-card__header">
312
+ <h2 class="chart-card__title">Fast Reviews</h2>
313
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Single-reviewer quality gate results." data-tooltip="Single-reviewer quality gate results."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
314
+ <p class="chart-card__desc">Single-reviewer quality gate results</p>
315
+ </div>
316
+ <div class="chart-card__body chart-card__body--table" id="reviews-table"></div>
317
+ </section>
318
+
226
319
  </div><!-- .view-convoy-detail -->
227
320
 
228
321
 
@@ -539,7 +632,7 @@ try {
539
632
  async function loadConvoyDetail(convoyId) {
540
633
  if (!convoyId) {
541
634
  renderConvoyDetailHeader(null);
542
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
635
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
543
636
  var el = document.getElementById(id);
544
637
  if (el) el.style.display = 'none';
545
638
  });
@@ -556,15 +649,24 @@ try {
556
649
  renderPhaseBreakdown(detail.tasks || []);
557
650
  const tasksSection = document.getElementById('tasks-section');
558
651
  if (tasksSection) tasksSection.style.display = '';
652
+ renderDetailPipeline(detail.tasks || []);
653
+ renderDetailAgentChart(detail.tasks || []);
654
+ renderDetailOutcomeChart(detail.tasks || []);
655
+ renderDetailTierChart(detail.tasks || []);
656
+ renderDetailMechanismChart(detail.events || []);
657
+ renderDetailModelChart(detail.tasks || []);
559
658
  renderQualitySection(detail);
560
659
  renderReliabilitySection(detail);
561
660
  renderDriftSection(detail);
562
661
  renderOutputsSection(detail);
662
+ renderDetailExecutionLog(detail.tasks || []);
563
663
  renderEventTimeline(detail);
664
+ renderDetailPanelChart(detail.tasks || []);
665
+ renderDetailReviewsTable(detail.tasks || []);
564
666
  } catch (e) {
565
667
  console.error('Failed to load convoy detail:', e);
566
668
  renderConvoyDetailHeader(null);
567
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
669
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
568
670
  var el = document.getElementById(id);
569
671
  if (el) el.style.display = 'none';
570
672
  });
@@ -580,7 +682,7 @@ try {
580
682
  if (nameEl) nameEl.textContent = 'No convoy selected';
581
683
  if (statusEl) { statusEl.textContent = ''; statusEl.className = 'status-badge'; }
582
684
  if (metaEl) metaEl.innerHTML = '';
583
- ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
685
+ ['tasks-section', 'pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
584
686
  var el = document.getElementById(id);
585
687
  if (el) el.style.display = 'none';
586
688
  });
@@ -778,6 +880,390 @@ try {
778
880
  el.innerHTML = html;
779
881
  }
780
882
 
883
+ // ── Convoy Detail: Derive Tier from Model ────────────────
884
+
885
+ function deriveTier(model) {
886
+ if (!model) return 'unknown';
887
+ var m = model.toLowerCase();
888
+ if (m.includes('opus')) return 'premium';
889
+ if (m.includes('sonnet') || m.includes('pro')) return 'standard';
890
+ if (m.includes('haiku') || m.includes('flash') || m.includes('mini')) return 'economy';
891
+ return 'utility';
892
+ }
893
+
894
+ // ── Convoy Detail: Pipeline View ─────────────────────────
895
+
896
+ function renderDetailPipeline(tasks) {
897
+ var el = document.getElementById('pipeline-view');
898
+ if (!el) return;
899
+
900
+ if (!tasks || tasks.length === 0) {
901
+ el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Tasks will flow through execution phases here.');
902
+ return;
903
+ }
904
+
905
+ var phases = {};
906
+ tasks.forEach(function(t) {
907
+ var p = t.phase != null ? t.phase : 1;
908
+ if (!phases[p]) phases[p] = 0;
909
+ phases[p]++;
910
+ });
911
+
912
+ var stageConfig = [
913
+ { label: 'Foundation', phase: 1, iconClass: 'pending', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="13" y2="14"/></svg>' },
914
+ { label: 'Integration', phase: 2, iconClass: 'active', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' },
915
+ { label: 'Validation', phase: 3, iconClass: 'review', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' },
916
+ { label: 'QA Gate', phase: 4, iconClass: 'done', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' },
917
+ ];
918
+
919
+ el.innerHTML =
920
+ '<div class="pipeline">' +
921
+ stageConfig.map(function(stage, i) {
922
+ return (i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
923
+ '<div class="pipeline-stage">' +
924
+ '<div class="pipeline-stage__icon pipeline-stage__icon--' + stage.iconClass + '">' + stage.icon + '</div>' +
925
+ '<span class="pipeline-stage__count">' + (phases[stage.phase] || 0) + '</span>' +
926
+ '<span class="pipeline-stage__label">' + stage.label + '</span>' +
927
+ '</div>';
928
+ }).join('') +
929
+ '</div>';
930
+ }
931
+
932
+ // ── Convoy Detail: Agent Chart ───────────────────────────
933
+
934
+ function renderDetailAgentChart(tasks) {
935
+ var el = document.getElementById('agent-chart');
936
+ if (!el) return;
937
+
938
+ if (!tasks || tasks.length === 0) {
939
+ el.innerHTML = emptyStateHtml('agents', 'No agent data yet', 'A breakdown of tasks per agent will appear here.');
940
+ return;
941
+ }
942
+
943
+ var agentMap = {};
944
+ tasks.forEach(function(t) {
945
+ var agent = t.agent || 'unknown';
946
+ if (!agentMap[agent]) agentMap[agent] = { done: 0, failed: 0, running: 0, other: 0, total: 0 };
947
+ if (t.status === 'done') agentMap[agent].done++;
948
+ else if (t.status === 'running') agentMap[agent].running++;
949
+ else if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(t.status)) agentMap[agent].failed++;
950
+ else agentMap[agent].other++;
951
+ agentMap[agent].total++;
952
+ });
953
+
954
+ var agents = Object.entries(agentMap).sort(function(a, b) { return b[1].total - a[1].total; });
955
+ var maxTotal = Math.max.apply(null, agents.map(function(a) { return a[1].total; }));
956
+
957
+ el.innerHTML = agents.map(function(entry) {
958
+ var name = entry[0];
959
+ var data = entry[1];
960
+ return '<div class="bar-row">' +
961
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
962
+ '<div class="bar-track">' +
963
+ (data.done > 0 ? '<div class="bar-segment bar--success" style="width: ' + ((data.done / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
964
+ (data.running > 0 ? '<div class="bar-segment" style="width: ' + ((data.running / maxTotal) * 100).toFixed(1) + '%; background: #3b82f6"></div>' : '') +
965
+ (data.failed > 0 ? '<div class="bar-segment bar--failed" style="width: ' + ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
966
+ (data.other > 0 ? '<div class="bar-segment" style="width: ' + ((data.other / maxTotal) * 100).toFixed(1) + '%; background: #64748b"></div>' : '') +
967
+ '</div>' +
968
+ '<span class="bar-value">' + data.total + '</span>' +
969
+ '</div>';
970
+ }).join('');
971
+ }
972
+
973
+ // ── Convoy Detail: Tier Donut Chart ──────────────────────
974
+
975
+ function renderDetailTierChart(tasks) {
976
+ var el = document.getElementById('tier-chart');
977
+ if (!el) return;
978
+
979
+ if (!tasks || tasks.length === 0) {
980
+ el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution will be shown as a donut chart.');
981
+ return;
982
+ }
983
+
984
+ var tierCounts = {};
985
+ tasks.forEach(function(t) {
986
+ var tier = deriveTier(t.model);
987
+ tierCounts[tier] = (tierCounts[tier] || 0) + 1;
988
+ });
989
+
990
+ var order = ['premium', 'standard', 'utility', 'economy', 'unknown'];
991
+ var tiers = order.filter(function(t) { return tierCounts[t]; }).map(function(t) { return { name: t, count: tierCounts[t] }; });
992
+ var total = tasks.length;
993
+ var r = 70;
994
+ var circumference = 2 * Math.PI * r;
995
+ var cumOffset = 0;
996
+
997
+ var circles = tiers.map(function(t) {
998
+ var pct = t.count / total;
999
+ var dashLen = pct * circumference;
1000
+ var linecap = tiers.length === 1 ? 'butt' : 'round';
1001
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (TIER_COLORS[t.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
1002
+ cumOffset += dashLen;
1003
+ return segment;
1004
+ });
1005
+
1006
+ var legend = tiers.map(function(t) {
1007
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (TIER_COLORS[t.name] || '#64748b') + '"></span><span class="legend-name">' + t.name + '</span><span class="legend-count">' + t.count + ' (' + Math.round((t.count / total) * 100) + '%)</span></div>';
1008
+ }).join('');
1009
+
1010
+ el.innerHTML =
1011
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
1012
+ }
1013
+
1014
+ // ── Convoy Detail: Mechanism Donut Chart ─────────────────
1015
+
1016
+ function renderDetailMechanismChart(events) {
1017
+ var el = document.getElementById('mechanism-chart');
1018
+ if (!el) return;
1019
+
1020
+ var MECH_COLORS = { 'sub-agent': '#3b82f6', 'background': '#a78bfa', 'unknown': '#64748b' };
1021
+ var MECH_LABELS = { 'sub-agent': 'Sub-agent (inline)', 'background': 'Background (worktree)', 'unknown': 'Unknown' };
1022
+
1023
+ if (!events || events.length === 0) {
1024
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
1025
+ return;
1026
+ }
1027
+
1028
+ var mechCounts = {};
1029
+ events.forEach(function(e) {
1030
+ if (e.type === 'task_assigned' || e.type === 'task_started') {
1031
+ var mech = (e.data && e.data.mechanism) || 'unknown';
1032
+ mechCounts[mech] = (mechCounts[mech] || 0) + 1;
1033
+ }
1034
+ });
1035
+
1036
+ var mechOrder = ['sub-agent', 'background', 'unknown'];
1037
+ var mechs = mechOrder.filter(function(m) { return mechCounts[m]; }).map(function(m) { return { name: m, count: mechCounts[m] }; });
1038
+
1039
+ if (mechs.length === 0) {
1040
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
1041
+ return;
1042
+ }
1043
+
1044
+ var total = mechs.reduce(function(s, m) { return s + m.count; }, 0);
1045
+ var r = 70;
1046
+ var circumference = 2 * Math.PI * r;
1047
+ var cumOffset = 0;
1048
+
1049
+ var circles = mechs.map(function(m) {
1050
+ var pct = m.count / total;
1051
+ var dashLen = pct * circumference;
1052
+ var linecap = mechs.length === 1 ? 'butt' : 'round';
1053
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
1054
+ cumOffset += dashLen;
1055
+ return segment;
1056
+ });
1057
+
1058
+ var legend = mechs.map(function(m) {
1059
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span><span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span><span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span></div>';
1060
+ }).join('');
1061
+
1062
+ el.innerHTML =
1063
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
1064
+ }
1065
+
1066
+ // ── Convoy Detail: Delegation Outcome Chart ──────────────
1067
+
1068
+ function renderDetailOutcomeChart(tasks) {
1069
+ var el = document.getElementById('delegation-outcome-chart');
1070
+ if (!el) return;
1071
+
1072
+ if (!tasks || tasks.length === 0) {
1073
+ el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Task outcome distribution will be shown here.');
1074
+ return;
1075
+ }
1076
+
1077
+ var OUTCOME_COLORS = { done: '#22c55e', running: '#3b82f6', pending: '#64748b', assigned: '#64748b', failed: '#ef4444', 'gate-failed': '#f59e0b', 'timed-out': '#a78bfa', 'hook-failed': '#64748b', 'review-blocked': '#f59e0b', skipped: '#94a3b8' };
1078
+
1079
+ var outcomeCounts = {};
1080
+ tasks.forEach(function(t) {
1081
+ var outcome = t.status || 'unknown';
1082
+ outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
1083
+ });
1084
+
1085
+ var outcomes = Object.entries(outcomeCounts).sort(function(a, b) { return b[1] - a[1]; });
1086
+ var maxCount = Math.max.apply(null, outcomes.map(function(o) { return o[1]; }));
1087
+
1088
+ el.innerHTML = outcomes.map(function(entry) {
1089
+ var name = entry[0];
1090
+ var count = entry[1];
1091
+ return '<div class="bar-row">' +
1092
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
1093
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div></div>' +
1094
+ '<span class="bar-value">' + count + '</span>' +
1095
+ '</div>';
1096
+ }).join('');
1097
+ }
1098
+
1099
+ // ── Convoy Detail: Model Chart ───────────────────────────
1100
+
1101
+ function renderDetailModelChart(tasks) {
1102
+ var el = document.getElementById('model-chart');
1103
+ if (!el) return;
1104
+
1105
+ if (!tasks || tasks.length === 0) {
1106
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
1107
+ return;
1108
+ }
1109
+
1110
+ var modelCounts = {};
1111
+ tasks.forEach(function(t) {
1112
+ if (t.model) modelCounts[t.model] = (modelCounts[t.model] || 0) + 1;
1113
+ });
1114
+
1115
+ var models = Object.entries(modelCounts).sort(function(a, b) { return b[1] - a[1]; });
1116
+
1117
+ if (models.length === 0) {
1118
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
1119
+ return;
1120
+ }
1121
+
1122
+ var maxCount = Math.max.apply(null, models.map(function(m) { return m[1]; }));
1123
+
1124
+ el.innerHTML = models.map(function(entry) {
1125
+ var name = entry[0];
1126
+ var count = entry[1];
1127
+ return '<div class="bar-row">' +
1128
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
1129
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (MODEL_COLORS[name] || '#64748b') + '"></div></div>' +
1130
+ '<span class="bar-value">' + count + '</span>' +
1131
+ '</div>';
1132
+ }).join('');
1133
+ }
1134
+
1135
+ // ── Convoy Detail: Execution Log ─────────────────────────
1136
+
1137
+ function renderDetailExecutionLog(tasks) {
1138
+ var el = document.getElementById('execution-log');
1139
+ if (!el) return;
1140
+
1141
+ if (!tasks || tasks.length === 0) {
1142
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
1143
+ return;
1144
+ }
1145
+
1146
+ var sorted = tasks.slice().filter(function(t) { return t.started_at; }).sort(function(a, b) { return new Date(b.started_at) - new Date(a.started_at); }).slice(0, 10);
1147
+
1148
+ if (sorted.length === 0) {
1149
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
1150
+ return;
1151
+ }
1152
+
1153
+ var statusToOutcome = function(s) {
1154
+ if (s === 'done') return 'success';
1155
+ if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(s)) return 'failed';
1156
+ return 'partial';
1157
+ };
1158
+
1159
+ el.innerHTML =
1160
+ '<div class="exec-log">' +
1161
+ sorted.map(function(t, i) {
1162
+ var outcome = statusToOutcome(t.status);
1163
+ var dur = formatDuration(t.started_at, t.finished_at);
1164
+ var fileCount = t.files ? t.files.length : 0;
1165
+ return '<div class="exec-step">' +
1166
+ '<div class="exec-step__indicator">' +
1167
+ '<div class="exec-step__dot exec-step__dot--' + outcome + '">' + (OUTCOME_ICONS[outcome] || '') + '</div>' +
1168
+ (i < sorted.length - 1 ? '<div class="exec-step__line"></div>' : '') +
1169
+ '</div>' +
1170
+ '<div class="exec-step__content">' +
1171
+ '<div class="exec-step__header">' +
1172
+ '<span class="exec-step__agent">' + escapeHtml(t.agent || 'unknown') + '</span>' +
1173
+ '<span class="exec-step__badge exec-step__badge--' + outcome + '">' + escapeHtml(t.status) + '</span>' +
1174
+ '</div>' +
1175
+ '<div class="exec-step__task">' + escapeHtml(t.id) + '</div>' +
1176
+ '<div class="exec-step__meta">' +
1177
+ '<span class="exec-step__meta-item">\uD83D\uDD52 ' + formatTime(t.started_at) + '</span>' +
1178
+ (dur ? '<span class="exec-step__meta-item">\u23F1 ' + dur + '</span>' : '') +
1179
+ (fileCount > 0 ? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' + fileCount + ' files</span>' : '') +
1180
+ (t.model ? '<span class="exec-step__meta-item">\uD83E\uDD16 ' + escapeHtml(t.model) + '</span>' : '') +
1181
+ (t.retries > 0 ? '<span class="exec-step__meta-item">\uD83D\uDD04 ' + t.retries + ' retries</span>' : '') +
1182
+ (t.total_tokens != null ? '<span class="exec-step__meta-item">\uD83D\uDD24 ' + formatTokens(t.total_tokens) + '</span>' : '') +
1183
+ '</div>' +
1184
+ '</div>' +
1185
+ '</div>';
1186
+ }).join('') +
1187
+ '</div>';
1188
+ }
1189
+
1190
+ // ── Convoy Detail: Panel Reviews ─────────────────────────
1191
+
1192
+ function renderDetailPanelChart(tasks) {
1193
+ var el = document.getElementById('panel-chart');
1194
+ if (!el) return;
1195
+
1196
+ var panelTasks = (tasks || []).filter(function(t) { return t.panel_attempts > 0; });
1197
+
1198
+ if (panelTasks.length === 0) {
1199
+ el.innerHTML = emptyStateHtml('panels', 'No panel reviews yet', 'Quality gate verdicts from majority-vote panels will be shown here.');
1200
+ return;
1201
+ }
1202
+
1203
+ el.innerHTML =
1204
+ '<div class="panel-grid">' +
1205
+ panelTasks.map(function(t) {
1206
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : 'PENDING';
1207
+ var verdictClass = verdictUpper === 'PASS' ? 'PASS' : 'BLOCK';
1208
+ return '<div class="panel-item">' +
1209
+ '<div class="panel-item__header">' +
1210
+ '<span class="panel-item__key">' + escapeHtml(t.id) + '</span>' +
1211
+ '<span class="panel-item__verdict panel-item__verdict--' + verdictClass + '">' + verdictUpper + '</span>' +
1212
+ '</div>' +
1213
+ '<div class="panel-item__votes">' +
1214
+ (verdictUpper === 'PASS' ? '<div class="panel-item__vote panel-item__vote--pass">\u2713</div>' : '<div class="panel-item__vote panel-item__vote--block">\u2717</div>') +
1215
+ '</div>' +
1216
+ '<div class="panel-item__meta">' +
1217
+ '<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(t.review_model || 'unknown') + '</span>' +
1218
+ (t.panel_attempts > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 ' + t.panel_attempts + ' attempts</span>' : '') +
1219
+ (t.review_tokens != null ? '<span class="panel-item__meta-item">\uD83D\uDD24 ' + formatTokens(t.review_tokens) + '</span>' : '') +
1220
+ '</div>' +
1221
+ '</div>';
1222
+ }).join('') +
1223
+ '</div>';
1224
+ }
1225
+
1226
+ // ── Convoy Detail: Fast Reviews ──────────────────────────
1227
+
1228
+ function renderDetailReviewsTable(tasks) {
1229
+ var el = document.getElementById('reviews-table');
1230
+ if (!el) return;
1231
+
1232
+ var reviewedTasks = (tasks || []).filter(function(t) { return t.review_level != null; });
1233
+
1234
+ if (reviewedTasks.length === 0) {
1235
+ el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results will be listed here.');
1236
+ return;
1237
+ }
1238
+
1239
+ el.innerHTML =
1240
+ '<table class="sessions-table">' +
1241
+ '<thead><tr>' +
1242
+ '<th scope="col">Task ID</th>' +
1243
+ '<th scope="col">Agent</th>' +
1244
+ '<th scope="col">Review Level</th>' +
1245
+ '<th scope="col">Verdict</th>' +
1246
+ '<th scope="col">Model</th>' +
1247
+ '<th scope="col">Tokens</th>' +
1248
+ '</tr></thead>' +
1249
+ '<tbody>' +
1250
+ reviewedTasks.map(function(t) {
1251
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : '';
1252
+ var verdictBadge = t.review_verdict
1253
+ ? '<span class="status-badge status-badge--' + (verdictUpper === 'PASS' ? 'done' : 'failed') + '">' + escapeHtml(t.review_verdict) + '</span>'
1254
+ : '<span style="opacity:0.4">\u2014</span>';
1255
+ return '<tr>' +
1256
+ '<td class="td-task">' + escapeHtml(t.id || '\u2014') + '</td>' +
1257
+ '<td class="td-agent">' + escapeHtml(t.agent || '\u2014') + '</td>' +
1258
+ '<td>' + escapeHtml(t.review_level || '\u2014') + '</td>' +
1259
+ '<td>' + verdictBadge + '</td>' +
1260
+ '<td>' + escapeHtml(t.review_model || '\u2014') + '</td>' +
1261
+ '<td class="td-num">' + (t.review_tokens != null ? formatTokens(t.review_tokens) : '\u2014') + '</td>' +
1262
+ '</tr>';
1263
+ }).join('') +
1264
+ '</tbody></table>';
1265
+ }
1266
+
781
1267
  // ── Quality Section ──────────────────────────────────────
782
1268
 
783
1269
  function renderQualitySection(detail) {
@@ -1227,7 +1713,7 @@ try {
1227
1713
 
1228
1714
  sections.forEach((s) => observer.observe(s));
1229
1715
 
1230
- const CONVOY_DETAIL_SECTIONS = ["tasks-section", "quality-section", "reliability-section", "drift-section", "outputs-section", "event-timeline-section"];
1716
+ const CONVOY_DETAIL_SECTIONS = ["tasks-section", "pipeline-section", "agent-section", "tier-section", "model-section", "quality-section", "reliability-section", "drift-section", "outputs-section", "execution-section", "event-timeline-section", "panel-section", "reviews-section"];
1231
1717
 
1232
1718
  links.forEach((link) => {
1233
1719
  link.addEventListener("click", async (e) => {
@@ -109,6 +109,19 @@ When calling `runSubagent`, always specify which custom agent to use by name: *"
109
109
 
110
110
  **After each sub-agent returns**, log the delegation record before doing anything else (before review, before verification). This is a **⛔ hard gate** — do NOT proceed to review or any other action until the delegation is logged. Use the **observability-logging** skill's delegation record command (`--mechanism sub-agent`).
111
111
 
112
+ ### Empty Output Handling
113
+
114
+ If a sub-agent returns empty, minimal, or off-topic output:
115
+
116
+ 1. **Never fall back to writing content yourself** — Rule #1 still applies
117
+ 2. **Retry with an explicit prompt** — Restate the objective with:
118
+ - Exact deliverables expected (e.g., "Return the full revised text, not a summary")
119
+ - The Output Contract from the agent's definition (paste it into the prompt)
120
+ - An example of what good output looks like
121
+ 3. **Escalate the model** — If the Economy-tier agent fails twice, re-delegate to a Standard-tier agent (e.g., use Developer or UI/UX Expert for content tasks that require codebase context)
122
+ 4. **Log the failure** — Even if retry succeeds, log the empty-output attempt as a delegation with `outcome: failed` and `failure_reason: empty_output`
123
+ 5. **Max 3 attempts** — After 3 empty returns → DLQ the task to `.opencastle/AGENT-FAILURES.md`
124
+
112
125
  > **`model` and `tier` must come from the agent registry** — not the Team Lead's own model. Look up the agent in [agent-registry.md](../.opencastle/agents/agent-registry.md) and use their assigned model and tier. For example, delegating to Developer → `"model":"claude-sonnet-4-6","tier":"quality"`, not the Team Lead's `claude-opus-4-6`.
113
126
 
114
127
  ### Background Agents — Delegate Session
@@ -0,0 +1,58 @@
1
+ ---
2
+ description: 'Fix validation errors in a PRD. Goal is the broken PRD markdown; context is the error list from the validation step.'
3
+ agent: 'Team Lead (OpenCastle)'
4
+ output: prd
5
+ ---
6
+
7
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
8
+
9
+ # Fix PRD
10
+
11
+ You are the Team Lead. The PRD below failed validation. Fix **every reported issue** and output a complete, corrected PRD.
12
+
13
+ ## Failing PRD
14
+
15
+ {{goal}}
16
+
17
+ ## Validation Errors
18
+
19
+ {{context}}
20
+
21
+ ---
22
+
23
+ ## Fix Instructions
24
+
25
+ 1. Read every reported issue before making changes.
26
+ 2. Fix **all** reported issues — do not partially fix.
27
+ 3. Do not change the intent, goals, or scope of the feature. Only fix what the validator flagged.
28
+ 4. Preserve all content that is not part of the reported issues.
29
+
30
+ ### Common Fix Patterns
31
+
32
+ **Missing sections**
33
+ - Add the missing section with concrete, specific content — not placeholder text
34
+ - If the section needs real data you cannot infer, write a reasonable default and mark with `<!-- TODO: verify -->`
35
+
36
+ **Vague acceptance criteria**
37
+ - Replace subjective language ("looks good", "feels responsive") with measurable conditions ("renders within 200ms", "meets WCAG 2.2 AA contrast ratio")
38
+ - Replace modal verbs ("should", "might", "could") with definitive language ("must", "will")
39
+
40
+ **Broad implementation scope**
41
+ - Replace broad paths (`src/`, `the frontend`) with specific file names or subdirectory names
42
+ - Remove glob patterns (`*`, `**`) from scope tables — use actual directory or file names
43
+
44
+ **File partition conflicts**
45
+ - If two parallel workstreams claim the same file, move one to a later phase with explicit dependency
46
+ - Or split the file's responsibilities across the two workstreams so each touches distinct files
47
+
48
+ **Missing dependency declarations**
49
+ - Add `depends on: Phase N` to phases that require output from earlier phases
50
+
51
+ **Placeholder text**
52
+ - Replace template filler ("2–3 sentences about…", "Description here") with real content derived from the feature description
53
+
54
+ ---
55
+
56
+ ## Output
57
+
58
+ Return the **complete corrected PRD** as raw Markdown starting with the `#` heading. Do not wrap the output in a code fence. Do not add explanatory prose before or after the PRD.