@ra3orblade/swarm 0.10.0 → 0.11.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/dist/swarmd.js CHANGED
@@ -68,6 +68,44 @@ function clearDaemonInfo() {
68
68
  function daemonCommand() {
69
69
  return resolveBin("swarmd");
70
70
  }
71
+ // packages/core/src/abtrial.ts
72
+ var ms = (a, b) => b ? Math.max(0, new Date(b).getTime() - new Date(a).getTime()) : null;
73
+ function splitArmTask(id) {
74
+ const i = id.lastIndexOf("#");
75
+ return i < 0 ? { task: id, arm: null } : { task: id.slice(0, i), arm: id.slice(i + 1) };
76
+ }
77
+ var armTask = (task, label) => `${task}#${label}`;
78
+ function scoreTrial(task, arms) {
79
+ const scored = arms.map((a) => {
80
+ const ineligibleFor = a.state === "running" ? "still running" : a.state === "failed" ? "crashed" : a.gatesFailed > 0 ? "failed a gate" : null;
81
+ return {
82
+ ...a,
83
+ wallMs: ms(a.startedAt, a.endedAt),
84
+ churn: a.insertions === null && a.deletions === null ? null : (a.insertions ?? 0) + (a.deletions ?? 0),
85
+ eligible: ineligibleFor === null,
86
+ ineligibleFor,
87
+ winner: false
88
+ };
89
+ });
90
+ const eligible = scored.filter((a) => a.eligible).sort((a, b) => a.costUsd - b.costUsd || (a.wallMs ?? Number.POSITIVE_INFINITY) - (b.wallMs ?? Number.POSITIVE_INFINITY) || a.label.localeCompare(b.label));
91
+ const best = eligible[0] ?? null;
92
+ if (best)
93
+ best.winner = true;
94
+ const finished = scored.filter((a) => a.state !== "running").length;
95
+ const dearest = eligible.at(-1);
96
+ return {
97
+ task,
98
+ arms: scored.sort((a, b) => Number(b.eligible) - Number(a.eligible) || a.costUsd - b.costUsd || a.label.localeCompare(b.label)),
99
+ winner: best?.label ?? null,
100
+ verdict: best ? "winner" : scored.some((a) => a.state === "running") ? "undecided" : "all-failed",
101
+ totals: {
102
+ arms: scored.length,
103
+ finished,
104
+ costUsd: scored.reduce((a, x) => a + x.costUsd, 0),
105
+ savedUsd: best && dearest ? Math.max(0, dearest.costUsd - best.costUsd) : 0
106
+ }
107
+ };
108
+ }
71
109
  // packages/core/src/actor.ts
72
110
  var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
73
111
  var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
@@ -691,6 +729,38 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
691
729
  payload.prompt = raw.prompt;
692
730
  return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
693
731
  }
732
+ // packages/core/src/art.ts
733
+ var ROBOT = [
734
+ " g g ",
735
+ " X X ",
736
+ " X X ",
737
+ " ddddddddddddd ",
738
+ " dXXXXXXXXXXXd ",
739
+ " dggXXXXXXXXXd ",
740
+ " dddXXddXXXddXXddd ",
741
+ " dddXXddXXXddXXddd ",
742
+ " dddXXXXXXXXXXXddd ",
743
+ " dddXXdddddddXXddd ",
744
+ " dXXXXXXXXXXXd ",
745
+ " dXXXXXXXXXXXd ",
746
+ " ddddddddddddd ",
747
+ " XXX ",
748
+ " ddddd ",
749
+ " ddddddddddd ",
750
+ " dXXdggXXXXXXXdXXd ",
751
+ " dXXdXggggXddddXXd ",
752
+ " dXXdXggggXXXXdXXd ",
753
+ " dXXdXXXXXXddddXXd ",
754
+ " dXXdXXXXXXXXXdXXd ",
755
+ " dXXdXdddXXXXXdXXd ",
756
+ " ddddXXXXXXXXXdddd ",
757
+ " ddddddddddd ",
758
+ " XX XX ",
759
+ " XX XX ",
760
+ " XXXX XXXX ",
761
+ " dddd dddd "
762
+ ];
763
+ var ROBOT_HEAD = ROBOT.slice(0, 13);
694
764
  // packages/core/src/audit.ts
695
765
  var AUDIT_TYPES = new Set([
696
766
  "session.started",
@@ -836,8 +906,8 @@ function sinceToIso(since, now = Date.now()) {
836
906
  const m = /^(\d+)([dhm])$/.exec(since.trim());
837
907
  if (m) {
838
908
  const n = Number(m[1]);
839
- const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
840
- return new Date(now - n * ms).toISOString();
909
+ const ms2 = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
910
+ return new Date(now - n * ms2).toISOString();
841
911
  }
842
912
  const t = Date.parse(since);
843
913
  return Number.isNaN(t) ? null : new Date(t).toISOString();
@@ -1231,6 +1301,238 @@ function loadConfigDetailed(opts = {}) {
1231
1301
  function loadConfig(opts = {}) {
1232
1302
  return loadConfigDetailed(opts).config;
1233
1303
  }
1304
+ // packages/core/src/context.ts
1305
+ var CHARS_PER_TOKEN = 4;
1306
+ var estTokens = (chars) => Math.round(chars / CHARS_PER_TOKEN);
1307
+ var CONTEXT_DEFAULTS = { worstLimit: 5, toolLimit: 12 };
1308
+ function rereadWaste(reads) {
1309
+ const byPath = new Map;
1310
+ for (const r of reads) {
1311
+ const hit = byPath.get(r.path);
1312
+ if (hit) {
1313
+ hit.reads++;
1314
+ hit.chars += r.chars;
1315
+ } else {
1316
+ byPath.set(r.path, { reads: 1, chars: r.chars, first: r.chars });
1317
+ }
1318
+ }
1319
+ const out = [];
1320
+ for (const [path, v] of byPath) {
1321
+ if (v.reads < 2)
1322
+ continue;
1323
+ out.push({ path, reads: v.reads, chars: v.chars, wastedChars: v.chars - v.first });
1324
+ }
1325
+ return out.sort((a, b) => b.wastedChars - a.wastedChars || a.path.localeCompare(b.path));
1326
+ }
1327
+ function contextReport(results, reads, turns, opts = {}) {
1328
+ const o = { ...CONTEXT_DEFAULTS, ...opts };
1329
+ const ids = new Set;
1330
+ for (const r of results)
1331
+ ids.add(r.sessionId);
1332
+ for (const r of reads)
1333
+ ids.add(r.sessionId);
1334
+ for (const t of turns)
1335
+ ids.add(t.sessionId);
1336
+ const readsBySession = new Map;
1337
+ for (const r of reads) {
1338
+ const list = readsBySession.get(r.sessionId);
1339
+ if (list)
1340
+ list.push(r);
1341
+ else
1342
+ readsBySession.set(r.sessionId, [r]);
1343
+ }
1344
+ const charsBySession = new Map;
1345
+ for (const r of results)
1346
+ charsBySession.set(r.sessionId, (charsBySession.get(r.sessionId) ?? 0) + r.chars);
1347
+ const tokensBySession = new Map;
1348
+ for (const t of turns) {
1349
+ const hit = tokensBySession.get(t.sessionId);
1350
+ if (hit) {
1351
+ hit.input += t.input;
1352
+ hit.cacheRead += t.cacheRead;
1353
+ hit.cacheWrite += t.cacheWrite;
1354
+ hit.thinking += t.thinking;
1355
+ hit.output += t.output;
1356
+ } else
1357
+ tokensBySession.set(t.sessionId, { ...t });
1358
+ }
1359
+ const sessions = [];
1360
+ for (const sessionId of ids) {
1361
+ const worstAll = rereadWaste(readsBySession.get(sessionId) ?? []);
1362
+ const wastedChars2 = worstAll.reduce((a, w) => a + w.wastedChars, 0);
1363
+ const toolChars2 = charsBySession.get(sessionId) ?? 0;
1364
+ const tk = tokensBySession.get(sessionId);
1365
+ const cacheable = (tk?.cacheRead ?? 0) + (tk?.input ?? 0);
1366
+ sessions.push({
1367
+ sessionId,
1368
+ toolChars: toolChars2,
1369
+ toolTokens: estTokens(toolChars2),
1370
+ thinking: tk?.thinking ?? 0,
1371
+ cacheRead: tk?.cacheRead ?? 0,
1372
+ input: tk?.input ?? 0,
1373
+ cacheHit: cacheable ? (tk?.cacheRead ?? 0) / cacheable : 0,
1374
+ reads: (readsBySession.get(sessionId) ?? []).length,
1375
+ rereadFiles: worstAll.length,
1376
+ wastedChars: wastedChars2,
1377
+ wasteShare: toolChars2 ? wastedChars2 / toolChars2 : 0,
1378
+ worst: worstAll.slice(0, o.worstLimit)
1379
+ });
1380
+ }
1381
+ sessions.sort((a, b) => b.wastedChars - a.wastedChars || b.toolChars - a.toolChars);
1382
+ const byToolMap = new Map;
1383
+ for (const r of results) {
1384
+ const hit = byToolMap.get(r.tool);
1385
+ if (hit) {
1386
+ hit.calls++;
1387
+ hit.chars += r.chars;
1388
+ } else
1389
+ byToolMap.set(r.tool, { tool: r.tool, calls: 1, chars: r.chars, tokens: 0 });
1390
+ }
1391
+ const byTool = [...byToolMap.values()].map((t) => ({ ...t, tokens: estTokens(t.chars) })).sort((a, b) => b.chars - a.chars || a.tool.localeCompare(b.tool)).slice(0, o.toolLimit);
1392
+ const toolChars = sessions.reduce((a, s) => a + s.toolChars, 0);
1393
+ const wastedChars = sessions.reduce((a, s) => a + s.wastedChars, 0);
1394
+ const cacheRead = sessions.reduce((a, s) => a + s.cacheRead, 0);
1395
+ const input = sessions.reduce((a, s) => a + s.input, 0);
1396
+ return {
1397
+ sessions,
1398
+ byTool,
1399
+ totals: {
1400
+ sessions: sessions.length,
1401
+ toolChars,
1402
+ toolTokens: estTokens(toolChars),
1403
+ wastedChars,
1404
+ wastedTokens: estTokens(wastedChars),
1405
+ wasteShare: toolChars ? wastedChars / toolChars : 0,
1406
+ rereadFiles: sessions.reduce((a, s) => a + s.rereadFiles, 0),
1407
+ cacheHit: cacheRead + input ? cacheRead / (cacheRead + input) : 0
1408
+ }
1409
+ };
1410
+ }
1411
+ // packages/core/src/dag.ts
1412
+ var DAG_DEFAULTS = { dx: 190, dy: 34, passes: 2 };
1413
+ var edgeKey = (from, to) => JSON.stringify([from, to]);
1414
+ function findBackEdges(nodes, edges) {
1415
+ const ids = nodes.map((n) => n.id);
1416
+ const known = new Set(ids);
1417
+ const out = new Map;
1418
+ for (const id of ids)
1419
+ out.set(id, []);
1420
+ for (const e of edges)
1421
+ if (known.has(e.from) && known.has(e.to) && e.from !== e.to)
1422
+ out.get(e.from)?.push(e.to);
1423
+ for (const [, list] of out)
1424
+ list.sort();
1425
+ const back = new Set;
1426
+ const state = new Map;
1427
+ const visit = (start) => {
1428
+ const stack = [{ id: start, i: 0 }];
1429
+ state.set(start, 1);
1430
+ while (stack.length) {
1431
+ const top = stack[stack.length - 1];
1432
+ const next = out.get(top.id) ?? [];
1433
+ if (top.i >= next.length) {
1434
+ state.set(top.id, 2);
1435
+ stack.pop();
1436
+ continue;
1437
+ }
1438
+ const to = next[top.i++];
1439
+ const st = state.get(to) ?? 0;
1440
+ if (st === 1)
1441
+ back.add(edgeKey(top.id, to));
1442
+ else if (st === 0) {
1443
+ state.set(to, 1);
1444
+ stack.push({ id: to, i: 0 });
1445
+ }
1446
+ }
1447
+ };
1448
+ for (const id of [...ids].sort())
1449
+ if ((state.get(id) ?? 0) === 0)
1450
+ visit(id);
1451
+ return back;
1452
+ }
1453
+ function assignLayers(nodes, forward) {
1454
+ const layer = new Map;
1455
+ const parents = new Map;
1456
+ for (const n of nodes)
1457
+ parents.set(n.id, []);
1458
+ for (const e of forward)
1459
+ parents.get(e.to)?.push(e.from);
1460
+ const resolving = new Set;
1461
+ const depth = (id) => {
1462
+ const fixed = nodes.find((n) => n.id === id)?.layer;
1463
+ if (typeof fixed === "number") {
1464
+ layer.set(id, fixed);
1465
+ return fixed;
1466
+ }
1467
+ const hit = layer.get(id);
1468
+ if (hit !== undefined)
1469
+ return hit;
1470
+ if (resolving.has(id))
1471
+ return 0;
1472
+ resolving.add(id);
1473
+ const ps = parents.get(id) ?? [];
1474
+ const d = ps.length ? Math.max(...ps.map((p) => depth(p) + 1)) : 0;
1475
+ resolving.delete(id);
1476
+ layer.set(id, d);
1477
+ return d;
1478
+ };
1479
+ for (const n of [...nodes].sort((a, b) => a.id.localeCompare(b.id)))
1480
+ depth(n.id);
1481
+ return layer;
1482
+ }
1483
+ function layoutDag(nodes, edges, opts = {}) {
1484
+ const o = { ...DAG_DEFAULTS, ...opts };
1485
+ if (!nodes.length)
1486
+ return { nodes: [], edges: [], width: 0, height: 0, layers: 0 };
1487
+ const known = new Set(nodes.map((n) => n.id));
1488
+ const clean = edges.filter((e) => known.has(e.from) && known.has(e.to) && e.from !== e.to);
1489
+ const back = findBackEdges(nodes, clean);
1490
+ const isBack = (e) => back.has(edgeKey(e.from, e.to));
1491
+ const forward = clean.filter((e) => !isBack(e));
1492
+ const layerOf = assignLayers(nodes, forward);
1493
+ const maxLayer = Math.max(0, ...nodes.map((n) => layerOf.get(n.id) ?? 0));
1494
+ const groups = Array.from({ length: maxLayer + 1 }, () => []);
1495
+ for (const n of [...nodes].sort((a, b) => {
1496
+ const sa = o.seed?.[a.id] ?? Number.POSITIVE_INFINITY;
1497
+ const sb = o.seed?.[b.id] ?? Number.POSITIVE_INFINITY;
1498
+ return sa - sb || a.id.localeCompare(b.id);
1499
+ }))
1500
+ groups[layerOf.get(n.id) ?? 0].push(n.id);
1501
+ const parentsOf = new Map;
1502
+ for (const n of nodes)
1503
+ parentsOf.set(n.id, []);
1504
+ for (const e of forward)
1505
+ parentsOf.get(e.to)?.push(e.from);
1506
+ for (let pass = 0;pass < o.passes; pass++) {
1507
+ for (let l = 1;l <= maxLayer; l++) {
1508
+ const prev = groups[l - 1];
1509
+ const idx = new Map(prev.map((id, i) => [id, i]));
1510
+ const group = groups[l];
1511
+ const bary = new Map;
1512
+ for (const id of group) {
1513
+ const ps = (parentsOf.get(id) ?? []).map((p) => idx.get(p)).filter((v) => v !== undefined);
1514
+ bary.set(id, ps.length ? ps.reduce((a, b) => a + b, 0) / ps.length : group.indexOf(id));
1515
+ }
1516
+ group.sort((a, b) => bary.get(a) - bary.get(b) || a.localeCompare(b));
1517
+ }
1518
+ }
1519
+ const widest = Math.max(1, ...groups.map((g) => g.length));
1520
+ const placed = [];
1521
+ groups.forEach((group, l) => {
1522
+ const offset = (widest - group.length) * o.dy / 2;
1523
+ group.forEach((id, i) => {
1524
+ placed.push({ id, layer: l, order: i, x: l * o.dx, y: offset + i * o.dy });
1525
+ });
1526
+ });
1527
+ placed.sort((a, b) => a.layer - b.layer || a.order - b.order);
1528
+ return {
1529
+ nodes: placed,
1530
+ edges: clean.map((e) => ({ ...e, back: isBack(e) })),
1531
+ width: maxLayer * o.dx,
1532
+ height: Math.max(0, (widest - 1) * o.dy),
1533
+ layers: maxLayer + 1
1534
+ };
1535
+ }
1234
1536
  // packages/core/src/dispatch.ts
1235
1537
  function planDispatch(tasks, wanted, opts) {
1236
1538
  const byId = new Map(tasks.map((t) => [t.id, t]));
@@ -1666,6 +1968,83 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
1666
1968
  return { title, body: b.join(`
1667
1969
  `) };
1668
1970
  }
1971
+ // packages/core/src/gatehealth.ts
1972
+ var ms2 = (iso) => new Date(iso).getTime();
1973
+ function percentile(xs, p) {
1974
+ if (!xs.length)
1975
+ return null;
1976
+ const s = [...xs].sort((a, b) => a - b);
1977
+ const rank = Math.max(1, Math.ceil(p / 100 * s.length));
1978
+ return s[Math.min(s.length, rank) - 1];
1979
+ }
1980
+ var GATE_HEALTH_DEFAULTS = { historyLimit: 20 };
1981
+ function gateHealth(samples, opts = {}) {
1982
+ const o = { ...GATE_HEALTH_DEFAULTS, ...opts };
1983
+ const byGate = new Map;
1984
+ for (const r of samples) {
1985
+ const list = byGate.get(r.gate);
1986
+ if (list)
1987
+ list.push(r);
1988
+ else
1989
+ byGate.set(r.gate, [r]);
1990
+ }
1991
+ const gates = [];
1992
+ for (const [gate, runsUnsorted] of byGate) {
1993
+ const runs = [...runsUnsorted].sort((a, b) => ms2(a.at) - ms2(b.at));
1994
+ const passes = runs.filter((r) => r.verdict === "pass").length;
1995
+ const byTask = new Map;
1996
+ for (const r of runs) {
1997
+ const key = `${r.projectId ?? ""}\x00${r.task}`;
1998
+ const list = byTask.get(key);
1999
+ if (list)
2000
+ list.push(r);
2001
+ else
2002
+ byTask.set(key, [r]);
2003
+ }
2004
+ let flips = 0;
2005
+ let flakyTasks = 0;
2006
+ for (const list of byTask.values()) {
2007
+ let changed = 0;
2008
+ for (let i = 1;i < list.length; i++)
2009
+ if (list[i].verdict !== list[i - 1].verdict)
2010
+ changed++;
2011
+ flips += changed;
2012
+ if (list.some((r) => r.verdict === "pass") && list.some((r) => r.verdict === "fail"))
2013
+ flakyTasks++;
2014
+ }
2015
+ const timed = runs.map((r) => r.durationMs).filter((d) => typeof d === "number");
2016
+ const last = runs.at(-1);
2017
+ gates.push({
2018
+ gate,
2019
+ runs: runs.length,
2020
+ passes,
2021
+ fails: runs.length - passes,
2022
+ passRate: passes / runs.length,
2023
+ flips,
2024
+ flakyTasks,
2025
+ flaky: flakyTasks > 0,
2026
+ p50Ms: percentile(timed, 50),
2027
+ p95Ms: percentile(timed, 95),
2028
+ maxMs: timed.length ? Math.max(...timed) : null,
2029
+ totalMs: timed.reduce((a, b) => a + b, 0),
2030
+ timedRuns: timed.length,
2031
+ lastVerdict: last?.verdict ?? null,
2032
+ lastAt: last?.at ?? null,
2033
+ history: runs.slice(-o.historyLimit).reverse().map((r) => ({ verdict: r.verdict, at: r.at, task: r.task, durationMs: r.durationMs }))
2034
+ });
2035
+ }
2036
+ gates.sort((a, b) => Number(b.flaky) - Number(a.flaky) || b.flakyTasks - a.flakyTasks || b.totalMs - a.totalMs);
2037
+ return {
2038
+ gates,
2039
+ totals: {
2040
+ gates: gates.length,
2041
+ runs: samples.length,
2042
+ fails: gates.reduce((a, g) => a + g.fails, 0),
2043
+ flakyGates: gates.filter((g) => g.flaky).length,
2044
+ totalMs: gates.reduce((a, g) => a + g.totalMs, 0)
2045
+ }
2046
+ };
2047
+ }
1669
2048
  // packages/core/src/gates.ts
1670
2049
  var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
1671
2050
  function validateGateRun(input) {
@@ -1725,7 +2104,8 @@ function executedGateInput(task, gate, cmd, outcome) {
1725
2104
  gate,
1726
2105
  verdict: outcome.exitCode === 0 && !outcome.timedOut ? "pass" : "fail",
1727
2106
  rubric: `ran \`${cmd}\` \u2014 ${how} in ${(outcome.durationMs / 1000).toFixed(1)}s`,
1728
- evidence: evidenceTail(outcome.output) || null
2107
+ evidence: evidenceTail(outcome.output) || null,
2108
+ durationMs: outcome.durationMs
1729
2109
  };
1730
2110
  }
1731
2111
  // packages/core/src/graphs.ts
@@ -1763,6 +2143,159 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
1763
2143
  contested: out.filter((f) => f.contested).length
1764
2144
  };
1765
2145
  }
2146
+ // packages/core/src/worktree.ts
2147
+ import { join as join3 } from "path";
2148
+ function planBootstrap(cfg, repoRoot, worktree) {
2149
+ const seen = new Set;
2150
+ const copies = [];
2151
+ for (const raw of cfg.worktree.copy) {
2152
+ if (!isRepoRelative(raw))
2153
+ continue;
2154
+ const rel = raw.trim().replace(/^\.\//, "");
2155
+ if (seen.has(rel))
2156
+ continue;
2157
+ seen.add(rel);
2158
+ copies.push({ rel, from: join3(repoRoot, rel), to: join3(worktree, rel) });
2159
+ }
2160
+ return { copies, setup: cfg.worktree.setup };
2161
+ }
2162
+ var needsBootstrap = (plan) => plan.copies.length > 0 || plan.setup !== null;
2163
+ function summarizeBootstrap(o) {
2164
+ const parts = [];
2165
+ if (o.copied.length)
2166
+ parts.push(`copied ${o.copied.join(", ")}`);
2167
+ if (o.skipped.length)
2168
+ parts.push(`skipped ${o.skipped.join(", ")} (missing)`);
2169
+ if (o.setup)
2170
+ parts.push(`${o.setup.command} \u2192 ${o.setup.exitCode === 0 ? "ok" : `exit ${o.setup.exitCode}`} in ${(o.setup.durationMs / 1000).toFixed(1)}s`);
2171
+ return parts.join("; ") || "nothing to do";
2172
+ }
2173
+ function canRemoveWorktree(w, heldByClaim, force) {
2174
+ if (w.main)
2175
+ return { ok: false, reason: "main" };
2176
+ if (heldByClaim)
2177
+ return { ok: false, reason: "held" };
2178
+ if (force)
2179
+ return { ok: true };
2180
+ if (w.dirty > 0)
2181
+ return { ok: false, reason: "dirty" };
2182
+ if (w.ahead > 0)
2183
+ return { ok: false, reason: "unpushed" };
2184
+ return { ok: true };
2185
+ }
2186
+ function removeRefusalMessage(reason, path, task) {
2187
+ switch (reason) {
2188
+ case "main":
2189
+ return `${path} is the main checkout \u2014 it is never removed`;
2190
+ case "held":
2191
+ return `${path} is held by claim ${task ?? "?"} \u2014 release the claim instead`;
2192
+ case "dirty":
2193
+ return `${path} has uncommitted changes \u2014 commit or stash them, or --force to discard`;
2194
+ case "unpushed":
2195
+ return `${path} has unpushed commits \u2014 push them, or --force to discard`;
2196
+ }
2197
+ }
2198
+ function planGc(worktrees, claims) {
2199
+ const held = new Map(claims.filter((c) => c.state === "held").map((c) => [c.worktree, c.task]));
2200
+ const stale = new Set(claims.filter((c) => c.state !== "held").map((c) => c.worktree));
2201
+ const out = [];
2202
+ for (const w of worktrees) {
2203
+ if (w.main || held.has(w.path))
2204
+ continue;
2205
+ const why = w.merged ? "merged" : stale.has(w.path) ? "released-claim" : null;
2206
+ if (!why)
2207
+ continue;
2208
+ const can = canRemoveWorktree(w, null, false);
2209
+ out.push({
2210
+ path: w.path,
2211
+ branch: w.branch,
2212
+ why,
2213
+ removable: can.ok,
2214
+ blocker: can.ok ? null : can.reason
2215
+ });
2216
+ }
2217
+ return out;
2218
+ }
2219
+
2220
+ // packages/core/src/hygiene.ts
2221
+ var HYGIENE_DEFAULTS = {
2222
+ staleDays: 7,
2223
+ abandonedDays: 30,
2224
+ hungryRssKb: 1024 * 1024,
2225
+ heavyKb: 2 * 1024 * 1024
2226
+ };
2227
+ var days2 = (ms3) => ms3 / 86400000;
2228
+ function classifyProcess(p, opts = {}) {
2229
+ const o = { ...HYGIENE_DEFAULTS, ...opts };
2230
+ if (!p.alive)
2231
+ return {
2232
+ ...p,
2233
+ issue: "dead",
2234
+ note: `pid ${p.pid} is gone but the registry still lists it${p.port ? ` holding port ${p.port}` : ""}`,
2235
+ reclaimable: true
2236
+ };
2237
+ if (p.sessionId && !p.sessionLive)
2238
+ return {
2239
+ ...p,
2240
+ issue: "orphaned",
2241
+ note: `still running after its session ended${p.port ? ` \u2014 port ${p.port} stays taken` : ""}`,
2242
+ reclaimable: true
2243
+ };
2244
+ if (p.rssKb !== null && p.rssKb >= o.hungryRssKb)
2245
+ return {
2246
+ ...p,
2247
+ issue: "hungry",
2248
+ note: `holding ${Math.round(p.rssKb / 1024)} MB of RSS`,
2249
+ reclaimable: false
2250
+ };
2251
+ return { ...p, issue: null, note: null, reclaimable: false };
2252
+ }
2253
+ function classifyWorktree(w, opts = {}) {
2254
+ const o = { ...HYGIENE_DEFAULTS, ...opts };
2255
+ const kb = w.diskKb ?? 0;
2256
+ const done = (issue, note, reclaimable) => ({
2257
+ ...w,
2258
+ issue,
2259
+ note,
2260
+ reclaimable,
2261
+ reclaimableKb: reclaimable ? kb : 0
2262
+ });
2263
+ if (w.main)
2264
+ return done(null, null, false);
2265
+ const removable = canRemoveWorktree(w, w.heldByClaim, false).ok;
2266
+ const inUse = w.liveSessions > 0 || w.heldByClaim !== null;
2267
+ const unsafe = w.dirty > 0 || w.ahead > 0;
2268
+ const idleDays = w.idleMs === null ? null : days2(w.idleMs);
2269
+ if (w.merged && !inUse && removable && idleDays !== null && idleDays >= o.staleDays)
2270
+ return done("stale", `merged and untouched for ${Math.floor(idleDays)} days${kb ? ` \u2014 ${Math.round(kb / 1024)} MB to reclaim` : ""}`, true);
2271
+ if (!w.merged && !inUse && idleDays !== null && idleDays >= o.abandonedDays)
2272
+ return done("abandoned", unsafe ? `untouched for ${Math.floor(idleDays)} days with ${w.dirty > 0 ? "uncommitted" : "unpushed"} work \u2014 review before removing` : `untouched for ${Math.floor(idleDays)} days and never merged`, false);
2273
+ if (kb >= o.heavyKb)
2274
+ return done("heavy", `${Math.round(kb / 1024)} MB on disk`, false);
2275
+ return done(null, null, false);
2276
+ }
2277
+ function hygieneReport(procs, worktrees, opts = {}) {
2278
+ const processes = procs.map((p) => classifyProcess(p, opts));
2279
+ const trees = worktrees.map((w) => classifyWorktree(w, opts));
2280
+ const issues = processes.filter((p) => p.issue).length + trees.filter((w) => w.issue).length;
2281
+ const rank = (i) => i ? 0 : 1;
2282
+ processes.sort((a, b) => rank(a.issue) - rank(b.issue) || (b.rssKb ?? 0) - (a.rssKb ?? 0));
2283
+ trees.sort((a, b) => rank(a.issue) - rank(b.issue) || (b.diskKb ?? 0) - (a.diskKb ?? 0));
2284
+ return {
2285
+ processes,
2286
+ worktrees: trees,
2287
+ totals: {
2288
+ processes: processes.length,
2289
+ deadProcesses: processes.filter((p) => p.issue === "dead").length,
2290
+ orphanedProcesses: processes.filter((p) => p.issue === "orphaned").length,
2291
+ worktrees: trees.length,
2292
+ staleWorktrees: trees.filter((w) => w.issue === "stale").length,
2293
+ diskKb: trees.reduce((a, w) => a + (w.diskKb ?? 0), 0),
2294
+ reclaimableKb: trees.reduce((a, w) => a + w.reclaimableKb, 0),
2295
+ issues
2296
+ }
2297
+ };
2298
+ }
1766
2299
  // packages/core/src/ledger.ts
1767
2300
  var DEFAULT_LEASE_MINUTES = 45;
1768
2301
  function isExpired(claim, now) {
@@ -1970,6 +2503,250 @@ function incidentKey(inc) {
1970
2503
  return `protected_ports:${portsIn(inc.command).join(",")}`;
1971
2504
  return inc.rule;
1972
2505
  }
2506
+ // packages/core/src/lineage.ts
2507
+ var LINEAGE_MAX_NODES = 40;
2508
+ var LINEAGE_FANOUT = 4;
2509
+ var groupId = (parent, kind) => `group:${parent}:${kind}`;
2510
+ function collapseFanout(sessions, edges, fanout, expanded) {
2511
+ const fans = new Map;
2512
+ for (const e of edges) {
2513
+ const k = groupId(e.from, e.kind);
2514
+ const list = fans.get(k) ?? [];
2515
+ list.push(e);
2516
+ fans.set(k, list);
2517
+ }
2518
+ const collapse = [...fans.entries()].filter(([id, list]) => list.length > fanout && !expanded.has(id));
2519
+ if (!collapse.length)
2520
+ return { sessions, edges, sizes: new Map };
2521
+ const hidden = new Set;
2522
+ const dropped = new Set;
2523
+ const extraSessions = [];
2524
+ const extraEdges = [];
2525
+ const sizes = new Map;
2526
+ for (const [id, list] of collapse) {
2527
+ const first = list[0];
2528
+ const parent = sessions.find((x) => x.id === first.from);
2529
+ sizes.set(id, list.length);
2530
+ for (const e of list) {
2531
+ dropped.add(e);
2532
+ hidden.add(e.to);
2533
+ }
2534
+ extraSessions.push({
2535
+ id,
2536
+ projectId: parent?.projectId ?? null,
2537
+ title: `${list.length} ${first.kind}${list.length === 1 ? "" : "s"}`,
2538
+ agent: parent?.agent ?? null,
2539
+ kind: "group",
2540
+ state: "ended",
2541
+ startedAt: first.at,
2542
+ endedAt: null,
2543
+ costUsd: null,
2544
+ outcome: null
2545
+ });
2546
+ extraEdges.push({ from: first.from, to: id, kind: first.kind, at: first.at });
2547
+ }
2548
+ const stillLinked = new Set;
2549
+ for (const e of edges) {
2550
+ if (dropped.has(e))
2551
+ continue;
2552
+ stillLinked.add(e.from);
2553
+ stillLinked.add(e.to);
2554
+ }
2555
+ const keptSessions = sessions.filter((x) => !hidden.has(x.id) || stillLinked.has(x.id));
2556
+ const keptEdges = edges.filter((e) => !dropped.has(e));
2557
+ return {
2558
+ sessions: [...keptSessions, ...extraSessions],
2559
+ edges: [...keptEdges, ...extraEdges],
2560
+ sizes
2561
+ };
2562
+ }
2563
+ function handoffEdges(holds) {
2564
+ const byTask = new Map;
2565
+ for (const h of holds) {
2566
+ if (!h.sessionId)
2567
+ continue;
2568
+ const key = `${h.projectId ?? ""}\x00${h.task}`;
2569
+ const list = byTask.get(key) ?? [];
2570
+ list.push({ sessionId: h.sessionId, at: h.at, task: h.task });
2571
+ byTask.set(key, list);
2572
+ }
2573
+ const out = [];
2574
+ for (const list of byTask.values()) {
2575
+ list.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime());
2576
+ for (let i = 1;i < list.length; i++) {
2577
+ const prev = list[i - 1];
2578
+ const cur = list[i];
2579
+ if (prev.sessionId === cur.sessionId)
2580
+ continue;
2581
+ out.push({
2582
+ from: prev.sessionId,
2583
+ to: cur.sessionId,
2584
+ kind: "handoff",
2585
+ at: cur.at,
2586
+ label: cur.task
2587
+ });
2588
+ }
2589
+ }
2590
+ return out;
2591
+ }
2592
+ function lineageGraph(sessions, edges, opts = {}) {
2593
+ const maxNodes = opts.maxNodes ?? LINEAGE_MAX_NODES;
2594
+ const known = new Map(sessions.map((s) => [s.id, s]));
2595
+ const seen = new Set;
2596
+ const usable = [];
2597
+ for (const e of edges) {
2598
+ if (!known.has(e.from) || !known.has(e.to) || e.from === e.to)
2599
+ continue;
2600
+ const key = JSON.stringify([e.from, e.to, e.kind]);
2601
+ if (seen.has(key))
2602
+ continue;
2603
+ seen.add(key);
2604
+ usable.push(e);
2605
+ }
2606
+ const collapsed = collapseFanout(sessions, usable, opts.fanout ?? LINEAGE_FANOUT, new Set(opts.expanded ?? []));
2607
+ const shownSessions = collapsed.sessions;
2608
+ const shownEdges = collapsed.edges;
2609
+ const groupSizes = collapsed.sizes;
2610
+ const byId = new Map(shownSessions.map((x) => [x.id, x]));
2611
+ const involved = new Set;
2612
+ for (const e of shownEdges) {
2613
+ involved.add(e.from);
2614
+ involved.add(e.to);
2615
+ }
2616
+ const all = shownSessions.filter((s) => involved.has(s.id));
2617
+ const deg0 = new Map;
2618
+ for (const e of shownEdges) {
2619
+ deg0.set(e.from, (deg0.get(e.from) ?? 0) + 1);
2620
+ deg0.set(e.to, (deg0.get(e.to) ?? 0) + 1);
2621
+ }
2622
+ const nodes = all.length <= maxNodes ? all : [...all].sort((a, b) => (deg0.get(b.id) ?? 0) - (deg0.get(a.id) ?? 0) || a.id.localeCompare(b.id)).slice(0, maxNodes);
2623
+ const truncated = all.length - nodes.length;
2624
+ if (!nodes.length)
2625
+ return {
2626
+ nodes: [],
2627
+ edges: [],
2628
+ width: 0,
2629
+ height: 0,
2630
+ roots: 0,
2631
+ byKind: { subagent: 0, dispatch: 0, message: 0, handoff: 0 },
2632
+ truncated: 0
2633
+ };
2634
+ const kept = new Set(nodes.map((s) => s.id));
2635
+ const shown = shownEdges.filter((e) => kept.has(e.from) && kept.has(e.to));
2636
+ const layout = layoutDag(nodes.map((s) => ({ id: s.id })), shown.map((e) => ({ from: e.from, to: e.to })), opts);
2637
+ const degree = new Map;
2638
+ for (const e of shown) {
2639
+ degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
2640
+ degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
2641
+ }
2642
+ const backSet = new Set(layout.edges.filter((e) => e.back).map((e) => JSON.stringify([e.from, e.to])));
2643
+ const byKind = {
2644
+ subagent: 0,
2645
+ dispatch: 0,
2646
+ message: 0,
2647
+ handoff: 0
2648
+ };
2649
+ for (const e of shown)
2650
+ byKind[e.kind]++;
2651
+ return {
2652
+ nodes: layout.nodes.map((p) => {
2653
+ const s = byId.get(p.id);
2654
+ const size = groupSizes.get(p.id) ?? null;
2655
+ const parts = size ? p.id.split(":") : null;
2656
+ return {
2657
+ ...s,
2658
+ layer: p.layer,
2659
+ order: p.order,
2660
+ x: p.x,
2661
+ y: p.y,
2662
+ degree: degree.get(p.id) ?? 0,
2663
+ groupSize: size,
2664
+ groupOf: parts ? parts.slice(1, -1).join(":") ?? null : null,
2665
+ groupKind: parts ? parts.at(-1) ?? null : null
2666
+ };
2667
+ }),
2668
+ edges: shown.map((e) => ({ ...e, back: backSet.has(JSON.stringify([e.from, e.to])) })),
2669
+ width: layout.width,
2670
+ height: layout.height,
2671
+ roots: layout.nodes.filter((p) => p.layer === 0).length,
2672
+ byKind,
2673
+ truncated
2674
+ };
2675
+ }
2676
+ // packages/core/src/mcphealth.ts
2677
+ var BUILTIN = "builtin";
2678
+ var MCP_HEALTH_DEFAULTS = { toolLimit: 6 };
2679
+ function serverOf(tool) {
2680
+ const m = /^mcp__([^_]+(?:_[^_]+)*?)__/.exec(tool);
2681
+ return m?.[1] ?? BUILTIN;
2682
+ }
2683
+ function toolOf(tool) {
2684
+ return tool.replace(/^mcp__[^_]+(?:_[^_]+)*?__/, "");
2685
+ }
2686
+ var stat = (tool, calls) => {
2687
+ const timed = calls.map((c) => c.ms).filter((m) => typeof m === "number");
2688
+ return {
2689
+ tool,
2690
+ calls: calls.length,
2691
+ errors: calls.filter((c) => c.errored).length,
2692
+ p50Ms: percentile(timed, 50),
2693
+ p95Ms: percentile(timed, 95),
2694
+ maxMs: timed.length ? Math.max(...timed) : null,
2695
+ totalMs: timed.reduce((a, b) => a + b, 0)
2696
+ };
2697
+ };
2698
+ function mcpHealth(calls, opts = {}) {
2699
+ const o = { ...MCP_HEALTH_DEFAULTS, ...opts };
2700
+ const byServer = new Map;
2701
+ for (const c of calls) {
2702
+ const s = serverOf(c.tool);
2703
+ const list = byServer.get(s);
2704
+ if (list)
2705
+ list.push(c);
2706
+ else
2707
+ byServer.set(s, [c]);
2708
+ }
2709
+ const servers = [];
2710
+ for (const [server, list] of byServer) {
2711
+ const byTool = new Map;
2712
+ for (const c of list) {
2713
+ const t = toolOf(c.tool);
2714
+ const tl = byTool.get(t);
2715
+ if (tl)
2716
+ tl.push(c);
2717
+ else
2718
+ byTool.set(t, [c]);
2719
+ }
2720
+ const tools = [...byTool.entries()].map(([t, cs]) => stat(t, cs)).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool)).slice(0, o.toolLimit);
2721
+ const all = stat(server, list);
2722
+ servers.push({
2723
+ server,
2724
+ mcp: server !== BUILTIN,
2725
+ calls: all.calls,
2726
+ errors: all.errors,
2727
+ errorRate: all.calls ? all.errors / all.calls : 0,
2728
+ unanswered: list.filter((c) => c.ms === null).length,
2729
+ sessions: new Set(list.map((c) => c.sessionId)).size,
2730
+ p50Ms: all.p50Ms,
2731
+ p95Ms: all.p95Ms,
2732
+ maxMs: all.maxMs,
2733
+ totalMs: all.totalMs,
2734
+ tools,
2735
+ lastAt: list.map((c) => c.at).sort().at(-1) ?? null
2736
+ });
2737
+ }
2738
+ servers.sort((a, b) => Number(b.mcp) - Number(a.mcp) || b.totalMs - a.totalMs || b.calls - a.calls);
2739
+ return {
2740
+ servers,
2741
+ totals: {
2742
+ servers: servers.filter((s) => s.mcp).length,
2743
+ calls: calls.length,
2744
+ errors: servers.reduce((a, s) => a + s.errors, 0),
2745
+ totalMs: servers.reduce((a, s) => a + s.totalMs, 0),
2746
+ mcpMs: servers.filter((s) => s.mcp).reduce((a, s) => a + s.totalMs, 0)
2747
+ }
2748
+ };
2749
+ }
1973
2750
  // packages/core/src/memory.ts
1974
2751
  var MEMORY_KINDS = ["handoff", "incident", "gate", "session"];
1975
2752
  function handoffDoc(projectId, id, h, sessionId) {
@@ -2087,11 +2864,11 @@ function parseTo(to) {
2087
2864
  return { kind: "session", id: t };
2088
2865
  return { kind: "task", task: t };
2089
2866
  }
2090
- function formatMessages(ms) {
2091
- if (!ms.length)
2867
+ function formatMessages(ms3) {
2868
+ if (!ms3.length)
2092
2869
  return null;
2093
- const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
2094
- return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
2870
+ const lines = ms3.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
2871
+ return `[swarm] While you were working, message${ms3.length === 1 ? "" : "s"} arrived:
2095
2872
  ${lines.join(`
2096
2873
  `)}
2097
2874
  Reply with swarm_send if a reply is expected.`;
@@ -2366,6 +3143,147 @@ function projectIdentity(opts) {
2366
3143
  const name = parts[parts.length - 1] ?? opts.root;
2367
3144
  return { id: `p_${fnv1a(key)}`, root: opts.root, commonDir: opts.commonDir, name };
2368
3145
  }
3146
+ // packages/core/src/provenance.ts
3147
+ var SEVERITY = {
3148
+ "no-task": 0,
3149
+ "open-pr": 1,
3150
+ "no-pr": 2,
3151
+ "no-branch": 3,
3152
+ "no-session": 4,
3153
+ unclaimed: 5,
3154
+ complete: 6
3155
+ };
3156
+ var severity = (b) => SEVERITY[b ?? "complete"];
3157
+ function breakOf(l) {
3158
+ if (!l.task)
3159
+ return "no-task";
3160
+ if (!l.claim)
3161
+ return "unclaimed";
3162
+ if (!l.session)
3163
+ return "no-session";
3164
+ if (!l.branch)
3165
+ return "no-branch";
3166
+ if (!l.pr)
3167
+ return "no-pr";
3168
+ if (!l.merged)
3169
+ return "open-pr";
3170
+ return null;
3171
+ }
3172
+ function provenance(tasks, claims, sessions, branches) {
3173
+ const claimsByTask = new Map;
3174
+ for (const c of claims) {
3175
+ const list = claimsByTask.get(c.task);
3176
+ if (list)
3177
+ list.push(c);
3178
+ else
3179
+ claimsByTask.set(c.task, [c]);
3180
+ }
3181
+ const branchRow = new Map(branches.map((b) => [b.branch, b]));
3182
+ const sessionsByBranch = new Map;
3183
+ for (const s of sessions) {
3184
+ if (!s.branch)
3185
+ continue;
3186
+ const list = sessionsByBranch.get(s.branch);
3187
+ if (list)
3188
+ list.push(s);
3189
+ else
3190
+ sessionsByBranch.set(s.branch, [s]);
3191
+ }
3192
+ const chains = [];
3193
+ for (const t of tasks) {
3194
+ const cs = (claimsByTask.get(t.id) ?? []).sort((a, b) => new Date(a.acquiredAt).getTime() - new Date(b.acquiredAt).getTime());
3195
+ const branch = cs.map((c) => c.branch).find((b) => !!b) ?? null;
3196
+ const worktree = cs.map((c) => c.worktree).find((w) => !!w) ?? null;
3197
+ const row = branch ? branchRow.get(branch) : undefined;
3198
+ const byId = new Map;
3199
+ for (const s of branch ? sessionsByBranch.get(branch) ?? [] : [])
3200
+ byId.set(s.id, s);
3201
+ for (const c of cs) {
3202
+ if (!c.sessionId || byId.has(c.sessionId))
3203
+ continue;
3204
+ const hit = sessions.find((s) => s.id === c.sessionId);
3205
+ if (hit)
3206
+ byId.set(hit.id, hit);
3207
+ }
3208
+ const sess = [...byId.values()];
3209
+ const links = {
3210
+ task: true,
3211
+ claim: cs.length > 0,
3212
+ session: sess.length > 0,
3213
+ branch: !!branch,
3214
+ pr: !!row?.prNumber,
3215
+ merged: row?.outcome === "merged"
3216
+ };
3217
+ chains.push({
3218
+ task: t.id,
3219
+ fromTask: true,
3220
+ title: t.title,
3221
+ status: t.status,
3222
+ url: t.url ?? null,
3223
+ holders: [...new Set(cs.map((c) => c.owner).filter((o) => !!o))],
3224
+ claimedAt: cs[0]?.acquiredAt ?? null,
3225
+ worktree,
3226
+ branch,
3227
+ sessions: sess,
3228
+ costUsd: row?.costUsd ?? sess.reduce((a, s) => a + (s.costUsd ?? 0), 0),
3229
+ prNumber: row?.prNumber ?? null,
3230
+ prUrl: row?.url ?? null,
3231
+ outcome: row?.outcome ?? null,
3232
+ mergedAt: row?.mergedAt ?? null,
3233
+ leadHours: row?.leadHours ?? null,
3234
+ links,
3235
+ depth: Object.values(links).filter(Boolean).length,
3236
+ brokenAt: breakOf(links)
3237
+ });
3238
+ }
3239
+ const explained = new Set(chains.map((c) => c.branch).filter((b) => !!b));
3240
+ for (const b of branches) {
3241
+ if (explained.has(b.branch))
3242
+ continue;
3243
+ const links = {
3244
+ task: false,
3245
+ claim: false,
3246
+ session: b.sessions.length > 0,
3247
+ branch: true,
3248
+ pr: !!b.prNumber,
3249
+ merged: b.outcome === "merged"
3250
+ };
3251
+ chains.push({
3252
+ task: b.branch,
3253
+ fromTask: false,
3254
+ title: b.title ?? b.branch,
3255
+ status: b.outcome,
3256
+ url: null,
3257
+ holders: [],
3258
+ claimedAt: null,
3259
+ worktree: null,
3260
+ branch: b.branch,
3261
+ sessions: b.sessions.map((id) => sessions.find((s) => s.id === id)).filter((s) => !!s),
3262
+ costUsd: b.costUsd,
3263
+ prNumber: b.prNumber,
3264
+ prUrl: b.url,
3265
+ outcome: b.outcome,
3266
+ mergedAt: b.mergedAt,
3267
+ leadHours: b.leadHours,
3268
+ links,
3269
+ depth: Object.values(links).filter(Boolean).length,
3270
+ brokenAt: "no-task"
3271
+ });
3272
+ }
3273
+ chains.sort((a, b) => severity(a.brokenAt) - severity(b.brokenAt) || b.costUsd - a.costUsd || a.task.localeCompare(b.task));
3274
+ return {
3275
+ chains,
3276
+ totals: {
3277
+ tasks: chains.length,
3278
+ complete: chains.filter((c) => c.brokenAt === null).length,
3279
+ broken: chains.filter((c) => c.brokenAt !== null).length,
3280
+ unclaimed: chains.filter((c) => c.brokenAt === "unclaimed").length,
3281
+ noPr: chains.filter((c) => c.brokenAt === "no-pr").length,
3282
+ untracked: chains.filter((c) => c.brokenAt === "no-task").length,
3283
+ costUsd: chains.reduce((a, c) => a + c.costUsd, 0)
3284
+ }
3285
+ };
3286
+ }
2369
3287
  // packages/core/src/questions.ts
2370
3288
  function validateQuestion(text, options) {
2371
3289
  const t = typeof text === "string" ? text.trim() : "";
@@ -2753,88 +3671,128 @@ function clusterProjectKey(remoteUrl) {
2753
3671
  return null;
2754
3672
  return `${host}/${m[2]}`;
2755
3673
  }
2756
- // packages/core/src/worktree.ts
2757
- import { join as join3 } from "path";
2758
- function planBootstrap(cfg, repoRoot, worktree) {
2759
- const seen = new Set;
2760
- const copies = [];
2761
- for (const raw of cfg.worktree.copy) {
2762
- if (!isRepoRelative(raw))
2763
- continue;
2764
- const rel = raw.trim().replace(/^\.\//, "");
2765
- if (seen.has(rel))
2766
- continue;
2767
- seen.add(rel);
2768
- copies.push({ rel, from: join3(repoRoot, rel), to: join3(worktree, rel) });
2769
- }
2770
- return { copies, setup: cfg.worktree.setup };
2771
- }
2772
- var needsBootstrap = (plan) => plan.copies.length > 0 || plan.setup !== null;
2773
- function summarizeBootstrap(o) {
2774
- const parts = [];
2775
- if (o.copied.length)
2776
- parts.push(`copied ${o.copied.join(", ")}`);
2777
- if (o.skipped.length)
2778
- parts.push(`skipped ${o.skipped.join(", ")} (missing)`);
2779
- if (o.setup)
2780
- parts.push(`${o.setup.command} \u2192 ${o.setup.exitCode === 0 ? "ok" : `exit ${o.setup.exitCode}`} in ${(o.setup.durationMs / 1000).toFixed(1)}s`);
2781
- return parts.join("; ") || "nothing to do";
2782
- }
2783
- function canRemoveWorktree(w, heldByClaim, force) {
2784
- if (w.main)
2785
- return { ok: false, reason: "main" };
2786
- if (heldByClaim)
2787
- return { ok: false, reason: "held" };
2788
- if (force)
2789
- return { ok: true };
2790
- if (w.dirty > 0)
2791
- return { ok: false, reason: "dirty" };
2792
- if (w.ahead > 0)
2793
- return { ok: false, reason: "unpushed" };
2794
- return { ok: true };
2795
- }
2796
- function removeRefusalMessage(reason, path, task) {
2797
- switch (reason) {
2798
- case "main":
2799
- return `${path} is the main checkout \u2014 it is never removed`;
2800
- case "held":
2801
- return `${path} is held by claim ${task ?? "?"} \u2014 release the claim instead`;
2802
- case "dirty":
2803
- return `${path} has uncommitted changes \u2014 commit or stash them, or --force to discard`;
2804
- case "unpushed":
2805
- return `${path} has unpushed commits \u2014 push them, or --force to discard`;
2806
- }
3674
+ // packages/core/src/waiting.ts
3675
+ var emptyByKind = () => ({
3676
+ permission: { episodes: 0, blockedMs: 0 },
3677
+ question: { episodes: 0, blockedMs: 0 },
3678
+ notification: { episodes: 0, blockedMs: 0 }
3679
+ });
3680
+ var ms3 = (iso) => new Date(iso).getTime();
3681
+ function median2(xs) {
3682
+ if (!xs.length)
3683
+ return 0;
3684
+ const s = [...xs].sort((a, b) => a - b);
3685
+ const mid = s.length >> 1;
3686
+ return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
2807
3687
  }
2808
- function planGc(worktrees, claims) {
2809
- const held = new Map(claims.filter((c) => c.state === "held").map((c) => [c.worktree, c.task]));
2810
- const stale = new Set(claims.filter((c) => c.state !== "held").map((c) => c.worktree));
3688
+ function pairWaits(samples, now, sessionEnds = {}) {
3689
+ const nowMs = ms3(now);
3690
+ const open = new Map;
2811
3691
  const out = [];
2812
- for (const w of worktrees) {
2813
- if (w.main || held.has(w.path))
3692
+ const id = (s) => `${s.sessionId}\x00${s.kind}\x00${s.key}`;
3693
+ const rank = (p) => p === "start" ? 0 : 1;
3694
+ const ordered = [...samples].sort((a, b) => ms3(a.ts) - ms3(b.ts) || rank(a.phase) - rank(b.phase));
3695
+ for (const s of ordered) {
3696
+ const k = id(s);
3697
+ if (s.phase === "start") {
3698
+ if (!open.has(k))
3699
+ open.set(k, s);
2814
3700
  continue;
2815
- const why = w.merged ? "merged" : stale.has(w.path) ? "released-claim" : null;
2816
- if (!why)
3701
+ }
3702
+ const start = open.get(k);
3703
+ if (!start)
2817
3704
  continue;
2818
- const can = canRemoveWorktree(w, null, false);
3705
+ open.delete(k);
2819
3706
  out.push({
2820
- path: w.path,
2821
- branch: w.branch,
2822
- why,
2823
- removable: can.ok,
2824
- blocker: can.ok ? null : can.reason
3707
+ sessionId: start.sessionId,
3708
+ projectId: start.projectId ?? null,
3709
+ kind: start.kind,
3710
+ key: start.key,
3711
+ startedAt: start.ts,
3712
+ endedAt: s.ts,
3713
+ ms: Math.max(0, ms3(s.ts) - ms3(start.ts)),
3714
+ open: false,
3715
+ label: start.label ?? s.label ?? null
2825
3716
  });
2826
3717
  }
2827
- return out;
3718
+ for (const start of open.values()) {
3719
+ const ended = sessionEnds[start.sessionId];
3720
+ const until = ended ? Math.min(ms3(ended), nowMs) : nowMs;
3721
+ out.push({
3722
+ sessionId: start.sessionId,
3723
+ projectId: start.projectId ?? null,
3724
+ kind: start.kind,
3725
+ key: start.key,
3726
+ startedAt: start.ts,
3727
+ endedAt: ended ?? null,
3728
+ ms: Math.max(0, until - ms3(start.ts)),
3729
+ open: !ended,
3730
+ label: start.label ?? null
3731
+ });
3732
+ }
3733
+ return out.sort((a, b) => ms3(a.startedAt) - ms3(b.startedAt));
3734
+ }
3735
+ function waitingReport(episodes) {
3736
+ const bySession = new Map;
3737
+ for (const e of episodes) {
3738
+ const list = bySession.get(e.sessionId);
3739
+ if (list)
3740
+ list.push(e);
3741
+ else
3742
+ bySession.set(e.sessionId, [e]);
3743
+ }
3744
+ const sessions = [];
3745
+ for (const [sessionId, list] of bySession) {
3746
+ const byKind = emptyByKind();
3747
+ for (const e of list) {
3748
+ const k = byKind[e.kind];
3749
+ k.episodes++;
3750
+ k.blockedMs += e.ms;
3751
+ }
3752
+ const openEp = list.filter((e) => e.open).sort((a, b) => ms3(a.startedAt) - ms3(b.startedAt))[0];
3753
+ const durations = list.map((e) => e.ms);
3754
+ sessions.push({
3755
+ sessionId,
3756
+ projectId: list[0]?.projectId ?? null,
3757
+ episodes: list.length,
3758
+ blockedMs: durations.reduce((a, b) => a + b, 0),
3759
+ longestMs: durations.length ? Math.max(...durations) : 0,
3760
+ medianMs: median2(durations),
3761
+ openSince: openEp?.startedAt ?? null,
3762
+ openKind: openEp?.kind ?? null,
3763
+ openLabel: openEp?.label ?? null,
3764
+ byKind
3765
+ });
3766
+ }
3767
+ sessions.sort((a, b) => b.blockedMs - a.blockedMs);
3768
+ const all = episodes.map((e) => e.ms);
3769
+ const totalsByKind = emptyByKind();
3770
+ for (const e of episodes) {
3771
+ const k = totalsByKind[e.kind];
3772
+ k.episodes++;
3773
+ k.blockedMs += e.ms;
3774
+ }
3775
+ return {
3776
+ sessions,
3777
+ totals: {
3778
+ episodes: episodes.length,
3779
+ blockedMs: all.reduce((a, b) => a + b, 0),
3780
+ medianMs: median2(all),
3781
+ longestMs: all.length ? Math.max(...all) : 0,
3782
+ waitingNow: sessions.filter((s) => s.openSince).length,
3783
+ byKind: totalsByKind
3784
+ }
3785
+ };
2828
3786
  }
2829
3787
  // packages/daemon/src/app.ts
2830
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
3788
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
2831
3789
  import { homedir as homedir4 } from "os";
2832
3790
  import { dirname as dirname4, join as join10 } from "path";
2833
3791
  import { fileURLToPath as fileURLToPath2 } from "url";
2834
3792
 
2835
3793
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
2836
3794
  var compose = (middleware, onError, onNotFound) => {
2837
- return (context, next) => {
3795
+ return (context2, next) => {
2838
3796
  let index = -1;
2839
3797
  return dispatch2(0);
2840
3798
  async function dispatch2(i) {
@@ -2847,31 +3805,31 @@ var compose = (middleware, onError, onNotFound) => {
2847
3805
  let handler;
2848
3806
  if (middleware[i]) {
2849
3807
  handler = middleware[i][0][0];
2850
- context.req.routeIndex = i;
3808
+ context2.req.routeIndex = i;
2851
3809
  } else {
2852
3810
  handler = i === middleware.length && next || undefined;
2853
3811
  }
2854
3812
  if (handler) {
2855
3813
  try {
2856
- res = await handler(context, () => dispatch2(i + 1));
3814
+ res = await handler(context2, () => dispatch2(i + 1));
2857
3815
  } catch (err) {
2858
3816
  if (err instanceof Error && onError) {
2859
- context.error = err;
2860
- res = await onError(err, context);
3817
+ context2.error = err;
3818
+ res = await onError(err, context2);
2861
3819
  isError = true;
2862
3820
  } else {
2863
3821
  throw err;
2864
3822
  }
2865
3823
  }
2866
3824
  } else {
2867
- if (context.finalized === false && onNotFound) {
2868
- res = await onNotFound(context);
3825
+ if (context2.finalized === false && onNotFound) {
3826
+ res = await onNotFound(context2);
2869
3827
  }
2870
3828
  }
2871
- if (res && (context.finalized === false || isError)) {
2872
- context.res = res;
3829
+ if (res && (context2.finalized === false || isError)) {
3830
+ context2.res = res;
2873
3831
  }
2874
- return context;
3832
+ return context2;
2875
3833
  }
2876
3834
  };
2877
3835
  };
@@ -3292,7 +4250,7 @@ var raw = (value, callbacks) => {
3292
4250
  escapedString.callbacks = callbacks;
3293
4251
  return escapedString;
3294
4252
  };
3295
- var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
4253
+ var resolveCallback = async (str, phase, preserveCallbacks, context2, buffer) => {
3296
4254
  if (typeof str === "object" && !(str instanceof String)) {
3297
4255
  if (!(str instanceof Promise)) {
3298
4256
  str = str.toString();
@@ -3310,7 +4268,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
3310
4268
  } else {
3311
4269
  buffer = [str];
3312
4270
  }
3313
- const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))).then(() => buffer[0]));
4271
+ const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context: context2 }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context2, buffer))).then(() => buffer[0]));
3314
4272
  if (preserveCallbacks) {
3315
4273
  return raw(await resStr, callbacks);
3316
4274
  } else {
@@ -3709,11 +4667,11 @@ var Hono = class _Hono {
3709
4667
  const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
3710
4668
  return (async () => {
3711
4669
  try {
3712
- const context = await composed(c);
3713
- if (!context.finalized) {
4670
+ const context2 = await composed(c);
4671
+ if (!context2.finalized) {
3714
4672
  throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
3715
4673
  }
3716
- return context.res;
4674
+ return context2.res;
3717
4675
  } catch (err) {
3718
4676
  return this.#handleError(err, c);
3719
4677
  }
@@ -3786,7 +4744,7 @@ var Node = class _Node {
3786
4744
  #index;
3787
4745
  #varIndex;
3788
4746
  #children = /* @__PURE__ */ Object.create(null);
3789
- insert(tokens, index, paramMap, context, isStatic) {
4747
+ insert(tokens, index, paramMap, context2, isStatic) {
3790
4748
  let node = this;
3791
4749
  for (let i = 0, len = tokens.length;i < len; i++) {
3792
4750
  const token = tokens[i];
@@ -3819,7 +4777,7 @@ var Node = class _Node {
3819
4777
  nextNode = node.#children[regexpStr] = new _Node;
3820
4778
  }
3821
4779
  if (name !== "") {
3822
- nextNode.#varIndex ??= context.varIndex++;
4780
+ nextNode.#varIndex ??= context2.varIndex++;
3823
4781
  paramMap.push([name, nextNode.#varIndex]);
3824
4782
  }
3825
4783
  } else {
@@ -4344,8 +5302,8 @@ var StreamingApi = class {
4344
5302
  `);
4345
5303
  return this;
4346
5304
  }
4347
- sleep(ms) {
4348
- return new Promise((res) => setTimeout(res, ms));
5305
+ sleep(ms4) {
5306
+ return new Promise((res) => setTimeout(res, ms4));
4349
5307
  }
4350
5308
  async close() {
4351
5309
  this.closed = true;
@@ -4599,6 +5557,7 @@ class Dispatcher {
4599
5557
  task: t.id,
4600
5558
  runId: r.run.id,
4601
5559
  worktree: r.run.worktree,
5560
+ by: this.opts.get(projectId)?.owner ?? null,
4602
5561
  summary: `dispatch ${t.id} \u2192 run ${r.run.id}`
4603
5562
  }
4604
5563
  });
@@ -4760,6 +5719,16 @@ class ForgeService {
4760
5719
  }));
4761
5720
  }
4762
5721
  outcomeCache = new Map;
5722
+ outcomeInflight = new Map;
5723
+ mergedCached(projectId, root) {
5724
+ const hit = this.outcomeCache.get(projectId);
5725
+ const fresh = !!hit && Date.now() - hit.at < 600000;
5726
+ if (!fresh && !this.outcomeInflight.has(projectId)) {
5727
+ const run2 = this.merged(projectId, root).finally(() => this.outcomeInflight.delete(projectId));
5728
+ this.outcomeInflight.set(projectId, run2);
5729
+ }
5730
+ return { merged: hit?.merged ?? [], reverted: hit?.reverted ?? [], fresh };
5731
+ }
4763
5732
  async merged(projectId, root) {
4764
5733
  const hit = this.outcomeCache.get(projectId);
4765
5734
  if (hit && Date.now() - hit.at < 600000)
@@ -5676,7 +6645,7 @@ CREATE TABLE IF NOT EXISTS processes (
5676
6645
  CREATE INDEX IF NOT EXISTS processes_live ON processes(ended_at, project_id);
5677
6646
  CREATE TABLE IF NOT EXISTS gates (
5678
6647
  id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, gate TEXT, verdict TEXT,
5679
- rubric TEXT, evidence TEXT, session_id TEXT, created_at TEXT
6648
+ rubric TEXT, evidence TEXT, session_id TEXT, duration_ms INTEGER, created_at TEXT
5680
6649
  );
5681
6650
  CREATE INDEX IF NOT EXISTS gates_task ON gates(project_id, task, created_at);
5682
6651
  CREATE TABLE IF NOT EXISTS handoffs (
@@ -5819,7 +6788,7 @@ class Store {
5819
6788
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
5820
6789
  }
5821
6790
  }
5822
- static SCHEMA_VERSION = 1;
6791
+ static SCHEMA_VERSION = 2;
5823
6792
  schemaVersion() {
5824
6793
  return Number(this.meta("schema_version") ?? 0);
5825
6794
  }
@@ -5857,6 +6826,16 @@ class Store {
5857
6826
  fill("incident_acks", "'dashboard'", null, "acks");
5858
6827
  fill("sessions", "NULL", "id", "sessions");
5859
6828
  fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
6829
+ },
6830
+ (db) => {
6831
+ this.ensureColumn("gates", "duration_ms", "INTEGER");
6832
+ const rows = db.query("SELECT id, rubric FROM gates WHERE duration_ms IS NULL AND rubric LIKE '%s'").all();
6833
+ const upd = db.query("UPDATE gates SET duration_ms = ? WHERE id = ?");
6834
+ for (const r of rows) {
6835
+ const m = /\bin ([0-9]+(?:\.[0-9]+)?)s$/.exec(r.rubric ?? "");
6836
+ if (m)
6837
+ upd.run(Math.round(Number(m[1]) * 1000), r.id);
6838
+ }
5860
6839
  }
5861
6840
  ];
5862
6841
  for (let v = this.schemaVersion();v < steps.length; v++) {
@@ -6364,10 +7343,10 @@ class Store {
6364
7343
  OR (to_kind = 'task' AND project_id = ? AND task IS ?)
6365
7344
  OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
6366
7345
  ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
6367
- const ms = rows.map(rowToMessage);
6368
- if (ms.length && !opts.peek)
6369
- this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms.map((m) => m.id));
6370
- return ms;
7346
+ const ms4 = rows.map(rowToMessage);
7347
+ if (ms4.length && !opts.peek)
7348
+ this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms4.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms4.map((m) => m.id));
7349
+ return ms4;
6371
7350
  }
6372
7351
  markMessageDelivered(id, sessionId) {
6373
7352
  this.db.query("UPDATE messages SET delivered_at = ?, session_id = COALESCE(?, session_id) WHERE id = ? AND delivered_at IS NULL").run(new Date().toISOString(), sessionId, id);
@@ -6422,6 +7401,7 @@ class Store {
6422
7401
  rubric: r.rubric,
6423
7402
  evidence: r.evidence ?? null,
6424
7403
  sessionId: r.session_id ?? null,
7404
+ durationMs: r.duration_ms ?? null,
6425
7405
  createdAt: r.created_at
6426
7406
  };
6427
7407
  }
@@ -6572,10 +7552,10 @@ class Store {
6572
7552
  };
6573
7553
  const done = (async () => {
6574
7554
  let diffText = "";
6575
- let stat = "";
7555
+ let stat2 = "";
6576
7556
  try {
6577
7557
  const diff = await worktreeDiff(p.root, where.worktree);
6578
- stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
7558
+ stat2 = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
6579
7559
  `);
6580
7560
  diffText = await worktreePatch(where.worktree, diff.base);
6581
7561
  } catch (e) {
@@ -6597,7 +7577,7 @@ class Store {
6597
7577
  task,
6598
7578
  title: taskRow?.title ?? null,
6599
7579
  branch: w?.branch ?? null,
6600
- stat,
7580
+ stat: stat2,
6601
7581
  patch: diffText
6602
7582
  });
6603
7583
  writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
@@ -6755,8 +7735,8 @@ ${err}
6755
7735
  return v;
6756
7736
  const createdAt = new Date().toISOString();
6757
7737
  const sessionId = this.knownSession(input.sessionId);
6758
- const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
6759
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
7738
+ const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, duration_ms, created_at, actor_kind, actor_id)
7739
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, typeof input.durationMs === "number" ? Math.max(0, Math.round(input.durationMs)) : null, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
6760
7740
  const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
6761
7741
  this.remember(gateDoc(projectId, run2.id, run2, sessionId));
6762
7742
  this.append({
@@ -7338,9 +8318,9 @@ ${p2.reason ?? ""}`.trim(),
7338
8318
  const rows = this.db.query(`SELECT * FROM (SELECT ${WIRE_COLS} FROM events WHERE ${where.join(" AND ")} ORDER BY seq DESC LIMIT ?) ORDER BY seq`).all(...args, limit);
7339
8319
  return rows.map((r) => auditRow(wireRowToEvent(r)));
7340
8320
  }
7341
- prune(days2) {
8321
+ prune(days3) {
7342
8322
  const cfg = this.policyFor(null).config;
7343
- const chatter = days2 ?? cfg.events.retain_days;
8323
+ const chatter = days3 ?? cfg.events.retain_days;
7344
8324
  const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
7345
8325
  let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
7346
8326
  if (cfg.audit.retain_days > 0) {
@@ -7869,7 +8849,8 @@ ${p2.reason ?? ""}`.trim(),
7869
8849
  acquiredAt: r.acquired_at,
7870
8850
  expiresAt: r.expires_at,
7871
8851
  releasedAt: r.released_at ?? null,
7872
- state: r.state
8852
+ state: r.state,
8853
+ sessionId: r.actor_kind === "agent" ? r.actor_id ?? null : null
7873
8854
  }));
7874
8855
  const now = Date.now();
7875
8856
  for (const c of rows)
@@ -7997,6 +8978,436 @@ ${p2.reason ?? ""}`.trim(),
7997
8978
  expiresAt: r.expires_at
7998
8979
  }));
7999
8980
  }
8981
+ waiting(projectId, days3 = 7) {
8982
+ const since = new Date(Date.now() - days3 * 86400000).toISOString();
8983
+ const pArgs = projectId ? [projectId] : [];
8984
+ const paired = this.db.query(`SELECT type, session_id, project_id, ts,
8985
+ COALESCE(json_extract(payload,'$.requestId'), json_extract(payload,'$.id')) AS key,
8986
+ COALESCE(json_extract(payload,'$.tool'), json_extract(payload,'$.text')) AS label
8987
+ FROM events
8988
+ WHERE type IN ('permission.requested','permission.resolved','question.asked','question.answered')
8989
+ AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
8990
+ const notes = this.db.query(`SELECT n.seq, n.session_id, n.project_id, n.ts,
8991
+ json_extract(n.payload,'$.summary') AS label,
8992
+ (SELECT MIN(a.ts) FROM events a
8993
+ WHERE a.session_id = n.session_id AND a.seq > n.seq
8994
+ AND a.type IN ('prompt.submitted','tool.requested')) AS resumed
8995
+ FROM events n
8996
+ WHERE n.type = 'session.notification' AND n.ts >= ?${projectId ? " AND n.project_id = ?" : ""}`).all(since, ...pArgs);
8997
+ const samples = [];
8998
+ for (const r of paired) {
8999
+ if (!r.session_id || r.key === null)
9000
+ continue;
9001
+ const kind = r.type.startsWith("permission") ? "permission" : "question";
9002
+ samples.push({
9003
+ sessionId: r.session_id,
9004
+ projectId: r.project_id,
9005
+ kind,
9006
+ key: String(r.key),
9007
+ phase: r.type.endsWith(".requested") || r.type.endsWith(".asked") ? "start" : "end",
9008
+ ts: r.ts,
9009
+ ...r.label ? { label: r.label.slice(0, 120) } : {}
9010
+ });
9011
+ }
9012
+ for (const n of notes) {
9013
+ if (!n.session_id)
9014
+ continue;
9015
+ const key = String(n.seq);
9016
+ samples.push({
9017
+ sessionId: n.session_id,
9018
+ projectId: n.project_id,
9019
+ kind: "notification",
9020
+ key,
9021
+ phase: "start",
9022
+ ts: n.ts,
9023
+ ...n.label ? { label: n.label.slice(0, 120) } : {}
9024
+ });
9025
+ if (n.resumed)
9026
+ samples.push({
9027
+ sessionId: n.session_id,
9028
+ projectId: n.project_id,
9029
+ kind: "notification",
9030
+ key,
9031
+ phase: "end",
9032
+ ts: n.resumed
9033
+ });
9034
+ }
9035
+ const ends = {};
9036
+ for (const r of this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all())
9037
+ ends[r.id] = r.ended_at;
9038
+ const report = waitingReport(pairWaits(samples, new Date().toISOString(), ends));
9039
+ const meta = new Map(this.db.query("SELECT id, title, agent, project_id FROM sessions").all().map((r) => [r.id, r]));
9040
+ return {
9041
+ ...report,
9042
+ sessions: report.sessions.map((s) => ({
9043
+ ...s,
9044
+ title: meta.get(s.sessionId)?.title ?? null,
9045
+ agent: meta.get(s.sessionId)?.agent ?? "claude-code",
9046
+ projectId: s.projectId ?? meta.get(s.sessionId)?.project_id ?? null
9047
+ }))
9048
+ };
9049
+ }
9050
+ gateHealth(projectId, days3 = 30) {
9051
+ const since = new Date(Date.now() - days3 * 86400000).toISOString();
9052
+ const rows = this.db.query(`SELECT project_id, task, gate, verdict, duration_ms, created_at FROM gates
9053
+ WHERE created_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
9054
+ return gateHealth(rows.filter((r) => r.verdict === "pass" || r.verdict === "fail").map((r) => ({
9055
+ projectId: r.project_id,
9056
+ task: r.task,
9057
+ gate: r.gate,
9058
+ verdict: r.verdict,
9059
+ durationMs: r.duration_ms,
9060
+ at: r.created_at
9061
+ })));
9062
+ }
9063
+ static worktreeIdleMs(path) {
9064
+ let newest = 0;
9065
+ for (const f of [path, join8(path, ".git")]) {
9066
+ try {
9067
+ newest = Math.max(newest, statSync(f).mtimeMs);
9068
+ } catch {}
9069
+ }
9070
+ return newest ? Math.max(0, Date.now() - newest) : null;
9071
+ }
9072
+ duCache = new Map;
9073
+ duInflight = null;
9074
+ refreshDisk(paths, ttlMs) {
9075
+ if (this.duInflight)
9076
+ return;
9077
+ const stale = paths.filter((p) => {
9078
+ const hit = this.duCache.get(p);
9079
+ return !hit || Date.now() - hit.t >= ttlMs;
9080
+ });
9081
+ if (!stale.length)
9082
+ return;
9083
+ this.duInflight = (async () => {
9084
+ for (const path of stale) {
9085
+ let v = null;
9086
+ try {
9087
+ const proc = Bun.spawn(["du", "-sk", "-x", path], { stdout: "pipe", stderr: "ignore" });
9088
+ const out = await new Response(proc.stdout).text();
9089
+ if (await proc.exited === 0) {
9090
+ const n = Number.parseInt(out.trim().split(/\s+/)[0] ?? "", 10);
9091
+ if (Number.isFinite(n))
9092
+ v = n;
9093
+ }
9094
+ } catch {}
9095
+ this.duCache.set(path, { v, t: Date.now() });
9096
+ }
9097
+ })().finally(() => {
9098
+ this.duInflight = null;
9099
+ });
9100
+ }
9101
+ hygiene(projectId, diskTtlMs = 600000) {
9102
+ const rows = this.db.query(`SELECT * FROM processes WHERE ended_at IS NULL${projectId ? " AND project_id = ?" : ""}
9103
+ ORDER BY started_at DESC`).all(...projectId ? [projectId] : []).map((r) => this.rowToProcess(r));
9104
+ const usage = new Map;
9105
+ if (rows.length) {
9106
+ try {
9107
+ const out = Bun.spawnSync([
9108
+ "ps",
9109
+ "-o",
9110
+ "pid=,pcpu=,rss=",
9111
+ "-p",
9112
+ rows.map((r) => r.pid).join(",")
9113
+ ]);
9114
+ for (const line of new TextDecoder().decode(out.stdout).split(`
9115
+ `)) {
9116
+ const [pid, cpu, rss] = line.trim().split(/\s+/);
9117
+ if (pid)
9118
+ usage.set(Number(pid), { cpu: Number(cpu) || 0, rss: Number(rss) || 0 });
9119
+ }
9120
+ } catch {}
9121
+ }
9122
+ const liveSessionIds = new Set(this.db.query("SELECT id FROM sessions WHERE ended_at IS NULL AND state IN ('active','waiting')").all().map((r) => r.id));
9123
+ const procs = rows.map((p) => {
9124
+ const u = usage.get(p.pid);
9125
+ return {
9126
+ pid: p.pid,
9127
+ name: p.name,
9128
+ kind: p.kind,
9129
+ projectId: p.projectId,
9130
+ sessionId: p.sessionId,
9131
+ port: p.port,
9132
+ startedAt: p.startedAt,
9133
+ alive: this.processIsOurs(p),
9134
+ sessionLive: !p.sessionId || liveSessionIds.has(p.sessionId),
9135
+ cpuPct: u ? u.cpu : null,
9136
+ rssKb: u ? u.rss : null
9137
+ };
9138
+ });
9139
+ const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
9140
+ const claims = this.claims().filter((c) => c.state !== "released");
9141
+ const trees = [];
9142
+ for (const pid of projects)
9143
+ for (const w of this.worktrees(pid)) {
9144
+ const held = claims.find((c) => c.worktree === w.path);
9145
+ trees.push({
9146
+ projectId: pid,
9147
+ path: w.path,
9148
+ branch: w.branch,
9149
+ main: w.main,
9150
+ dirty: w.dirty,
9151
+ ahead: w.ahead,
9152
+ merged: w.merged,
9153
+ idleMs: Store.worktreeIdleMs(w.path),
9154
+ diskKb: this.duCache.get(w.path)?.v ?? null,
9155
+ heldByClaim: held?.task ?? null,
9156
+ liveSessions: this.sessions().filter((s) => s.cwd?.startsWith(w.path) && !s.endedAt && s.state !== "ended").length
9157
+ });
9158
+ }
9159
+ this.refreshDisk(trees.map((t) => t.path), diskTtlMs);
9160
+ return hygieneReport(procs, trees);
9161
+ }
9162
+ lineage(projectId, days3 = 14, expanded = []) {
9163
+ const since = new Date(Date.now() - days3 * 86400000).toISOString();
9164
+ const pArgs = projectId ? [projectId] : [];
9165
+ const rows = this.db.query(`SELECT id, project_id, title, agent, kind, state, parent_id, started_at, ended_at
9166
+ FROM sessions WHERE last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
9167
+ const cost = new Map(this.db.query("SELECT session_id, SUM(cost_usd) AS c FROM turns WHERE ts >= ? GROUP BY session_id").all(since).map((r) => [r.session_id, r.c]));
9168
+ const sessions = rows.map((r) => ({
9169
+ id: r.id,
9170
+ projectId: r.project_id,
9171
+ title: r.title,
9172
+ agent: r.agent ?? "claude-code",
9173
+ kind: r.kind,
9174
+ state: r.state,
9175
+ startedAt: r.started_at,
9176
+ endedAt: r.ended_at,
9177
+ costUsd: cost.get(r.id) ?? null,
9178
+ outcome: null
9179
+ }));
9180
+ const known = new Set(sessions.map((s) => s.id));
9181
+ const edges = [];
9182
+ for (const a of this.db.query(`SELECT t.session_id, t.agent_id, MIN(t.ts) AS first_ts, MAX(t.ts) AS last_ts,
9183
+ SUM(t.cost_usd) AS cost, COUNT(*) AS turns
9184
+ FROM turns t WHERE t.agent_id IS NOT NULL AND t.ts >= ?
9185
+ GROUP BY t.session_id, t.agent_id`).all(since)) {
9186
+ const parent = sessions.find((x) => x.id === a.session_id);
9187
+ if (!parent)
9188
+ continue;
9189
+ const id = `sub:${a.session_id}:${a.agent_id}`;
9190
+ sessions.push({
9191
+ id,
9192
+ projectId: parent.projectId,
9193
+ title: `subagent ${a.agent_id.slice(0, 8)} \xB7 ${a.turns} turn${a.turns === 1 ? "" : "s"}`,
9194
+ agent: parent.agent,
9195
+ kind: "subagent",
9196
+ state: "ended",
9197
+ startedAt: a.first_ts,
9198
+ endedAt: a.last_ts,
9199
+ costUsd: a.cost,
9200
+ outcome: null
9201
+ });
9202
+ known.add(id);
9203
+ edges.push({ from: a.session_id, to: id, kind: "subagent", at: a.first_ts });
9204
+ }
9205
+ for (const d of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.by') AS by, json_extract(payload,'$.task') AS task
9206
+ FROM events WHERE type = 'dispatch.started' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs)) {
9207
+ if (!d.session_id || !d.by)
9208
+ continue;
9209
+ const a = actorFrom(d.by, null);
9210
+ if (a.kind === "agent" && known.has(a.id) && a.id !== d.session_id)
9211
+ edges.push({ from: a.id, to: d.session_id, kind: "dispatch", at: d.ts, label: d.task });
9212
+ }
9213
+ for (const m of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.recipient') AS to_session,
9214
+ json_extract(payload,'$.text') AS text
9215
+ FROM events WHERE type = 'message.sent' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs))
9216
+ if (m.session_id && m.to_session)
9217
+ edges.push({
9218
+ from: m.session_id,
9219
+ to: m.to_session,
9220
+ kind: "message",
9221
+ at: m.ts,
9222
+ label: m.text?.slice(0, 80) ?? null
9223
+ });
9224
+ const holds = this.db.query(`SELECT task, project_id, actor_kind, actor_id, acquired_at FROM claims
9225
+ WHERE acquired_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs).map((c) => ({
9226
+ task: c.task,
9227
+ projectId: c.project_id,
9228
+ sessionId: c.actor_kind === "agent" ? c.actor_id : null,
9229
+ at: c.acquired_at
9230
+ }));
9231
+ edges.push(...handoffEdges(holds));
9232
+ return lineageGraph(sessions, edges, { expanded });
9233
+ }
9234
+ mcpHealth(projectId, days3 = 7) {
9235
+ const since = new Date(Date.now() - days3 * 86400000).toISOString();
9236
+ const rows = this.db.query(`SELECT session_id, seq, ts, type,
9237
+ json_extract(payload,'$.tool') AS tool,
9238
+ json_extract(payload,'$.toolResponse') AS response
9239
+ FROM events
9240
+ WHERE type IN ('tool.requested','tool.completed') AND ts >= ?
9241
+ AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
9242
+ ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
9243
+ const calls = [];
9244
+ const pending = new Map;
9245
+ const abandon = (sessionId, tool, at) => {
9246
+ calls.push({ sessionId, tool, ms: null, errored: false, at });
9247
+ };
9248
+ for (const r of rows) {
9249
+ if (!r.session_id)
9250
+ continue;
9251
+ const perSession = pending.get(r.session_id) ?? new Map;
9252
+ pending.set(r.session_id, perSession);
9253
+ if (r.type === "tool.requested") {
9254
+ const open = perSession.get(r.tool);
9255
+ if (open)
9256
+ abandon(r.session_id, r.tool, open);
9257
+ perSession.set(r.tool, r.ts);
9258
+ continue;
9259
+ }
9260
+ const startedAt = perSession.get(r.tool);
9261
+ perSession.delete(r.tool);
9262
+ calls.push({
9263
+ sessionId: r.session_id,
9264
+ tool: r.tool,
9265
+ ms: startedAt ? Math.max(0, new Date(r.ts).getTime() - new Date(startedAt).getTime()) : null,
9266
+ errored: toolResponseErrored(safeJson(r.response)),
9267
+ at: r.ts
9268
+ });
9269
+ }
9270
+ for (const [sessionId, perSession] of pending)
9271
+ for (const [tool, at] of perSession)
9272
+ abandon(sessionId, tool, at);
9273
+ return mcpHealth(calls);
9274
+ }
9275
+ context(projectId, days3 = 7) {
9276
+ const since = new Date(Date.now() - days3 * 86400000).toISOString();
9277
+ const pArgs = projectId ? [projectId] : [];
9278
+ const where = projectId ? " AND project_id = ?" : "";
9279
+ const results = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
9280
+ LENGTH(json_extract(payload,'$.toolResponse')) AS chars
9281
+ FROM events
9282
+ WHERE type = 'tool.completed' AND ts >= ?${where}
9283
+ AND json_extract(payload,'$.tool') IS NOT NULL
9284
+ AND json_extract(payload,'$.toolResponse') IS NOT NULL`).all(since, ...pArgs).filter((r) => r.session_id).map((r) => ({ sessionId: r.session_id, tool: r.tool, chars: r.chars ?? 0 }));
9285
+ const reads = this.db.query(`SELECT req.session_id,
9286
+ json_extract(req.payload,'$.toolInput.file_path') AS path,
9287
+ (SELECT LENGTH(json_extract(done.payload,'$.toolResponse')) FROM events done
9288
+ WHERE done.session_id = req.session_id AND done.seq > req.seq
9289
+ AND done.type = 'tool.completed'
9290
+ AND json_extract(done.payload,'$.tool') = 'Read'
9291
+ ORDER BY done.seq LIMIT 1) AS chars
9292
+ FROM events req
9293
+ WHERE req.type = 'tool.requested' AND json_extract(req.payload,'$.tool') = 'Read'
9294
+ AND req.ts >= ?${where.replace("project_id", "req.project_id")}
9295
+ AND json_extract(req.payload,'$.toolInput.file_path') IS NOT NULL`).all(since, ...pArgs).filter((r) => r.session_id).map((r) => ({ sessionId: r.session_id, path: r.path, chars: r.chars ?? 0 }));
9296
+ const turns = this.db.query(`SELECT t.session_id, SUM(t.input) AS input, SUM(t.cache_read) AS cache_read,
9297
+ SUM(t.cache_write) AS cache_write, SUM(t.thinking) AS thinking, SUM(t.output) AS output
9298
+ FROM turns t${projectId ? " JOIN sessions s ON s.id = t.session_id" : ""}
9299
+ WHERE t.ts >= ?${projectId ? " AND s.project_id = ?" : ""}
9300
+ GROUP BY t.session_id`).all(since, ...pArgs).map((r) => ({
9301
+ sessionId: r.session_id,
9302
+ input: r.input ?? 0,
9303
+ cacheRead: r.cache_read ?? 0,
9304
+ cacheWrite: r.cache_write ?? 0,
9305
+ thinking: r.thinking ?? 0,
9306
+ output: r.output ?? 0
9307
+ }));
9308
+ const report = contextReport(results, reads, turns);
9309
+ const meta = new Map(this.db.query("SELECT id, title, agent FROM sessions").all().map((r) => [r.id, r]));
9310
+ return {
9311
+ ...report,
9312
+ sessions: report.sessions.map((s) => ({
9313
+ ...s,
9314
+ title: meta.get(s.sessionId)?.title ?? null,
9315
+ agent: meta.get(s.sessionId)?.agent ?? "claude-code"
9316
+ }))
9317
+ };
9318
+ }
9319
+ abTrials(projectId) {
9320
+ const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
9321
+ const out = [];
9322
+ for (const pid of projects) {
9323
+ const tasks2 = [
9324
+ ...new Set(this.claims(pid).map((c) => splitArmTask(c.task)).filter((x) => x.arm).map((x) => x.task))
9325
+ ];
9326
+ for (const t of tasks2.sort())
9327
+ out.push({ ...this.abTrial(pid, t), projectId: pid });
9328
+ }
9329
+ const rank = { undecided: 0, "all-failed": 1, winner: 2 };
9330
+ return out.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.totals.costUsd - a.totals.costUsd);
9331
+ }
9332
+ diffCache = new Map;
9333
+ diffInflight = null;
9334
+ refreshDiffs(worktrees, base, ttlMs = 30000) {
9335
+ if (this.diffInflight)
9336
+ return;
9337
+ const stale = worktrees.filter((w) => {
9338
+ const hit = this.diffCache.get(w);
9339
+ return !hit || Date.now() - hit.t >= ttlMs;
9340
+ });
9341
+ if (!stale.length)
9342
+ return;
9343
+ this.diffInflight = (async () => {
9344
+ for (const wt of stale) {
9345
+ let v = null;
9346
+ try {
9347
+ const proc = Bun.spawn(["git", "diff", "--shortstat", `${base}...HEAD`], {
9348
+ cwd: wt,
9349
+ stdout: "pipe",
9350
+ stderr: "ignore"
9351
+ });
9352
+ const out = await new Response(proc.stdout).text();
9353
+ if (await proc.exited === 0) {
9354
+ const f = /(\d+) files? changed/.exec(out)?.[1];
9355
+ const i = /(\d+) insertions?/.exec(out)?.[1];
9356
+ const d = /(\d+) deletions?/.exec(out)?.[1];
9357
+ v = [Number(f ?? 0), Number(i ?? 0), Number(d ?? 0)];
9358
+ }
9359
+ } catch {}
9360
+ this.diffCache.set(wt, { v, t: Date.now() });
9361
+ }
9362
+ })().finally(() => {
9363
+ this.diffInflight = null;
9364
+ });
9365
+ }
9366
+ abTrial(projectId, task) {
9367
+ const claims = this.claims(projectId).filter((c) => splitArmTask(c.task).task === task);
9368
+ const all = this.sessions();
9369
+ const sessions = new Map(all.map((s) => [s.id, s]));
9370
+ const byCwd = new Map;
9371
+ for (const s of all)
9372
+ if (s.cwd && !byCwd.has(s.cwd))
9373
+ byCwd.set(s.cwd, s);
9374
+ const gateRuns = this.gateRuns(projectId, undefined, 2000);
9375
+ const arms = [];
9376
+ for (const c of claims) {
9377
+ const label = splitArmTask(c.task).arm;
9378
+ if (!label)
9379
+ continue;
9380
+ const sess = (c.worktree ? byCwd.get(c.worktree) : undefined) ?? (c.sessionId ? sessions.get(c.sessionId) : undefined);
9381
+ const sid = sess?.id ?? c.sessionId;
9382
+ const g = gateRuns.filter((x) => x.task === c.task);
9383
+ const latest = new Map;
9384
+ for (const run2 of [...g].sort((a, b) => a.createdAt < b.createdAt ? -1 : 1))
9385
+ latest.set(run2.gate, run2);
9386
+ const verdicts = [...latest.values()];
9387
+ const diff = c.worktree ? this.diffCache.get(c.worktree)?.v : null;
9388
+ arms.push({
9389
+ label,
9390
+ task: c.task,
9391
+ model: sess?.model ?? null,
9392
+ agent: sess?.agent ?? "claude-code",
9393
+ sessionId: sid,
9394
+ worktree: c.worktree || null,
9395
+ startedAt: c.acquiredAt,
9396
+ endedAt: sess?.endedAt ?? null,
9397
+ state: sess && sess.state !== "ended" ? "running" : sess ? "done" : c.state === "held" ? "running" : "failed",
9398
+ costUsd: sess?.costUsd ?? 0,
9399
+ turns: sess?.turns ?? 0,
9400
+ gatesPassed: verdicts.filter((v) => v.verdict === "pass").length,
9401
+ gatesFailed: verdicts.filter((v) => v.verdict === "fail").length,
9402
+ filesChanged: diff ? diff[0] : null,
9403
+ insertions: diff ? diff[1] : null,
9404
+ deletions: diff ? diff[2] : null
9405
+ });
9406
+ }
9407
+ const base = this.worktrees(projectId).find((w) => w.main)?.branch ?? "main";
9408
+ this.refreshDiffs(arms.map((a) => a.worktree).filter((w) => !!w), base);
9409
+ return scoreTrial(task, arms);
9410
+ }
8000
9411
  collisions(projectId) {
8001
9412
  const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
8002
9413
  const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
@@ -8771,6 +10182,16 @@ ${p2.reason ?? ""}`.trim(),
8771
10182
  WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
8772
10183
  return r.n;
8773
10184
  }
10185
+ openIncidentsByProject() {
10186
+ const rows = this.db.query(`SELECT e.project_id AS pid, COUNT(*) AS n FROM events e
10187
+ LEFT JOIN incident_acks a ON a.seq = e.seq
10188
+ WHERE e.type = 'incident.opened' AND a.seq IS NULL AND e.project_id IS NOT NULL
10189
+ GROUP BY e.project_id`).all();
10190
+ const out = {};
10191
+ for (const r of rows)
10192
+ out[r.pid] = r.n;
10193
+ return out;
10194
+ }
8774
10195
  ackIncident(seq, by) {
8775
10196
  const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
8776
10197
  if (!row)
@@ -9094,14 +10515,14 @@ ${p2.reason ?? ""}`.trim(),
9094
10515
  const from = localDayIso(-13);
9095
10516
  const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
9096
10517
  FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
9097
- const days2 = [];
10518
+ const days3 = [];
9098
10519
  for (let i = 13;i >= 0; i--)
9099
- days2.push(localDayIso(-i).slice(0, 10));
10520
+ days3.push(localDayIso(-i).slice(0, 10));
9100
10521
  const out = {};
9101
10522
  for (const r of rows) {
9102
10523
  out[r.pid] ??= new Array(14).fill(0);
9103
10524
  const arr = out[r.pid];
9104
- const i = days2.indexOf(r.day);
10525
+ const i = days3.indexOf(r.day);
9105
10526
  if (i >= 0)
9106
10527
  arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
9107
10528
  }
@@ -9122,12 +10543,22 @@ ${p2.reason ?? ""}`.trim(),
9122
10543
  processes: this.memoised("processes", 5000, () => this.processes()),
9123
10544
  incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
9124
10545
  openIncidents: this.memoised("openIncidents", 30000, () => this.openIncidents()),
10546
+ openIncidentsByProject: this.memoised("openIncidentsByProject", 30000, () => this.openIncidentsByProject()),
9125
10547
  questions: this.questions({ open: true, limit: 50 }),
9126
10548
  resources: this.resources(),
9127
10549
  seq: this.seq()
9128
10550
  };
9129
10551
  }
9130
10552
  }
10553
+ function safeJson(v) {
10554
+ if (v === null)
10555
+ return null;
10556
+ try {
10557
+ return JSON.parse(v);
10558
+ } catch {
10559
+ return v;
10560
+ }
10561
+ }
9131
10562
  var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
9132
10563
  var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
9133
10564
  var TOOL_INPUT_MAX = 2048;
@@ -9630,7 +11061,7 @@ class WorkflowEngine {
9630
11061
  }
9631
11062
 
9632
11063
  // packages/daemon/src/app.ts
9633
- var VERSION = "0.10.0";
11064
+ var VERSION = "0.11.1";
9634
11065
  var WEB_DIR = (() => {
9635
11066
  if (process.env.SWARM_WEB_DIR)
9636
11067
  return process.env.SWARM_WEB_DIR;
@@ -9756,12 +11187,18 @@ function createApp(store = new Store, hooks2 = {}) {
9756
11187
  app.get("/v1/state", (c) => c.json(store.snapshot()));
9757
11188
  app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
9758
11189
  app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
11190
+ app.get("/v1/graphs/lineage", (c) => c.json(store.lineage(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)), c.req.queries("expand") ?? [])));
11191
+ app.get("/v1/context", (c) => c.json(store.context(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
11192
+ app.get("/v1/mcp/health", (c) => c.json(store.mcpHealth(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
11193
+ app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
11194
+ app.get("/v1/gates/health", (c) => c.json(store.gateHealth(c.req.query("project") || undefined, Math.max(1, Math.min(365, Number(c.req.query("days") ?? 30) || 30)))));
11195
+ app.get("/v1/waiting", (c) => c.json(store.waiting(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
9759
11196
  app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
9760
11197
  open: c.req.query("open") === "1",
9761
11198
  projectId: c.req.query("project") || undefined
9762
11199
  })));
9763
- app.get("/v1/outcomes", async (c) => {
9764
- const project = c.req.query("project") || undefined;
11200
+ const outcomesFor = async (project, opts = {}) => {
11201
+ const blocking = opts.blocking !== false;
9765
11202
  const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
9766
11203
  id: s.id,
9767
11204
  branch: s.branch,
@@ -9772,8 +11209,11 @@ function createApp(store = new Store, hooks2 = {}) {
9772
11209
  }));
9773
11210
  const prs = [];
9774
11211
  const reverted = new Set;
11212
+ let stale = false;
9775
11213
  for (const p of store.projects().filter((x) => !project || x.id === project)) {
9776
- const o = await forge2.merged(p.id, p.root);
11214
+ const o = blocking ? { ...await forge2.merged(p.id, p.root), fresh: true } : forge2.mergedCached(p.id, p.root);
11215
+ if (!o.fresh)
11216
+ stale = true;
9777
11217
  for (const m of o.merged)
9778
11218
  prs.push({ ...m, state: "merged" });
9779
11219
  for (const sha of o.reverted)
@@ -9791,7 +11231,96 @@ function createApp(store = new Store, hooks2 = {}) {
9791
11231
  mergedAt: null,
9792
11232
  mergeSha: null
9793
11233
  });
9794
- return c.json(outcomeReport(sessions, prs, reverted));
11234
+ return { ...outcomeReport(sessions, prs, reverted), stale };
11235
+ };
11236
+ app.get("/v1/outcomes", async (c) => c.json(await outcomesFor(c.req.query("project") || undefined)));
11237
+ app.get("/v1/ab", (c) => {
11238
+ const project = c.req.query("project") || undefined;
11239
+ const task = c.req.query("task");
11240
+ if (task) {
11241
+ if (!project)
11242
+ return c.json({ error: "project is required with task" }, 400);
11243
+ return c.json(store.abTrial(project, task));
11244
+ }
11245
+ return c.json({ trials: store.abTrials(project) });
11246
+ });
11247
+ app.post("/v1/ab", async (c) => {
11248
+ const b = await c.req.json().catch(() => ({}));
11249
+ if (!b.projectId || !b.task)
11250
+ return c.json({ error: "projectId and task are required" }, 400);
11251
+ const arms = (b.arms ?? []).filter((a) => a.model || a.label);
11252
+ if (arms.length < 2)
11253
+ return c.json({ error: "a trial needs at least two arms" }, 400);
11254
+ const cfg = store.config(b.projectId);
11255
+ const started = [];
11256
+ const failed = [];
11257
+ for (const a of arms) {
11258
+ const label = (a.label ?? a.model ?? "").replace(/[^a-zA-Z0-9._-]+/g, "-");
11259
+ const id = armTask(b.task, label);
11260
+ const r = await runner.start({
11261
+ projectId: b.projectId,
11262
+ task: id,
11263
+ prompt: taskPrompt({ id: b.task, title: b.title ?? b.task }, {
11264
+ requiredGates: cfg.gates.required,
11265
+ executableGates: cfg.gates.required.filter((g) => cfg.gates.defs[g]),
11266
+ openPr: false
11267
+ }),
11268
+ owner: `ab:${label}`,
11269
+ permissionMode: cfg.dispatch.permission_mode ?? "acceptEdits",
11270
+ ...a.model ? { model: a.model } : {},
11271
+ ...cfg.dispatch.max_turns ? { maxTurns: cfg.dispatch.max_turns } : {}
11272
+ });
11273
+ if (r.ok)
11274
+ started.push(label);
11275
+ else
11276
+ failed.push({ arm: label, reason: r.reason });
11277
+ }
11278
+ store.append({
11279
+ ts: new Date().toISOString(),
11280
+ type: "dispatch.queued",
11281
+ projectId: b.projectId,
11282
+ sessionId: null,
11283
+ payload: {
11284
+ task: b.task,
11285
+ arms: arms.map((a) => a.label ?? a.model),
11286
+ summary: `A/B ${b.task}: ${started.length} arm${started.length === 1 ? "" : "s"} started`
11287
+ }
11288
+ });
11289
+ return c.json({ ok: failed.length === 0, started, failed }, failed.length ? 207 : 200);
11290
+ });
11291
+ app.get("/v1/provenance", async (c) => {
11292
+ const project = c.req.query("project") || undefined;
11293
+ const limit = Math.max(1, Math.min(500, Number(c.req.query("limit") ?? 50) || 50));
11294
+ const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0);
11295
+ const { branches, stale } = await outcomesFor(project, { blocking: false });
11296
+ const projects = store.projects().filter((p) => !project || p.id === project);
11297
+ const tasks2 = [];
11298
+ for (const p of projects)
11299
+ for (const t of store.tasks(p.id)?.tasks ?? [])
11300
+ tasks2.push({ id: t.id, title: t.title, status: t.statusText, url: null });
11301
+ const claims = store.claims(project).map((cl) => ({
11302
+ task: cl.task,
11303
+ sessionId: cl.sessionId,
11304
+ owner: cl.owner || null,
11305
+ worktree: cl.worktree || null,
11306
+ branch: cl.branch || null,
11307
+ acquiredAt: cl.acquiredAt,
11308
+ state: cl.state
11309
+ }));
11310
+ const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
11311
+ id: s.id,
11312
+ title: s.title,
11313
+ agent: s.agent,
11314
+ branch: s.branch,
11315
+ costUsd: s.costUsd
11316
+ }));
11317
+ const full = provenance(tasks2, claims, sessions, branches);
11318
+ return c.json({
11319
+ ...full,
11320
+ chains: full.chains.slice(offset, offset + limit),
11321
+ page: { limit, offset, total: full.chains.length },
11322
+ stale
11323
+ });
9795
11324
  });
9796
11325
  app.get("/v1/memory", (c) => {
9797
11326
  const q = c.req.query("q") ?? "";
@@ -9831,8 +11360,8 @@ function createApp(store = new Store, hooks2 = {}) {
9831
11360
  const p = id ? store.project(id) : null;
9832
11361
  if (id && !p)
9833
11362
  return c.json({ error: "unknown project" }, 404);
9834
- const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
9835
- return c.json({ ...policy2, provenance, overridden });
11363
+ const { provenance: provenance3, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
11364
+ return c.json({ ...policy2, provenance: provenance3, overridden });
9836
11365
  });
9837
11366
  app.get("/v1/audit", (c) => {
9838
11367
  const since = sinceToIso(c.req.query("since"));
@@ -10384,8 +11913,14 @@ function createApp(store = new Store, hooks2 = {}) {
10384
11913
  const p = join10(WEB_DIR, f);
10385
11914
  if (!existsSync7(p))
10386
11915
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
11916
+ const st = statSync2(p);
11917
+ const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
11918
+ if (c.req.header("if-none-match") === etag)
11919
+ return c.body(null, 304, { etag });
10387
11920
  return c.body(readFileSync4(p, "utf8"), 200, {
10388
- "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
11921
+ "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain",
11922
+ "cache-control": "no-cache",
11923
+ etag
10389
11924
  });
10390
11925
  });
10391
11926
  return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };