opencastle 0.27.0 → 0.27.2

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 (242) hide show
  1. package/bin/cli.mjs +6 -0
  2. package/dist/cli/agents.d.ts +3 -0
  3. package/dist/cli/agents.d.ts.map +1 -0
  4. package/dist/cli/agents.js +161 -0
  5. package/dist/cli/agents.js.map +1 -0
  6. package/dist/cli/baselines.d.ts +3 -0
  7. package/dist/cli/baselines.d.ts.map +1 -0
  8. package/dist/cli/baselines.js +128 -0
  9. package/dist/cli/baselines.js.map +1 -0
  10. package/dist/cli/convoy/dashboard-types.d.ts +146 -0
  11. package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
  12. package/dist/cli/convoy/dashboard-types.js +2 -0
  13. package/dist/cli/convoy/dashboard-types.js.map +1 -0
  14. package/dist/cli/convoy/engine.d.ts +67 -2
  15. package/dist/cli/convoy/engine.d.ts.map +1 -1
  16. package/dist/cli/convoy/engine.js +2036 -28
  17. package/dist/cli/convoy/engine.js.map +1 -1
  18. package/dist/cli/convoy/engine.test.js +1659 -70
  19. package/dist/cli/convoy/engine.test.js.map +1 -1
  20. package/dist/cli/convoy/event-schemas.d.ts +9 -0
  21. package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
  22. package/dist/cli/convoy/event-schemas.js +185 -0
  23. package/dist/cli/convoy/event-schemas.js.map +1 -0
  24. package/dist/cli/convoy/events.d.ts +12 -1
  25. package/dist/cli/convoy/events.d.ts.map +1 -1
  26. package/dist/cli/convoy/events.js +186 -13
  27. package/dist/cli/convoy/events.js.map +1 -1
  28. package/dist/cli/convoy/events.test.js +325 -28
  29. package/dist/cli/convoy/events.test.js.map +1 -1
  30. package/dist/cli/convoy/expertise.d.ts +16 -0
  31. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  32. package/dist/cli/convoy/expertise.js +121 -0
  33. package/dist/cli/convoy/expertise.js.map +1 -0
  34. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  35. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/expertise.test.js +96 -0
  37. package/dist/cli/convoy/expertise.test.js.map +1 -0
  38. package/dist/cli/convoy/export.test.js +1 -0
  39. package/dist/cli/convoy/export.test.js.map +1 -1
  40. package/dist/cli/convoy/formula.d.ts +19 -0
  41. package/dist/cli/convoy/formula.d.ts.map +1 -0
  42. package/dist/cli/convoy/formula.js +142 -0
  43. package/dist/cli/convoy/formula.js.map +1 -0
  44. package/dist/cli/convoy/formula.test.d.ts +2 -0
  45. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  46. package/dist/cli/convoy/formula.test.js +342 -0
  47. package/dist/cli/convoy/formula.test.js.map +1 -0
  48. package/dist/cli/convoy/gates.d.ts +128 -0
  49. package/dist/cli/convoy/gates.d.ts.map +1 -0
  50. package/dist/cli/convoy/gates.js +606 -0
  51. package/dist/cli/convoy/gates.js.map +1 -0
  52. package/dist/cli/convoy/gates.test.d.ts +2 -0
  53. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  54. package/dist/cli/convoy/gates.test.js +976 -0
  55. package/dist/cli/convoy/gates.test.js.map +1 -0
  56. package/dist/cli/convoy/health.d.ts +11 -0
  57. package/dist/cli/convoy/health.d.ts.map +1 -1
  58. package/dist/cli/convoy/health.js +54 -0
  59. package/dist/cli/convoy/health.js.map +1 -1
  60. package/dist/cli/convoy/health.test.js +56 -1
  61. package/dist/cli/convoy/health.test.js.map +1 -1
  62. package/dist/cli/convoy/issues.d.ts +8 -0
  63. package/dist/cli/convoy/issues.d.ts.map +1 -0
  64. package/dist/cli/convoy/issues.js +98 -0
  65. package/dist/cli/convoy/issues.js.map +1 -0
  66. package/dist/cli/convoy/issues.test.d.ts +2 -0
  67. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/issues.test.js +107 -0
  69. package/dist/cli/convoy/issues.test.js.map +1 -0
  70. package/dist/cli/convoy/knowledge.d.ts +5 -0
  71. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  72. package/dist/cli/convoy/knowledge.js +116 -0
  73. package/dist/cli/convoy/knowledge.js.map +1 -0
  74. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  75. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/knowledge.test.js +87 -0
  77. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  78. package/dist/cli/convoy/lessons.d.ts +17 -0
  79. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  80. package/dist/cli/convoy/lessons.js +149 -0
  81. package/dist/cli/convoy/lessons.js.map +1 -0
  82. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  83. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/lessons.test.js +135 -0
  85. package/dist/cli/convoy/lessons.test.js.map +1 -0
  86. package/dist/cli/convoy/lock.d.ts +13 -0
  87. package/dist/cli/convoy/lock.d.ts.map +1 -0
  88. package/dist/cli/convoy/lock.js +88 -0
  89. package/dist/cli/convoy/lock.js.map +1 -0
  90. package/dist/cli/convoy/lock.test.d.ts +2 -0
  91. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  92. package/dist/cli/convoy/lock.test.js +136 -0
  93. package/dist/cli/convoy/lock.test.js.map +1 -0
  94. package/dist/cli/convoy/log-merge.test.d.ts +2 -0
  95. package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
  96. package/dist/cli/convoy/log-merge.test.js +147 -0
  97. package/dist/cli/convoy/log-merge.test.js.map +1 -0
  98. package/dist/cli/convoy/merge.d.ts +4 -0
  99. package/dist/cli/convoy/merge.d.ts.map +1 -1
  100. package/dist/cli/convoy/merge.js +18 -1
  101. package/dist/cli/convoy/merge.js.map +1 -1
  102. package/dist/cli/convoy/merge.test.js +6 -7
  103. package/dist/cli/convoy/merge.test.js.map +1 -1
  104. package/dist/cli/convoy/partition.d.ts +51 -0
  105. package/dist/cli/convoy/partition.d.ts.map +1 -0
  106. package/dist/cli/convoy/partition.js +186 -0
  107. package/dist/cli/convoy/partition.js.map +1 -0
  108. package/dist/cli/convoy/partition.test.d.ts +2 -0
  109. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  110. package/dist/cli/convoy/partition.test.js +315 -0
  111. package/dist/cli/convoy/partition.test.js.map +1 -0
  112. package/dist/cli/convoy/pipeline.test.js +6 -0
  113. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  114. package/dist/cli/convoy/store.d.ts +99 -7
  115. package/dist/cli/convoy/store.d.ts.map +1 -1
  116. package/dist/cli/convoy/store.js +764 -31
  117. package/dist/cli/convoy/store.js.map +1 -1
  118. package/dist/cli/convoy/store.test.js +1810 -18
  119. package/dist/cli/convoy/store.test.js.map +1 -1
  120. package/dist/cli/convoy/types.d.ts +427 -5
  121. package/dist/cli/convoy/types.d.ts.map +1 -1
  122. package/dist/cli/convoy/types.js +42 -1
  123. package/dist/cli/convoy/types.js.map +1 -1
  124. package/dist/cli/log.d.ts +11 -0
  125. package/dist/cli/log.d.ts.map +1 -1
  126. package/dist/cli/log.js +114 -2
  127. package/dist/cli/log.js.map +1 -1
  128. package/dist/cli/run/adapters/claude.d.ts +2 -0
  129. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  130. package/dist/cli/run/adapters/claude.js +89 -49
  131. package/dist/cli/run/adapters/claude.js.map +1 -1
  132. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  133. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  134. package/dist/cli/run/adapters/claude.test.js +205 -0
  135. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  137. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  138. package/dist/cli/run/adapters/copilot.js +84 -46
  139. package/dist/cli/run/adapters/copilot.js.map +1 -1
  140. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  141. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  142. package/dist/cli/run/adapters/copilot.test.js +195 -0
  143. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  145. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  146. package/dist/cli/run/adapters/cursor.js +83 -47
  147. package/dist/cli/run/adapters/cursor.js.map +1 -1
  148. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  149. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  150. package/dist/cli/run/adapters/cursor.test.js +129 -0
  151. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  153. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  154. package/dist/cli/run/adapters/opencode.js +81 -47
  155. package/dist/cli/run/adapters/opencode.js.map +1 -1
  156. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  157. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  158. package/dist/cli/run/adapters/opencode.test.js +119 -0
  159. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  160. package/dist/cli/run/executor.js +1 -1
  161. package/dist/cli/run/executor.js.map +1 -1
  162. package/dist/cli/run/schema.d.ts.map +1 -1
  163. package/dist/cli/run/schema.js +245 -4
  164. package/dist/cli/run/schema.js.map +1 -1
  165. package/dist/cli/run/schema.test.js +669 -0
  166. package/dist/cli/run/schema.test.js.map +1 -1
  167. package/dist/cli/run.d.ts.map +1 -1
  168. package/dist/cli/run.js +362 -22
  169. package/dist/cli/run.js.map +1 -1
  170. package/dist/cli/types.d.ts +85 -2
  171. package/dist/cli/types.d.ts.map +1 -1
  172. package/dist/cli/types.js.map +1 -1
  173. package/dist/cli/watch.d.ts +15 -0
  174. package/dist/cli/watch.d.ts.map +1 -0
  175. package/dist/cli/watch.js +279 -0
  176. package/dist/cli/watch.js.map +1 -0
  177. package/package.json +5 -1
  178. package/src/cli/agents.ts +177 -0
  179. package/src/cli/baselines.ts +143 -0
  180. package/src/cli/convoy/TELEMETRY.md +203 -0
  181. package/src/cli/convoy/dashboard-types.ts +141 -0
  182. package/src/cli/convoy/engine.test.ts +1937 -70
  183. package/src/cli/convoy/engine.ts +2350 -40
  184. package/src/cli/convoy/event-schemas.ts +195 -0
  185. package/src/cli/convoy/events.test.ts +384 -39
  186. package/src/cli/convoy/events.ts +202 -16
  187. package/src/cli/convoy/expertise.test.ts +128 -0
  188. package/src/cli/convoy/expertise.ts +163 -0
  189. package/src/cli/convoy/export.test.ts +1 -0
  190. package/src/cli/convoy/formula.test.ts +405 -0
  191. package/src/cli/convoy/formula.ts +174 -0
  192. package/src/cli/convoy/gates.test.ts +1169 -0
  193. package/src/cli/convoy/gates.ts +774 -0
  194. package/src/cli/convoy/health.test.ts +64 -2
  195. package/src/cli/convoy/health.ts +80 -2
  196. package/src/cli/convoy/issues.test.ts +143 -0
  197. package/src/cli/convoy/issues.ts +136 -0
  198. package/src/cli/convoy/knowledge.test.ts +101 -0
  199. package/src/cli/convoy/knowledge.ts +132 -0
  200. package/src/cli/convoy/lessons.test.ts +188 -0
  201. package/src/cli/convoy/lessons.ts +164 -0
  202. package/src/cli/convoy/lock.test.ts +181 -0
  203. package/src/cli/convoy/lock.ts +103 -0
  204. package/src/cli/convoy/log-merge.test.ts +179 -0
  205. package/src/cli/convoy/merge.test.ts +6 -7
  206. package/src/cli/convoy/merge.ts +19 -1
  207. package/src/cli/convoy/partition.test.ts +423 -0
  208. package/src/cli/convoy/partition.ts +232 -0
  209. package/src/cli/convoy/pipeline.test.ts +6 -0
  210. package/src/cli/convoy/store.test.ts +2041 -20
  211. package/src/cli/convoy/store.ts +945 -46
  212. package/src/cli/convoy/types.ts +278 -4
  213. package/src/cli/log.ts +120 -2
  214. package/src/cli/run/adapters/claude.test.ts +234 -0
  215. package/src/cli/run/adapters/claude.ts +45 -5
  216. package/src/cli/run/adapters/copilot.test.ts +224 -0
  217. package/src/cli/run/adapters/copilot.ts +34 -4
  218. package/src/cli/run/adapters/cursor.test.ts +144 -0
  219. package/src/cli/run/adapters/cursor.ts +33 -2
  220. package/src/cli/run/adapters/opencode.test.ts +135 -0
  221. package/src/cli/run/adapters/opencode.ts +30 -2
  222. package/src/cli/run/executor.ts +1 -1
  223. package/src/cli/run/schema.test.ts +758 -0
  224. package/src/cli/run/schema.ts +300 -25
  225. package/src/cli/run.ts +341 -21
  226. package/src/cli/types.ts +86 -1
  227. package/src/cli/watch.ts +298 -0
  228. package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
  229. package/src/dashboard/dist/data/.gitkeep +0 -0
  230. package/src/dashboard/dist/data/convoy-list.json +1 -0
  231. package/src/dashboard/dist/data/overall-stats.json +24 -0
  232. package/src/dashboard/dist/index.html +701 -3
  233. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  234. package/src/dashboard/public/data/.gitkeep +0 -0
  235. package/src/dashboard/public/data/convoy-list.json +1 -0
  236. package/src/dashboard/public/data/overall-stats.json +24 -0
  237. package/src/dashboard/scripts/etl.test.ts +210 -0
  238. package/src/dashboard/scripts/etl.ts +108 -0
  239. package/src/dashboard/scripts/integration-test.ts +504 -0
  240. package/src/dashboard/src/pages/index.astro +854 -15
  241. package/src/dashboard/src/styles/dashboard.css +557 -1
  242. package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
@@ -3,9 +3,20 @@ import Layout from '../layouts/Layout.astro';
3
3
  import '../styles/dashboard.css';
4
4
 
5
5
  const base = import.meta.env.BASE_URL;
6
+ let overallStats = null
7
+ let convoyList: unknown[] = []
8
+ try {
9
+ const statsModule = await import('../../public/data/overall-stats.json')
10
+ overallStats = statsModule.default ?? statsModule
11
+ const listModule = await import('../../public/data/convoy-list.json')
12
+ convoyList = listModule.default ?? listModule
13
+ } catch { /* data not yet generated */ }
6
14
  ---
7
15
 
8
16
  <Layout title="Observability Dashboard — OpenCastle">
17
+ <script define:vars={{ overallStats, convoyList }}>
18
+ window.__DASHBOARD_DATA__ = { overallStats, convoyList }
19
+ </script>
9
20
  <!-- Header -->
10
21
  <header class="dash-header">
11
22
  <div class="dash-header__inner">
@@ -14,7 +25,13 @@ const base = import.meta.env.BASE_URL;
14
25
  <h1 class="dash-header__title">Observability Dashboard</h1>
15
26
  </div>
16
27
  <div class="dash-header__actions">
17
- <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON">
28
+ <div class="convoy-selector">
29
+ <label class="convoy-selector__label" for="convoy-select">Convoy</label>
30
+ <select class="convoy-selector__select" id="convoy-select" aria-label="Select a convoy to view its details">
31
+ <option value="">Select convoy…</option>
32
+ </select>
33
+ </div>
34
+ <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON" aria-label="Export dashboard data as JSON">
18
35
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
19
36
  Export
20
37
  </button>
@@ -26,23 +43,151 @@ const base = import.meta.env.BASE_URL;
26
43
  <!-- Sidebar Navigation -->
27
44
  <nav class="dash-sidebar" id="dash-sidebar">
28
45
  <ul class="dash-sidebar__list">
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>
31
- <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li>
32
- <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Task Flow</a></li>
33
- <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li>
34
- <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li>
35
- <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li>
36
- <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li>
37
- <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li>
38
- <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li>
39
- <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li>
40
- <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section">Reviews</a></li>
41
- <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li>
46
+ <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#overall-section" data-section="overall-section" aria-label="Overview section">Overview</a></li>
47
+ <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section" aria-label="Convoy section">Convoy</a></li>
48
+ <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" aria-label="Tasks section">Tasks</a></li>
49
+ <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" aria-label="Quality section">Quality</a></li>
50
+ <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" aria-label="Reliability section">Reliability</a></li>
51
+ <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" aria-label="Drift section">Drift</a></li>
52
+ <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" aria-label="Outputs section">Outputs</a></li>
53
+ <li><a class="dash-sidebar__link" href="#event-timeline-section" data-section="event-timeline-section" aria-label="Event Log section">Event Log</a></li>
42
54
  </ul>
43
55
  </nav>
44
56
 
45
57
  <main class="dash-main">
58
+ <!-- Overall Stats Section -->
59
+ <section class="overall-stats" id="overall-section" data-nav-section>
60
+ <div class="overall-stats__header">
61
+ <h2 class="overall-stats__title">Overall Stats</h2>
62
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: How all runs behave across your project." data-tooltip="How all runs behave across your project.">ℹ️</span>
63
+ </div>
64
+ <div class="overall-stats__grid">
65
+ <div class="overall-kpi" id="overall-total-runs">
66
+ <span class="overall-kpi__label">Total Runs <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Number of convoy runs executed" data-tooltip="Number of convoy runs executed">ℹ️</span></span>
67
+ <span class="overall-kpi__value">&mdash;</span>
68
+ </div>
69
+ <div class="overall-kpi" id="overall-running">
70
+ <span class="overall-kpi__label">Running Now</span>
71
+ <span class="overall-kpi__value">&mdash;</span>
72
+ </div>
73
+ <div class="overall-kpi" id="overall-success-rate">
74
+ <span class="overall-kpi__label">Success Rate</span>
75
+ <span class="overall-kpi__value">&mdash;</span>
76
+ </div>
77
+ <div class="overall-kpi" id="overall-avg-duration">
78
+ <span class="overall-kpi__label">Avg Duration</span>
79
+ <span class="overall-kpi__value">&mdash;</span>
80
+ </div>
81
+ <div class="overall-kpi" id="overall-total-tokens">
82
+ <span class="overall-kpi__label">Total Tokens</span>
83
+ <span class="overall-kpi__value">&mdash;</span>
84
+ </div>
85
+ <div class="overall-kpi" id="overall-total-cost">
86
+ <span class="overall-kpi__label">Total Cost</span>
87
+ <span class="overall-kpi__value">&mdash;</span>
88
+ </div>
89
+ </div>
90
+ </section>
91
+
92
+ <!-- Selected Convoy Header -->
93
+ <section class="convoy-detail-header" id="convoy-detail-header">
94
+ <div class="convoy-detail-header__top">
95
+ <h2 class="convoy-detail-header__name" id="selected-convoy-name">No convoy selected</h2>
96
+ <span class="status-badge" id="selected-convoy-status" role="status"></span>
97
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: A convoy is a set of AI tasks working together on one goal." data-tooltip="A convoy is a set of AI tasks working together on one goal.">ℹ️</span>
98
+ </div>
99
+ <p class="convoy-status-explanation" id="convoy-status-explanation" role="status" aria-live="polite"></p>
100
+ <div class="convoy-detail-header__meta" id="selected-convoy-meta">
101
+ <!-- Populated by JS: branch, timestamps, duration, tokens, cost -->
102
+ </div>
103
+ </section>
104
+
105
+ <!-- Tasks Section -->
106
+ <section class="chart-card" id="tasks-section" data-nav-section style="display:none">
107
+ <div class="chart-card__header">
108
+ <h2 class="chart-card__title">Tasks</h2>
109
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Individual units of work in this convoy." data-tooltip="Individual units of work in this convoy.">ℹ️</span>
110
+ <p class="chart-card__desc" id="tasks-section-desc">Task breakdown for the selected convoy</p>
111
+ </div>
112
+ <div class="chart-card__body" id="tasks-section-body">
113
+ <div id="task-summary-cards" class="task-summary-cards"></div>
114
+ <div id="phase-breakdown" class="phase-breakdown"></div>
115
+ <div id="task-table-wrap" class="task-table-wrap"></div>
116
+ </div>
117
+ </section>
118
+
119
+ <!-- Quality Section -->
120
+ <section class="chart-card" id="quality-section" data-nav-section style="display:none">
121
+ <div class="chart-card__header">
122
+ <h2 class="chart-card__title">Quality / Review</h2>
123
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Code review results and dispute resolution." data-tooltip="Code review results and dispute resolution.">ℹ️</span>
124
+ <p class="chart-card__desc">Code review results and quality gate outcomes for the selected convoy</p>
125
+ </div>
126
+ <div class="chart-card__body">
127
+ <div id="quality-cards" class="task-summary-cards"></div>
128
+ <div id="quality-review-table"></div>
129
+ </div>
130
+ </section>
131
+
132
+ <!-- Reliability Section -->
133
+ <section class="chart-card" id="reliability-section" data-nav-section style="display:none">
134
+ <div class="chart-card__header">
135
+ <h2 class="chart-card__title">Reliability</h2>
136
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Errors, retry attempts, and safety mechanisms." data-tooltip="Errors, retry attempts, and safety mechanisms.">ℹ️</span>
137
+ <p class="chart-card__desc">Retry queue and error overview for the selected convoy</p>
138
+ </div>
139
+ <div class="chart-card__body">
140
+ <div id="reliability-dlq-card"></div>
141
+ <div id="reliability-dlq-table"></div>
142
+ <div style="margin-top:24px">
143
+ <h3 style="font-size:1rem;font-weight:600;color:var(--text-secondary);margin:0 0 12px">Error Overview</h3>
144
+ <div id="reliability-error-overview"></div>
145
+ </div>
146
+ </div>
147
+ </section>
148
+
149
+ <!-- Drift Section -->
150
+ <section class="chart-card" id="drift-section" data-nav-section style="display:none">
151
+ <div class="chart-card__header">
152
+ <h2 class="chart-card__title">Drift</h2>
153
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: How far the actual work deviated from the original plan." data-tooltip="How far the actual work deviated from the original plan.">ℹ️</span>
154
+ <p class="chart-card__desc">Plan adherence and deviation metrics for the selected convoy</p>
155
+ </div>
156
+ <div class="chart-card__body">
157
+ <div id="drift-cards" class="task-summary-cards"></div>
158
+ <div id="drift-secret-banner" style="display:none"></div>
159
+ </div>
160
+ </section>
161
+
162
+ <!-- Outputs Section -->
163
+ <section class="chart-card" id="outputs-section" data-nav-section style="display:none">
164
+ <div class="chart-card__header">
165
+ <h2 class="chart-card__title">Outputs &amp; Artifacts</h2>
166
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Files, data, and summaries produced by this convoy." data-tooltip="Files, data, and summaries produced by this convoy.">ℹ️</span>
167
+ <p class="chart-card__desc">Files, summaries, and structured data produced by tasks in this convoy</p>
168
+ </div>
169
+ <div class="chart-card__body">
170
+ <div id="outputs-cards" class="task-summary-cards"></div>
171
+ <div id="artifact-table-wrap"></div>
172
+ </div>
173
+ </section>
174
+
175
+ <!-- Event Timeline Section -->
176
+ <section class="chart-card" id="event-timeline-section" data-nav-section style="display:none">
177
+ <div class="chart-card__header">
178
+ <h2 class="chart-card__title">Event Timeline</h2>
179
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Chronological log of everything that happened during this run." data-tooltip="Chronological log of everything that happened during this run.">ℹ️</span>
180
+ <p class="chart-card__desc">Convoy events in reverse chronological order</p>
181
+ </div>
182
+ <div class="chart-card__body">
183
+ <div id="event-timeline-filters" class="timeline-filters"></div>
184
+ <div id="event-timeline-list"></div>
185
+ <div id="event-timeline-load-more" style="display:none;text-align:center;padding:16px">
186
+ <button class="dash-btn dash-btn--ghost" id="event-timeline-more-btn" type="button">Load more</button>
187
+ </div>
188
+ </div>
189
+ </section>
190
+
46
191
  <!-- Filter Bar -->
47
192
  <div class="filter-bar" id="filter-bar">
48
193
  <div class="filter-group">
@@ -1674,9 +1819,684 @@ const base = import.meta.env.BASE_URL;
1674
1819
  });
1675
1820
  }
1676
1821
 
1822
+ // ── Overall Stats Rendering ──────────────────────────────
1823
+
1824
+ function renderOverallStats() {
1825
+ const data = window.__DASHBOARD_DATA__;
1826
+ if (!data || !data.overallStats) return;
1827
+ const stats = data.overallStats;
1828
+ const cc = stats.convoyCounts || {};
1829
+ const tc = stats.tokenCostTotals || {};
1830
+ const ds = stats.durationStats || {};
1831
+
1832
+ setKpiValue('overall-total-runs', String(cc.total || 0));
1833
+ setKpiValue('overall-running', String(cc.running || 0));
1834
+
1835
+ const total = cc.total || 0;
1836
+ const success = cc.done || 0;
1837
+ const rate = total > 0 ? ((success / total) * 100).toFixed(1) + '%' : '\u2014';
1838
+ setKpiValue('overall-success-rate', rate);
1839
+
1840
+ const avgSec = ds.avg_sec;
1841
+ setKpiValue('overall-avg-duration', avgSec != null ? formatDurationSec(avgSec) : '\u2014');
1842
+ setKpiValue('overall-total-tokens', formatTokens(tc.total_tokens || 0));
1843
+ setKpiValue('overall-total-cost', '$' + (tc.total_cost_usd || 0).toFixed(2));
1844
+ }
1845
+
1846
+ function setKpiValue(id, value) {
1847
+ const el = document.getElementById(id);
1848
+ if (el) {
1849
+ const valEl = el.querySelector('.overall-kpi__value');
1850
+ if (valEl) valEl.textContent = value;
1851
+ }
1852
+ }
1853
+
1854
+ function formatDurationSec(sec) {
1855
+ if (sec < 60) return Math.round(sec) + 's';
1856
+ const min = Math.floor(sec / 60);
1857
+ const remSec = Math.round(sec % 60);
1858
+ if (min < 60) return min + 'm ' + remSec + 's';
1859
+ const hr = Math.floor(min / 60);
1860
+ const remMin = min % 60;
1861
+ return hr + 'h ' + remMin + 'm';
1862
+ }
1863
+
1864
+ // ── Convoy Selector ──────────────────────────────────────
1865
+
1866
+ function populateConvoySelector() {
1867
+ const data = window.__DASHBOARD_DATA__;
1868
+ const select = document.getElementById('convoy-select');
1869
+ if (!select || !data || !data.convoyList) return;
1870
+
1871
+ select.innerHTML = '<option value="">Select convoy\u2026</option>';
1872
+ const list = data.convoyList;
1873
+ for (const c of list) {
1874
+ const opt = document.createElement('option');
1875
+ opt.value = c.id;
1876
+ const dateStr = c.created_at ? c.created_at.slice(0, 10) : '';
1877
+ opt.textContent = (c.name || c.id) + ' \u2014 ' + c.status + ' (' + dateStr + ')';
1878
+ select.appendChild(opt);
1879
+ }
1880
+
1881
+ // Default: select latest (first in list)
1882
+ if (list.length > 0) {
1883
+ select.value = list[0].id;
1884
+ loadConvoyDetail(list[0].id);
1885
+ }
1886
+ }
1887
+
1888
+ async function loadConvoyDetail(convoyId) {
1889
+ if (!convoyId) {
1890
+ renderConvoyDetailHeader(null);
1891
+ ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
1892
+ var el = document.getElementById(id);
1893
+ if (el) el.style.display = 'none';
1894
+ });
1895
+ return;
1896
+ }
1897
+ try {
1898
+ const resp = await fetch(base + 'data/convoys/' + encodeURIComponent(convoyId) + '.json');
1899
+ if (!resp.ok) throw new Error('Failed to load convoy detail');
1900
+ const detail = await resp.json();
1901
+ renderConvoyDetailHeader(detail);
1902
+ window.__SELECTED_CONVOY__ = detail;
1903
+ renderTaskSummaryCards(detail.tasks || []);
1904
+ renderTaskTable(detail.tasks || []);
1905
+ renderPhaseBreakdown(detail.tasks || []);
1906
+ const tasksSection = document.getElementById('tasks-section');
1907
+ if (tasksSection) tasksSection.style.display = '';
1908
+ renderQualitySection(detail);
1909
+ renderReliabilitySection(detail);
1910
+ renderDriftSection(detail);
1911
+ renderOutputsSection(detail);
1912
+ renderEventTimeline(detail);
1913
+ } catch (e) {
1914
+ console.error('Failed to load convoy detail:', e);
1915
+ renderConvoyDetailHeader(null);
1916
+ ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
1917
+ var el = document.getElementById(id);
1918
+ if (el) el.style.display = 'none';
1919
+ });
1920
+ }
1921
+ }
1922
+
1923
+ function renderConvoyDetailHeader(detail) {
1924
+ const nameEl = document.getElementById('selected-convoy-name');
1925
+ const statusEl = document.getElementById('selected-convoy-status');
1926
+ const metaEl = document.getElementById('selected-convoy-meta');
1927
+
1928
+ if (!detail || !detail.convoy) {
1929
+ if (nameEl) nameEl.textContent = 'No convoy selected';
1930
+ if (statusEl) { statusEl.textContent = ''; statusEl.className = 'status-badge'; }
1931
+ if (metaEl) metaEl.innerHTML = '';
1932
+ ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
1933
+ var el = document.getElementById(id);
1934
+ if (el) el.style.display = 'none';
1935
+ });
1936
+ return;
1937
+ }
1938
+
1939
+ const c = detail.convoy;
1940
+ if (nameEl) nameEl.textContent = c.name || c.id;
1941
+ if (statusEl) {
1942
+ statusEl.textContent = c.status;
1943
+ statusEl.className = 'status-badge status-badge--' + c.status;
1944
+ }
1945
+ // Status explanation
1946
+ const statusExplanations = {
1947
+ 'running': 'This run is still in progress; some tasks are being executed.',
1948
+ 'done': 'This run completed successfully; all tasks finished.',
1949
+ 'failed': 'This run stopped because at least one task could not recover.',
1950
+ 'gate-failed': 'This run stopped because a quality check failed.',
1951
+ 'hook-failed': 'This run stopped because a lifecycle script failed.',
1952
+ };
1953
+ const explanationEl = document.getElementById('convoy-status-explanation');
1954
+ if (explanationEl) {
1955
+ explanationEl.textContent = statusExplanations[c.status] || '';
1956
+ explanationEl.style.display = statusExplanations[c.status] ? '' : 'none';
1957
+ }
1958
+ if (metaEl) {
1959
+ let html = '';
1960
+ if (c.branch) html += '<span class="convoy-meta__item">🌿 ' + escapeHtml(c.branch) + '</span>';
1961
+ if (c.created_at) html += '<span class="convoy-meta__item">\u25B6 ' + formatTime(c.created_at) + '</span>';
1962
+ if (c.finished_at) html += '<span class="convoy-meta__item">\u25A0 ' + formatTime(c.finished_at) + '</span>';
1963
+ if (c.created_at && c.finished_at) {
1964
+ const dur = formatDuration(c.created_at, c.finished_at);
1965
+ if (dur) html += '<span class="convoy-meta__item">\u23F1 ' + dur + '</span>';
1966
+ }
1967
+ if (c.total_tokens != null) html += '<span class="convoy-meta__item">\uD83D\uDD24 ' + formatTokens(c.total_tokens) + ' tokens</span>';
1968
+ if (c.total_cost_usd != null) html += '<span class="convoy-meta__item">\uD83D\uDCB0 $' + c.total_cost_usd.toFixed(2) + '</span>';
1969
+ metaEl.innerHTML = html;
1970
+ }
1971
+ }
1972
+
1973
+ // ── Task Section ─────────────────────────────────────────
1974
+
1975
+ const STATUS_TOOLTIPS = {
1976
+ 'done': 'Finished successfully.',
1977
+ 'running': 'Currently executing.',
1978
+ 'pending': 'Waiting to start.',
1979
+ 'assigned': 'Assigned to a worker, starting soon.',
1980
+ 'failed': 'Stopped due to an error.',
1981
+ 'gate-failed': 'A quality check did not pass.',
1982
+ 'timed-out': 'Exceeded the time limit.',
1983
+ 'review-blocked': 'Paused because a reviewer requested changes.',
1984
+ 'skipped': 'Skipped because a dependency failed.',
1985
+ 'hook-failed': 'A pre/post-task script encountered an error.',
1986
+ 'disputed': 'Under dispute after review disagreement.',
1987
+ 'wait-for-input': 'Paused \u2014 needs your input.',
1988
+ };
1989
+
1990
+ let taskSortCol = 'phase';
1991
+ let taskSortAsc = true;
1992
+
1993
+ function renderTaskSummaryCards(tasks) {
1994
+ const el = document.getElementById('task-summary-cards');
1995
+ if (!el) return;
1996
+ if (!tasks || tasks.length === 0) { el.innerHTML = ''; return; }
1997
+ const done = tasks.filter(t => t.status === 'done').length;
1998
+ const waiting = tasks.filter(t => t.status === 'pending' || t.status === 'assigned').length;
1999
+ const running = tasks.filter(t => t.status === 'running').length;
2000
+ const errors = tasks.filter(t => ['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(t.status)).length;
2001
+ const waitInput = tasks.filter(t => t.status === 'wait-for-input').length;
2002
+ const cards = [
2003
+ { label: 'Tasks Completed', value: done, tooltip: 'Tasks that finished successfully.', mod: 'done' },
2004
+ { label: 'Tasks Waiting', value: waiting, tooltip: 'Tasks not yet started.', mod: 'waiting' },
2005
+ { label: 'Tasks Running', value: running, tooltip: 'Tasks currently being executed.', mod: 'running' },
2006
+ { label: 'Tasks With Errors', value: errors, tooltip: 'Tasks that stopped due to errors.', mod: 'errors' },
2007
+ { label: 'Waiting For Input', value: waitInput, tooltip: 'Tasks paused until you provide input.', mod: 'input' },
2008
+ ];
2009
+ el.innerHTML = cards.map(card =>
2010
+ '<div class="task-summary-card task-summary-card--' + card.mod + '">' +
2011
+ '<span class="task-summary-card__label">' + escapeHtml(card.label) +
2012
+ ' <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: ' + escapeHtml(card.tooltip) + '" data-tooltip="' + escapeHtml(card.tooltip) + '">\u2139\uFE0F</span>' +
2013
+ '</span>' +
2014
+ '<span class="task-summary-card__value">' + card.value + '</span>' +
2015
+ '</div>'
2016
+ ).join('');
2017
+ }
2018
+
2019
+ function renderTaskTable(tasks) {
2020
+ const el = document.getElementById('task-table-wrap');
2021
+ if (!el) return;
2022
+ if (!tasks || tasks.length === 0) {
2023
+ el.innerHTML = emptyStateHtml('pipeline', 'No tasks', 'This convoy has no tasks recorded.');
2024
+ return;
2025
+ }
2026
+ const cols = [
2027
+ { key: 'phase', label: 'Phase' },
2028
+ { key: 'agent', label: 'Agent' },
2029
+ { key: 'status', label: 'Status' },
2030
+ { key: 'duration', label: 'Duration' },
2031
+ { key: 'retries', label: 'Retries' },
2032
+ { key: 'files', label: 'Files' },
2033
+ { key: 'total_tokens', label: 'Tokens' },
2034
+ ];
2035
+ const sorted = tasks.slice().sort((a, b) => {
2036
+ let av, bv;
2037
+ if (taskSortCol === 'duration') {
2038
+ av = (a.started_at && a.finished_at) ? (new Date(a.finished_at) - new Date(a.started_at)) : -1;
2039
+ bv = (b.started_at && b.finished_at) ? (new Date(b.finished_at) - new Date(b.started_at)) : -1;
2040
+ } else if (taskSortCol === 'files') {
2041
+ av = a.files ? a.files.length : 0;
2042
+ bv = b.files ? b.files.length : 0;
2043
+ } else {
2044
+ av = a[taskSortCol] != null ? a[taskSortCol] : '';
2045
+ bv = b[taskSortCol] != null ? b[taskSortCol] : '';
2046
+ }
2047
+ if (av < bv) return taskSortAsc ? -1 : 1;
2048
+ if (av > bv) return taskSortAsc ? 1 : -1;
2049
+ return 0;
2050
+ });
2051
+ const thead = '<thead><tr>' + cols.map(col => {
2052
+ const isActive = col.key === taskSortCol;
2053
+ const indicator = isActive ? (taskSortAsc ? ' \u25B2' : ' \u25BC') : ' \u25B6';
2054
+ const ariaSort = isActive ? (taskSortAsc ? 'ascending' : 'descending') : 'none';
2055
+ return '<th class="sortable-th' + (isActive ? ' sortable-th--active' : '') + '" data-sort-key="' + col.key + '" scope="col" aria-sort="' + ariaSort + '">' +
2056
+ escapeHtml(col.label) +
2057
+ '<span class="sort-indicator">' + indicator + '</span>' +
2058
+ '</th>';
2059
+ }).join('') + '</tr></thead>';
2060
+ const tbody = '<tbody>' + sorted.map(t => {
2061
+ const statusTip = STATUS_TOOLTIPS[t.status] || '';
2062
+ const statusBadge = '<span class="status-badge status-badge--' + escapeHtml(t.status) + '" title="' + escapeHtml(statusTip) + '" role="status">' + escapeHtml(t.status) + '</span>';
2063
+ const dur = formatDuration(t.started_at, t.finished_at) || '\u2014';
2064
+ const fileCount = t.files ? t.files.length : 0;
2065
+ const fileTip = t.files && t.files.length > 0 ? t.files.join('\n') : '';
2066
+ const filesCell = fileCount > 0
2067
+ ? '<span class="tooltip-trigger" data-tooltip="' + escapeHtml(fileTip) + '">' + fileCount + ' file' + (fileCount !== 1 ? 's' : '') + '</span>'
2068
+ : '<span style="opacity:0.4">\u2014</span>';
2069
+ const tokens = t.total_tokens != null ? formatTokens(t.total_tokens) : '\u2014';
2070
+ return '<tr>' +
2071
+ '<td>' + (t.phase != null ? t.phase : '\u2014') + '</td>' +
2072
+ '<td class="td-agent">' + escapeHtml(t.agent || '\u2014') + '</td>' +
2073
+ '<td>' + statusBadge + '</td>' +
2074
+ '<td>' + dur + '</td>' +
2075
+ '<td class="td-num">' + t.retries + '</td>' +
2076
+ '<td>' + filesCell + '</td>' +
2077
+ '<td class="td-num">' + tokens + '</td>' +
2078
+ '</tr>';
2079
+ }).join('') + '</tbody>';
2080
+ el.innerHTML = '<table class="sessions-table task-table">' + thead + tbody + '</table>';
2081
+ el.querySelectorAll('.sortable-th').forEach(th => {
2082
+ th.addEventListener('click', function() {
2083
+ const key = this.dataset.sortKey;
2084
+ if (key === taskSortCol) {
2085
+ taskSortAsc = !taskSortAsc;
2086
+ } else {
2087
+ taskSortCol = key;
2088
+ taskSortAsc = true;
2089
+ }
2090
+ const convoy = window.__SELECTED_CONVOY__;
2091
+ renderTaskTable((convoy && convoy.tasks) ? convoy.tasks : []);
2092
+ });
2093
+ });
2094
+ }
2095
+
2096
+ function renderPhaseBreakdown(tasks) {
2097
+ const el = document.getElementById('phase-breakdown');
2098
+ if (!el) return;
2099
+ if (!tasks || tasks.length === 0) { el.innerHTML = ''; return; }
2100
+ const phaseMap = {};
2101
+ for (const t of tasks) {
2102
+ const p = t.phase != null ? String(t.phase) : '0';
2103
+ if (!phaseMap[p]) phaseMap[p] = { done: 0, running: 0, waiting: 0, failed: 0 };
2104
+ if (t.status === 'done') phaseMap[p].done++;
2105
+ else if (t.status === 'running') phaseMap[p].running++;
2106
+ else if (t.status === 'pending' || t.status === 'assigned') phaseMap[p].waiting++;
2107
+ else phaseMap[p].failed++;
2108
+ }
2109
+ const phases = Object.keys(phaseMap).sort((a, b) => Number(a) - Number(b));
2110
+ if (phases.length === 0) { el.innerHTML = ''; return; }
2111
+ const html = phases.map(p => {
2112
+ const d = phaseMap[p];
2113
+ const total = d.done + d.running + d.waiting + d.failed;
2114
+ if (total === 0) return '';
2115
+ const pct = v => Math.round((v / total) * 100);
2116
+ const segments = [
2117
+ { cls: 'done', v: d.done },
2118
+ { cls: 'running', v: d.running },
2119
+ { cls: 'waiting', v: d.waiting },
2120
+ { cls: 'failed', v: d.failed },
2121
+ ].filter(s => s.v > 0);
2122
+ return '<div class="phase-breakdown__row">' +
2123
+ '<span class="phase-breakdown__label">Phase ' + escapeHtml(p) + '</span>' +
2124
+ '<div class="phase-breakdown__bar">' +
2125
+ segments.map(s =>
2126
+ '<div class="phase-breakdown__seg phase-breakdown__seg--' + s.cls + '" style="width:' + pct(s.v) + '%" title="' + s.cls + ': ' + s.v + '"></div>'
2127
+ ).join('') +
2128
+ '</div>' +
2129
+ '<span class="phase-breakdown__count">' + total + ' task' + (total !== 1 ? 's' : '') + '</span>' +
2130
+ '</div>';
2131
+ }).join('');
2132
+ el.innerHTML = html;
2133
+ }
2134
+
2135
+ // ── Quality Section ──────────────────────────────────────
2136
+
2137
+ function renderQualitySection(detail) {
2138
+ var section = document.getElementById('quality-section');
2139
+ if (!section) return;
2140
+ section.style.display = '';
2141
+
2142
+ var q = detail.quality || {};
2143
+ var qCards = [
2144
+ { label: 'Tasks Reviewed', value: q.reviewed_tasks != null ? q.reviewed_tasks : 0, tooltip: 'Tasks that completed a code review check.', mod: 'done' },
2145
+ { label: 'Blocked by Review', value: q.review_blocked_tasks != null ? q.review_blocked_tasks : 0, tooltip: 'Tasks paused because a reviewer requested changes.', mod: 'errors' },
2146
+ { label: 'Disputes Opened', value: q.disputed_tasks != null ? q.disputed_tasks : 0, tooltip: 'Tasks where reviewers disagreed and opened a formal dispute.', mod: 'waiting' },
2147
+ { label: 'Panel Reviews', value: q.panel_reviews != null ? q.panel_reviews : 0, tooltip: 'Tasks that went to a 3-reviewer panel vote for a final decision.', mod: 'running' },
2148
+ ];
2149
+
2150
+ var cardsEl = document.getElementById('quality-cards');
2151
+ if (cardsEl) {
2152
+ cardsEl.innerHTML = qCards.map(function(card) {
2153
+ return '<div class="task-summary-card task-summary-card--' + card.mod + '">' +
2154
+ '<span class="task-summary-card__label">' + escapeHtml(card.label) +
2155
+ ' <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: ' + escapeHtml(card.tooltip) + '" data-tooltip="' + escapeHtml(card.tooltip) + '">\u2139\uFE0F</span>' +
2156
+ '</span>' +
2157
+ '<span class="task-summary-card__value">' + card.value + '</span>' +
2158
+ '</div>';
2159
+ }).join('');
2160
+ }
2161
+
2162
+ var reviewTasks = (detail.tasks || []).filter(function(t) { return t.review_level != null; });
2163
+ var tableEl = document.getElementById('quality-review-table');
2164
+ if (!tableEl) return;
2165
+
2166
+ if (reviewTasks.length === 0) {
2167
+ tableEl.innerHTML = emptyStateHtml('panels', 'No reviewed tasks', 'Tasks with code reviews will appear here.');
2168
+ return;
2169
+ }
2170
+
2171
+ tableEl.innerHTML =
2172
+ '<table class="sessions-table" style="margin-top:16px">' +
2173
+ '<thead><tr>' +
2174
+ '<th scope="col">Task ID</th>' +
2175
+ '<th scope="col">Review Level</th>' +
2176
+ '<th scope="col">Verdict</th>' +
2177
+ '<th scope="col">Reviewer Model</th>' +
2178
+ '<th scope="col">Review Tokens</th>' +
2179
+ '<th scope="col">Panel Attempts</th>' +
2180
+ '</tr></thead>' +
2181
+ '<tbody>' +
2182
+ reviewTasks.map(function(t) {
2183
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : '';
2184
+ var verdictBadge = t.review_verdict
2185
+ ? '<span class="status-badge status-badge--' + (verdictUpper === 'PASS' ? 'done' : 'failed') + '">' + escapeHtml(t.review_verdict) + '</span>'
2186
+ : '<span style="opacity:0.4">\u2014</span>';
2187
+ return '<tr>' +
2188
+ '<td class="td-task">' + escapeHtml(t.id || '\u2014') + '</td>' +
2189
+ '<td>' + escapeHtml(t.review_level || '\u2014') + '</td>' +
2190
+ '<td>' + verdictBadge + '</td>' +
2191
+ '<td>' + escapeHtml(t.review_model || '\u2014') + '</td>' +
2192
+ '<td class="td-num">' + (t.review_tokens != null ? formatTokens(t.review_tokens) : '\u2014') + '</td>' +
2193
+ '<td class="td-num">' + (t.panel_attempts != null ? t.panel_attempts : '\u2014') + '</td>' +
2194
+ '</tr>';
2195
+ }).join('') +
2196
+ '</tbody></table>';
2197
+ }
2198
+
2199
+ // ── Reliability Section ──────────────────────────────────
2200
+
2201
+ function renderReliabilitySection(detail) {
2202
+ var section = document.getElementById('reliability-section');
2203
+ if (!section) return;
2204
+ section.style.display = '';
2205
+
2206
+ var dlqCount = detail.dlq_count || 0;
2207
+ var dlqEntries = detail.dlq_entries || [];
2208
+
2209
+ var dlqCardEl = document.getElementById('reliability-dlq-card');
2210
+ if (dlqCardEl) {
2211
+ dlqCardEl.innerHTML =
2212
+ '<div class="task-summary-card task-summary-card--' + (dlqCount > 0 ? 'errors' : 'done') + '">' +
2213
+ '<span class="task-summary-card__label">Retry Queue <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Tasks that failed too many times and need manual attention. Also known as Dead Letter Queue (DLQ)." data-tooltip="Tasks that failed too many times and need manual attention. Also known as Dead Letter Queue (DLQ).">\u2139\uFE0F</span></span>' +
2214
+ '<span class="task-summary-card__value">' + dlqCount + '</span>' +
2215
+ '</div>';
2216
+ }
2217
+
2218
+ var dlqTableEl = document.getElementById('reliability-dlq-table');
2219
+ if (dlqTableEl) {
2220
+ if (dlqEntries.length === 0) {
2221
+ dlqTableEl.innerHTML = '<p class="reliability-empty">No retry queue entries \u2014 all tasks completed without exceeding retry limits.</p>';
2222
+ } else {
2223
+ dlqTableEl.innerHTML =
2224
+ '<table class="sessions-table" style="margin-top:16px">' +
2225
+ '<thead><tr>' +
2226
+ '<th scope="col">Task ID</th><th scope="col">Agent</th><th scope="col">Failure Type</th><th scope="col">Attempts</th><th scope="col">Resolved</th>' +
2227
+ '</tr></thead>' +
2228
+ '<tbody>' +
2229
+ dlqEntries.map(function(e) {
2230
+ return '<tr>' +
2231
+ '<td class="td-task">' + escapeHtml(e.task_id || '\u2014') + '</td>' +
2232
+ '<td class="td-agent">' + escapeHtml(e.agent || '\u2014') + '</td>' +
2233
+ '<td>' + escapeHtml(e.failure_type || '\u2014') + '</td>' +
2234
+ '<td class="td-num">' + (e.attempts != null ? e.attempts : '\u2014') + '</td>' +
2235
+ '<td class="td-num">' + (e.resolved ? '\u2713' : '\u2014') + '</td>' +
2236
+ '</tr>';
2237
+ }).join('') +
2238
+ '</tbody></table>';
2239
+ }
2240
+ }
2241
+
2242
+ var errorEl = document.getElementById('reliability-error-overview');
2243
+ if (errorEl) {
2244
+ var tasks = detail.tasks || [];
2245
+ var errorCategories = [
2246
+ { key: 'failed', label: 'Failed', color: '#ef4444' },
2247
+ { key: 'gate-failed', label: 'Quality Check Failed', color: '#f59e0b' },
2248
+ { key: 'timed-out', label: 'Timed Out', color: '#a78bfa' },
2249
+ { key: 'hook-failed', label: 'Lifecycle Script Failed', color: '#64748b' },
2250
+ ];
2251
+ var counts = errorCategories.map(function(cat) {
2252
+ return { key: cat.key, label: cat.label, color: cat.color, count: tasks.filter(function(t) { return t.status === cat.key; }).length };
2253
+ });
2254
+ var maxCount = Math.max(1, counts.reduce(function(m, c) { return Math.max(m, c.count); }, 0));
2255
+ var hasErrors = counts.some(function(c) { return c.count > 0; });
2256
+
2257
+ if (!hasErrors) {
2258
+ errorEl.innerHTML = '<p class="reliability-empty">\u2713 No errors in this convoy.</p>';
2259
+ } else {
2260
+ errorEl.innerHTML = counts.map(function(cat) {
2261
+ return '<div class="bar-row">' +
2262
+ '<span class="bar-label">' + escapeHtml(cat.label) + '</span>' +
2263
+ '<div class="bar-track">' +
2264
+ '<div class="bar-segment" style="width: ' + ((cat.count / maxCount) * 100).toFixed(1) + '%; background: ' + cat.color + '"></div>' +
2265
+ '</div>' +
2266
+ '<span class="bar-value">' + cat.count + '</span>' +
2267
+ '</div>';
2268
+ }).join('');
2269
+ }
2270
+ }
2271
+ }
2272
+
2273
+ // ── Drift Section ────────────────────────────────────────
2274
+
2275
+ function renderDriftSection(detail) {
2276
+ var section = document.getElementById('drift-section');
2277
+ if (!section) return;
2278
+ section.style.display = '';
2279
+
2280
+ var d = detail.drift || {};
2281
+ var maxScore = d.max_drift_score != null ? Math.round(d.max_drift_score * 100) : 0;
2282
+ var driftCards = [
2283
+ { label: 'Tasks With Drift', value: d.tasks_with_drift != null ? d.tasks_with_drift : 0, tooltip: 'Tasks where the agent deviated from the original plan or instructions.', mod: 'waiting' },
2284
+ { label: 'Max Drift Score', value: maxScore + '%', tooltip: 'The highest drift score recorded. 0% = on-track, 100% = fully off-plan.', mod: 'errors' },
2285
+ { label: 'Drift Retried', value: d.drift_retried_tasks != null ? d.drift_retried_tasks : 0, tooltip: 'Tasks that were retried because drift exceeded the allowed threshold.', mod: 'running' },
2286
+ ];
2287
+
2288
+ var cardsEl = document.getElementById('drift-cards');
2289
+ if (cardsEl) {
2290
+ cardsEl.innerHTML = driftCards.map(function(card) {
2291
+ return '<div class="task-summary-card task-summary-card--' + card.mod + '">' +
2292
+ '<span class="task-summary-card__label">' + escapeHtml(String(card.label)) +
2293
+ ' <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: ' + escapeHtml(card.tooltip) + '" data-tooltip="' + escapeHtml(card.tooltip) + '">\u2139\uFE0F</span>' +
2294
+ '</span>' +
2295
+ '<span class="task-summary-card__value">' + card.value + '</span>' +
2296
+ '</div>';
2297
+ }).join('');
2298
+ }
2299
+
2300
+ var bannerEl = document.getElementById('drift-secret-banner');
2301
+ if (bannerEl) {
2302
+ var events = detail.events || [];
2303
+ var leakEvents = events.filter(function(e) { return e.type === 'secret_leak_prevented'; });
2304
+ if (leakEvents.length > 0) {
2305
+ bannerEl.style.display = '';
2306
+ bannerEl.innerHTML =
2307
+ '<div class="secret-leak-banner">' +
2308
+ '<span class="secret-leak-banner__icon">\u26A0\uFE0F</span>' +
2309
+ '<div class="secret-leak-banner__text">' +
2310
+ '<strong>' + leakEvents.length + ' secret leak' + (leakEvents.length !== 1 ? 's' : '') + ' prevented</strong>' +
2311
+ '<span>The security gate blocked credentials or tokens from being exposed. No action needed \u2014 the agent handled it correctly.</span>' +
2312
+ '</div>' +
2313
+ '</div>';
2314
+ } else {
2315
+ bannerEl.style.display = 'none';
2316
+ }
2317
+ }
2318
+ }
2319
+
2320
+ // ── Outputs Section ────────────────────────────────────
2321
+ function renderOutputsSection(detail) {
2322
+ var section = document.getElementById('outputs-section');
2323
+ if (!section) return;
2324
+ section.style.display = '';
2325
+
2326
+ var artifactCount = detail.artifact_count != null ? detail.artifact_count : 0;
2327
+ var artifacts = detail.artifacts || [];
2328
+
2329
+ var cardsEl = document.getElementById('outputs-cards');
2330
+ if (cardsEl) {
2331
+ cardsEl.innerHTML =
2332
+ '<div class="task-summary-card task-summary-card--done">' +
2333
+ '<span class="task-summary-card__label">Outputs Produced ' +
2334
+ '<span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Files, data, and summaries produced by this convoy." data-tooltip="Files, data, and summaries produced by this convoy.">\u2139\uFE0F</span>' +
2335
+ '</span>' +
2336
+ '<span class="task-summary-card__value">' + artifactCount + '</span>' +
2337
+ '</div>';
2338
+ }
2339
+
2340
+ var tableEl = document.getElementById('artifact-table-wrap');
2341
+ if (!tableEl) return;
2342
+
2343
+ if (artifacts.length === 0) {
2344
+ tableEl.innerHTML = emptyStateHtml('pipeline', 'No artifacts', 'Artifacts produced by tasks will appear here.');
2345
+ return;
2346
+ }
2347
+
2348
+ var TYPE_BADGE_COLORS = { file: '#64748b', summary: '#3b82f6', json: '#a78bfa' };
2349
+
2350
+ tableEl.innerHTML =
2351
+ '<table class="sessions-table" style="margin-top:16px">' +
2352
+ '<thead><tr><th scope="col">Name</th><th scope="col">Type</th><th scope="col">Task</th><th scope="col">Created At</th></tr></thead>' +
2353
+ '<tbody>' +
2354
+ artifacts.map(function(a) {
2355
+ var badgeColor = TYPE_BADGE_COLORS[a.type] || '#64748b';
2356
+ var typeBadge = '<span class="artifact-type-badge" style="background:' + badgeColor + '">' + escapeHtml(a.type) + '</span>';
2357
+ var taskCell = a.task_id ? escapeHtml(a.task_id) : '<span style="opacity:0.4">—</span>';
2358
+ var created = a.created_at ? formatTime(a.created_at) : '—';
2359
+ return '<tr>' +
2360
+ '<td>' + escapeHtml(a.name) + '</td>' +
2361
+ '<td>' + typeBadge + '</td>' +
2362
+ '<td class="td-task">' + taskCell + '</td>' +
2363
+ '<td>' + created + '</td>' +
2364
+ '</tr>';
2365
+ }).join('') +
2366
+ '</tbody></table>';
2367
+ }
2368
+
2369
+ // ── Event Timeline Section ─────────────────────────────
2370
+ var eventTimelinePageSize = 50;
2371
+ var eventTimelineCurrentPage = 1;
2372
+ var eventTimelineActiveFilter = 'all';
2373
+ var eventTimelineAllEvents = [];
2374
+
2375
+ var EVENT_CATEGORIES = {
2376
+ lifecycle: ['convoy_started', 'convoy_finished', 'convoy_failed'],
2377
+ tasks: ['task_started', 'task_done', 'task_failed', 'task_assigned', 'task_skipped', 'task_retried', 'task_timed_out', 'task_gate_failed', 'task_hook_failed', 'task_wait_input'],
2378
+ reviews: ['review_started', 'review_verdict', 'panel_started', 'panel_verdict'],
2379
+ system: ['circuit_breaker_open', 'circuit_breaker_close', 'circuit_breaker_half_open', 'merge_conflict_detected', 'merge_conflict_resolved', 'watch_started', 'watch_stopped', 'watch_cycle_start', 'watch_cycle_end', 'discovered_issue', 'secret_leak_prevented'],
2380
+ errors: []
2381
+ };
2382
+
2383
+ var EVENT_TYPE_COLORS = {};
2384
+ EVENT_CATEGORIES.lifecycle.forEach(function(t) { EVENT_TYPE_COLORS[t] = '#3b82f6'; });
2385
+ EVENT_CATEGORIES.tasks.forEach(function(t) { EVENT_TYPE_COLORS[t] = t.includes('fail') || t.includes('timed') ? '#ef4444' : '#10b981'; });
2386
+ EVENT_CATEGORIES.reviews.forEach(function(t) { EVENT_TYPE_COLORS[t] = '#a78bfa'; });
2387
+ EVENT_CATEGORIES.system.forEach(function(t) { EVENT_TYPE_COLORS[t] = '#f59e0b'; });
2388
+
2389
+ function getEventCategory(type) {
2390
+ if (EVENT_CATEGORIES.lifecycle.includes(type)) return 'lifecycle';
2391
+ if (EVENT_CATEGORIES.tasks.includes(type)) return 'tasks';
2392
+ if (EVENT_CATEGORIES.reviews.includes(type)) return 'reviews';
2393
+ if (EVENT_CATEGORIES.system.includes(type)) return 'system';
2394
+ return 'meta';
2395
+ }
2396
+
2397
+ function isErrorEvent(evt) {
2398
+ var type = evt.type || '';
2399
+ return type.includes('fail') || type.includes('block') || type.includes('error') || type.includes('timed_out') || type.includes('circuit_breaker_open');
2400
+ }
2401
+
2402
+ function renderEventTimeline(detail) {
2403
+ var section = document.getElementById('event-timeline-section');
2404
+ if (!section) return;
2405
+ section.style.display = '';
2406
+
2407
+ eventTimelineAllEvents = detail.events || [];
2408
+ eventTimelineCurrentPage = 1;
2409
+ eventTimelineActiveFilter = 'all';
2410
+
2411
+ renderEventTimelineFilters();
2412
+ renderEventTimelineList();
2413
+ }
2414
+
2415
+ function renderEventTimelineFilters() {
2416
+ var el = document.getElementById('event-timeline-filters');
2417
+ if (!el) return;
2418
+
2419
+ var filters = [
2420
+ { key: 'all', label: 'All' },
2421
+ { key: 'lifecycle', label: 'Lifecycle' },
2422
+ { key: 'tasks', label: 'Tasks' },
2423
+ { key: 'reviews', label: 'Reviews' },
2424
+ { key: 'system', label: 'System' },
2425
+ { key: 'errors', label: 'Errors' },
2426
+ ];
2427
+
2428
+ el.innerHTML = filters.map(function(f) {
2429
+ var active = f.key === eventTimelineActiveFilter ? ' timeline-filter-chip--active' : '';
2430
+ return '<button class="timeline-filter-chip' + active + '" data-filter="' + f.key + '" type="button" aria-label="Filter events: ' + escapeHtml(f.label) + '">' + escapeHtml(f.label) + '</button>';
2431
+ }).join('');
2432
+
2433
+ el.querySelectorAll('.timeline-filter-chip').forEach(function(btn) {
2434
+ btn.addEventListener('click', function() {
2435
+ eventTimelineActiveFilter = this.dataset.filter;
2436
+ eventTimelineCurrentPage = 1;
2437
+ renderEventTimelineFilters();
2438
+ renderEventTimelineList();
2439
+ });
2440
+ });
2441
+ }
2442
+
2443
+ function getFilteredEvents() {
2444
+ if (eventTimelineActiveFilter === 'all') return eventTimelineAllEvents;
2445
+ if (eventTimelineActiveFilter === 'errors') return eventTimelineAllEvents.filter(isErrorEvent);
2446
+ var cats = EVENT_CATEGORIES[eventTimelineActiveFilter];
2447
+ if (!cats) return eventTimelineAllEvents;
2448
+ return eventTimelineAllEvents.filter(function(evt) { return cats.includes(evt.type); });
2449
+ }
2450
+
2451
+ function renderEventTimelineList() {
2452
+ var el = document.getElementById('event-timeline-list');
2453
+ if (!el) return;
2454
+
2455
+ var filtered = getFilteredEvents();
2456
+ var visible = filtered.slice(0, eventTimelineCurrentPage * eventTimelinePageSize);
2457
+
2458
+ if (filtered.length === 0) {
2459
+ el.innerHTML = emptyStateHtml('timeline', 'No events', 'Events from this convoy will appear here.');
2460
+ document.getElementById('event-timeline-load-more').style.display = 'none';
2461
+ return;
2462
+ }
2463
+
2464
+ el.innerHTML = visible.map(function(evt, idx) {
2465
+ var typeColor = EVENT_TYPE_COLORS[evt.type] || '#64748b';
2466
+ var ts = evt.created_at ? formatTime(evt.created_at) : '—';
2467
+ var taskInfo = evt.task_id ? '<span class="event-timeline-context">' + escapeHtml(evt.task_id) + '</span>' : '';
2468
+ var dataJson = evt.data ? JSON.stringify(evt.data, null, 2) : null;
2469
+
2470
+ return '<div class="event-timeline-row" data-event-idx="' + idx + '" role="button" tabindex="0" aria-label="Event: ' + escapeHtml(evt.type) + (evt.task_id ? ', task: ' + escapeHtml(evt.task_id) : '') + '">' +
2471
+ '<div class="event-timeline-row__main">' +
2472
+ '<span class="event-timeline-ts">' + ts + '</span>' +
2473
+ '<span class="event-type-badge" style="background:' + typeColor + '">' + escapeHtml(evt.type) + '</span>' +
2474
+ taskInfo +
2475
+ '</div>' +
2476
+ (dataJson
2477
+ ? '<div class="event-timeline-detail" style="display:none"><pre class="event-timeline-json">' + escapeHtml(dataJson) + '</pre></div>'
2478
+ : '') +
2479
+ '</div>';
2480
+ }).join('');
2481
+
2482
+ el.querySelectorAll('.event-timeline-row').forEach(function(row) {
2483
+ row.addEventListener('click', function() {
2484
+ var detailEl = this.querySelector('.event-timeline-detail');
2485
+ if (detailEl) {
2486
+ detailEl.style.display = detailEl.style.display === 'none' ? '' : 'none';
2487
+ this.classList.toggle('event-timeline-row--expanded');
2488
+ }
2489
+ });
2490
+ });
2491
+
2492
+ var loadMoreEl = document.getElementById('event-timeline-load-more');
2493
+ if (loadMoreEl) {
2494
+ loadMoreEl.style.display = visible.length < filtered.length ? '' : 'none';
2495
+ }
2496
+ }
2497
+
1677
2498
  async function main() {
1678
2499
  const events = await loadNdjson(base + 'data/events.ndjson');
1679
- const convoys = await loadNdjson(base + 'data/convoys.ndjson');
1680
2500
  const pipelines = await loadNdjson(base + 'data/pipelines.ndjson');
1681
2501
 
1682
2502
  const sessions = events.filter((e) => e.type === 'session');
@@ -1716,6 +2536,25 @@ const base = import.meta.env.BASE_URL;
1716
2536
  // Apply convoy param after initial render (shows convoy section if needed)
1717
2537
  if (convoyParam) applyFilters();
1718
2538
 
2539
+ // ── Overall stats + convoy selector ──────────────────
2540
+ renderOverallStats();
2541
+ populateConvoySelector();
2542
+
2543
+ const convoySelectEl = document.getElementById('convoy-select');
2544
+ if (convoySelectEl) {
2545
+ convoySelectEl.addEventListener('change', function() {
2546
+ loadConvoyDetail(this.value);
2547
+ });
2548
+ }
2549
+
2550
+ var loadMoreBtn = document.getElementById('event-timeline-more-btn');
2551
+ if (loadMoreBtn) {
2552
+ loadMoreBtn.addEventListener('click', function() {
2553
+ eventTimelineCurrentPage++;
2554
+ renderEventTimelineList();
2555
+ });
2556
+ }
2557
+
1719
2558
  // ── Filter event listeners ────────────────────────────
1720
2559
  document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1721
2560
  document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);