@ra3orblade/swarm 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/dist/swarm-hook.js +39 -0
- package/dist/swarm.js +39 -1
- package/dist/swarmd.js +1629 -116
- package/package.json +1 -1
- package/web/app.js +681 -66
- package/web/icons.js +2 -2
- package/web/index.html +98 -27
- package/web/menus.js +7 -7
- package/web/release-notes.js +1 -1
- package/web/table.js +1 -1
- package/web/viz.js +76 -2
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
|
|
840
|
-
return new Date(now - n *
|
|
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(
|
|
2091
|
-
if (!
|
|
2867
|
+
function formatMessages(ms3) {
|
|
2868
|
+
if (!ms3.length)
|
|
2092
2869
|
return null;
|
|
2093
|
-
const lines =
|
|
2094
|
-
return `[swarm] While you were working, message${
|
|
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/
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
|
2809
|
-
const
|
|
2810
|
-
const
|
|
3688
|
+
function pairWaits(samples, now, sessionEnds = {}) {
|
|
3689
|
+
const nowMs = ms3(now);
|
|
3690
|
+
const open = new Map;
|
|
2811
3691
|
const out = [];
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
3701
|
+
}
|
|
3702
|
+
const start = open.get(k);
|
|
3703
|
+
if (!start)
|
|
2817
3704
|
continue;
|
|
2818
|
-
|
|
3705
|
+
open.delete(k);
|
|
2819
3706
|
out.push({
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
3814
|
+
res = await handler(context2, () => dispatch2(i + 1));
|
|
2857
3815
|
} catch (err) {
|
|
2858
3816
|
if (err instanceof Error && onError) {
|
|
2859
|
-
|
|
2860
|
-
res = await onError(err,
|
|
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 (
|
|
2868
|
-
res = await onNotFound(
|
|
3825
|
+
if (context2.finalized === false && onNotFound) {
|
|
3826
|
+
res = await onNotFound(context2);
|
|
2869
3827
|
}
|
|
2870
3828
|
}
|
|
2871
|
-
if (res && (
|
|
2872
|
-
|
|
3829
|
+
if (res && (context2.finalized === false || isError)) {
|
|
3830
|
+
context2.res = res;
|
|
2873
3831
|
}
|
|
2874
|
-
return
|
|
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,
|
|
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,
|
|
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
|
|
3713
|
-
if (!
|
|
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
|
|
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,
|
|
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 ??=
|
|
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(
|
|
4348
|
-
return new Promise((res) => setTimeout(res,
|
|
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
|
});
|
|
@@ -5676,7 +6635,7 @@ CREATE TABLE IF NOT EXISTS processes (
|
|
|
5676
6635
|
CREATE INDEX IF NOT EXISTS processes_live ON processes(ended_at, project_id);
|
|
5677
6636
|
CREATE TABLE IF NOT EXISTS gates (
|
|
5678
6637
|
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
|
|
6638
|
+
rubric TEXT, evidence TEXT, session_id TEXT, duration_ms INTEGER, created_at TEXT
|
|
5680
6639
|
);
|
|
5681
6640
|
CREATE INDEX IF NOT EXISTS gates_task ON gates(project_id, task, created_at);
|
|
5682
6641
|
CREATE TABLE IF NOT EXISTS handoffs (
|
|
@@ -5819,7 +6778,7 @@ class Store {
|
|
|
5819
6778
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
|
|
5820
6779
|
}
|
|
5821
6780
|
}
|
|
5822
|
-
static SCHEMA_VERSION =
|
|
6781
|
+
static SCHEMA_VERSION = 2;
|
|
5823
6782
|
schemaVersion() {
|
|
5824
6783
|
return Number(this.meta("schema_version") ?? 0);
|
|
5825
6784
|
}
|
|
@@ -5857,6 +6816,16 @@ class Store {
|
|
|
5857
6816
|
fill("incident_acks", "'dashboard'", null, "acks");
|
|
5858
6817
|
fill("sessions", "NULL", "id", "sessions");
|
|
5859
6818
|
fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
|
|
6819
|
+
},
|
|
6820
|
+
(db) => {
|
|
6821
|
+
this.ensureColumn("gates", "duration_ms", "INTEGER");
|
|
6822
|
+
const rows = db.query("SELECT id, rubric FROM gates WHERE duration_ms IS NULL AND rubric LIKE '%s'").all();
|
|
6823
|
+
const upd = db.query("UPDATE gates SET duration_ms = ? WHERE id = ?");
|
|
6824
|
+
for (const r of rows) {
|
|
6825
|
+
const m = /\bin ([0-9]+(?:\.[0-9]+)?)s$/.exec(r.rubric ?? "");
|
|
6826
|
+
if (m)
|
|
6827
|
+
upd.run(Math.round(Number(m[1]) * 1000), r.id);
|
|
6828
|
+
}
|
|
5860
6829
|
}
|
|
5861
6830
|
];
|
|
5862
6831
|
for (let v = this.schemaVersion();v < steps.length; v++) {
|
|
@@ -6364,10 +7333,10 @@ class Store {
|
|
|
6364
7333
|
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
6365
7334
|
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
6366
7335
|
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
6367
|
-
const
|
|
6368
|
-
if (
|
|
6369
|
-
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${
|
|
6370
|
-
return
|
|
7336
|
+
const ms4 = rows.map(rowToMessage);
|
|
7337
|
+
if (ms4.length && !opts.peek)
|
|
7338
|
+
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));
|
|
7339
|
+
return ms4;
|
|
6371
7340
|
}
|
|
6372
7341
|
markMessageDelivered(id, sessionId) {
|
|
6373
7342
|
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 +7391,7 @@ class Store {
|
|
|
6422
7391
|
rubric: r.rubric,
|
|
6423
7392
|
evidence: r.evidence ?? null,
|
|
6424
7393
|
sessionId: r.session_id ?? null,
|
|
7394
|
+
durationMs: r.duration_ms ?? null,
|
|
6425
7395
|
createdAt: r.created_at
|
|
6426
7396
|
};
|
|
6427
7397
|
}
|
|
@@ -6572,10 +7542,10 @@ class Store {
|
|
|
6572
7542
|
};
|
|
6573
7543
|
const done = (async () => {
|
|
6574
7544
|
let diffText = "";
|
|
6575
|
-
let
|
|
7545
|
+
let stat2 = "";
|
|
6576
7546
|
try {
|
|
6577
7547
|
const diff = await worktreeDiff(p.root, where.worktree);
|
|
6578
|
-
|
|
7548
|
+
stat2 = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
6579
7549
|
`);
|
|
6580
7550
|
diffText = await worktreePatch(where.worktree, diff.base);
|
|
6581
7551
|
} catch (e) {
|
|
@@ -6597,7 +7567,7 @@ class Store {
|
|
|
6597
7567
|
task,
|
|
6598
7568
|
title: taskRow?.title ?? null,
|
|
6599
7569
|
branch: w?.branch ?? null,
|
|
6600
|
-
stat,
|
|
7570
|
+
stat: stat2,
|
|
6601
7571
|
patch: diffText
|
|
6602
7572
|
});
|
|
6603
7573
|
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
@@ -6755,8 +7725,8 @@ ${err}
|
|
|
6755
7725
|
return v;
|
|
6756
7726
|
const createdAt = new Date().toISOString();
|
|
6757
7727
|
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)));
|
|
7728
|
+
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)
|
|
7729
|
+
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
7730
|
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
6761
7731
|
this.remember(gateDoc(projectId, run2.id, run2, sessionId));
|
|
6762
7732
|
this.append({
|
|
@@ -7338,9 +8308,9 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7338
8308
|
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
8309
|
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
7340
8310
|
}
|
|
7341
|
-
prune(
|
|
8311
|
+
prune(days3) {
|
|
7342
8312
|
const cfg = this.policyFor(null).config;
|
|
7343
|
-
const chatter =
|
|
8313
|
+
const chatter = days3 ?? cfg.events.retain_days;
|
|
7344
8314
|
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
7345
8315
|
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
7346
8316
|
if (cfg.audit.retain_days > 0) {
|
|
@@ -7869,7 +8839,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7869
8839
|
acquiredAt: r.acquired_at,
|
|
7870
8840
|
expiresAt: r.expires_at,
|
|
7871
8841
|
releasedAt: r.released_at ?? null,
|
|
7872
|
-
state: r.state
|
|
8842
|
+
state: r.state,
|
|
8843
|
+
sessionId: r.actor_kind === "agent" ? r.actor_id ?? null : null
|
|
7873
8844
|
}));
|
|
7874
8845
|
const now = Date.now();
|
|
7875
8846
|
for (const c of rows)
|
|
@@ -7997,6 +8968,436 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7997
8968
|
expiresAt: r.expires_at
|
|
7998
8969
|
}));
|
|
7999
8970
|
}
|
|
8971
|
+
waiting(projectId, days3 = 7) {
|
|
8972
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
8973
|
+
const pArgs = projectId ? [projectId] : [];
|
|
8974
|
+
const paired = this.db.query(`SELECT type, session_id, project_id, ts,
|
|
8975
|
+
COALESCE(json_extract(payload,'$.requestId'), json_extract(payload,'$.id')) AS key,
|
|
8976
|
+
COALESCE(json_extract(payload,'$.tool'), json_extract(payload,'$.text')) AS label
|
|
8977
|
+
FROM events
|
|
8978
|
+
WHERE type IN ('permission.requested','permission.resolved','question.asked','question.answered')
|
|
8979
|
+
AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8980
|
+
const notes = this.db.query(`SELECT n.seq, n.session_id, n.project_id, n.ts,
|
|
8981
|
+
json_extract(n.payload,'$.summary') AS label,
|
|
8982
|
+
(SELECT MIN(a.ts) FROM events a
|
|
8983
|
+
WHERE a.session_id = n.session_id AND a.seq > n.seq
|
|
8984
|
+
AND a.type IN ('prompt.submitted','tool.requested')) AS resumed
|
|
8985
|
+
FROM events n
|
|
8986
|
+
WHERE n.type = 'session.notification' AND n.ts >= ?${projectId ? " AND n.project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8987
|
+
const samples = [];
|
|
8988
|
+
for (const r of paired) {
|
|
8989
|
+
if (!r.session_id || r.key === null)
|
|
8990
|
+
continue;
|
|
8991
|
+
const kind = r.type.startsWith("permission") ? "permission" : "question";
|
|
8992
|
+
samples.push({
|
|
8993
|
+
sessionId: r.session_id,
|
|
8994
|
+
projectId: r.project_id,
|
|
8995
|
+
kind,
|
|
8996
|
+
key: String(r.key),
|
|
8997
|
+
phase: r.type.endsWith(".requested") || r.type.endsWith(".asked") ? "start" : "end",
|
|
8998
|
+
ts: r.ts,
|
|
8999
|
+
...r.label ? { label: r.label.slice(0, 120) } : {}
|
|
9000
|
+
});
|
|
9001
|
+
}
|
|
9002
|
+
for (const n of notes) {
|
|
9003
|
+
if (!n.session_id)
|
|
9004
|
+
continue;
|
|
9005
|
+
const key = String(n.seq);
|
|
9006
|
+
samples.push({
|
|
9007
|
+
sessionId: n.session_id,
|
|
9008
|
+
projectId: n.project_id,
|
|
9009
|
+
kind: "notification",
|
|
9010
|
+
key,
|
|
9011
|
+
phase: "start",
|
|
9012
|
+
ts: n.ts,
|
|
9013
|
+
...n.label ? { label: n.label.slice(0, 120) } : {}
|
|
9014
|
+
});
|
|
9015
|
+
if (n.resumed)
|
|
9016
|
+
samples.push({
|
|
9017
|
+
sessionId: n.session_id,
|
|
9018
|
+
projectId: n.project_id,
|
|
9019
|
+
kind: "notification",
|
|
9020
|
+
key,
|
|
9021
|
+
phase: "end",
|
|
9022
|
+
ts: n.resumed
|
|
9023
|
+
});
|
|
9024
|
+
}
|
|
9025
|
+
const ends = {};
|
|
9026
|
+
for (const r of this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all())
|
|
9027
|
+
ends[r.id] = r.ended_at;
|
|
9028
|
+
const report = waitingReport(pairWaits(samples, new Date().toISOString(), ends));
|
|
9029
|
+
const meta = new Map(this.db.query("SELECT id, title, agent, project_id FROM sessions").all().map((r) => [r.id, r]));
|
|
9030
|
+
return {
|
|
9031
|
+
...report,
|
|
9032
|
+
sessions: report.sessions.map((s) => ({
|
|
9033
|
+
...s,
|
|
9034
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9035
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code",
|
|
9036
|
+
projectId: s.projectId ?? meta.get(s.sessionId)?.project_id ?? null
|
|
9037
|
+
}))
|
|
9038
|
+
};
|
|
9039
|
+
}
|
|
9040
|
+
gateHealth(projectId, days3 = 30) {
|
|
9041
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9042
|
+
const rows = this.db.query(`SELECT project_id, task, gate, verdict, duration_ms, created_at FROM gates
|
|
9043
|
+
WHERE created_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
9044
|
+
return gateHealth(rows.filter((r) => r.verdict === "pass" || r.verdict === "fail").map((r) => ({
|
|
9045
|
+
projectId: r.project_id,
|
|
9046
|
+
task: r.task,
|
|
9047
|
+
gate: r.gate,
|
|
9048
|
+
verdict: r.verdict,
|
|
9049
|
+
durationMs: r.duration_ms,
|
|
9050
|
+
at: r.created_at
|
|
9051
|
+
})));
|
|
9052
|
+
}
|
|
9053
|
+
static worktreeIdleMs(path) {
|
|
9054
|
+
let newest = 0;
|
|
9055
|
+
for (const f of [path, join8(path, ".git")]) {
|
|
9056
|
+
try {
|
|
9057
|
+
newest = Math.max(newest, statSync(f).mtimeMs);
|
|
9058
|
+
} catch {}
|
|
9059
|
+
}
|
|
9060
|
+
return newest ? Math.max(0, Date.now() - newest) : null;
|
|
9061
|
+
}
|
|
9062
|
+
duCache = new Map;
|
|
9063
|
+
duInflight = null;
|
|
9064
|
+
refreshDisk(paths, ttlMs) {
|
|
9065
|
+
if (this.duInflight)
|
|
9066
|
+
return;
|
|
9067
|
+
const stale = paths.filter((p) => {
|
|
9068
|
+
const hit = this.duCache.get(p);
|
|
9069
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9070
|
+
});
|
|
9071
|
+
if (!stale.length)
|
|
9072
|
+
return;
|
|
9073
|
+
this.duInflight = (async () => {
|
|
9074
|
+
for (const path of stale) {
|
|
9075
|
+
let v = null;
|
|
9076
|
+
try {
|
|
9077
|
+
const proc = Bun.spawn(["du", "-sk", "-x", path], { stdout: "pipe", stderr: "ignore" });
|
|
9078
|
+
const out = await new Response(proc.stdout).text();
|
|
9079
|
+
if (await proc.exited === 0) {
|
|
9080
|
+
const n = Number.parseInt(out.trim().split(/\s+/)[0] ?? "", 10);
|
|
9081
|
+
if (Number.isFinite(n))
|
|
9082
|
+
v = n;
|
|
9083
|
+
}
|
|
9084
|
+
} catch {}
|
|
9085
|
+
this.duCache.set(path, { v, t: Date.now() });
|
|
9086
|
+
}
|
|
9087
|
+
})().finally(() => {
|
|
9088
|
+
this.duInflight = null;
|
|
9089
|
+
});
|
|
9090
|
+
}
|
|
9091
|
+
hygiene(projectId, diskTtlMs = 600000) {
|
|
9092
|
+
const rows = this.db.query(`SELECT * FROM processes WHERE ended_at IS NULL${projectId ? " AND project_id = ?" : ""}
|
|
9093
|
+
ORDER BY started_at DESC`).all(...projectId ? [projectId] : []).map((r) => this.rowToProcess(r));
|
|
9094
|
+
const usage = new Map;
|
|
9095
|
+
if (rows.length) {
|
|
9096
|
+
try {
|
|
9097
|
+
const out = Bun.spawnSync([
|
|
9098
|
+
"ps",
|
|
9099
|
+
"-o",
|
|
9100
|
+
"pid=,pcpu=,rss=",
|
|
9101
|
+
"-p",
|
|
9102
|
+
rows.map((r) => r.pid).join(",")
|
|
9103
|
+
]);
|
|
9104
|
+
for (const line of new TextDecoder().decode(out.stdout).split(`
|
|
9105
|
+
`)) {
|
|
9106
|
+
const [pid, cpu, rss] = line.trim().split(/\s+/);
|
|
9107
|
+
if (pid)
|
|
9108
|
+
usage.set(Number(pid), { cpu: Number(cpu) || 0, rss: Number(rss) || 0 });
|
|
9109
|
+
}
|
|
9110
|
+
} catch {}
|
|
9111
|
+
}
|
|
9112
|
+
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));
|
|
9113
|
+
const procs = rows.map((p) => {
|
|
9114
|
+
const u = usage.get(p.pid);
|
|
9115
|
+
return {
|
|
9116
|
+
pid: p.pid,
|
|
9117
|
+
name: p.name,
|
|
9118
|
+
kind: p.kind,
|
|
9119
|
+
projectId: p.projectId,
|
|
9120
|
+
sessionId: p.sessionId,
|
|
9121
|
+
port: p.port,
|
|
9122
|
+
startedAt: p.startedAt,
|
|
9123
|
+
alive: this.processIsOurs(p),
|
|
9124
|
+
sessionLive: !p.sessionId || liveSessionIds.has(p.sessionId),
|
|
9125
|
+
cpuPct: u ? u.cpu : null,
|
|
9126
|
+
rssKb: u ? u.rss : null
|
|
9127
|
+
};
|
|
9128
|
+
});
|
|
9129
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9130
|
+
const claims = this.claims().filter((c) => c.state !== "released");
|
|
9131
|
+
const trees = [];
|
|
9132
|
+
for (const pid of projects)
|
|
9133
|
+
for (const w of this.worktrees(pid)) {
|
|
9134
|
+
const held = claims.find((c) => c.worktree === w.path);
|
|
9135
|
+
trees.push({
|
|
9136
|
+
projectId: pid,
|
|
9137
|
+
path: w.path,
|
|
9138
|
+
branch: w.branch,
|
|
9139
|
+
main: w.main,
|
|
9140
|
+
dirty: w.dirty,
|
|
9141
|
+
ahead: w.ahead,
|
|
9142
|
+
merged: w.merged,
|
|
9143
|
+
idleMs: Store.worktreeIdleMs(w.path),
|
|
9144
|
+
diskKb: this.duCache.get(w.path)?.v ?? null,
|
|
9145
|
+
heldByClaim: held?.task ?? null,
|
|
9146
|
+
liveSessions: this.sessions().filter((s) => s.cwd?.startsWith(w.path) && !s.endedAt && s.state !== "ended").length
|
|
9147
|
+
});
|
|
9148
|
+
}
|
|
9149
|
+
this.refreshDisk(trees.map((t) => t.path), diskTtlMs);
|
|
9150
|
+
return hygieneReport(procs, trees);
|
|
9151
|
+
}
|
|
9152
|
+
lineage(projectId, days3 = 14, expanded = []) {
|
|
9153
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9154
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9155
|
+
const rows = this.db.query(`SELECT id, project_id, title, agent, kind, state, parent_id, started_at, ended_at
|
|
9156
|
+
FROM sessions WHERE last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
9157
|
+
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]));
|
|
9158
|
+
const sessions = rows.map((r) => ({
|
|
9159
|
+
id: r.id,
|
|
9160
|
+
projectId: r.project_id,
|
|
9161
|
+
title: r.title,
|
|
9162
|
+
agent: r.agent ?? "claude-code",
|
|
9163
|
+
kind: r.kind,
|
|
9164
|
+
state: r.state,
|
|
9165
|
+
startedAt: r.started_at,
|
|
9166
|
+
endedAt: r.ended_at,
|
|
9167
|
+
costUsd: cost.get(r.id) ?? null,
|
|
9168
|
+
outcome: null
|
|
9169
|
+
}));
|
|
9170
|
+
const known = new Set(sessions.map((s) => s.id));
|
|
9171
|
+
const edges = [];
|
|
9172
|
+
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,
|
|
9173
|
+
SUM(t.cost_usd) AS cost, COUNT(*) AS turns
|
|
9174
|
+
FROM turns t WHERE t.agent_id IS NOT NULL AND t.ts >= ?
|
|
9175
|
+
GROUP BY t.session_id, t.agent_id`).all(since)) {
|
|
9176
|
+
const parent = sessions.find((x) => x.id === a.session_id);
|
|
9177
|
+
if (!parent)
|
|
9178
|
+
continue;
|
|
9179
|
+
const id = `sub:${a.session_id}:${a.agent_id}`;
|
|
9180
|
+
sessions.push({
|
|
9181
|
+
id,
|
|
9182
|
+
projectId: parent.projectId,
|
|
9183
|
+
title: `subagent ${a.agent_id.slice(0, 8)} \xB7 ${a.turns} turn${a.turns === 1 ? "" : "s"}`,
|
|
9184
|
+
agent: parent.agent,
|
|
9185
|
+
kind: "subagent",
|
|
9186
|
+
state: "ended",
|
|
9187
|
+
startedAt: a.first_ts,
|
|
9188
|
+
endedAt: a.last_ts,
|
|
9189
|
+
costUsd: a.cost,
|
|
9190
|
+
outcome: null
|
|
9191
|
+
});
|
|
9192
|
+
known.add(id);
|
|
9193
|
+
edges.push({ from: a.session_id, to: id, kind: "subagent", at: a.first_ts });
|
|
9194
|
+
}
|
|
9195
|
+
for (const d of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.by') AS by, json_extract(payload,'$.task') AS task
|
|
9196
|
+
FROM events WHERE type = 'dispatch.started' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs)) {
|
|
9197
|
+
if (!d.session_id || !d.by)
|
|
9198
|
+
continue;
|
|
9199
|
+
const a = actorFrom(d.by, null);
|
|
9200
|
+
if (a.kind === "agent" && known.has(a.id) && a.id !== d.session_id)
|
|
9201
|
+
edges.push({ from: a.id, to: d.session_id, kind: "dispatch", at: d.ts, label: d.task });
|
|
9202
|
+
}
|
|
9203
|
+
for (const m of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.recipient') AS to_session,
|
|
9204
|
+
json_extract(payload,'$.text') AS text
|
|
9205
|
+
FROM events WHERE type = 'message.sent' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs))
|
|
9206
|
+
if (m.session_id && m.to_session)
|
|
9207
|
+
edges.push({
|
|
9208
|
+
from: m.session_id,
|
|
9209
|
+
to: m.to_session,
|
|
9210
|
+
kind: "message",
|
|
9211
|
+
at: m.ts,
|
|
9212
|
+
label: m.text?.slice(0, 80) ?? null
|
|
9213
|
+
});
|
|
9214
|
+
const holds = this.db.query(`SELECT task, project_id, actor_kind, actor_id, acquired_at FROM claims
|
|
9215
|
+
WHERE acquired_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs).map((c) => ({
|
|
9216
|
+
task: c.task,
|
|
9217
|
+
projectId: c.project_id,
|
|
9218
|
+
sessionId: c.actor_kind === "agent" ? c.actor_id : null,
|
|
9219
|
+
at: c.acquired_at
|
|
9220
|
+
}));
|
|
9221
|
+
edges.push(...handoffEdges(holds));
|
|
9222
|
+
return lineageGraph(sessions, edges, { expanded });
|
|
9223
|
+
}
|
|
9224
|
+
mcpHealth(projectId, days3 = 7) {
|
|
9225
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9226
|
+
const rows = this.db.query(`SELECT session_id, seq, ts, type,
|
|
9227
|
+
json_extract(payload,'$.tool') AS tool,
|
|
9228
|
+
json_extract(payload,'$.toolResponse') AS response
|
|
9229
|
+
FROM events
|
|
9230
|
+
WHERE type IN ('tool.requested','tool.completed') AND ts >= ?
|
|
9231
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
9232
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
9233
|
+
const calls = [];
|
|
9234
|
+
const pending = new Map;
|
|
9235
|
+
const abandon = (sessionId, tool, at) => {
|
|
9236
|
+
calls.push({ sessionId, tool, ms: null, errored: false, at });
|
|
9237
|
+
};
|
|
9238
|
+
for (const r of rows) {
|
|
9239
|
+
if (!r.session_id)
|
|
9240
|
+
continue;
|
|
9241
|
+
const perSession = pending.get(r.session_id) ?? new Map;
|
|
9242
|
+
pending.set(r.session_id, perSession);
|
|
9243
|
+
if (r.type === "tool.requested") {
|
|
9244
|
+
const open = perSession.get(r.tool);
|
|
9245
|
+
if (open)
|
|
9246
|
+
abandon(r.session_id, r.tool, open);
|
|
9247
|
+
perSession.set(r.tool, r.ts);
|
|
9248
|
+
continue;
|
|
9249
|
+
}
|
|
9250
|
+
const startedAt = perSession.get(r.tool);
|
|
9251
|
+
perSession.delete(r.tool);
|
|
9252
|
+
calls.push({
|
|
9253
|
+
sessionId: r.session_id,
|
|
9254
|
+
tool: r.tool,
|
|
9255
|
+
ms: startedAt ? Math.max(0, new Date(r.ts).getTime() - new Date(startedAt).getTime()) : null,
|
|
9256
|
+
errored: toolResponseErrored(safeJson(r.response)),
|
|
9257
|
+
at: r.ts
|
|
9258
|
+
});
|
|
9259
|
+
}
|
|
9260
|
+
for (const [sessionId, perSession] of pending)
|
|
9261
|
+
for (const [tool, at] of perSession)
|
|
9262
|
+
abandon(sessionId, tool, at);
|
|
9263
|
+
return mcpHealth(calls);
|
|
9264
|
+
}
|
|
9265
|
+
context(projectId, days3 = 7) {
|
|
9266
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9267
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9268
|
+
const where = projectId ? " AND project_id = ?" : "";
|
|
9269
|
+
const results = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
9270
|
+
LENGTH(json_extract(payload,'$.toolResponse')) AS chars
|
|
9271
|
+
FROM events
|
|
9272
|
+
WHERE type = 'tool.completed' AND ts >= ?${where}
|
|
9273
|
+
AND json_extract(payload,'$.tool') IS NOT NULL
|
|
9274
|
+
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 }));
|
|
9275
|
+
const reads = this.db.query(`SELECT req.session_id,
|
|
9276
|
+
json_extract(req.payload,'$.toolInput.file_path') AS path,
|
|
9277
|
+
(SELECT LENGTH(json_extract(done.payload,'$.toolResponse')) FROM events done
|
|
9278
|
+
WHERE done.session_id = req.session_id AND done.seq > req.seq
|
|
9279
|
+
AND done.type = 'tool.completed'
|
|
9280
|
+
AND json_extract(done.payload,'$.tool') = 'Read'
|
|
9281
|
+
ORDER BY done.seq LIMIT 1) AS chars
|
|
9282
|
+
FROM events req
|
|
9283
|
+
WHERE req.type = 'tool.requested' AND json_extract(req.payload,'$.tool') = 'Read'
|
|
9284
|
+
AND req.ts >= ?${where.replace("project_id", "req.project_id")}
|
|
9285
|
+
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 }));
|
|
9286
|
+
const turns = this.db.query(`SELECT t.session_id, SUM(t.input) AS input, SUM(t.cache_read) AS cache_read,
|
|
9287
|
+
SUM(t.cache_write) AS cache_write, SUM(t.thinking) AS thinking, SUM(t.output) AS output
|
|
9288
|
+
FROM turns t${projectId ? " JOIN sessions s ON s.id = t.session_id" : ""}
|
|
9289
|
+
WHERE t.ts >= ?${projectId ? " AND s.project_id = ?" : ""}
|
|
9290
|
+
GROUP BY t.session_id`).all(since, ...pArgs).map((r) => ({
|
|
9291
|
+
sessionId: r.session_id,
|
|
9292
|
+
input: r.input ?? 0,
|
|
9293
|
+
cacheRead: r.cache_read ?? 0,
|
|
9294
|
+
cacheWrite: r.cache_write ?? 0,
|
|
9295
|
+
thinking: r.thinking ?? 0,
|
|
9296
|
+
output: r.output ?? 0
|
|
9297
|
+
}));
|
|
9298
|
+
const report = contextReport(results, reads, turns);
|
|
9299
|
+
const meta = new Map(this.db.query("SELECT id, title, agent FROM sessions").all().map((r) => [r.id, r]));
|
|
9300
|
+
return {
|
|
9301
|
+
...report,
|
|
9302
|
+
sessions: report.sessions.map((s) => ({
|
|
9303
|
+
...s,
|
|
9304
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9305
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code"
|
|
9306
|
+
}))
|
|
9307
|
+
};
|
|
9308
|
+
}
|
|
9309
|
+
abTrials(projectId) {
|
|
9310
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9311
|
+
const out = [];
|
|
9312
|
+
for (const pid of projects) {
|
|
9313
|
+
const tasks2 = [
|
|
9314
|
+
...new Set(this.claims(pid).map((c) => splitArmTask(c.task)).filter((x) => x.arm).map((x) => x.task))
|
|
9315
|
+
];
|
|
9316
|
+
for (const t of tasks2.sort())
|
|
9317
|
+
out.push({ ...this.abTrial(pid, t), projectId: pid });
|
|
9318
|
+
}
|
|
9319
|
+
const rank = { undecided: 0, "all-failed": 1, winner: 2 };
|
|
9320
|
+
return out.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.totals.costUsd - a.totals.costUsd);
|
|
9321
|
+
}
|
|
9322
|
+
diffCache = new Map;
|
|
9323
|
+
diffInflight = null;
|
|
9324
|
+
refreshDiffs(worktrees, base, ttlMs = 30000) {
|
|
9325
|
+
if (this.diffInflight)
|
|
9326
|
+
return;
|
|
9327
|
+
const stale = worktrees.filter((w) => {
|
|
9328
|
+
const hit = this.diffCache.get(w);
|
|
9329
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9330
|
+
});
|
|
9331
|
+
if (!stale.length)
|
|
9332
|
+
return;
|
|
9333
|
+
this.diffInflight = (async () => {
|
|
9334
|
+
for (const wt of stale) {
|
|
9335
|
+
let v = null;
|
|
9336
|
+
try {
|
|
9337
|
+
const proc = Bun.spawn(["git", "diff", "--shortstat", `${base}...HEAD`], {
|
|
9338
|
+
cwd: wt,
|
|
9339
|
+
stdout: "pipe",
|
|
9340
|
+
stderr: "ignore"
|
|
9341
|
+
});
|
|
9342
|
+
const out = await new Response(proc.stdout).text();
|
|
9343
|
+
if (await proc.exited === 0) {
|
|
9344
|
+
const f = /(\d+) files? changed/.exec(out)?.[1];
|
|
9345
|
+
const i = /(\d+) insertions?/.exec(out)?.[1];
|
|
9346
|
+
const d = /(\d+) deletions?/.exec(out)?.[1];
|
|
9347
|
+
v = [Number(f ?? 0), Number(i ?? 0), Number(d ?? 0)];
|
|
9348
|
+
}
|
|
9349
|
+
} catch {}
|
|
9350
|
+
this.diffCache.set(wt, { v, t: Date.now() });
|
|
9351
|
+
}
|
|
9352
|
+
})().finally(() => {
|
|
9353
|
+
this.diffInflight = null;
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
abTrial(projectId, task) {
|
|
9357
|
+
const claims = this.claims(projectId).filter((c) => splitArmTask(c.task).task === task);
|
|
9358
|
+
const all = this.sessions();
|
|
9359
|
+
const sessions = new Map(all.map((s) => [s.id, s]));
|
|
9360
|
+
const byCwd = new Map;
|
|
9361
|
+
for (const s of all)
|
|
9362
|
+
if (s.cwd && !byCwd.has(s.cwd))
|
|
9363
|
+
byCwd.set(s.cwd, s);
|
|
9364
|
+
const gateRuns = this.gateRuns(projectId, undefined, 2000);
|
|
9365
|
+
const arms = [];
|
|
9366
|
+
for (const c of claims) {
|
|
9367
|
+
const label = splitArmTask(c.task).arm;
|
|
9368
|
+
if (!label)
|
|
9369
|
+
continue;
|
|
9370
|
+
const sess = (c.worktree ? byCwd.get(c.worktree) : undefined) ?? (c.sessionId ? sessions.get(c.sessionId) : undefined);
|
|
9371
|
+
const sid = sess?.id ?? c.sessionId;
|
|
9372
|
+
const g = gateRuns.filter((x) => x.task === c.task);
|
|
9373
|
+
const latest = new Map;
|
|
9374
|
+
for (const run2 of [...g].sort((a, b) => a.createdAt < b.createdAt ? -1 : 1))
|
|
9375
|
+
latest.set(run2.gate, run2);
|
|
9376
|
+
const verdicts = [...latest.values()];
|
|
9377
|
+
const diff = c.worktree ? this.diffCache.get(c.worktree)?.v : null;
|
|
9378
|
+
arms.push({
|
|
9379
|
+
label,
|
|
9380
|
+
task: c.task,
|
|
9381
|
+
model: sess?.model ?? null,
|
|
9382
|
+
agent: sess?.agent ?? "claude-code",
|
|
9383
|
+
sessionId: sid,
|
|
9384
|
+
worktree: c.worktree || null,
|
|
9385
|
+
startedAt: c.acquiredAt,
|
|
9386
|
+
endedAt: sess?.endedAt ?? null,
|
|
9387
|
+
state: sess && sess.state !== "ended" ? "running" : sess ? "done" : c.state === "held" ? "running" : "failed",
|
|
9388
|
+
costUsd: sess?.costUsd ?? 0,
|
|
9389
|
+
turns: sess?.turns ?? 0,
|
|
9390
|
+
gatesPassed: verdicts.filter((v) => v.verdict === "pass").length,
|
|
9391
|
+
gatesFailed: verdicts.filter((v) => v.verdict === "fail").length,
|
|
9392
|
+
filesChanged: diff ? diff[0] : null,
|
|
9393
|
+
insertions: diff ? diff[1] : null,
|
|
9394
|
+
deletions: diff ? diff[2] : null
|
|
9395
|
+
});
|
|
9396
|
+
}
|
|
9397
|
+
const base = this.worktrees(projectId).find((w) => w.main)?.branch ?? "main";
|
|
9398
|
+
this.refreshDiffs(arms.map((a) => a.worktree).filter((w) => !!w), base);
|
|
9399
|
+
return scoreTrial(task, arms);
|
|
9400
|
+
}
|
|
8000
9401
|
collisions(projectId) {
|
|
8001
9402
|
const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8002
9403
|
const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
|
|
@@ -8771,6 +10172,16 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8771
10172
|
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
|
|
8772
10173
|
return r.n;
|
|
8773
10174
|
}
|
|
10175
|
+
openIncidentsByProject() {
|
|
10176
|
+
const rows = this.db.query(`SELECT e.project_id AS pid, COUNT(*) AS n FROM events e
|
|
10177
|
+
LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10178
|
+
WHERE e.type = 'incident.opened' AND a.seq IS NULL AND e.project_id IS NOT NULL
|
|
10179
|
+
GROUP BY e.project_id`).all();
|
|
10180
|
+
const out = {};
|
|
10181
|
+
for (const r of rows)
|
|
10182
|
+
out[r.pid] = r.n;
|
|
10183
|
+
return out;
|
|
10184
|
+
}
|
|
8774
10185
|
ackIncident(seq, by) {
|
|
8775
10186
|
const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
|
|
8776
10187
|
if (!row)
|
|
@@ -9094,14 +10505,14 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9094
10505
|
const from = localDayIso(-13);
|
|
9095
10506
|
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
10507
|
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
9097
|
-
const
|
|
10508
|
+
const days3 = [];
|
|
9098
10509
|
for (let i = 13;i >= 0; i--)
|
|
9099
|
-
|
|
10510
|
+
days3.push(localDayIso(-i).slice(0, 10));
|
|
9100
10511
|
const out = {};
|
|
9101
10512
|
for (const r of rows) {
|
|
9102
10513
|
out[r.pid] ??= new Array(14).fill(0);
|
|
9103
10514
|
const arr = out[r.pid];
|
|
9104
|
-
const i =
|
|
10515
|
+
const i = days3.indexOf(r.day);
|
|
9105
10516
|
if (i >= 0)
|
|
9106
10517
|
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
9107
10518
|
}
|
|
@@ -9122,12 +10533,22 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9122
10533
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
9123
10534
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
9124
10535
|
openIncidents: this.memoised("openIncidents", 30000, () => this.openIncidents()),
|
|
10536
|
+
openIncidentsByProject: this.memoised("openIncidentsByProject", 30000, () => this.openIncidentsByProject()),
|
|
9125
10537
|
questions: this.questions({ open: true, limit: 50 }),
|
|
9126
10538
|
resources: this.resources(),
|
|
9127
10539
|
seq: this.seq()
|
|
9128
10540
|
};
|
|
9129
10541
|
}
|
|
9130
10542
|
}
|
|
10543
|
+
function safeJson(v) {
|
|
10544
|
+
if (v === null)
|
|
10545
|
+
return null;
|
|
10546
|
+
try {
|
|
10547
|
+
return JSON.parse(v);
|
|
10548
|
+
} catch {
|
|
10549
|
+
return v;
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
9131
10552
|
var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
9132
10553
|
var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
|
|
9133
10554
|
var TOOL_INPUT_MAX = 2048;
|
|
@@ -9630,7 +11051,7 @@ class WorkflowEngine {
|
|
|
9630
11051
|
}
|
|
9631
11052
|
|
|
9632
11053
|
// packages/daemon/src/app.ts
|
|
9633
|
-
var VERSION = "0.
|
|
11054
|
+
var VERSION = "0.11.0";
|
|
9634
11055
|
var WEB_DIR = (() => {
|
|
9635
11056
|
if (process.env.SWARM_WEB_DIR)
|
|
9636
11057
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -9756,12 +11177,17 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9756
11177
|
app.get("/v1/state", (c) => c.json(store.snapshot()));
|
|
9757
11178
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
9758
11179
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11180
|
+
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") ?? [])));
|
|
11181
|
+
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)))));
|
|
11182
|
+
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)))));
|
|
11183
|
+
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|
|
11184
|
+
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)))));
|
|
11185
|
+
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
11186
|
app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
|
|
9760
11187
|
open: c.req.query("open") === "1",
|
|
9761
11188
|
projectId: c.req.query("project") || undefined
|
|
9762
11189
|
})));
|
|
9763
|
-
|
|
9764
|
-
const project = c.req.query("project") || undefined;
|
|
11190
|
+
const outcomesFor = async (project) => {
|
|
9765
11191
|
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
9766
11192
|
id: s.id,
|
|
9767
11193
|
branch: s.branch,
|
|
@@ -9791,7 +11217,88 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9791
11217
|
mergedAt: null,
|
|
9792
11218
|
mergeSha: null
|
|
9793
11219
|
});
|
|
9794
|
-
return
|
|
11220
|
+
return outcomeReport(sessions, prs, reverted);
|
|
11221
|
+
};
|
|
11222
|
+
app.get("/v1/outcomes", async (c) => c.json(await outcomesFor(c.req.query("project") || undefined)));
|
|
11223
|
+
app.get("/v1/ab", (c) => {
|
|
11224
|
+
const project = c.req.query("project") || undefined;
|
|
11225
|
+
const task = c.req.query("task");
|
|
11226
|
+
if (task) {
|
|
11227
|
+
if (!project)
|
|
11228
|
+
return c.json({ error: "project is required with task" }, 400);
|
|
11229
|
+
return c.json(store.abTrial(project, task));
|
|
11230
|
+
}
|
|
11231
|
+
return c.json({ trials: store.abTrials(project) });
|
|
11232
|
+
});
|
|
11233
|
+
app.post("/v1/ab", async (c) => {
|
|
11234
|
+
const b = await c.req.json().catch(() => ({}));
|
|
11235
|
+
if (!b.projectId || !b.task)
|
|
11236
|
+
return c.json({ error: "projectId and task are required" }, 400);
|
|
11237
|
+
const arms = (b.arms ?? []).filter((a) => a.model || a.label);
|
|
11238
|
+
if (arms.length < 2)
|
|
11239
|
+
return c.json({ error: "a trial needs at least two arms" }, 400);
|
|
11240
|
+
const cfg = store.config(b.projectId);
|
|
11241
|
+
const started = [];
|
|
11242
|
+
const failed = [];
|
|
11243
|
+
for (const a of arms) {
|
|
11244
|
+
const label = (a.label ?? a.model ?? "").replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
11245
|
+
const id = armTask(b.task, label);
|
|
11246
|
+
const r = await runner.start({
|
|
11247
|
+
projectId: b.projectId,
|
|
11248
|
+
task: id,
|
|
11249
|
+
prompt: taskPrompt({ id: b.task, title: b.title ?? b.task }, {
|
|
11250
|
+
requiredGates: cfg.gates.required,
|
|
11251
|
+
executableGates: cfg.gates.required.filter((g) => cfg.gates.defs[g]),
|
|
11252
|
+
openPr: false
|
|
11253
|
+
}),
|
|
11254
|
+
owner: `ab:${label}`,
|
|
11255
|
+
permissionMode: cfg.dispatch.permission_mode ?? "acceptEdits",
|
|
11256
|
+
...a.model ? { model: a.model } : {},
|
|
11257
|
+
...cfg.dispatch.max_turns ? { maxTurns: cfg.dispatch.max_turns } : {}
|
|
11258
|
+
});
|
|
11259
|
+
if (r.ok)
|
|
11260
|
+
started.push(label);
|
|
11261
|
+
else
|
|
11262
|
+
failed.push({ arm: label, reason: r.reason });
|
|
11263
|
+
}
|
|
11264
|
+
store.append({
|
|
11265
|
+
ts: new Date().toISOString(),
|
|
11266
|
+
type: "dispatch.queued",
|
|
11267
|
+
projectId: b.projectId,
|
|
11268
|
+
sessionId: null,
|
|
11269
|
+
payload: {
|
|
11270
|
+
task: b.task,
|
|
11271
|
+
arms: arms.map((a) => a.label ?? a.model),
|
|
11272
|
+
summary: `A/B ${b.task}: ${started.length} arm${started.length === 1 ? "" : "s"} started`
|
|
11273
|
+
}
|
|
11274
|
+
});
|
|
11275
|
+
return c.json({ ok: failed.length === 0, started, failed }, failed.length ? 207 : 200);
|
|
11276
|
+
});
|
|
11277
|
+
app.get("/v1/provenance", async (c) => {
|
|
11278
|
+
const project = c.req.query("project") || undefined;
|
|
11279
|
+
const { branches } = await outcomesFor(project);
|
|
11280
|
+
const projects = store.projects().filter((p) => !project || p.id === project);
|
|
11281
|
+
const tasks2 = [];
|
|
11282
|
+
for (const p of projects)
|
|
11283
|
+
for (const t of store.tasks(p.id)?.tasks ?? [])
|
|
11284
|
+
tasks2.push({ id: t.id, title: t.title, status: t.statusText, url: null });
|
|
11285
|
+
const claims = store.claims(project).map((cl) => ({
|
|
11286
|
+
task: cl.task,
|
|
11287
|
+
sessionId: cl.sessionId,
|
|
11288
|
+
owner: cl.owner || null,
|
|
11289
|
+
worktree: cl.worktree || null,
|
|
11290
|
+
branch: cl.branch || null,
|
|
11291
|
+
acquiredAt: cl.acquiredAt,
|
|
11292
|
+
state: cl.state
|
|
11293
|
+
}));
|
|
11294
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
11295
|
+
id: s.id,
|
|
11296
|
+
title: s.title,
|
|
11297
|
+
agent: s.agent,
|
|
11298
|
+
branch: s.branch,
|
|
11299
|
+
costUsd: s.costUsd
|
|
11300
|
+
}));
|
|
11301
|
+
return c.json(provenance(tasks2, claims, sessions, branches));
|
|
9795
11302
|
});
|
|
9796
11303
|
app.get("/v1/memory", (c) => {
|
|
9797
11304
|
const q = c.req.query("q") ?? "";
|
|
@@ -9831,8 +11338,8 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9831
11338
|
const p = id ? store.project(id) : null;
|
|
9832
11339
|
if (id && !p)
|
|
9833
11340
|
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 });
|
|
11341
|
+
const { provenance: provenance3, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
11342
|
+
return c.json({ ...policy2, provenance: provenance3, overridden });
|
|
9836
11343
|
});
|
|
9837
11344
|
app.get("/v1/audit", (c) => {
|
|
9838
11345
|
const since = sinceToIso(c.req.query("since"));
|
|
@@ -10384,8 +11891,14 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
10384
11891
|
const p = join10(WEB_DIR, f);
|
|
10385
11892
|
if (!existsSync7(p))
|
|
10386
11893
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
11894
|
+
const st = statSync2(p);
|
|
11895
|
+
const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
|
|
11896
|
+
if (c.req.header("if-none-match") === etag)
|
|
11897
|
+
return c.body(null, 304, { etag });
|
|
10387
11898
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
10388
|
-
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
11899
|
+
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain",
|
|
11900
|
+
"cache-control": "no-cache",
|
|
11901
|
+
etag
|
|
10389
11902
|
});
|
|
10390
11903
|
});
|
|
10391
11904
|
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
|