opencastle 0.8.0 → 0.8.1

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 (87) hide show
  1. package/README.md +27 -0
  2. package/bin/cli.mjs +2 -0
  3. package/dist/cli/adapters/claude-code.d.ts +2 -5
  4. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  5. package/dist/cli/adapters/claude-code.js +12 -251
  6. package/dist/cli/adapters/claude-code.js.map +1 -1
  7. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  8. package/dist/cli/adapters/cursor.js +3 -17
  9. package/dist/cli/adapters/cursor.js.map +1 -1
  10. package/dist/cli/adapters/frontmatter.d.ts +26 -0
  11. package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
  12. package/dist/cli/adapters/frontmatter.js +40 -0
  13. package/dist/cli/adapters/frontmatter.js.map +1 -0
  14. package/dist/cli/adapters/index.d.ts +5 -0
  15. package/dist/cli/adapters/index.d.ts.map +1 -0
  16. package/dist/cli/adapters/index.js +9 -0
  17. package/dist/cli/adapters/index.js.map +1 -0
  18. package/dist/cli/adapters/opencode.d.ts +2 -5
  19. package/dist/cli/adapters/opencode.d.ts.map +1 -1
  20. package/dist/cli/adapters/opencode.js +12 -250
  21. package/dist/cli/adapters/opencode.js.map +1 -1
  22. package/dist/cli/adapters/single-file-base.d.ts +40 -0
  23. package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
  24. package/dist/cli/adapters/single-file-base.js +246 -0
  25. package/dist/cli/adapters/single-file-base.js.map +1 -0
  26. package/dist/cli/dashboard.d.ts.map +1 -1
  27. package/dist/cli/dashboard.js +3 -2
  28. package/dist/cli/dashboard.js.map +1 -1
  29. package/dist/cli/detect.d.ts.map +1 -1
  30. package/dist/cli/detect.js +13 -11
  31. package/dist/cli/detect.js.map +1 -1
  32. package/dist/cli/doctor.d.ts +3 -0
  33. package/dist/cli/doctor.d.ts.map +1 -0
  34. package/dist/cli/doctor.js +205 -0
  35. package/dist/cli/doctor.js.map +1 -0
  36. package/dist/cli/init.d.ts.map +1 -1
  37. package/dist/cli/init.js +31 -19
  38. package/dist/cli/init.js.map +1 -1
  39. package/dist/cli/run/schema.d.ts +1 -5
  40. package/dist/cli/run/schema.d.ts.map +1 -1
  41. package/dist/cli/run/schema.js +6 -330
  42. package/dist/cli/run/schema.js.map +1 -1
  43. package/dist/cli/run.d.ts.map +1 -1
  44. package/dist/cli/run.js +14 -1
  45. package/dist/cli/run.js.map +1 -1
  46. package/dist/cli/types.d.ts +0 -5
  47. package/dist/cli/types.d.ts.map +1 -1
  48. package/dist/cli/update.d.ts.map +1 -1
  49. package/dist/cli/update.js +4 -17
  50. package/dist/cli/update.js.map +1 -1
  51. package/package.json +7 -2
  52. package/src/cli/adapters/claude-code.ts +13 -304
  53. package/src/cli/adapters/cursor.ts +3 -23
  54. package/src/cli/adapters/frontmatter.ts +47 -0
  55. package/src/cli/adapters/index.ts +13 -0
  56. package/src/cli/adapters/opencode.ts +12 -301
  57. package/src/cli/adapters/single-file-base.ts +320 -0
  58. package/src/cli/dashboard.ts +3 -2
  59. package/src/cli/detect.ts +19 -15
  60. package/src/cli/doctor.ts +235 -0
  61. package/src/cli/init.ts +31 -24
  62. package/src/cli/run/schema.ts +7 -365
  63. package/src/cli/run.ts +17 -1
  64. package/src/cli/types.ts +0 -6
  65. package/src/cli/update.ts +5 -23
  66. package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
  67. package/src/dashboard/dist/index.html +170 -11
  68. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  69. package/src/dashboard/seed-data/reviews.ndjson +6 -0
  70. package/src/dashboard/src/pages/index.astro +213 -10
  71. package/src/dashboard/src/styles/dashboard.css +196 -0
  72. package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
  73. package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
  74. package/src/orchestrator/agent-workflows/database-migration.md +2 -2
  75. package/src/orchestrator/agent-workflows/feature-implementation.md +2 -2
  76. package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
  77. package/src/orchestrator/agent-workflows/refactoring.md +2 -2
  78. package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
  79. package/src/orchestrator/agent-workflows/security-audit.md +2 -2
  80. package/src/orchestrator/agents/data-expert.agent.md +2 -2
  81. package/src/orchestrator/agents/researcher.agent.md +0 -16
  82. package/src/orchestrator/agents/team-lead.agent.md +17 -6
  83. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
  84. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
  85. package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
  86. package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
  87. package/src/orchestrator/prompts/metrics-report.prompt.md +0 -144
@@ -1,4 +1,6 @@
1
- <!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Observability Dashboard — OpenCastle</title><meta name="description" content="Real-time observability for OpenCastle multi-agent orchestration — sessions, delegations, model tiers, and quality gates."><meta name="theme-color" content="#0a0a0f"><link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png"><link rel="stylesheet" href="/_astro/index.CWVzbF4T.css"></head> <body> <header class="dash-header"> <div class="dash-header__inner"> <div class="dash-header__brand"> <img class="dash-header__icon" src="/icon-192.png" alt="OpenCastle" width="32" height="32"> <h1 class="dash-header__title">Observability Dashboard</h1> </div> </div> </header> <div class="dash-layout"> <!-- Sidebar Navigation --> <nav class="dash-sidebar" id="dash-sidebar"> <ul class="dash-sidebar__list"> <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li> <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li> <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li> </ul> </nav> <main class="dash-main"> <!-- KPI Row --> <section class="kpi-row" id="kpi-row" data-nav-section> <div class="kpi-card" id="kpi-sessions"> <span class="kpi-card__label">Total Sessions</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-success"> <span class="kpi-card__label">Success Rate</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-delegations"> <span class="kpi-card__label">Total Delegations</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-duration"> <span class="kpi-card__label">Avg Duration</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-retries"> <span class="kpi-card__label">Total Retries</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-lessons"> <span class="kpi-card__label">Lessons Added</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> </section> <!-- Pipeline View (Steroids-inspired) --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <p class="chart-card__desc">Delegation flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"> <div class="loading-skeleton"></div> </div> </section> <!-- Charts Row 1 --> <div class="charts-row" id="agent-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Sessions by Agent</h2> <p class="chart-card__desc">Stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="tier-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Delegation model tiers</p> </div> <div class="chart-card__body" id="tier-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row: Delegation Insights --> <div class="charts-row" id="delegation-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Mechanism</h2> <p class="chart-card__desc">Sub-agent vs background split</p> </div> <div class="chart-card__body" id="mechanism-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Success rate by delegation</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row 2 --> <div class="charts-row" id="timeline-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Timeline</h2> <p class="chart-card__desc">Sessions and delegations over time</p> </div> <div class="chart-card__body" id="timeline-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="model-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Model Usage</h2> <p class="chart-card__desc">Sessions by model</p> </div> <div class="chart-card__body" id="model-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Execution Log (Duvo-inspired) --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"> <div class="loading-skeleton"></div> </div> </section> <!-- Panel Results --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"> <div class="loading-skeleton"></div> </div> </section> <!-- Sessions Table --> <section class="chart-card" id="sessions-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Recent Sessions</h2> <p class="chart-card__desc">Last 15 sessions by timestamp</p> </div> <div class="chart-card__body chart-card__body--table" id="sessions-table"> <div class="loading-skeleton"></div> </div> </section> </main> </div> </body></html> <script>(function(){const base = "/";
1
+ <!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Observability Dashboard — OpenCastle</title><meta name="description" content="Real-time observability for OpenCastle multi-agent orchestration — sessions, delegations, model tiers, and quality gates."><meta name="theme-color" content="#0a0a0f"><link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png"><link rel="stylesheet" href="/_astro/index.Bnq19_1M.css"></head> <body> <header class="dash-header"> <div class="dash-header__inner"> <div class="dash-header__brand"> <img class="dash-header__icon" src="/icon-192.png" alt="OpenCastle" width="32" height="32"> <h1 class="dash-header__title">Observability Dashboard</h1> </div> <div class="dash-header__actions"> <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON"> <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"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
2
+ Export
3
+ </button> </div> </div> </header> <div class="dash-layout"> <!-- Sidebar Navigation --> <nav class="dash-sidebar" id="dash-sidebar"> <ul class="dash-sidebar__list"> <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li> <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li> <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section">Reviews</a></li> <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li> </ul> </nav> <main class="dash-main"> <!-- Filter Bar --> <div class="filter-bar" id="filter-bar"> <div class="filter-group"> <label class="filter-label" for="filter-date-from">From</label> <input class="filter-input" type="date" id="filter-date-from"> </div> <div class="filter-group"> <label class="filter-label" for="filter-date-to">To</label> <input class="filter-input" type="date" id="filter-date-to"> </div> <div class="filter-group"> <label class="filter-label" for="filter-agent">Agent</label> <select class="filter-select" id="filter-agent"> <option value="">All agents</option> </select> </div> <div class="filter-group"> <label class="filter-label" for="filter-outcome">Outcome</label> <select class="filter-select" id="filter-outcome"> <option value="">All outcomes</option> <option value="success">Success</option> <option value="partial">Partial</option> <option value="failed">Failed</option> </select> </div> <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button> </div> <!-- KPI Row --> <section class="kpi-row" id="kpi-row" data-nav-section> <div class="kpi-card" id="kpi-sessions"> <span class="kpi-card__label">Total Sessions</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-success"> <span class="kpi-card__label">Success Rate</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-delegations"> <span class="kpi-card__label">Total Delegations</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-duration"> <span class="kpi-card__label">Avg Duration</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-retries"> <span class="kpi-card__label">Total Retries</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-lessons"> <span class="kpi-card__label">Lessons Added</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> </section> <!-- Pipeline View (Steroids-inspired) --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <p class="chart-card__desc">Delegation flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"> <div class="loading-skeleton"></div> </div> </section> <!-- Charts Row 1 --> <div class="charts-row" id="agent-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Sessions by Agent</h2> <p class="chart-card__desc">Stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="tier-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Delegation model tiers</p> </div> <div class="chart-card__body" id="tier-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row: Delegation Insights --> <div class="charts-row" id="delegation-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Mechanism</h2> <p class="chart-card__desc">Sub-agent vs background split</p> </div> <div class="chart-card__body" id="mechanism-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Success rate by delegation</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row 2 --> <div class="charts-row" id="timeline-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Timeline</h2> <p class="chart-card__desc">Sessions and delegations over time</p> </div> <div class="chart-card__body" id="timeline-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="model-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Model Usage</h2> <p class="chart-card__desc">Sessions by model</p> </div> <div class="chart-card__body" id="model-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Execution Log (Duvo-inspired) --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"> <div class="loading-skeleton"></div> </div> </section> <!-- Panel Results --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"> <div class="loading-skeleton"></div> </div> </section> <!-- Fast Reviews --> <section class="chart-card" id="reviews-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Fast Reviews</h2> <p class="chart-card__desc">Single-reviewer quality gate results</p> </div> <div class="chart-card__body chart-card__body--table" id="reviews-table"> <div class="loading-skeleton"></div> </div> </section> <!-- Sessions Table --> <section class="chart-card" id="sessions-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Recent Sessions</h2> <p class="chart-card__desc">Last 15 sessions by timestamp</p> </div> <div class="chart-card__body chart-card__body--table" id="sessions-table"> <div class="loading-skeleton"></div> </div> </section> </main> </div> </body></html> <script>(function(){const base = "/";
2
4
 
3
5
  // ── Data Loading ──────────────────────────────────────────
4
6
 
@@ -139,7 +141,7 @@
139
141
 
140
142
  // ── KPI Rendering ────────────────────────────────────────
141
143
 
142
- function renderKpis(sessions, delegations) {
144
+ function renderKpis(sessions, delegations, reviews) {
143
145
  const total = sessions.length;
144
146
  const isEmpty = total === 0;
145
147
  const successCount = sessions.filter((s) => s.outcome === 'success').length;
@@ -921,22 +923,75 @@
921
923
 
922
924
  // ── Main ──────────────────────────────────────────────────
923
925
 
924
- async function main() {
925
- const [sessions, delegations, panels] = await Promise.all([
926
- loadNdjson(base + 'data/sessions.ndjson'),
927
- loadNdjson(base + 'data/delegations.ndjson'),
928
- loadNdjson(base + 'data/panels.ndjson'),
929
- ]);
926
+ // Store raw data globally for filtering/export
927
+ let rawSessions = [];
928
+ let rawDelegations = [];
929
+ let rawPanels = [];
930
+ let rawReviews = [];
931
+
932
+ function applyFilters() {
933
+ const dateFrom = document.getElementById('filter-date-from').value;
934
+ const dateTo = document.getElementById('filter-date-to').value;
935
+ const agentFilter = document.getElementById('filter-agent').value;
936
+ const outcomeFilter = document.getElementById('filter-outcome').value;
937
+
938
+ function matchDate(ts) {
939
+ const date = ts.slice(0, 10);
940
+ if (dateFrom && date < dateFrom) return false;
941
+ if (dateTo && date > dateTo) return false;
942
+ return true;
943
+ }
944
+
945
+ const sessions = rawSessions.filter((s) => {
946
+ if (!matchDate(s.timestamp)) return false;
947
+ if (agentFilter && s.agent !== agentFilter) return false;
948
+ if (outcomeFilter && s.outcome !== outcomeFilter) return false;
949
+ return true;
950
+ });
930
951
 
931
- // Show/hide welcome banner
932
- const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0;
952
+ const delegations = rawDelegations.filter((d) => {
953
+ if (!matchDate(d.timestamp)) return false;
954
+ if (agentFilter && d.agent !== agentFilter) return false;
955
+ if (outcomeFilter && d.outcome !== outcomeFilter) return false;
956
+ return true;
957
+ });
958
+
959
+ const panels = rawPanels.filter((p) => matchDate(p.timestamp));
960
+ const reviews = rawReviews.filter((r) => {
961
+ if (!matchDate(r.timestamp)) return false;
962
+ if (agentFilter && r.agent !== agentFilter) return false;
963
+ return true;
964
+ });
965
+
966
+ renderAll(sessions, delegations, panels, reviews);
967
+ }
968
+
969
+ function populateAgentFilter(sessions, delegations, reviews) {
970
+ const agents = new Set();
971
+ sessions.forEach((s) => agents.add(s.agent));
972
+ delegations.forEach((d) => agents.add(d.agent));
973
+ reviews.forEach((r) => agents.add(r.agent));
974
+ const select = document.getElementById('filter-agent');
975
+ if (!select) return;
976
+ // Keep the "All agents" option, remove old dynamic options
977
+ while (select.options.length > 1) select.remove(1);
978
+ Array.from(agents).sort().forEach((a) => {
979
+ const opt = document.createElement('option');
980
+ opt.value = a;
981
+ opt.textContent = a;
982
+ select.appendChild(opt);
983
+ });
984
+ }
985
+
986
+ function renderAll(sessions, delegations, panels, reviews) {
987
+ const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0 && reviews.length === 0;
933
988
  if (allEmpty) {
934
989
  renderWelcomeBanner();
935
990
  } else {
936
991
  removeWelcomeBanner();
937
992
  }
938
993
 
939
- renderKpis(sessions, delegations);
994
+ renderKpis(sessions, delegations, reviews);
940
995
  renderPipeline(delegations);
941
996
  renderAgentChart(sessions);
942
997
  renderTierChart(delegations);
@@ -946,7 +1001,111 @@
946
1001
  renderModelChart(sessions);
947
1002
  renderExecutionLog(sessions);
948
1003
  renderPanelChart(panels);
1004
+ renderReviewsTable(reviews);
949
1005
  renderSessionsTable(sessions);
1006
+ }
1007
+
1008
+ // ── Reviews Table ─────────────────────────────────────────
1009
+
1010
+ function renderReviewsTable(reviews) {
1011
+ const el = document.getElementById('reviews-table');
1012
+ if (!el) return;
1013
+
1014
+ const sorted = reviews
1015
+ .slice()
1016
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1017
+ .slice(0, 20);
1018
+
1019
+ if (sorted.length === 0) {
1020
+ el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results — with verdicts, issue counts, and escalation status — will be listed here.');
1021
+ return;
1022
+ }
1023
+
1024
+ el.innerHTML =
1025
+ '<table class="sessions-table">' +
1026
+ '<thead><tr>' +
1027
+ '<th>Timestamp</th>' +
1028
+ '<th>Agent</th>' +
1029
+ '<th>Verdict</th>' +
1030
+ '<th>Critical</th>' +
1031
+ '<th>Major</th>' +
1032
+ '<th>Minor</th>' +
1033
+ '<th>Confidence</th>' +
1034
+ '<th>Attempt</th>' +
1035
+ '<th>Escalated</th>' +
1036
+ '<th>Issue</th>' +
1037
+ '</tr></thead>' +
1038
+ '<tbody>' +
1039
+ sorted
1040
+ .map(
1041
+ (r) =>
1042
+ '<tr>' +
1043
+ '<td>' + formatTime(r.timestamp) + '</td>' +
1044
+ '<td class="td-agent">' + escapeHtml(r.agent || '') + '</td>' +
1045
+ '<td><span class="outcome-badge outcome-badge--' + (r.verdict === 'pass' ? 'success' : 'failed') + '">' + r.verdict + '</span></td>' +
1046
+ '<td class="td-num">' + (r.issues_critical ?? 0) + '</td>' +
1047
+ '<td class="td-num">' + (r.issues_major ?? 0) + '</td>' +
1048
+ '<td class="td-num">' + (r.issues_minor ?? 0) + '</td>' +
1049
+ '<td class="td-num">' + (r.confidence || '\u2014') + '</td>' +
1050
+ '<td class="td-num">' + (r.attempt ?? 1) + '</td>' +
1051
+ '<td class="td-num">' + (r.escalated ? '\u26A0' : '\u2014') + '</td>' +
1052
+ '<td class="td-issue">' + (r.linear_issue ? escapeHtml(r.linear_issue) : '\u2014') + '</td>' +
1053
+ '</tr>'
1054
+ )
1055
+ .join('') +
1056
+ '</tbody></table>';
1057
+ }
1058
+
1059
+ // ── Export ─────────────────────────────────────────────────
1060
+
1061
+ function exportData() {
1062
+ const data = {
1063
+ exported_at: new Date().toISOString(),
1064
+ sessions: rawSessions,
1065
+ delegations: rawDelegations,
1066
+ panels: rawPanels,
1067
+ reviews: rawReviews,
1068
+ };
1069
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1070
+ const url = URL.createObjectURL(blob);
1071
+ const a = document.createElement('a');
1072
+ a.href = url;
1073
+ a.download = 'opencastle-dashboard-' + new Date().toISOString().slice(0, 10) + '.json';
1074
+ a.click();
1075
+ URL.revokeObjectURL(url);
1076
+ }
1077
+
1078
+ async function main() {
1079
+ const [sessions, delegations, panels, reviews] = await Promise.all([
1080
+ loadNdjson(base + 'data/sessions.ndjson'),
1081
+ loadNdjson(base + 'data/delegations.ndjson'),
1082
+ loadNdjson(base + 'data/panels.ndjson'),
1083
+ loadNdjson(base + 'data/reviews.ndjson'),
1084
+ ]);
1085
+
1086
+ rawSessions = sessions;
1087
+ rawDelegations = delegations;
1088
+ rawPanels = panels;
1089
+ rawReviews = reviews;
1090
+
1091
+ populateAgentFilter(sessions, delegations, reviews);
1092
+ renderAll(sessions, delegations, panels, reviews);
1093
+
1094
+ // ── Filter event listeners ────────────────────────────
1095
+ document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1096
+ document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
1097
+ document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1098
+ document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1099
+ document.getElementById('filter-reset')?.addEventListener('click', () => {
1100
+ document.getElementById('filter-date-from').value = '';
1101
+ document.getElementById('filter-date-to').value = '';
1102
+ document.getElementById('filter-agent').value = '';
1103
+ document.getElementById('filter-outcome').value = '';
1104
+ applyFilters();
1105
+ });
1106
+
1107
+ // ── Export button ─────────────────────────────────────
1108
+ document.getElementById('export-btn')?.addEventListener('click', exportData);
950
1109
 
951
1110
  // ── Sidebar Navigation ────────────────────────────────
952
1111
  initSidebarNav();
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "ef84f2a1",
2
+ "hash": "4161240c",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "fc571d9a",
5
- "browserHash": "822a1467",
4
+ "lockfileHash": "2aeef64b",
5
+ "browserHash": "5a4c5dc7",
6
6
  "optimized": {
7
7
  "astro > cssesc": {
8
8
  "src": "../../../../../node_modules/cssesc/cssesc.js",
9
9
  "file": "astro___cssesc.js",
10
- "fileHash": "d234c1d0",
10
+ "fileHash": "dd8ecd22",
11
11
  "needsInterop": true
12
12
  },
13
13
  "astro > aria-query": {
14
14
  "src": "../../../../../node_modules/aria-query/lib/index.js",
15
15
  "file": "astro___aria-query.js",
16
- "fileHash": "bcc57b41",
16
+ "fileHash": "61e34ef8",
17
17
  "needsInterop": true
18
18
  },
19
19
  "astro > axobject-query": {
20
20
  "src": "../../../../../node_modules/axobject-query/lib/index.js",
21
21
  "file": "astro___axobject-query.js",
22
- "fileHash": "196f4f3c",
22
+ "fileHash": "680a4037",
23
23
  "needsInterop": true
24
24
  }
25
25
  },
@@ -0,0 +1,6 @@
1
+ {"timestamp":"2026-02-28T10:30:00Z","linear_issue":"TAS-12","agent":"Developer","reviewer_model":"gpt-5-mini","verdict":"pass","attempt":1,"issues_critical":0,"issues_major":0,"issues_minor":1,"confidence":"high","escalated":false,"duration_sec":38}
2
+ {"timestamp":"2026-02-28T14:15:00Z","linear_issue":"TAS-14","agent":"UI-UX Expert","reviewer_model":"gpt-5-mini","verdict":"pass","attempt":1,"issues_critical":0,"issues_major":1,"issues_minor":2,"confidence":"medium","escalated":false,"duration_sec":52}
3
+ {"timestamp":"2026-03-01T09:00:00Z","linear_issue":"TAS-18","agent":"Developer","reviewer_model":"gpt-5-mini","verdict":"fail","attempt":1,"issues_critical":1,"issues_major":0,"issues_minor":0,"confidence":"high","escalated":true,"duration_sec":41}
4
+ {"timestamp":"2026-03-01T09:20:00Z","linear_issue":"TAS-18","agent":"Developer","reviewer_model":"gpt-5-mini","verdict":"pass","attempt":2,"issues_critical":0,"issues_major":0,"issues_minor":1,"confidence":"high","escalated":false,"duration_sec":35}
5
+ {"timestamp":"2026-03-01T11:45:00Z","linear_issue":"TAS-20","agent":"Database Engineer","reviewer_model":"gpt-5-mini","verdict":"pass","attempt":1,"issues_critical":0,"issues_major":0,"issues_minor":0,"confidence":"high","escalated":false,"duration_sec":28}
6
+ {"timestamp":"2026-03-01T16:30:00Z","linear_issue":"TAS-22","agent":"Security Expert","reviewer_model":"claude-opus-4-6","verdict":"fail","attempt":1,"issues_critical":2,"issues_major":1,"issues_minor":0,"confidence":"high","escalated":true,"duration_sec":65}
@@ -13,6 +13,12 @@ const base = import.meta.env.BASE_URL;
13
13
  <img class="dash-header__icon" src={`${base}icon-192.png`} alt="OpenCastle" width="32" height="32" />
14
14
  <h1 class="dash-header__title">Observability Dashboard</h1>
15
15
  </div>
16
+ <div class="dash-header__actions">
17
+ <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON">
18
+ <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
+ Export
20
+ </button>
21
+ </div>
16
22
  </div>
17
23
  </header>
18
24
 
@@ -29,11 +35,40 @@ const base = import.meta.env.BASE_URL;
29
35
  <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li>
30
36
  <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li>
31
37
  <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li>
38
+ <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section">Reviews</a></li>
32
39
  <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li>
33
40
  </ul>
34
41
  </nav>
35
42
 
36
43
  <main class="dash-main">
44
+ <!-- Filter Bar -->
45
+ <div class="filter-bar" id="filter-bar">
46
+ <div class="filter-group">
47
+ <label class="filter-label" for="filter-date-from">From</label>
48
+ <input class="filter-input" type="date" id="filter-date-from" />
49
+ </div>
50
+ <div class="filter-group">
51
+ <label class="filter-label" for="filter-date-to">To</label>
52
+ <input class="filter-input" type="date" id="filter-date-to" />
53
+ </div>
54
+ <div class="filter-group">
55
+ <label class="filter-label" for="filter-agent">Agent</label>
56
+ <select class="filter-select" id="filter-agent">
57
+ <option value="">All agents</option>
58
+ </select>
59
+ </div>
60
+ <div class="filter-group">
61
+ <label class="filter-label" for="filter-outcome">Outcome</label>
62
+ <select class="filter-select" id="filter-outcome">
63
+ <option value="">All outcomes</option>
64
+ <option value="success">Success</option>
65
+ <option value="partial">Partial</option>
66
+ <option value="failed">Failed</option>
67
+ </select>
68
+ </div>
69
+ <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button>
70
+ </div>
71
+
37
72
  <!-- KPI Row -->
38
73
  <section class="kpi-row" id="kpi-row" data-nav-section>
39
74
  <div class="kpi-card" id="kpi-sessions">
@@ -167,6 +202,17 @@ const base = import.meta.env.BASE_URL;
167
202
  </div>
168
203
  </section>
169
204
 
205
+ <!-- Fast Reviews -->
206
+ <section class="chart-card" id="reviews-section" data-nav-section>
207
+ <div class="chart-card__header">
208
+ <h2 class="chart-card__title">Fast Reviews</h2>
209
+ <p class="chart-card__desc">Single-reviewer quality gate results</p>
210
+ </div>
211
+ <div class="chart-card__body chart-card__body--table" id="reviews-table">
212
+ <div class="loading-skeleton"></div>
213
+ </div>
214
+ </section>
215
+
170
216
  <!-- Sessions Table -->
171
217
  <section class="chart-card" id="sessions-section" data-nav-section>
172
218
  <div class="chart-card__header">
@@ -321,7 +367,7 @@ const base = import.meta.env.BASE_URL;
321
367
 
322
368
  // ── KPI Rendering ────────────────────────────────────────
323
369
 
324
- function renderKpis(sessions, delegations) {
370
+ function renderKpis(sessions, delegations, reviews) {
325
371
  const total = sessions.length;
326
372
  const isEmpty = total === 0;
327
373
  const successCount = sessions.filter((s) => s.outcome === 'success').length;
@@ -1103,22 +1149,75 @@ const base = import.meta.env.BASE_URL;
1103
1149
 
1104
1150
  // ── Main ──────────────────────────────────────────────────
1105
1151
 
1106
- async function main() {
1107
- const [sessions, delegations, panels] = await Promise.all([
1108
- loadNdjson(base + 'data/sessions.ndjson'),
1109
- loadNdjson(base + 'data/delegations.ndjson'),
1110
- loadNdjson(base + 'data/panels.ndjson'),
1111
- ]);
1152
+ // Store raw data globally for filtering/export
1153
+ let rawSessions = [];
1154
+ let rawDelegations = [];
1155
+ let rawPanels = [];
1156
+ let rawReviews = [];
1157
+
1158
+ function applyFilters() {
1159
+ const dateFrom = document.getElementById('filter-date-from').value;
1160
+ const dateTo = document.getElementById('filter-date-to').value;
1161
+ const agentFilter = document.getElementById('filter-agent').value;
1162
+ const outcomeFilter = document.getElementById('filter-outcome').value;
1163
+
1164
+ function matchDate(ts) {
1165
+ const date = ts.slice(0, 10);
1166
+ if (dateFrom && date < dateFrom) return false;
1167
+ if (dateTo && date > dateTo) return false;
1168
+ return true;
1169
+ }
1170
+
1171
+ const sessions = rawSessions.filter((s) => {
1172
+ if (!matchDate(s.timestamp)) return false;
1173
+ if (agentFilter && s.agent !== agentFilter) return false;
1174
+ if (outcomeFilter && s.outcome !== outcomeFilter) return false;
1175
+ return true;
1176
+ });
1177
+
1178
+ const delegations = rawDelegations.filter((d) => {
1179
+ if (!matchDate(d.timestamp)) return false;
1180
+ if (agentFilter && d.agent !== agentFilter) return false;
1181
+ if (outcomeFilter && d.outcome !== outcomeFilter) return false;
1182
+ return true;
1183
+ });
1184
+
1185
+ const panels = rawPanels.filter((p) => matchDate(p.timestamp));
1186
+ const reviews = rawReviews.filter((r) => {
1187
+ if (!matchDate(r.timestamp)) return false;
1188
+ if (agentFilter && r.agent !== agentFilter) return false;
1189
+ return true;
1190
+ });
1191
+
1192
+ renderAll(sessions, delegations, panels, reviews);
1193
+ }
1112
1194
 
1113
- // Show/hide welcome banner
1114
- const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0;
1195
+ function populateAgentFilter(sessions, delegations, reviews) {
1196
+ const agents = new Set();
1197
+ sessions.forEach((s) => agents.add(s.agent));
1198
+ delegations.forEach((d) => agents.add(d.agent));
1199
+ reviews.forEach((r) => agents.add(r.agent));
1200
+ const select = document.getElementById('filter-agent');
1201
+ if (!select) return;
1202
+ // Keep the "All agents" option, remove old dynamic options
1203
+ while (select.options.length > 1) select.remove(1);
1204
+ Array.from(agents).sort().forEach((a) => {
1205
+ const opt = document.createElement('option');
1206
+ opt.value = a;
1207
+ opt.textContent = a;
1208
+ select.appendChild(opt);
1209
+ });
1210
+ }
1211
+
1212
+ function renderAll(sessions, delegations, panels, reviews) {
1213
+ const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0 && reviews.length === 0;
1115
1214
  if (allEmpty) {
1116
1215
  renderWelcomeBanner();
1117
1216
  } else {
1118
1217
  removeWelcomeBanner();
1119
1218
  }
1120
1219
 
1121
- renderKpis(sessions, delegations);
1220
+ renderKpis(sessions, delegations, reviews);
1122
1221
  renderPipeline(delegations);
1123
1222
  renderAgentChart(sessions);
1124
1223
  renderTierChart(delegations);
@@ -1128,7 +1227,111 @@ const base = import.meta.env.BASE_URL;
1128
1227
  renderModelChart(sessions);
1129
1228
  renderExecutionLog(sessions);
1130
1229
  renderPanelChart(panels);
1230
+ renderReviewsTable(reviews);
1131
1231
  renderSessionsTable(sessions);
1232
+ }
1233
+
1234
+ // ── Reviews Table ─────────────────────────────────────────
1235
+
1236
+ function renderReviewsTable(reviews) {
1237
+ const el = document.getElementById('reviews-table');
1238
+ if (!el) return;
1239
+
1240
+ const sorted = reviews
1241
+ .slice()
1242
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1243
+ .slice(0, 20);
1244
+
1245
+ if (sorted.length === 0) {
1246
+ el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results — with verdicts, issue counts, and escalation status — will be listed here.');
1247
+ return;
1248
+ }
1249
+
1250
+ el.innerHTML =
1251
+ '<table class="sessions-table">' +
1252
+ '<thead><tr>' +
1253
+ '<th>Timestamp</th>' +
1254
+ '<th>Agent</th>' +
1255
+ '<th>Verdict</th>' +
1256
+ '<th>Critical</th>' +
1257
+ '<th>Major</th>' +
1258
+ '<th>Minor</th>' +
1259
+ '<th>Confidence</th>' +
1260
+ '<th>Attempt</th>' +
1261
+ '<th>Escalated</th>' +
1262
+ '<th>Issue</th>' +
1263
+ '</tr></thead>' +
1264
+ '<tbody>' +
1265
+ sorted
1266
+ .map(
1267
+ (r) =>
1268
+ '<tr>' +
1269
+ '<td>' + formatTime(r.timestamp) + '</td>' +
1270
+ '<td class="td-agent">' + escapeHtml(r.agent || '') + '</td>' +
1271
+ '<td><span class="outcome-badge outcome-badge--' + (r.verdict === 'pass' ? 'success' : 'failed') + '">' + r.verdict + '</span></td>' +
1272
+ '<td class="td-num">' + (r.issues_critical ?? 0) + '</td>' +
1273
+ '<td class="td-num">' + (r.issues_major ?? 0) + '</td>' +
1274
+ '<td class="td-num">' + (r.issues_minor ?? 0) + '</td>' +
1275
+ '<td class="td-num">' + (r.confidence || '\u2014') + '</td>' +
1276
+ '<td class="td-num">' + (r.attempt ?? 1) + '</td>' +
1277
+ '<td class="td-num">' + (r.escalated ? '\u26A0' : '\u2014') + '</td>' +
1278
+ '<td class="td-issue">' + (r.linear_issue ? escapeHtml(r.linear_issue) : '\u2014') + '</td>' +
1279
+ '</tr>'
1280
+ )
1281
+ .join('') +
1282
+ '</tbody></table>';
1283
+ }
1284
+
1285
+ // ── Export ─────────────────────────────────────────────────
1286
+
1287
+ function exportData() {
1288
+ const data = {
1289
+ exported_at: new Date().toISOString(),
1290
+ sessions: rawSessions,
1291
+ delegations: rawDelegations,
1292
+ panels: rawPanels,
1293
+ reviews: rawReviews,
1294
+ };
1295
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1296
+ const url = URL.createObjectURL(blob);
1297
+ const a = document.createElement('a');
1298
+ a.href = url;
1299
+ a.download = 'opencastle-dashboard-' + new Date().toISOString().slice(0, 10) + '.json';
1300
+ a.click();
1301
+ URL.revokeObjectURL(url);
1302
+ }
1303
+
1304
+ async function main() {
1305
+ const [sessions, delegations, panels, reviews] = await Promise.all([
1306
+ loadNdjson(base + 'data/sessions.ndjson'),
1307
+ loadNdjson(base + 'data/delegations.ndjson'),
1308
+ loadNdjson(base + 'data/panels.ndjson'),
1309
+ loadNdjson(base + 'data/reviews.ndjson'),
1310
+ ]);
1311
+
1312
+ rawSessions = sessions;
1313
+ rawDelegations = delegations;
1314
+ rawPanels = panels;
1315
+ rawReviews = reviews;
1316
+
1317
+ populateAgentFilter(sessions, delegations, reviews);
1318
+ renderAll(sessions, delegations, panels, reviews);
1319
+
1320
+ // ── Filter event listeners ────────────────────────────
1321
+ document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1322
+ document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
1323
+ document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1324
+ document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1325
+ document.getElementById('filter-reset')?.addEventListener('click', () => {
1326
+ document.getElementById('filter-date-from').value = '';
1327
+ document.getElementById('filter-date-to').value = '';
1328
+ document.getElementById('filter-agent').value = '';
1329
+ document.getElementById('filter-outcome').value = '';
1330
+ applyFilters();
1331
+ });
1332
+
1333
+ // ── Export button ─────────────────────────────────────
1334
+ document.getElementById('export-btn')?.addEventListener('click', exportData);
1132
1335
 
1133
1336
  // ── Sidebar Navigation ────────────────────────────────
1134
1337
  initSidebarNav();