@mikenguyen69/harness 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/build-identity.json +1 -0
- package/dist/cancellation.js +138 -0
- package/dist/cli.js +47 -0
- package/dist/dispatch.js +70 -0
- package/dist/help.js +47 -0
- package/dist/internal/agent/adapter/conformance.js +53 -0
- package/dist/internal/agent/adapter/hook.js +113 -0
- package/dist/internal/agent/adapter/install.js +30 -0
- package/dist/internal/agent/adapter/scope.js +55 -0
- package/dist/internal/agent/adapter/session.js +63 -0
- package/dist/internal/agent/adapter/settings.js +51 -0
- package/dist/internal/agent/cli.js +79 -0
- package/dist/internal/agent/evals/index.js +111 -0
- package/dist/internal/agent/index.js +15 -0
- package/dist/internal/agent/ledger-read.js +27 -0
- package/dist/internal/agent/routing/cli.js +104 -0
- package/dist/internal/agent/routing/index.js +123 -0
- package/dist/internal/agent/telemetry/index.js +97 -0
- package/dist/internal/orchestration/activity/index.js +111 -0
- package/dist/internal/orchestration/analyze/index.js +237 -0
- package/dist/internal/orchestration/cli.js +475 -0
- package/dist/internal/orchestration/config/index.js +66 -0
- package/dist/internal/orchestration/doctor/index.js +202 -0
- package/dist/internal/orchestration/forge/index.js +121 -0
- package/dist/internal/orchestration/git/index.js +181 -0
- package/dist/internal/orchestration/harness/cli.js +378 -0
- package/dist/internal/orchestration/harness/index.js +186 -0
- package/dist/internal/orchestration/harness/shim.js +182 -0
- package/dist/internal/orchestration/index.js +29 -0
- package/dist/internal/orchestration/loop/index.js +1107 -0
- package/dist/internal/orchestration/prompts/index.js +199 -0
- package/dist/internal/orchestration/report/index.js +125 -0
- package/dist/internal/orchestration/routes/index.js +85 -0
- package/dist/internal/orchestration/run/index.js +221 -0
- package/dist/internal/orchestration/runner/index.js +90 -0
- package/dist/internal/orchestration/runners/claude/index.js +246 -0
- package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
- package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
- package/dist/internal/orchestration/runners/codex/exec.js +114 -0
- package/dist/internal/orchestration/runners/codex/index.js +72 -0
- package/dist/internal/orchestration/runners/conformance/index.js +169 -0
- package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
- package/dist/internal/orchestration/runners/cursor/index.js +184 -0
- package/dist/internal/orchestration/runners/stub/index.js +168 -0
- package/dist/internal/orchestration/runners/telemetry.js +36 -0
- package/dist/internal/orchestration/runstate/index.js +213 -0
- package/dist/internal/orchestration/runstate/publish.js +115 -0
- package/dist/internal/orchestration/worktree/index.js +89 -0
- package/dist/internal/system/adapters/index.js +19 -0
- package/dist/internal/system/agentops/index.js +16 -0
- package/dist/internal/system/agentops/scope.js +67 -0
- package/dist/internal/system/assets.js +45 -0
- package/dist/internal/system/board/cli.js +56 -0
- package/dist/internal/system/board/index.js +276 -0
- package/dist/internal/system/board/live.js +277 -0
- package/dist/internal/system/calibration/cli.js +45 -0
- package/dist/internal/system/calibration/index.js +141 -0
- package/dist/internal/system/cli.js +174 -0
- package/dist/internal/system/corpus/replay.js +79 -0
- package/dist/internal/system/digest/cli.js +23 -0
- package/dist/internal/system/digest/index.js +145 -0
- package/dist/internal/system/doors/classify.js +94 -0
- package/dist/internal/system/doors/cli.js +184 -0
- package/dist/internal/system/doors/index.js +249 -0
- package/dist/internal/system/doors/metrics.js +47 -0
- package/dist/internal/system/escapes/cli.js +45 -0
- package/dist/internal/system/escapes/index.js +81 -0
- package/dist/internal/system/explore/cli.js +77 -0
- package/dist/internal/system/explore/index.js +91 -0
- package/dist/internal/system/gates/cli.js +43 -0
- package/dist/internal/system/gates/detectors.js +55 -0
- package/dist/internal/system/gates/pre.js +51 -0
- package/dist/internal/system/hub/cli.js +51 -0
- package/dist/internal/system/hub/index.js +146 -0
- package/dist/internal/system/init/index.js +83 -0
- package/dist/internal/system/ledger/cli.js +54 -0
- package/dist/internal/system/ledger/index.js +176 -0
- package/dist/internal/system/manifest/index.js +207 -0
- package/dist/internal/system/provenance/index.js +13 -0
- package/dist/internal/system/report/index.js +15 -0
- package/dist/internal/system/runner/index.js +74 -0
- package/dist/internal/system/runstatus/cli.js +86 -0
- package/dist/internal/system/runstatus/index.js +168 -0
- package/dist/internal/system/sequencer/briefing.js +75 -0
- package/dist/internal/system/sequencer/cli.js +170 -0
- package/dist/internal/system/sequencer/index.js +156 -0
- package/dist/internal/system/spec/cli.js +110 -0
- package/dist/internal/system/spec/index.js +212 -0
- package/dist/internal/system/spec/materialize.js +111 -0
- package/dist/internal/system/spec/openspec-parse.js +83 -0
- package/dist/internal/system/spec/project.js +208 -0
- package/dist/internal/system/substrate/index.js +14 -0
- package/dist/internal/system/system/index.js +159 -0
- package/dist/internal/system/units/index.js +87 -0
- package/dist/internal/system/verify/cli.js +103 -0
- package/dist/internal/system/verify/declined-reasons.js +61 -0
- package/dist/internal/system/verify/index.js +188 -0
- package/dist/internal/system/verify/signing.js +79 -0
- package/dist/paths.js +50 -0
- package/dist/routes.js +93 -0
- package/dist/version.js +30 -0
- package/dist/win32-process-group.js +360 -0
- package/package.json +41 -0
- package/profiles/python/bindings.toml +59 -0
- package/profiles/terraform/bindings.toml +66 -0
- package/profiles/typescript/bindings.toml +64 -0
- package/profiles/typescript/smoke/README.md +8 -0
- package/schema/baseline.schema.json +26 -0
- package/schema/decision-classes.yaml +45 -0
- package/schema/envelope.schema.json +186 -0
- package/schema/ledger-event.schema.json +235 -0
- package/schema/manifest.schema.json +119 -0
- package/schema/routes.schema.json +55 -0
- package/schema/run-status.schema.json +162 -0
- package/schema/session-event.schema.json +74 -0
- package/schema/spec.schema.json +114 -0
- package/schema/system.schema.json +142 -0
- package/templates/ci/verify.yml +23 -0
- package/templates/target-kit/README.md +78 -0
- package/templates/target-kit/component.toml +20 -0
- package/templates/target-kit/orchestration.json +9 -0
- package/templates/target-kit/profiles/target/bindings.toml +6 -0
- package/templates/target-kit/routes.toml +5 -0
- package/templates/target-kit/specs/first-unit.yaml +11 -0
- package/templates/target-kit/system.toml +7 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/** Lane `activity_at` older than this renders muted as "quiet for <age>" (own freshness, not run heartbeat). */
|
|
2
|
+
export const ACTIVITY_QUIET_THRESHOLD_MS = 60_000;
|
|
3
|
+
/** "3m" / "1h 4m" from a ms age — for the "machine silent since …" label. */
|
|
4
|
+
function humanAge(ms) {
|
|
5
|
+
if (ms < 0)
|
|
6
|
+
return "unknown";
|
|
7
|
+
const m = Math.floor(ms / 60_000);
|
|
8
|
+
if (m < 60)
|
|
9
|
+
return `${m}m`;
|
|
10
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
11
|
+
}
|
|
12
|
+
function runPanel(overlay) {
|
|
13
|
+
if (!overlay || overlay.runs.length === 0) {
|
|
14
|
+
return { title: "Runs — live overlay", status: "insufficient-data", data: [] };
|
|
15
|
+
}
|
|
16
|
+
const data = overlay.runs.map((r) => {
|
|
17
|
+
const d = r.document;
|
|
18
|
+
const counts = (d.counts ?? {});
|
|
19
|
+
const needs = (d.needs_you ?? []).map((n) => {
|
|
20
|
+
const kind = String(n.kind ?? "");
|
|
21
|
+
const unitId = n.unit_id ? String(n.unit_id) : null;
|
|
22
|
+
return {
|
|
23
|
+
kind,
|
|
24
|
+
unit_id: unitId,
|
|
25
|
+
message: String(n.message ?? ""),
|
|
26
|
+
unblock: String(n.unblock ?? ""),
|
|
27
|
+
door_anchor: /door/i.test(kind) && unitId ? `#door-${unitId}` : null,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const lanes = (d.lanes ?? []).map((l) => {
|
|
31
|
+
const lane = {
|
|
32
|
+
unit_id: String(l.unit_id ?? ""),
|
|
33
|
+
step: String(l.step ?? ""),
|
|
34
|
+
runner: String(l.runner ?? ""),
|
|
35
|
+
model: String(l.model ?? ""),
|
|
36
|
+
attempt: Number(l.attempt ?? 1),
|
|
37
|
+
retry_budget: Number(l.retry_budget ?? 0),
|
|
38
|
+
cost: Number(l.cost ?? 0),
|
|
39
|
+
reconstructed: l.emitter === "orchestrator",
|
|
40
|
+
};
|
|
41
|
+
if (l.activity !== undefined)
|
|
42
|
+
lane.activity = String(l.activity);
|
|
43
|
+
if (l.activity_at !== undefined)
|
|
44
|
+
lane.activity_at = String(l.activity_at);
|
|
45
|
+
if (l.turn !== undefined)
|
|
46
|
+
lane.turn = Number(l.turn);
|
|
47
|
+
if (l.tokens !== undefined) {
|
|
48
|
+
const t = l.tokens;
|
|
49
|
+
if (t.in !== undefined && t.out !== undefined) {
|
|
50
|
+
lane.tokens = { in: Number(t.in), out: Number(t.out) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (l.recent !== undefined) {
|
|
54
|
+
lane.recent = l.recent.map((r) => String(r));
|
|
55
|
+
}
|
|
56
|
+
return lane;
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
machine_id: r.machine_id,
|
|
60
|
+
run_id: r.run_id,
|
|
61
|
+
system_id: String(d.system_id ?? ""),
|
|
62
|
+
spec_ref: String(d.spec_ref ?? ""),
|
|
63
|
+
liveness: r.liveness === "stale" ? "stale" : "live",
|
|
64
|
+
silent_for: r.liveness === "stale" ? humanAge(r.heartbeat_age_ms) : null,
|
|
65
|
+
merged: Number(counts.merged ?? 0),
|
|
66
|
+
dag_size: Number(counts.dag_size ?? 0),
|
|
67
|
+
needs_you: needs,
|
|
68
|
+
lanes,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
return { title: "Runs — live overlay", status: "ok", data };
|
|
72
|
+
}
|
|
73
|
+
export function buildBoard(p, now = new Date().toISOString(), overlay) {
|
|
74
|
+
const merged = p.counts.units["merged"] ?? 0;
|
|
75
|
+
const totalCost = p.sessions.reduce((s, x) => s + x.costUsd, 0);
|
|
76
|
+
const modelForUnit = new Map();
|
|
77
|
+
for (const s of p.sessions)
|
|
78
|
+
if (s.unit)
|
|
79
|
+
modelForUnit.set(s.unit, s.model);
|
|
80
|
+
const lanes = {};
|
|
81
|
+
for (const u of p.units) {
|
|
82
|
+
const comp = u.component ?? "(unassigned)";
|
|
83
|
+
(lanes[comp] ??= []).push({
|
|
84
|
+
id: u.id,
|
|
85
|
+
state: u.state,
|
|
86
|
+
model: modelForUnit.get(`${comp}#${u.id}`) ?? null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const panel = (title, empty, data) => ({
|
|
90
|
+
title,
|
|
91
|
+
status: empty ? "insufficient-data" : "ok",
|
|
92
|
+
data,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
system: p.system,
|
|
96
|
+
generatedAt: now,
|
|
97
|
+
panels: {
|
|
98
|
+
portfolioPulse: panel("Portfolio pulse", p.counts.events === 0, {
|
|
99
|
+
units: p.counts.units,
|
|
100
|
+
doorsOpen: p.counts.doorsOpen,
|
|
101
|
+
systemHalts: p.counts.systemHalts,
|
|
102
|
+
escapes: p.counts.escapes,
|
|
103
|
+
costPerMergedUnit: merged > 0 ? Number((totalCost / merged).toFixed(2)) : null,
|
|
104
|
+
}),
|
|
105
|
+
doorsQueue: panel("Doors — your queue", p.doors.filter((d) => d.state === "opened").length === 0, p.doors
|
|
106
|
+
.filter((d) => d.state === "opened")
|
|
107
|
+
.sort((a, b) => Date.parse(a.openedAt) - Date.parse(b.openedAt))
|
|
108
|
+
.map((d) => ({
|
|
109
|
+
id: d.id,
|
|
110
|
+
unitId: d.unitId,
|
|
111
|
+
classes: d.classes,
|
|
112
|
+
openedAt: d.openedAt,
|
|
113
|
+
}))),
|
|
114
|
+
systemLanes: panel("Systems — in-flight by component", p.units.length === 0, lanes),
|
|
115
|
+
escapeFeed: panel("Escape feed → corpus", p.escapes.length === 0, p.escapes),
|
|
116
|
+
fleet: panel("Agent fleet", p.sessions.length === 0, p.sessions.map((s) => ({
|
|
117
|
+
id: s.id,
|
|
118
|
+
unit: s.unit,
|
|
119
|
+
model: s.model,
|
|
120
|
+
costUsd: Number(s.costUsd.toFixed(2)),
|
|
121
|
+
}))),
|
|
122
|
+
calibrationStrip: panel("Calibration — pending proposals", true, []),
|
|
123
|
+
runs: runPanel(overlay),
|
|
124
|
+
runAnalysis: panel("Run analysis", p.runAnalyses.every((r) => r.findings.length === 0), p.runAnalyses.flatMap((r) => r.findings.map((f) => ({
|
|
125
|
+
id: `${r.runId}:${f.unitId}:${f.rule}`,
|
|
126
|
+
runId: r.runId,
|
|
127
|
+
unitId: f.unitId,
|
|
128
|
+
rule: f.rule,
|
|
129
|
+
severity: f.severity,
|
|
130
|
+
count: f.count,
|
|
131
|
+
})))),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** A single self-contained HTML page embedding the board data + a tiny renderer. */
|
|
136
|
+
export function renderBoardHtml(board) {
|
|
137
|
+
const json = JSON.stringify(board, null, 2).replace(/</g, "\\u003c");
|
|
138
|
+
return `<!doctype html>
|
|
139
|
+
<html lang="en"><head><meta charset="utf-8"><title>Operations Board — ${board.system ?? "harness"}</title>
|
|
140
|
+
<style>
|
|
141
|
+
:root{color-scheme:light dark}
|
|
142
|
+
body{font:14px/1.5 ui-sans-serif,system-ui,sans-serif;margin:0;padding:24px;background:#0e1216;color:#e5e9ee}
|
|
143
|
+
h1{font-size:1.3rem;margin:0 0 4px} .sub{color:#9aa4b1;margin-bottom:20px;font-size:.85rem}
|
|
144
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
|
|
145
|
+
/* P7.3 — the live-runs strip spans the full width, above the auto-fit grid. */
|
|
146
|
+
.strip{margin-bottom:14px}
|
|
147
|
+
.strip .panel{padding:14px 16px}
|
|
148
|
+
.runlist{display:flex;flex-wrap:wrap;gap:12px}
|
|
149
|
+
.runlist>.run{flex:1 1 340px;min-width:300px;border:1px solid #222a33;border-radius:8px;padding:8px 10px;background:#12181e}
|
|
150
|
+
.panel{border:1px solid #2a333d;border-radius:10px;background:#161c22;padding:14px 16px}
|
|
151
|
+
.panel h2{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#9aa4b1;margin:0 0 10px}
|
|
152
|
+
.insufficient{color:#6b7583;font-style:italic}
|
|
153
|
+
.kv{display:flex;justify-content:space-between;border-bottom:1px solid #222a33;padding:3px 0}
|
|
154
|
+
.kv:last-child{border:0}
|
|
155
|
+
code{font-family:ui-monospace,monospace;font-size:.82em;color:#5bb8d4}
|
|
156
|
+
.chip{display:inline-block;font-family:ui-monospace,monospace;font-size:.72rem;border:1px solid #3b4653;border-radius:99px;padding:1px 7px;margin:2px 3px 0 0}
|
|
157
|
+
.lane-act{color:#9aa4b1;font-size:.78rem;margin-left:4px}.lane-act.quiet{color:#6b7583;font-style:italic}
|
|
158
|
+
.lane-log{display:block;margin-top:3px;font-family:ui-monospace,monospace;font-size:.78rem;color:#c3ccd6}
|
|
159
|
+
.log-line{overflow-wrap:anywhere;padding:1px 0}
|
|
160
|
+
.log-verb{color:#6b7583;margin-right:5px}
|
|
161
|
+
</style></head>
|
|
162
|
+
<body>
|
|
163
|
+
<h1>Operations Board</h1>
|
|
164
|
+
<div class="sub">system: <code>${board.system ?? "—"}</code> · generated ${board.generatedAt}</div>
|
|
165
|
+
<section class="strip" id="runs-strip"></section>
|
|
166
|
+
<div class="grid" id="root"></div>
|
|
167
|
+
<script id="data" type="application/json">${json}</script>
|
|
168
|
+
<script>
|
|
169
|
+
const B = JSON.parse(document.getElementById('data').textContent);
|
|
170
|
+
const root = document.getElementById('root');
|
|
171
|
+
const strip = document.getElementById('runs-strip');
|
|
172
|
+
const esc = s => String(s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]));
|
|
173
|
+
const ACTIVITY_QUIET_THRESHOLD_MS = ${ACTIVITY_QUIET_THRESHOLD_MS};
|
|
174
|
+
function humanAge(ms) {
|
|
175
|
+
if (ms < 0) return 'unknown';
|
|
176
|
+
const m = Math.floor(ms / 60_000);
|
|
177
|
+
if (m < 60) return m + 'm';
|
|
178
|
+
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
|
|
179
|
+
}
|
|
180
|
+
function laneHasActivity(l) {
|
|
181
|
+
return 'activity' in l || 'activity_at' in l || 'turn' in l || 'tokens' in l || 'recent' in l;
|
|
182
|
+
}
|
|
183
|
+
function laneActivityHtml(l, run, nowMs) {
|
|
184
|
+
// Lane freshness and run liveness are different questions — a lane can be
|
|
185
|
+
// actively streaming activity while its run's heartbeat hasn't ticked in a
|
|
186
|
+
// while (long implement steps). Gauge quiet/fresh from the lane's own
|
|
187
|
+
// activity_at, never from run.liveness.
|
|
188
|
+
if (!laneHasActivity(l)) return '';
|
|
189
|
+
let line = '';
|
|
190
|
+
if ('activity_at' in l || 'activity' in l) {
|
|
191
|
+
const atMs = l.activity_at ? Date.parse(l.activity_at) : NaN;
|
|
192
|
+
const ageMs = Number.isFinite(atMs) ? nowMs - atMs : 0;
|
|
193
|
+
if (Number.isFinite(atMs) && ageMs > ACTIVITY_QUIET_THRESHOLD_MS) {
|
|
194
|
+
line = '<span class="lane-act quiet">quiet for ' + esc(humanAge(ageMs)) + '</span>';
|
|
195
|
+
} else if ('activity' in l) {
|
|
196
|
+
line = '<span class="lane-act">' + esc(l.activity) + '</span>';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const meta = [];
|
|
200
|
+
if ('turn' in l) meta.push('turn ' + esc(l.turn));
|
|
201
|
+
// a runner that reports no usage (cursor) always carries {in:0,out:0} —
|
|
202
|
+
// showing "0/0" on every lane is noise, not signal.
|
|
203
|
+
if ('tokens' in l && (l.tokens.in || l.tokens.out)) meta.push(esc(l.tokens.in) + '/' + esc(l.tokens.out));
|
|
204
|
+
const metaHtml = meta.length ? '<span class="lane-act">' + meta.join(' · ') + '</span>' : '';
|
|
205
|
+
// newest first — recent is pushed oldest→newest, so read it back to front.
|
|
206
|
+
const recentHtml = ('recent' in l && l.recent.length)
|
|
207
|
+
? '<div class="lane-log">' + l.recent.slice().reverse().map(renderLogLine).join('') + '</div>'
|
|
208
|
+
: '';
|
|
209
|
+
return line + metaHtml + recentHtml;
|
|
210
|
+
}
|
|
211
|
+
/** "verb: target" renders with a dimmed verb and a bright target — a log line, not a status pill. */
|
|
212
|
+
function renderLogLine(text) {
|
|
213
|
+
const m = /^([A-Za-z][\\w-]*): ([\\s\\S]*)$/.exec(text);
|
|
214
|
+
if (!m) return '<div class="log-line">' + esc(text) + '</div>';
|
|
215
|
+
return '<div class="log-line"><span class="log-verb">' + esc(m[1]) + '</span> ' + esc(m[2]) + '</div>';
|
|
216
|
+
}
|
|
217
|
+
for (const [key, p] of Object.entries(B.panels)) {
|
|
218
|
+
// the runs section is a full-width strip ABOVE the grid, never a grid cell.
|
|
219
|
+
const host = key === 'runs' ? strip : root;
|
|
220
|
+
const el = document.createElement('div'); el.className = 'panel';
|
|
221
|
+
let inner = '<h2>' + esc(p.title) + '</h2>';
|
|
222
|
+
if (p.status === 'insufficient-data') inner += '<div class="insufficient">insufficient data</div>';
|
|
223
|
+
else inner += render(key, p.data);
|
|
224
|
+
el.innerHTML = inner; host.appendChild(el);
|
|
225
|
+
}
|
|
226
|
+
if (!strip.hasChildNodes()) strip.remove();
|
|
227
|
+
function render(key, d) {
|
|
228
|
+
if (key === 'portfolioPulse') return Object.entries({
|
|
229
|
+
...d.units, 'doors open': d.doorsOpen, 'system halts': d.systemHalts,
|
|
230
|
+
'escapes': d.escapes, '$/merged unit': d.costPerMergedUnit ?? 'n/a'
|
|
231
|
+
}).map(([k,v]) => '<div class="kv"><span>'+esc(k)+'</span><code>'+esc(v)+'</code></div>').join('');
|
|
232
|
+
if (key === 'systemLanes') return Object.entries(d).map(([c, units]) =>
|
|
233
|
+
'<div class="kv"><span><code>'+esc(c)+'</code></span><span>'+units.map(u=>'<span class="chip" id="unit-'+esc(u.id)+'">'+esc(u.id)+' · '+esc(u.state)+'</span>').join('')+'</span></div>').join('');
|
|
234
|
+
if (key === 'runs') return '<div class="runlist">' + d.map(r => {
|
|
235
|
+
const head = '<div class="kv"><span><code>'+esc(r.machine_id)+' / '+esc(r.run_id)+'</code> '
|
|
236
|
+
+ (r.liveness === 'stale'
|
|
237
|
+
? '<span class="chip" style="border-color:#7a5b2e;color:#e0b062">stale — machine silent '+esc(r.silent_for)+'</span>'
|
|
238
|
+
: '<span class="chip" style="border-color:#2e7a4b;color:#62e0a0">live</span>')
|
|
239
|
+
+ '</span><code>'+esc(r.merged)+'/'+esc(r.dag_size)+' merged</code></div>';
|
|
240
|
+
const nowMs = Date.parse(B.generatedAt);
|
|
241
|
+
const lanes = r.lanes.map(l => '<div class="kv"><span id="unit-'+esc(l.unit_id)+'">'
|
|
242
|
+
+ '<code>'+esc(l.unit_id)+'</code> '+esc(l.step)
|
|
243
|
+
+ ' <span class="chip">'+esc(l.runner)+' · '+esc(l.model)+'</span>'
|
|
244
|
+
+ (l.reconstructed ? ' <span class="chip" style="border-color:#555;color:#9aa4b1">reconstructed</span>' : '')
|
|
245
|
+
+ laneActivityHtml(l, r, nowMs)
|
|
246
|
+
+ '</span><code>#'+esc(l.attempt)+'/'+esc(l.retry_budget+1)+' · $'+esc(l.cost.toFixed ? l.cost.toFixed(2) : l.cost)+'</code></div>').join('');
|
|
247
|
+
const needs = r.needs_you.map(n => '<div class="kv"><span>'
|
|
248
|
+
+ '<span class="chip" style="border-color:#7a3b3b;color:#e79a9a">'+esc(n.kind)+'</span> '
|
|
249
|
+
+ (n.door_anchor ? '<a href="'+esc(n.door_anchor)+'">'+esc(n.unit_id||'')+'</a>' : esc(n.unit_id||''))
|
|
250
|
+
+ ' — '+esc(n.message)+'</span><code>'+esc(n.unblock)+'</code></div>').join('');
|
|
251
|
+
return '<div class="run">' + head + lanes + (needs ? '<div class="kv"><span>needs you</span></div>'+needs : '') + '</div>';
|
|
252
|
+
}).join('') + '</div>';
|
|
253
|
+
// doors + fleet render typed fields — never a raw JSON blob (G2.2).
|
|
254
|
+
if (key === 'doorsQueue') {
|
|
255
|
+
if (d.length === 0) return '<div class="insufficient">none</div>';
|
|
256
|
+
return d.map(row => '<div class="kv" id="door-'+esc(row.unitId)+'"><span><code>'+esc(row.id)+'</code> · '+esc(row.unitId)
|
|
257
|
+
+ ' · <span class="chip">'+(row.classes||[]).map(esc).join('</span><span class="chip">')+'</span></span>'
|
|
258
|
+
+ '<code>'+esc(row.openedAt)+'</code></div>').join('');
|
|
259
|
+
}
|
|
260
|
+
if (key === 'fleet') {
|
|
261
|
+
if (d.length === 0) return '<div class="insufficient">none</div>';
|
|
262
|
+
return d.map(row => '<div class="kv"><span><code>'+esc(row.id)+'</code> · '+esc(row.unit || '—')
|
|
263
|
+
+ ' · '+esc(row.model || '—')+'</span><code>$'+esc(Number(row.costUsd ?? 0).toFixed(2))+'</code></div>').join('');
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(d)) {
|
|
266
|
+
if (d.length === 0) return '<div class="insufficient">none</div>';
|
|
267
|
+
return d.map(row => '<div class="kv"><span>'+esc(row.id || row.scenario || row.unitId || '')+'</span><code>'
|
|
268
|
+
+ esc(Object.entries(row).filter(([k])=>k!=='id').map(([k,v])=>k+'='+(Array.isArray(v)?v.join('|'):v)).join(' · '))+'</code></div>').join('');
|
|
269
|
+
}
|
|
270
|
+
return '<pre>'+esc(JSON.stringify(d,null,1))+'</pre>';
|
|
271
|
+
}
|
|
272
|
+
</script>
|
|
273
|
+
</body></html>
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** Live, local-only operations dashboard over every initialized harness repo. */
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { buildBoard, ACTIVITY_QUIET_THRESHOLD_MS } from "./index.js";
|
|
6
|
+
import { project, projectLedger } from "../hub/index.js";
|
|
7
|
+
import { projectRuns } from "../runstatus/index.js";
|
|
8
|
+
const SKIP_DIRS = new Set([
|
|
9
|
+
".git", ".harness", ".worktrees", ".dry-run", "node_modules", "dist",
|
|
10
|
+
"coverage", "docs", "test", "tests", "e2e", "fixture", "fixtures", "examples",
|
|
11
|
+
]);
|
|
12
|
+
export function discoverHarnessRepositories(roots) {
|
|
13
|
+
const found = new Map();
|
|
14
|
+
const visit = (dir, isRoot) => {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
23
|
+
const manifest = names.has("system.toml")
|
|
24
|
+
? "system.toml"
|
|
25
|
+
: names.has("component.toml")
|
|
26
|
+
? "component.toml"
|
|
27
|
+
: undefined;
|
|
28
|
+
if (manifest) {
|
|
29
|
+
const root = resolve(dir);
|
|
30
|
+
found.set(root.toLowerCase(), {
|
|
31
|
+
name: basename(root),
|
|
32
|
+
root,
|
|
33
|
+
ledgerDir: join(root, ".harness", "ledger"),
|
|
34
|
+
spoolDir: join(root, ".harness", "spool"),
|
|
35
|
+
manifest,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
if (SKIP_DIRS.has(entry.name) && !isRoot)
|
|
42
|
+
continue;
|
|
43
|
+
if (SKIP_DIRS.has(entry.name))
|
|
44
|
+
continue;
|
|
45
|
+
visit(join(dir, entry.name), false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
for (const root of roots.map((path) => resolve(path)))
|
|
49
|
+
visit(root, true);
|
|
50
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name) || a.root.localeCompare(b.root));
|
|
51
|
+
}
|
|
52
|
+
export function buildLiveDashboard(roots) {
|
|
53
|
+
const sources = discoverHarnessRepositories(roots);
|
|
54
|
+
const repositories = sources.map((source) => {
|
|
55
|
+
try {
|
|
56
|
+
const projection = projectLedger(source.ledgerDir);
|
|
57
|
+
const overlay = existsSync(source.spoolDir) ? projectRuns(source.spoolDir) : undefined;
|
|
58
|
+
const board = buildBoard(projection, new Date().toISOString(), overlay);
|
|
59
|
+
return {
|
|
60
|
+
name: source.name,
|
|
61
|
+
root: source.root,
|
|
62
|
+
manifest: source.manifest,
|
|
63
|
+
system: projection.system ?? source.name,
|
|
64
|
+
status: projection.counts.events > 0 || (overlay?.runs.length ?? 0) > 0 ? "active" : "no-data",
|
|
65
|
+
events: projection.counts.events,
|
|
66
|
+
board,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
name: source.name,
|
|
72
|
+
root: source.root,
|
|
73
|
+
manifest: source.manifest,
|
|
74
|
+
system: source.name,
|
|
75
|
+
status: "error",
|
|
76
|
+
error: error.message,
|
|
77
|
+
events: 0,
|
|
78
|
+
board: buildBoard(project([])),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const units = (state) => repositories.reduce((total, repo) => total + (repo.board.panels.portfolioPulse.data.units[state] ?? 0), 0);
|
|
83
|
+
return {
|
|
84
|
+
generatedAt: new Date().toISOString(),
|
|
85
|
+
roots: roots.map((path) => resolve(path)),
|
|
86
|
+
summary: {
|
|
87
|
+
repositories: repositories.length,
|
|
88
|
+
repositoriesWithData: repositories.filter((repo) => repo.status === "active").length,
|
|
89
|
+
events: repositories.reduce((total, repo) => total + repo.events, 0),
|
|
90
|
+
planned: units("planned"),
|
|
91
|
+
executing: units("executing"),
|
|
92
|
+
inReview: units("in-review"),
|
|
93
|
+
merged: units("merged"),
|
|
94
|
+
doorsOpen: repositories.reduce((total, repo) => total + repo.board.panels.portfolioPulse.data.doorsOpen, 0),
|
|
95
|
+
// "active" means live — a stale run (silent past the threshold) is not
|
|
96
|
+
// work happening right now, and counting it alongside live runs made
|
|
97
|
+
// the headline number describe mostly zombies.
|
|
98
|
+
activeRuns: repositories.reduce((total, repo) => total + repo.board.panels.runs.data.filter((r) => r.liveness === "live").length, 0),
|
|
99
|
+
needsYou: repositories.reduce((total, repo) => total + repo.board.panels.runs.data.reduce((n, run) => n + run.needs_you.length, 0), 0),
|
|
100
|
+
},
|
|
101
|
+
repositories,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export async function startBoardServer(opts) {
|
|
105
|
+
const host = opts.host ?? "127.0.0.1";
|
|
106
|
+
if (!["127.0.0.1", "::1", "localhost"].includes(host)) {
|
|
107
|
+
throw new Error(`board serve is local-only; refusing host ${host}`);
|
|
108
|
+
}
|
|
109
|
+
const roots = opts.roots.map((path) => resolve(path));
|
|
110
|
+
const clients = new Set();
|
|
111
|
+
const dashboard = () => buildLiveDashboard(roots);
|
|
112
|
+
const server = createServer((request, response) => {
|
|
113
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
114
|
+
if (request.method !== "GET")
|
|
115
|
+
return send(response, 405, "text/plain; charset=utf-8", "method not allowed\n");
|
|
116
|
+
if (url.pathname === "/health")
|
|
117
|
+
return send(response, 200, "text/plain; charset=utf-8", "ok\n");
|
|
118
|
+
if (url.pathname === "/api/dashboard") {
|
|
119
|
+
return send(response, 200, "application/json; charset=utf-8", JSON.stringify(dashboard()));
|
|
120
|
+
}
|
|
121
|
+
if (url.pathname === "/events") {
|
|
122
|
+
response.writeHead(200, {
|
|
123
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
124
|
+
"cache-control": "no-cache, no-transform",
|
|
125
|
+
connection: "keep-alive",
|
|
126
|
+
"x-content-type-options": "nosniff",
|
|
127
|
+
});
|
|
128
|
+
response.write(`event: dashboard\ndata: ${JSON.stringify(dashboard())}\n\n`);
|
|
129
|
+
clients.add(response);
|
|
130
|
+
request.on("close", () => clients.delete(response));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
134
|
+
return send(response, 200, "text/html; charset=utf-8", LIVE_BOARD_HTML);
|
|
135
|
+
}
|
|
136
|
+
if (url.pathname === "/favicon.ico")
|
|
137
|
+
return send(response, 204, "image/x-icon", "");
|
|
138
|
+
return send(response, 404, "text/plain; charset=utf-8", "not found\n");
|
|
139
|
+
});
|
|
140
|
+
const timer = setInterval(() => {
|
|
141
|
+
if (!clients.size)
|
|
142
|
+
return;
|
|
143
|
+
const message = `event: dashboard\ndata: ${JSON.stringify(dashboard())}\n\n`;
|
|
144
|
+
for (const client of clients)
|
|
145
|
+
client.write(message);
|
|
146
|
+
}, opts.refreshMs ?? 2_000);
|
|
147
|
+
timer.unref();
|
|
148
|
+
await new Promise((accept, reject) => {
|
|
149
|
+
server.once("error", reject);
|
|
150
|
+
server.listen(opts.port ?? 4317, host, () => accept());
|
|
151
|
+
});
|
|
152
|
+
const address = server.address();
|
|
153
|
+
const port = typeof address === "object" && address ? address.port : opts.port ?? 4317;
|
|
154
|
+
return {
|
|
155
|
+
url: `http://${host === "::1" ? "[::1]" : host}:${port}/`,
|
|
156
|
+
close: () => new Promise((accept, reject) => {
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
for (const client of clients)
|
|
159
|
+
client.end();
|
|
160
|
+
server.close((error) => error ? reject(error) : accept());
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function send(response, status, type, body) {
|
|
165
|
+
response.writeHead(status, {
|
|
166
|
+
"content-type": type,
|
|
167
|
+
"cache-control": "no-store",
|
|
168
|
+
"x-content-type-options": "nosniff",
|
|
169
|
+
"content-security-policy": "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'",
|
|
170
|
+
});
|
|
171
|
+
response.end(body);
|
|
172
|
+
}
|
|
173
|
+
export const LIVE_BOARD_HTML = `<!doctype html>
|
|
174
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
175
|
+
<title>Harness Operations</title><style>
|
|
176
|
+
:root{color-scheme:dark;--bg:#0a0d0c;--panel:#111614;--line:#26302b;--muted:#84918a;--text:#edf3ef;--green:#79e6a4;--amber:#efc56b;--red:#ff8b83;--cyan:#72d8d0}
|
|
177
|
+
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 70% -10%,#163028 0,transparent 32rem),var(--bg);color:var(--text);font:14px/1.45 Inter,ui-sans-serif,system-ui,sans-serif}
|
|
178
|
+
main{width:min(1480px,calc(100% - 32px));margin:auto;padding:28px 0 60px}header{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:24px}h1{font-size:clamp(24px,4vw,42px);letter-spacing:-.045em;margin:0}.eyebrow{color:var(--green);font:700 11px ui-monospace,monospace;letter-spacing:.18em;text-transform:uppercase}.meta{color:var(--muted);text-align:right}.live{display:inline-flex;gap:7px;align-items:center;color:var(--green)}.live:before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 12px currentColor}.live.paused{color:var(--amber)}.live.paused:before{animation:none}
|
|
179
|
+
.toolbar{display:flex;gap:10px;align-items:center;justify-content:flex-end}.pause-btn{font:11px ui-monospace,monospace;letter-spacing:.04em;color:var(--muted);background:transparent;border:1px solid var(--line);border-radius:99px;padding:3px 10px;cursor:pointer}.pause-btn:hover{border-color:var(--muted);color:var(--text)}.pause-btn.active{border-color:#7a5b2e;color:var(--amber)}
|
|
180
|
+
.summary{display:grid;grid-template-columns:repeat(7,minmax(110px,1fr));gap:10px;margin-bottom:18px}.metric,.repo,.attention{border:1px solid var(--line);background:color-mix(in srgb,var(--panel) 94%,transparent);border-radius:14px}.metric{padding:13px 14px}.metric b{display:block;font:600 25px ui-monospace,monospace}.metric span{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}
|
|
181
|
+
.attention{display:none;padding:14px 16px;margin-bottom:18px;border-color:#68483c}.attention.show{display:block}.attention h2{color:var(--amber)}h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;margin:0 0 10px}.attention-row,.row{display:flex;justify-content:space-between;gap:18px;padding:7px 0;border-top:1px solid var(--line)}code{color:var(--cyan);font:12px ui-monospace,monospace;overflow-wrap:anywhere}
|
|
182
|
+
.repos{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:14px}.repo{padding:17px;min-width:0}.repo-head{display:flex;align-items:start;justify-content:space-between;gap:16px;margin-bottom:14px}.repo h3{font-size:19px;margin:2px 0}.path{color:var(--muted);font:11px ui-monospace,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:62ch}.badge,.chip{display:inline-flex;border:1px solid var(--line);border-radius:99px;padding:2px 7px;font:10px ui-monospace,monospace;color:var(--muted)}.badge.active{border-color:#2e6845;color:var(--green)}
|
|
183
|
+
.hero{margin-bottom:14px;padding:13px 15px;border:1px solid #2e6845;border-radius:10px;background:color-mix(in srgb,var(--panel) 85%,#0f2c1d 15%)}.hero h4{color:var(--green);font-size:11px;text-transform:uppercase;letter-spacing:.1em;margin:0 0 8px}.run-block{padding:6px 0;border-top:1px solid var(--line)}.run-block:first-child{border-top:0}
|
|
184
|
+
.stale-details{margin-top:8px;border-top:1px solid var(--line);padding-top:8px}.stale-details summary{cursor:pointer;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em}.stale-details[open] summary{margin-bottom:6px}
|
|
185
|
+
.sections{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px}.section h4{color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.1em;margin:0 0 5px}
|
|
186
|
+
.lane-act{color:var(--muted);font-size:12px}.lane-act.quiet{color:#5f6b65;font-style:italic}
|
|
187
|
+
.lane-log{display:block;margin-top:4px;font-family:ui-monospace,monospace;font-size:12px;color:#c3ccd6}.log-line{overflow-wrap:anywhere;padding:1px 0}.log-verb{color:var(--muted);margin-right:5px}
|
|
188
|
+
.empty{color:#5f6b65;font-style:italic;padding:7px 0}.state{font-family:ui-monospace,monospace}.state.merged{color:var(--green)}.state.executing,.state.in-review{color:var(--amber)}.state.live{color:var(--green)}.state.stale{color:var(--amber)}.door{color:var(--red)}
|
|
189
|
+
.quiet-repos{margin-top:18px}.quiet-repos h2{color:var(--muted)}.quiet-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);font-size:12px;color:var(--muted)}.quiet-row:first-child{border-top:0}.quiet-row b{color:var(--text);font-weight:600}.quiet-row .path{max-width:44ch}
|
|
190
|
+
footer{color:var(--muted);font-size:11px;margin-top:22px}
|
|
191
|
+
@media(max-width:900px){.summary{grid-template-columns:repeat(3,1fr)}header{align-items:start;flex-direction:column}.meta{text-align:left}.toolbar{justify-content:flex-start}}@media(max-width:560px){main{width:min(100% - 20px,1480px);padding-top:18px}.summary{grid-template-columns:repeat(2,1fr)}.sections{grid-template-columns:1fr}.attention-row,.row{display:block}.repo{padding:14px}}
|
|
192
|
+
</style></head><body><main><header><div><div class="eyebrow">System harness · local control plane</div><h1>Harness Operations</h1></div><div class="meta"><div class="toolbar"><button id="pauseBtn" class="pause-btn" type="button">⏸ pause</button><span class="live" id="connection">connecting</span></div><span id="updated">waiting for signal</span></div></header><section class="summary" id="summary"></section><section class="attention" id="attention"><h2>Needs you</h2><div id="attentionRows"></div></section><section class="repos" id="repos"></section><section class="quiet-repos" id="quietRepos"></section><footer>Read-only · durable truth comes from each repository ledger · in-flight state comes from run-status spools</footer></main>
|
|
193
|
+
<script>
|
|
194
|
+
const $=id=>document.getElementById(id), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
195
|
+
const ACTIVITY_QUIET_THRESHOLD_MS=${ACTIVITY_QUIET_THRESHOLD_MS};
|
|
196
|
+
function humanAge(ms){if(ms<0)return'unknown';const m=Math.floor(ms/60000);if(m<60)return m+'m';return Math.floor(m/60)+'h '+(m%60)+'m';}
|
|
197
|
+
function laneHasActivity(l){return'activity'in l||'activity_at'in l||'turn'in l||'tokens'in l||'recent'in l;}
|
|
198
|
+
// "verb: target" renders with a dimmed verb and a bright target — a log line, not a status pill.
|
|
199
|
+
function renderLogLine(text){const m=/^([A-Za-z][\\w-]*): ([\\s\\S]*)$/.exec(text);if(!m)return'<div class="log-line">'+esc(text)+'</div>';return'<div class="log-line"><span class="log-verb">'+esc(m[1])+'</span> '+esc(m[2])+'</div>';}
|
|
200
|
+
function laneActivityHtml(l,run,nowMs){
|
|
201
|
+
// Lane freshness and run liveness are different questions — a lane can be actively
|
|
202
|
+
// streaming activity through a long implement step while its run's heartbeat sits
|
|
203
|
+
// well past the staleness threshold. Gauge quiet/fresh from the lane's own
|
|
204
|
+
// activity_at, never from run.liveness (that was hiding the richest data on the board).
|
|
205
|
+
if(!laneHasActivity(l))return'';
|
|
206
|
+
let line='';
|
|
207
|
+
if('activity_at'in l||'activity'in l){
|
|
208
|
+
const atMs=l.activity_at?Date.parse(l.activity_at):NaN;
|
|
209
|
+
const ageMs=Number.isFinite(atMs)?nowMs-atMs:0;
|
|
210
|
+
if(Number.isFinite(atMs)&&ageMs>ACTIVITY_QUIET_THRESHOLD_MS)line='<div class="lane-act quiet">quiet for '+esc(humanAge(ageMs))+'</div>';
|
|
211
|
+
else if('activity'in l)line='<div class="lane-act">'+esc(l.activity)+'</div>';
|
|
212
|
+
}
|
|
213
|
+
const meta=[];
|
|
214
|
+
if('turn'in l)meta.push('turn '+esc(l.turn));
|
|
215
|
+
// a runner that reports no usage (cursor) always carries {in:0,out:0} — showing "0/0" on every lane is noise.
|
|
216
|
+
if('tokens'in l&&(l.tokens.in||l.tokens.out))meta.push(esc(l.tokens.in)+'/'+esc(l.tokens.out));
|
|
217
|
+
const metaHtml=meta.length?'<div class="lane-act">'+meta.join(' · ')+'</div>':'';
|
|
218
|
+
// newest first — recent is pushed oldest→newest, so read it back to front.
|
|
219
|
+
const recentHtml=('recent'in l&&l.recent.length)?'<div class="lane-log">'+l.recent.slice().reverse().map(renderLogLine).join('')+'</div>':'';
|
|
220
|
+
return line+metaHtml+recentHtml;
|
|
221
|
+
}
|
|
222
|
+
function renderRunBlock(x,nowMs){
|
|
223
|
+
const head='<div class="row"><span>'+esc(x.machine_id)+' · <code>'+esc(x.run_id)+'</code></span><span class="state '+esc(x.liveness)+'">'+esc(x.liveness)+(x.liveness==='stale'&&x.silent_for?' — silent '+esc(x.silent_for):'')+' · '+esc(x.merged)+'/'+esc(x.dag_size)+'</span></div>';
|
|
224
|
+
const lanes=x.lanes.map(l=>'<div class="row"><span><code>'+esc(l.unit_id)+'</code> · '+esc(l.step)+'</span><span class="chip">'+esc(l.runner)+' / '+esc(l.model)+'</span></div>'+laneActivityHtml(l,x,nowMs)).join('');
|
|
225
|
+
return '<div class="run-block">'+head+lanes+'</div>';
|
|
226
|
+
}
|
|
227
|
+
// A run can be "stale" by its own heartbeat cadence (once per full loop
|
|
228
|
+
// iteration) while a lane inside it is still visibly, freshly active — the
|
|
229
|
+
// exact mismatch laneActivityHtml corrects for a single lane. Grouping here
|
|
230
|
+
// must ask the same question, or a run doing real work gets buried behind
|
|
231
|
+
// the same "stale" collapse it was just exempted from at the lane level.
|
|
232
|
+
function runHasFreshActivity(x,nowMs){
|
|
233
|
+
return x.lanes.some(l=>{
|
|
234
|
+
if(!('activity_at'in l))return false;
|
|
235
|
+
const at=Date.parse(l.activity_at);
|
|
236
|
+
return Number.isFinite(at)&&(nowMs-at)<=ACTIVITY_QUIET_THRESHOLD_MS;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// The thing changing belongs above the things that aren't: a run that's live
|
|
240
|
+
// (or quietly stale by heartbeat but still visibly working) renders full-width
|
|
241
|
+
// and prominent; a run with nothing fresh anywhere collapses behind a
|
|
242
|
+
// disclosure so zombie runs don't out-weigh the one doing real work.
|
|
243
|
+
function renderRunsHero(runs,nowMs){
|
|
244
|
+
const promoted=runs.filter(x=>x.liveness==='live'||runHasFreshActivity(x,nowMs));
|
|
245
|
+
const quiet=runs.filter(x=>!(x.liveness==='live'||runHasFreshActivity(x,nowMs)));
|
|
246
|
+
const promotedHtml=promoted.map(x=>renderRunBlock(x,nowMs)).join('');
|
|
247
|
+
const quietHtml=quiet.length?('<details class="stale-details"><summary>'+esc(quiet.length)+' quiet run'+(quiet.length>1?'s':'')+'</summary>'+quiet.map(x=>renderRunBlock(x,nowMs)).join('')+'</details>'):'';
|
|
248
|
+
if(!promotedHtml&&!quietHtml)return'';
|
|
249
|
+
return '<div class="hero"><h4>Live runs</h4>'+promotedHtml+quietHtml+'</div>';
|
|
250
|
+
}
|
|
251
|
+
function render(d){$('updated').textContent='updated '+new Date(d.generatedAt).toLocaleTimeString();const s=d.summary;const metrics=[['repositories',s.repositories],['events',s.events],['planned',s.planned],['executing',s.executing],['in review',s.inReview],['merged',s.merged],['open doors',s.doorsOpen]];$('summary').innerHTML=metrics.map(([k,v])=>'<div class="metric"><b>'+esc(v)+'</b><span>'+esc(k)+'</span></div>').join('');
|
|
252
|
+
const needs=[];for(const repo of d.repositories)for(const run of repo.board.panels.runs.data)for(const n of run.needs_you)needs.push({...n,repo:repo.name,run:run.run_id});$('attention').classList.toggle('show',needs.length>0);$('attentionRows').innerHTML=needs.map(n=>'<div class="attention-row"><span><span class="chip door">'+esc(n.kind)+'</span> '+esc(n.repo)+' · '+esc(n.unit_id||n.run)+'</span><code>'+esc(n.unblock)+'</code></div>').join('');
|
|
253
|
+
const active=[],quiet=[];
|
|
254
|
+
for(const repo of d.repositories){
|
|
255
|
+
const p=repo.board.panels;
|
|
256
|
+
const hasSignal=p.runs.data.length>0||p.doorsQueue.data.length>0||p.fleet.data.length>0||p.runAnalysis.data.length>0||Object.values(p.systemLanes.data).some(list=>list.length>0);
|
|
257
|
+
(hasSignal?active:quiet).push(repo);
|
|
258
|
+
}
|
|
259
|
+
$('repos').innerHTML=active.map(renderRepo).join('')||'<div class="empty">No repository has signal yet</div>';
|
|
260
|
+
$('quietRepos').innerHTML=quiet.length?('<h2>Quiet — '+quiet.length+' repositor'+(quiet.length>1?'ies':'y')+' with no unit / run / door / fleet signal</h2>'+quiet.map(renderQuietRepo).join('')):'';
|
|
261
|
+
}
|
|
262
|
+
function renderQuietRepo(r){return '<div class="quiet-row"><span class="badge '+esc(r.status)+'">'+esc(r.status)+'</span><b>'+esc(r.name)+'</b><span class="path" title="'+esc(r.root)+'">'+esc(r.root)+'</span><code>'+esc(r.events)+' events</code></div>';}
|
|
263
|
+
function renderRepo(r){const p=r.board.panels,units=Object.entries(p.systemLanes.data).flatMap(([component,list])=>list.map(u=>({...u,component}))),runs=p.runs.data,doors=p.doorsQueue.data,fleet=p.fleet.data,runAnalysis=p.runAnalysis.data,nowMs=Date.parse(r.board.generatedAt);
|
|
264
|
+
const hero=renderRunsHero(runs,nowMs);
|
|
265
|
+
return '<article class="repo"><div class="repo-head"><div><div class="eyebrow">'+esc(r.system)+'</div><h3>'+esc(r.name)+'</h3><div class="path" title="'+esc(r.root)+'">'+esc(r.root)+'</div></div><span class="badge '+esc(r.status)+'">'+esc(r.status)+' · '+esc(r.events)+' events</span></div>'+hero+'<div class="sections"><section class="section"><h4>Units</h4>'+(units.length?units.map(u=>'<div class="row"><span><code>'+esc(u.component)+'</code> · '+esc(u.id)+'</span><span class="state '+esc(u.state)+'">'+esc(u.state)+'</span></div>').join(''):'<div class="empty">No unit signal</div>')+'</section><section class="section"><h4>Doors</h4>'+(doors.length?doors.map(x=>'<div class="row"><span class="door">'+esc(x.unitId)+'</span><code>'+esc(x.classes.join(', '))+'</code></div>').join(''):'<div class="empty">No open doors</div>')+'</section><section class="section"><h4>Fleet</h4>'+(fleet.length?fleet.map(x=>'<div class="row"><span><code>'+esc(x.id)+'</code> · '+esc(x.model||'unknown')+'</span><span>$'+esc(Number(x.costUsd).toFixed(2))+'</span></div>').join(''):'<div class="empty">No session signal</div>')+'</section><section class="section"><h4>Run analysis</h4>'+(runAnalysis.length?runAnalysis.map(x=>'<div class="row"><span><code>'+esc(x.unitId)+'</code> · '+esc(x.rule)+'</span><span class="chip">'+esc(x.severity)+' ×'+esc(x.count)+'</span></div>').join(''):'<div class="empty">No run analysis yet</div>')+'</section></div></article>'}
|
|
266
|
+
// Pause freezes rendering (so you can select/copy text mid-stream) without dropping the
|
|
267
|
+
// connection — new pushes are buffered and counted, then applied in one shot on resume.
|
|
268
|
+
let paused=false,latestData=null,missed=0;
|
|
269
|
+
function updatePauseUi(){const c=$('connection');const btn=$('pauseBtn');if(paused){btn.textContent='▶ resume'+(missed?' ('+missed+' new)':'');btn.classList.add('active');c.classList.add('paused');c.textContent='paused';}else{btn.textContent='⏸ pause';btn.classList.remove('active');c.classList.remove('paused');}}
|
|
270
|
+
$('pauseBtn').addEventListener('click',()=>{paused=!paused;if(!paused){missed=0;if(latestData)render(latestData);c_setLive();}updatePauseUi();});
|
|
271
|
+
function c_setLive(){$('connection').textContent='live';}
|
|
272
|
+
const stream=new EventSource('/events');
|
|
273
|
+
stream.addEventListener('dashboard',e=>{latestData=JSON.parse(e.data);if(paused){missed++;updatePauseUi();}else{render(latestData);c_setLive();}});
|
|
274
|
+
stream.onerror=()=>{if(!paused)$('connection').textContent='reconnecting';};
|
|
275
|
+
fetch('/api/dashboard').then(r=>r.json()).then(d=>{latestData=d;render(d);});
|
|
276
|
+
</script></body></html>`;
|
|
277
|
+
//# sourceMappingURL=live.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness calibrate` — the weekly calibration job.
|
|
3
|
+
*
|
|
4
|
+
* harness calibrate [--ledger <dir>] [--baseline <baseline.json>]
|
|
5
|
+
* [--window 30] [--emit]
|
|
6
|
+
*
|
|
7
|
+
* Prints promote/demote/delete proposals, each with its evidence query. With
|
|
8
|
+
* --emit, appends them as calibration.proposed events. Below the volume floor
|
|
9
|
+
* it derates to monthly and proposes nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { emitProposals, proposeCalibration } from "./index.js";
|
|
13
|
+
function flag(args, name) {
|
|
14
|
+
const i = args.indexOf(`--${name}`);
|
|
15
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
|
|
16
|
+
}
|
|
17
|
+
export function calibrateCommand(args) {
|
|
18
|
+
const ledgerDir = flag(args, "ledger") ?? ".harness/ledger";
|
|
19
|
+
const baselinePath = flag(args, "baseline");
|
|
20
|
+
const baseline = baselinePath && existsSync(baselinePath)
|
|
21
|
+
? JSON.parse(readFileSync(baselinePath, "utf8"))
|
|
22
|
+
: undefined;
|
|
23
|
+
const report = proposeCalibration({
|
|
24
|
+
ledgerDir,
|
|
25
|
+
...(baseline ? { baseline } : {}),
|
|
26
|
+
windowDays: Number(flag(args, "window") ?? 30),
|
|
27
|
+
});
|
|
28
|
+
process.stdout.write(`cadence: ${report.cadence} (${report.envelopesInWindow} envelopes in ${report.windowDays}d)\n`);
|
|
29
|
+
if (report.insufficient) {
|
|
30
|
+
process.stdout.write(" insufficient data — no proposals\n");
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
for (const p of report.proposals) {
|
|
34
|
+
const move = p.from ? ` ${p.from} → ${p.to}` : "";
|
|
35
|
+
process.stdout.write(` ${p.action.toUpperCase()} ${p.target}${move} (${p.evidence})\n`);
|
|
36
|
+
}
|
|
37
|
+
if (report.proposals.length === 0)
|
|
38
|
+
process.stdout.write(" no changes proposed\n");
|
|
39
|
+
if (args.includes("--emit")) {
|
|
40
|
+
const n = emitProposals(report, ledgerDir);
|
|
41
|
+
process.stderr.write(`emitted ${n} calibration.proposed event(s)\n`);
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=cli.js.map
|