faberun 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- package/src/web/server.mjs +552 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>Faberun</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #f4f6fa; --surface: #ffffff; --surface-2: #f0f2f5; --border: #e1e4ea;
|
|
11
|
+
--text: #1b1f27; --text-2: #5a6272; --text-3: #8a91a0;
|
|
12
|
+
--accent: #315cf4; --accent-soft: #e7edff;
|
|
13
|
+
--ok: #15803d; --ok-soft: #dcfce7; --warn: #b45309; --warn-soft: #fef3c7;
|
|
14
|
+
--bad: #b91c1c; --bad-soft: #fee2e2; --violet: #6d28d9; --violet-soft: #ede9fe;
|
|
15
|
+
--muted: #64748b; --muted-soft: #e7eaf0;
|
|
16
|
+
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px rgba(16, 24, 40, 0.055);
|
|
17
|
+
--sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
|
|
18
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
19
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #0b0f17; --surface: #121823; --surface-2: #18202d; --border: #253044;
|
|
23
|
+
--text: #eef2f8; --text-2: #aab4c5; --text-3: #748096;
|
|
24
|
+
--accent: #7aa2ff; --accent-soft: #24448466;
|
|
25
|
+
--ok: #4ade80; --ok-soft: #14532d66; --warn: #fbbf24; --warn-soft: #78350f66;
|
|
26
|
+
--bad: #f87171; --bad-soft: #7f1d1d66; --violet: #a78bfa; --violet-soft: #4c1d9566;
|
|
27
|
+
--muted: #94a3b8; --muted-soft: #33415566;
|
|
28
|
+
--shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
* { box-sizing: border-box; }
|
|
32
|
+
[hidden] { display: none !important; }
|
|
33
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.5 var(--sans); }
|
|
34
|
+
a { color: var(--accent); }
|
|
35
|
+
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
|
|
36
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
37
|
+
.mono { font-family: var(--mono); font-size: 12.5px; }
|
|
38
|
+
.dim { color: var(--text-2); }
|
|
39
|
+
#top {
|
|
40
|
+
display: flex; align-items: center; gap: 14px; padding: 14px 20px;
|
|
41
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
42
|
+
}
|
|
43
|
+
#top .brand { font-weight: 650; }
|
|
44
|
+
#top select { font: inherit; background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; }
|
|
45
|
+
#top .spacer { flex: 1; }
|
|
46
|
+
#conn { font-size: 12px; color: var(--text-2); display: inline-flex; align-items: center; gap: 6px; }
|
|
47
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); display: inline-block; }
|
|
48
|
+
.dot.live { background: var(--ok); }
|
|
49
|
+
.dot.lost { background: var(--bad); }
|
|
50
|
+
#main { max-width: 1100px; margin: 0 auto; padding: 24px 20px 60px; }
|
|
51
|
+
section { margin-top: 22px; }
|
|
52
|
+
section > h2 { font-size: 12px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); margin: 0 0 10px; }
|
|
53
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); }
|
|
54
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 1px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; }
|
|
55
|
+
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
|
56
|
+
.pill.done { color: var(--ok); background: var(--ok-soft); }
|
|
57
|
+
.pill.active, .pill.running { color: var(--warn); background: var(--warn-soft); }
|
|
58
|
+
.pill.attention, .pill.blocked, .pill.failed, .pill.exhausted, .pill.stalled, .pill.canceled, .pill.orphaned { color: var(--bad); background: var(--bad-soft); }
|
|
59
|
+
.pill.idle, .pill.pending { color: var(--muted); background: var(--muted-soft); }
|
|
60
|
+
.pill.pass { color: var(--ok); background: var(--ok-soft); }
|
|
61
|
+
.pill.fail, .pill.invalid_judge_output { color: var(--bad); background: var(--bad-soft); }
|
|
62
|
+
#now { padding: 16px 18px; display: flex; flex-wrap: wrap; gap: 22px; align-items: baseline; }
|
|
63
|
+
#now .k { font-size: 11px; text-transform: uppercase; color: var(--text-3); }
|
|
64
|
+
#now .v { font-size: 15px; font-weight: 650; }
|
|
65
|
+
#needsyou { border: 1px solid color-mix(in srgb, var(--bad) 45%, transparent); background: var(--bad-soft); border-radius: 12px; padding: 4px 16px; }
|
|
66
|
+
#needsyou .item { padding: 8px 0; border-bottom: 1px solid color-mix(in srgb, var(--bad) 25%, transparent); font-size: 13px; }
|
|
67
|
+
#needsyou .item:last-child { border-bottom: none; }
|
|
68
|
+
#needsyou code { background: color-mix(in srgb, var(--text) 8%, transparent); border-radius: 6px; padding: 1px 6px; }
|
|
69
|
+
table { width: 100%; border-collapse: collapse; }
|
|
70
|
+
th { text-align: left; font-size: 11px; font-weight: 600; color: var(--text-3); padding: 8px 12px; border-bottom: 1px solid var(--border); text-transform: uppercase; }
|
|
71
|
+
td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
72
|
+
tr:last-child td { border-bottom: none; }
|
|
73
|
+
tr.clickable { cursor: pointer; }
|
|
74
|
+
tr.clickable:hover td { background: var(--surface-2); }
|
|
75
|
+
tr.selected td { background: var(--accent-soft); }
|
|
76
|
+
td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
77
|
+
td.goal { max-width: 42ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
78
|
+
#drawer { padding: 16px 18px; }
|
|
79
|
+
#drawer h3 { margin: 0 0 12px; font-size: 15px; }
|
|
80
|
+
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
|
|
81
|
+
.tabs button { padding: 7px 12px; font-size: 12.5px; border-radius: 8px 8px 0 0; color: var(--text-2); }
|
|
82
|
+
.tabs button.active { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
|
83
|
+
pre { white-space: pre-wrap; word-break: break-word; background: var(--surface-2); border-radius: 8px; padding: 10px 12px; font-family: var(--mono); font-size: 12px; max-height: 420px; overflow: auto; }
|
|
84
|
+
.finding { padding: 8px 0; border-bottom: 1px solid var(--border); }
|
|
85
|
+
.finding:last-child { border-bottom: none; }
|
|
86
|
+
#handoff pre { max-height: 500px; }
|
|
87
|
+
.empty { color: var(--text-3); padding: 10px 0; }
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<header id="top">
|
|
92
|
+
<span class="brand">Faberun</span>
|
|
93
|
+
<select id="campaignSelect" aria-label="Campaign"></select>
|
|
94
|
+
<span class="spacer"></span>
|
|
95
|
+
<span id="conn"><span class="dot"></span><span id="conntext">connecting…</span></span>
|
|
96
|
+
</header>
|
|
97
|
+
<main id="main"><p class="empty">loading .runs …</p></main>
|
|
98
|
+
<script>
|
|
99
|
+
"use strict";
|
|
100
|
+
const POLL_FALLBACK_MS = 3000;
|
|
101
|
+
const state = { snapshot: null, campaignId: null, runId: null, nodeId: null, tab: "log", source: null, pollTimer: null, transport: "none" };
|
|
102
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
103
|
+
const fmtUsd = (value) => typeof value === "number" ? `$${value.toFixed(2)}` : "–";
|
|
104
|
+
const fmtAgo = (iso) => {
|
|
105
|
+
const t = Date.parse(iso ?? "");
|
|
106
|
+
if (!Number.isFinite(t)) return "–";
|
|
107
|
+
const s = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
108
|
+
if (s < 60) return `${s}s ago`;
|
|
109
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
110
|
+
if (s < 86400) return `${(s / 3600).toFixed(1)}h ago`;
|
|
111
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
112
|
+
};
|
|
113
|
+
const fmtDur = (ms) => {
|
|
114
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "–";
|
|
115
|
+
const m = Math.round(ms / 60000);
|
|
116
|
+
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
117
|
+
};
|
|
118
|
+
const pill = (label) => `<span class="pill ${esc(String(label ?? "").toLowerCase())}">${esc(label ?? "-")}</span>`;
|
|
119
|
+
|
|
120
|
+
function nowHtml(now) {
|
|
121
|
+
if (!now || now.state === "idle") {
|
|
122
|
+
return `<section id="now"><h2>Now</h2><div class="card" id="now-strip">Idle${now?.runId ? ` · last run ${esc(now.runId)} finished ${fmtAgo(now.updatedAt)}` : ""}</div></section>`;
|
|
123
|
+
}
|
|
124
|
+
return `<section id="now"><h2>Now</h2><div class="card" id="now-strip">
|
|
125
|
+
<div><div class="k">Active run</div><div class="v mono">${esc(now.runId)}</div></div>
|
|
126
|
+
<div><div class="k">State</div><div class="v">${pill(now.state)}</div></div>
|
|
127
|
+
<div><div class="k">Running node</div><div class="v mono">${esc(now.nodeId ?? "–")} <span class="dim">${fmtDur(now.elapsedMs)}</span></div></div>
|
|
128
|
+
<div><div class="k">USD so far</div><div class="v">${fmtUsd(now.costUsd)}</div></div>
|
|
129
|
+
<div><div class="k">Updated</div><div class="v">${fmtAgo(now.updatedAt)}</div></div>
|
|
130
|
+
</div></section>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function needsYouHtml(items) {
|
|
134
|
+
if (!items?.length) return "";
|
|
135
|
+
const rows = items.map((item) => `<div class="item"><b>${esc(item.nodeId ?? item.runId)}</b> (${esc(item.status)}${item.errorCode ? ` · ${esc(item.errorCode)}` : ""}): <code>${esc(item.command)}</code></div>`).join("");
|
|
136
|
+
return `<section id="needsyou"><h2>Needs you</h2>${rows}</section>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runsHtml(runs, selectedRunId) {
|
|
140
|
+
const rows = (runs ?? []).map((run) => {
|
|
141
|
+
if (run.corrupt) return `<tr><td colspan="7">${esc(run.id)}: <span class="pill failed">unreadable</span> ${esc(run.corrupt)}</td></tr>`;
|
|
142
|
+
return `<tr class="clickable ${run.id === selectedRunId ? "selected" : ""}" data-run="${esc(run.id)}">
|
|
143
|
+
<td>${pill(run.state)}</td>
|
|
144
|
+
<td class="mono">${esc(run.id)}</td>
|
|
145
|
+
<td class="goal" title="${esc(run.goal)}">${esc(run.goal)}</td>
|
|
146
|
+
<td class="num">${run.nodesDone}/${run.nodesTotal}</td>
|
|
147
|
+
<td class="num">${run.attempts}</td>
|
|
148
|
+
<td class="num">${fmtDur(run.elapsedMs)}</td>
|
|
149
|
+
<td class="num">${fmtUsd(run.costUsd)}</td>
|
|
150
|
+
<td class="num">${fmtAgo(run.updatedAt)}</td>
|
|
151
|
+
</tr>`;
|
|
152
|
+
}).join("");
|
|
153
|
+
return `<section id="runs"><h2>Runs</h2><div class="card"><table>
|
|
154
|
+
<thead><tr><th>state</th><th>run</th><th>goal</th><th>nodes</th><th>attempts</th><th>elapsed</th><th>USD</th><th>updated</th></tr></thead>
|
|
155
|
+
<tbody>${rows || '<tr><td colspan="8" class="empty">no runs linked yet</td></tr>'}</tbody>
|
|
156
|
+
</table></div></section>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const TABS = [["log", "Worker log"], ["verification", "Verification"], ["diff", "Diff"], ["findings", "Findings"], ["prompt", "Prompt"]];
|
|
160
|
+
|
|
161
|
+
function tabBodyHtml(tab, detail) {
|
|
162
|
+
if (!detail) return `<p class="empty">select a node above</p>`;
|
|
163
|
+
if (tab === "log") return detail.log?.lines?.length ? `<pre>${esc(detail.log.lines.join("\n"))}</pre>` : `<p class="empty">no log yet</p>`;
|
|
164
|
+
if (tab === "verification") {
|
|
165
|
+
const commands = detail.verification?.commands ?? [];
|
|
166
|
+
if (!commands.length) return `<p class="empty">no verification recorded</p>`;
|
|
167
|
+
return commands.map((command) => `<div class="finding"><b>${command.passed ? pill("pass") : pill("fail")}</b> <span class="mono">${esc(command.command)}</span>${command.durationMs ? ` <span class="dim">${fmtDur(command.durationMs)}</span>` : ""}<pre>${esc(command.outputTail || "(no output)")}</pre></div>`).join("");
|
|
168
|
+
}
|
|
169
|
+
if (tab === "diff") {
|
|
170
|
+
const diff = detail.diff;
|
|
171
|
+
if (!diff) return `<p class="empty">no scope recorded</p>`;
|
|
172
|
+
const files = diff.files.map((path) => `<div class="mono">${esc(path)}</div>`).join("") || `<p class="empty">no files changed</p>`;
|
|
173
|
+
const unexpected = diff.unexpected.length ? `<p class="dim">unexpected: ${diff.unexpected.map(esc).join(", ")}</p>` : "";
|
|
174
|
+
return `<p>${esc(diff.stat)}</p>${unexpected}${files}`;
|
|
175
|
+
}
|
|
176
|
+
if (tab === "findings") {
|
|
177
|
+
const findings = detail.findings;
|
|
178
|
+
if (!findings) return `<p class="empty">no gate ran</p>`;
|
|
179
|
+
const list = findings.findings.map((finding) => `<div class="finding">${pill(finding.severity)} ${esc(finding.description)}<div class="dim">${esc(finding.evidence)}</div></div>`).join("") || `<p class="empty">no findings</p>`;
|
|
180
|
+
return `<p>${pill(findings.verdict)} ${esc(findings.summary ?? "")}</p>${list}`;
|
|
181
|
+
}
|
|
182
|
+
return detail.prompt ? `<pre>${esc(detail.prompt)}</pre>` : `<p class="empty">no prompt recorded</p>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function drawerHtml(drawer, tab) {
|
|
186
|
+
if (!drawer) return `<section id="drawer"><h2>Run drawer</h2><div class="card" style="padding:16px 18px"><p class="empty">select a run above</p></div></section>`;
|
|
187
|
+
const rows = drawer.nodes.map((node) => `<tr class="clickable ${node.id === drawer.selectedNodeId ? "selected" : ""}" data-node="${esc(node.id)}">
|
|
188
|
+
<td>${pill(node.status)}</td>
|
|
189
|
+
<td class="mono">${esc(node.id)}</td>
|
|
190
|
+
<td class="num">${node.attempt}</td>
|
|
191
|
+
<td class="mono">${esc(node.model ?? "–")}</td>
|
|
192
|
+
<td class="num">${fmtDur(node.elapsedMs)}</td>
|
|
193
|
+
<td class="num">${fmtUsd(node.costUsd)}</td>
|
|
194
|
+
<td>${node.verdict ? pill(node.verdict) : ""}</td>
|
|
195
|
+
<td class="goal" title="${esc(node.note ?? "")}">${esc(node.note ?? "")}</td>
|
|
196
|
+
</tr>`).join("");
|
|
197
|
+
const tabs = TABS.map(([key, label]) => `<button data-tab="${key}" class="${tab === key ? "active" : ""}">${esc(label)}</button>`).join("");
|
|
198
|
+
return `<section id="drawer"><h2>Run drawer · ${esc(drawer.runId)}</h2><div class="card" style="padding:16px 18px">
|
|
199
|
+
<table><thead><tr><th>state</th><th>node</th><th>try</th><th>model</th><th>elapsed</th><th>USD</th><th>verdict</th><th>note</th></tr></thead>
|
|
200
|
+
<tbody>${rows}</tbody></table>
|
|
201
|
+
<div class="tabs">${tabs}</div>
|
|
202
|
+
<div id="tabbody">${tabBodyHtml(tab, drawer.detail)}</div>
|
|
203
|
+
</div></section>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handoffHtml(handoff) {
|
|
207
|
+
return `<section id="handoff"><h2>Handoff</h2><div class="card" style="padding:16px 18px">${handoff ? `<pre>${esc(handoff)}</pre>` : '<p class="empty">no HANDOFF.md yet</p>'}</div></section>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderSectionsHtml(snapshot) {
|
|
211
|
+
if (!snapshot?.selectedCampaignId) return `<p class="empty">no campaign found under .runs; initialize one with: runner.mjs campaign init</p>`;
|
|
212
|
+
return nowHtml(snapshot.now) + needsYouHtml(snapshot.needsYou) + runsHtml(snapshot.runs, state.runId) + drawerHtml(snapshot.drawer, state.tab) + handoffHtml(snapshot.handoff);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderCampaignSelect(snapshot) {
|
|
216
|
+
const select = document.getElementById("campaignSelect");
|
|
217
|
+
select.innerHTML = (snapshot.campaigns ?? []).map((campaign) => `<option value="${esc(campaign.id)}" ${campaign.id === snapshot.selectedCampaignId ? "selected" : ""}>${esc(campaign.id)} (${esc(campaign.status)})</option>`).join("");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function applySnapshot(snapshot) {
|
|
221
|
+
state.snapshot = snapshot;
|
|
222
|
+
document.getElementById("main").innerHTML = renderSectionsHtml(snapshot);
|
|
223
|
+
renderCampaignSelect(snapshot);
|
|
224
|
+
setConnection(state.transport, snapshot.generatedAt);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setConnection(transport, at) {
|
|
228
|
+
const dot = document.getElementById("conn").querySelector(".dot");
|
|
229
|
+
dot.className = `dot ${transport === "stream" || transport === "poll" ? "live" : "lost"}`;
|
|
230
|
+
document.getElementById("conntext").textContent = transport === "none" ? "connection lost — reconnecting" : `updated ${new Date(Date.parse(at ?? "")).toLocaleTimeString([], { hour12: false })}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function streamUrl() {
|
|
234
|
+
const params = new URLSearchParams();
|
|
235
|
+
if (state.campaignId) params.set("campaign", state.campaignId);
|
|
236
|
+
if (state.runId) params.set("run", state.runId);
|
|
237
|
+
if (state.nodeId) params.set("node", state.nodeId);
|
|
238
|
+
const query = params.toString();
|
|
239
|
+
return `/api/stream${query ? `?${query}` : ""}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function connect() {
|
|
243
|
+
if (state.source) { state.source.close(); state.source = null; }
|
|
244
|
+
if (typeof EventSource !== "function") { startPolling(); return; }
|
|
245
|
+
const source = new EventSource(streamUrl());
|
|
246
|
+
state.source = source;
|
|
247
|
+
source.addEventListener("update", (event) => {
|
|
248
|
+
state.transport = "stream";
|
|
249
|
+
stopPolling();
|
|
250
|
+
try { applySnapshot(JSON.parse(event.data)); } catch (error) { console.error(error); }
|
|
251
|
+
});
|
|
252
|
+
source.addEventListener("error", () => { state.transport = "none"; setConnection("none"); startPolling(); });
|
|
253
|
+
}
|
|
254
|
+
async function pollOnce() {
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(`/api/snapshot${streamUrl().slice("/api/stream".length)}`, { signal: AbortSignal.timeout(6000) });
|
|
257
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
258
|
+
if (state.transport !== "stream") state.transport = "poll";
|
|
259
|
+
applySnapshot(await response.json());
|
|
260
|
+
} catch { state.transport = "none"; setConnection("none"); }
|
|
261
|
+
}
|
|
262
|
+
function startPolling() { if (state.pollTimer) return; pollOnce(); state.pollTimer = setInterval(() => { if (!document.hidden) pollOnce(); }, POLL_FALLBACK_MS); }
|
|
263
|
+
function stopPolling() { if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; } }
|
|
264
|
+
|
|
265
|
+
function selectCampaign(id) {
|
|
266
|
+
if (id === state.campaignId) return;
|
|
267
|
+
state.campaignId = id; state.runId = null; state.nodeId = null; state.tab = "log";
|
|
268
|
+
connect();
|
|
269
|
+
}
|
|
270
|
+
function selectRun(id) {
|
|
271
|
+
state.runId = id === state.runId ? null : id;
|
|
272
|
+
state.nodeId = null;
|
|
273
|
+
connect();
|
|
274
|
+
}
|
|
275
|
+
function selectNode(id) {
|
|
276
|
+
state.nodeId = id === state.nodeId ? null : id;
|
|
277
|
+
connect();
|
|
278
|
+
}
|
|
279
|
+
function selectTab(tab) {
|
|
280
|
+
state.tab = tab;
|
|
281
|
+
if (state.snapshot) document.getElementById("tabbody").innerHTML = tabBodyHtml(tab, state.snapshot.drawer?.detail);
|
|
282
|
+
document.querySelectorAll(".tabs button").forEach((button) => button.classList.toggle("active", button.dataset.tab === tab));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
document.getElementById("main").addEventListener("click", (event) => {
|
|
286
|
+
const run = event.target.closest("[data-run]");
|
|
287
|
+
if (run) return selectRun(run.dataset.run);
|
|
288
|
+
const node = event.target.closest("[data-node]");
|
|
289
|
+
if (node) return selectNode(node.dataset.node);
|
|
290
|
+
const tab = event.target.closest("[data-tab]");
|
|
291
|
+
if (tab) return selectTab(tab.dataset.tab);
|
|
292
|
+
});
|
|
293
|
+
document.getElementById("campaignSelect").addEventListener("change", (event) => selectCampaign(event.target.value));
|
|
294
|
+
setInterval(() => { if (state.snapshot && !document.hidden) applySnapshot(state.snapshot); }, 30_000);
|
|
295
|
+
connect();
|
|
296
|
+
window.__dashboardTestHooks = { renderSectionsHtml, applySnapshot };
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>
|