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.
- package/README.md +27 -0
- package/bin/cli.mjs +2 -0
- package/dist/cli/adapters/claude-code.d.ts +2 -5
- package/dist/cli/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/adapters/claude-code.js +12 -251
- package/dist/cli/adapters/claude-code.js.map +1 -1
- package/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +3 -17
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/adapters/frontmatter.d.ts +26 -0
- package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
- package/dist/cli/adapters/frontmatter.js +40 -0
- package/dist/cli/adapters/frontmatter.js.map +1 -0
- package/dist/cli/adapters/index.d.ts +5 -0
- package/dist/cli/adapters/index.d.ts.map +1 -0
- package/dist/cli/adapters/index.js +9 -0
- package/dist/cli/adapters/index.js.map +1 -0
- package/dist/cli/adapters/opencode.d.ts +2 -5
- package/dist/cli/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/adapters/opencode.js +12 -250
- package/dist/cli/adapters/opencode.js.map +1 -1
- package/dist/cli/adapters/single-file-base.d.ts +40 -0
- package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
- package/dist/cli/adapters/single-file-base.js +246 -0
- package/dist/cli/adapters/single-file-base.js.map +1 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +3 -2
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/detect.d.ts.map +1 -1
- package/dist/cli/detect.js +13 -11
- package/dist/cli/detect.js.map +1 -1
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +205 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +31 -19
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run/schema.d.ts +1 -5
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +6 -330
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +14 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +0 -5
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +4 -17
- package/dist/cli/update.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/adapters/claude-code.ts +13 -304
- package/src/cli/adapters/cursor.ts +3 -23
- package/src/cli/adapters/frontmatter.ts +47 -0
- package/src/cli/adapters/index.ts +13 -0
- package/src/cli/adapters/opencode.ts +12 -301
- package/src/cli/adapters/single-file-base.ts +320 -0
- package/src/cli/dashboard.ts +3 -2
- package/src/cli/detect.ts +19 -15
- package/src/cli/doctor.ts +235 -0
- package/src/cli/init.ts +31 -24
- package/src/cli/run/schema.ts +7 -365
- package/src/cli/run.ts +17 -1
- package/src/cli/types.ts +0 -6
- package/src/cli/update.ts +5 -23
- package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
- package/src/dashboard/dist/index.html +170 -11
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/seed-data/reviews.ndjson +6 -0
- package/src/dashboard/src/pages/index.astro +213 -10
- package/src/dashboard/src/styles/dashboard.css +196 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
- package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
- package/src/orchestrator/agent-workflows/database-migration.md +2 -2
- package/src/orchestrator/agent-workflows/feature-implementation.md +2 -2
- package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
- package/src/orchestrator/agent-workflows/refactoring.md +2 -2
- package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
- package/src/orchestrator/agent-workflows/security-audit.md +2 -2
- package/src/orchestrator/agents/data-expert.agent.md +2 -2
- package/src/orchestrator/agents/researcher.agent.md +0 -16
- package/src/orchestrator/agents/team-lead.agent.md +17 -6
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
- package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
- package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
- 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.
|
|
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">—</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">—</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">—</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">—</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">—</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">—</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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
932
|
-
|
|
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": "
|
|
2
|
+
"hash": "4161240c",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1114
|
-
const
|
|
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();
|