@ra3orblade/swarm 0.11.3 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/swarm-hook.js +4 -1
- package/dist/swarm.js +4 -1
- package/dist/swarmd.js +721 -8
- package/package.json +1 -1
- package/web/app.js +501 -19
- package/web/index.html +99 -12
- package/web/release-notes.js +1 -1
- package/web/viz.js +42 -5
package/dist/swarmd.js
CHANGED
|
@@ -830,6 +830,9 @@ var AUDIT_TYPES = new Set([
|
|
|
830
830
|
"claim.released",
|
|
831
831
|
"claim.expired",
|
|
832
832
|
"claim.orphaned",
|
|
833
|
+
"claim.denied",
|
|
834
|
+
"rules.changed",
|
|
835
|
+
"worktree.reclaimed",
|
|
833
836
|
"worktree.created",
|
|
834
837
|
"worktree.removed",
|
|
835
838
|
"worktree.bootstrapped",
|
|
@@ -2202,6 +2205,82 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
|
2202
2205
|
contested: out.filter((f) => f.contested).length
|
|
2203
2206
|
};
|
|
2204
2207
|
}
|
|
2208
|
+
// packages/core/src/heat.ts
|
|
2209
|
+
var HEAT_DEFAULTS = {
|
|
2210
|
+
floor: 2,
|
|
2211
|
+
candidateSessions: 2,
|
|
2212
|
+
candidateRereads: 3,
|
|
2213
|
+
candidateWriteShare: 0.2,
|
|
2214
|
+
top: 40
|
|
2215
|
+
};
|
|
2216
|
+
function dirOf(path) {
|
|
2217
|
+
const i = path.lastIndexOf("/");
|
|
2218
|
+
return i <= 0 ? i === 0 ? "/" : "." : path.slice(0, i);
|
|
2219
|
+
}
|
|
2220
|
+
function fileHeat(rows, opts = {}, writeTools = WRITE_TOOLS) {
|
|
2221
|
+
const o = { ...HEAT_DEFAULTS, ...opts };
|
|
2222
|
+
const files = new Map;
|
|
2223
|
+
for (const r of rows) {
|
|
2224
|
+
if (!r.path || !r.sessionId)
|
|
2225
|
+
continue;
|
|
2226
|
+
const f = files.get(r.path) ?? { touches: 0, reads: 0, writes: 0, perSession: new Map };
|
|
2227
|
+
f.touches++;
|
|
2228
|
+
if (writeTools.has(r.tool))
|
|
2229
|
+
f.writes++;
|
|
2230
|
+
else
|
|
2231
|
+
f.reads++;
|
|
2232
|
+
f.perSession.set(r.sessionId, (f.perSession.get(r.sessionId) ?? 0) + 1);
|
|
2233
|
+
files.set(r.path, f);
|
|
2234
|
+
}
|
|
2235
|
+
const all = [...files.entries()].map(([path, f]) => {
|
|
2236
|
+
const rereads = [...f.perSession.values()].reduce((n, c) => n + (c - 1), 0);
|
|
2237
|
+
const writeShare = f.touches ? f.writes / f.touches : 0;
|
|
2238
|
+
return {
|
|
2239
|
+
path,
|
|
2240
|
+
touches: f.touches,
|
|
2241
|
+
sessions: f.perSession.size,
|
|
2242
|
+
reads: f.reads,
|
|
2243
|
+
writes: f.writes,
|
|
2244
|
+
rereads,
|
|
2245
|
+
candidate: f.perSession.size >= o.candidateSessions && rereads >= o.candidateRereads && writeShare <= o.candidateWriteShare
|
|
2246
|
+
};
|
|
2247
|
+
});
|
|
2248
|
+
const byTouches = (a, b) => b.touches - a.touches || b.sessions - a.sessions || a.path.localeCompare(b.path);
|
|
2249
|
+
const dirs = new Map;
|
|
2250
|
+
for (const [path, f] of files) {
|
|
2251
|
+
const d = dirOf(path);
|
|
2252
|
+
const cur = dirs.get(d) ?? {
|
|
2253
|
+
touches: 0,
|
|
2254
|
+
writes: 0,
|
|
2255
|
+
sessions: new Set,
|
|
2256
|
+
files: new Set
|
|
2257
|
+
};
|
|
2258
|
+
cur.touches += f.touches;
|
|
2259
|
+
cur.writes += f.writes;
|
|
2260
|
+
cur.files.add(path);
|
|
2261
|
+
for (const s of f.perSession.keys())
|
|
2262
|
+
cur.sessions.add(s);
|
|
2263
|
+
dirs.set(d, cur);
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
files: all.filter((f) => f.touches >= o.floor).sort(byTouches).slice(0, o.top),
|
|
2267
|
+
dirs: [...dirs.entries()].map(([dir, d]) => ({
|
|
2268
|
+
dir,
|
|
2269
|
+
touches: d.touches,
|
|
2270
|
+
sessions: d.sessions.size,
|
|
2271
|
+
files: d.files.size,
|
|
2272
|
+
writes: d.writes
|
|
2273
|
+
})).sort((a, b) => b.touches - a.touches || a.dir.localeCompare(b.dir)).slice(0, o.top),
|
|
2274
|
+
candidates: all.filter((f) => f.candidate).sort((a, b) => b.rereads - a.rereads || byTouches(a, b)).slice(0, o.top),
|
|
2275
|
+
totals: {
|
|
2276
|
+
files: all.length,
|
|
2277
|
+
touches: all.reduce((n, f) => n + f.touches, 0),
|
|
2278
|
+
sessions: new Set(rows.filter((r) => r.path && r.sessionId).map((r) => r.sessionId)).size,
|
|
2279
|
+
rereads: all.reduce((n, f) => n + f.rereads, 0),
|
|
2280
|
+
cold: all.filter((f) => f.touches === 1).length
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2205
2284
|
// packages/core/src/worktree.ts
|
|
2206
2285
|
import { join as join3 } from "path";
|
|
2207
2286
|
function planBootstrap(cfg, repoRoot, worktree) {
|
|
@@ -2278,7 +2357,7 @@ function planGc(worktrees, claims) {
|
|
|
2278
2357
|
|
|
2279
2358
|
// packages/core/src/hygiene.ts
|
|
2280
2359
|
var HYGIENE_DEFAULTS = {
|
|
2281
|
-
staleDays:
|
|
2360
|
+
staleDays: 2,
|
|
2282
2361
|
abandonedDays: 30,
|
|
2283
2362
|
hungryRssKb: 1024 * 1024,
|
|
2284
2363
|
heavyKb: 2 * 1024 * 1024
|
|
@@ -2309,6 +2388,30 @@ function classifyProcess(p, opts = {}) {
|
|
|
2309
2388
|
};
|
|
2310
2389
|
return { ...p, issue: null, note: null, reclaimable: false };
|
|
2311
2390
|
}
|
|
2391
|
+
var BUILD_DIRS = ["node_modules", "target", "dist", ".next", ".turbo"];
|
|
2392
|
+
function reclaimPlan(w, dirs, others = []) {
|
|
2393
|
+
const refusals = [];
|
|
2394
|
+
if (w.main)
|
|
2395
|
+
refusals.push("this is the main checkout");
|
|
2396
|
+
if (w.liveSessions > 0)
|
|
2397
|
+
refusals.push(`${w.liveSessions} live session${w.liveSessions === 1 ? "" : "s"} in it`);
|
|
2398
|
+
if (w.heldByClaim)
|
|
2399
|
+
refusals.push(`claimed by ${w.heldByClaim}`);
|
|
2400
|
+
const inside = dirs.filter((d) => d.path.startsWith(`${w.path}/`));
|
|
2401
|
+
if (inside.length !== dirs.length)
|
|
2402
|
+
refusals.push("a candidate lay outside the worktree");
|
|
2403
|
+
const named = inside.filter((d) => BUILD_DIRS.includes(d.path.split("/").pop() ?? ""));
|
|
2404
|
+
if (named.length !== inside.length)
|
|
2405
|
+
refusals.push("a candidate was not a build directory");
|
|
2406
|
+
const nested = others.filter((o) => o !== w.path && o.startsWith(`${w.path}/`));
|
|
2407
|
+
const mine = named.filter((d) => !nested.some((n) => d.path.startsWith(`${n}/`)));
|
|
2408
|
+
return {
|
|
2409
|
+
path: w.path,
|
|
2410
|
+
dirs: mine.map((d) => d.path),
|
|
2411
|
+
kb: mine.reduce((n, d) => n + d.kb, 0),
|
|
2412
|
+
refusals
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2312
2415
|
function classifyWorktree(w, opts = {}) {
|
|
2313
2416
|
const o = { ...HYGIENE_DEFAULTS, ...opts };
|
|
2314
2417
|
const kb = w.diskKb ?? 0;
|
|
@@ -3366,6 +3469,107 @@ function formatOpenQuestions(qs) {
|
|
|
3366
3469
|
return null;
|
|
3367
3470
|
return `[swarm] waiting on a human for: ${open.map((q) => `#${q.id} "${q.text.slice(0, 120)}"`).join("; ")} \u2014 the answer arrives as context on a later tool call, or via swarm_inbox`;
|
|
3368
3471
|
}
|
|
3472
|
+
// packages/core/src/resourcegraph.ts
|
|
3473
|
+
var nodeId = (kind, name, projectId) => `${kind}:${projectId ?? ""}:${name}`;
|
|
3474
|
+
function resourceGraph(held, wanted = [], now = Date.now()) {
|
|
3475
|
+
const resources = new Map;
|
|
3476
|
+
const holders = new Map;
|
|
3477
|
+
for (const h of held) {
|
|
3478
|
+
if (!h.name || !h.owner)
|
|
3479
|
+
continue;
|
|
3480
|
+
const id = nodeId(h.kind, h.name, h.projectId);
|
|
3481
|
+
const expired = h.expiresAt ? Date.parse(h.expiresAt) < now : false;
|
|
3482
|
+
const orphaned = Boolean(h.sessionEndedAt) || expired;
|
|
3483
|
+
if (!resources.has(id))
|
|
3484
|
+
resources.set(id, {
|
|
3485
|
+
id,
|
|
3486
|
+
kind: h.kind,
|
|
3487
|
+
name: h.name,
|
|
3488
|
+
holder: h.owner,
|
|
3489
|
+
orphaned,
|
|
3490
|
+
wanted: [],
|
|
3491
|
+
projectId: h.projectId ?? null
|
|
3492
|
+
});
|
|
3493
|
+
const o = holders.get(h.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3494
|
+
o.holds++;
|
|
3495
|
+
if (!orphaned)
|
|
3496
|
+
o.live++;
|
|
3497
|
+
holders.set(h.owner, o);
|
|
3498
|
+
}
|
|
3499
|
+
for (const w of wanted) {
|
|
3500
|
+
if (!w.name || !w.owner)
|
|
3501
|
+
continue;
|
|
3502
|
+
const r = resources.get(nodeId(w.kind, w.name, w.projectId));
|
|
3503
|
+
if (!r || r.holder === w.owner)
|
|
3504
|
+
continue;
|
|
3505
|
+
if (!r.wanted.includes(w.owner))
|
|
3506
|
+
r.wanted.push(w.owner);
|
|
3507
|
+
const o = holders.get(w.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3508
|
+
o.wants++;
|
|
3509
|
+
holders.set(w.owner, o);
|
|
3510
|
+
}
|
|
3511
|
+
for (const r of resources.values())
|
|
3512
|
+
r.wanted.sort();
|
|
3513
|
+
const edges = [];
|
|
3514
|
+
for (const r of [...resources.values()].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
3515
|
+
if (r.holder)
|
|
3516
|
+
edges.push({ from: r.holder, to: r.id, kind: "holds" });
|
|
3517
|
+
for (const w of r.wanted)
|
|
3518
|
+
edges.push({ from: w, to: r.id, kind: "wants" });
|
|
3519
|
+
}
|
|
3520
|
+
const resourceList = [...resources.values()].sort((a, b) => Number(b.wanted.length > 0) - Number(a.wanted.length > 0) || Number(b.orphaned) - Number(a.orphaned) || a.id.localeCompare(b.id));
|
|
3521
|
+
const holderList = [...holders.entries()].map(([id, o]) => ({ id, holds: o.holds, wants: o.wants, gone: o.holds > 0 && o.live === 0 })).sort((a, b) => b.holds + b.wants - (a.holds + a.wants) || a.id.localeCompare(b.id));
|
|
3522
|
+
return {
|
|
3523
|
+
holders: holderList,
|
|
3524
|
+
resources: resourceList,
|
|
3525
|
+
edges,
|
|
3526
|
+
contention: findContention(resourceList),
|
|
3527
|
+
totals: {
|
|
3528
|
+
held: resourceList.length,
|
|
3529
|
+
orphaned: resourceList.filter((r) => r.orphaned).length,
|
|
3530
|
+
contested: resourceList.filter((r) => r.wanted.length > 0).length
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function findContention(resources) {
|
|
3535
|
+
const next = new Map;
|
|
3536
|
+
for (const r of resources) {
|
|
3537
|
+
if (!r.holder)
|
|
3538
|
+
continue;
|
|
3539
|
+
for (const w of r.wanted) {
|
|
3540
|
+
const list = next.get(w) ?? [];
|
|
3541
|
+
list.push({ to: r.holder, via: r.name });
|
|
3542
|
+
next.set(w, list);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
for (const list of next.values())
|
|
3546
|
+
list.sort((a, b) => a.to.localeCompare(b.to) || a.via.localeCompare(b.via));
|
|
3547
|
+
const found = new Map;
|
|
3548
|
+
const walk = (start, at, owners, vias, depth) => {
|
|
3549
|
+
if (depth > 6)
|
|
3550
|
+
return;
|
|
3551
|
+
for (const step of next.get(at) ?? []) {
|
|
3552
|
+
if (step.to === start) {
|
|
3553
|
+
const ring = [...owners];
|
|
3554
|
+
const res = [...vias, step.via];
|
|
3555
|
+
const pivot = ring.indexOf([...ring].sort()[0]);
|
|
3556
|
+
const key = [...ring.slice(pivot), ...ring.slice(0, pivot)].join(">");
|
|
3557
|
+
if (!found.has(key))
|
|
3558
|
+
found.set(key, {
|
|
3559
|
+
owners: [...ring.slice(pivot), ...ring.slice(0, pivot)],
|
|
3560
|
+
resources: [...res.slice(pivot), ...res.slice(0, pivot)]
|
|
3561
|
+
});
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
if (owners.includes(step.to))
|
|
3565
|
+
continue;
|
|
3566
|
+
walk(start, step.to, [...owners, step.to], [...vias, step.via], depth + 1);
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
for (const owner of [...next.keys()].sort())
|
|
3570
|
+
walk(owner, owner, [owner], [], 0);
|
|
3571
|
+
return [...found.values()].sort((a, b) => a.owners.length - b.owners.length || a.owners.join().localeCompare(b.owners.join()));
|
|
3572
|
+
}
|
|
3369
3573
|
// packages/core/src/resources.ts
|
|
3370
3574
|
var DEFAULT_RESOURCE_LEASE_MINUTES = 60;
|
|
3371
3575
|
function isTrackedPid(pid) {
|
|
@@ -3499,6 +3703,202 @@ ${lines.join(`
|
|
|
3499
3703
|
`)}` : ""}`
|
|
3500
3704
|
};
|
|
3501
3705
|
}
|
|
3706
|
+
// packages/core/src/ruleeffect.ts
|
|
3707
|
+
var DAY = 86400000;
|
|
3708
|
+
var dayOf = (iso) => iso.slice(0, 10);
|
|
3709
|
+
function commandSignature(command) {
|
|
3710
|
+
const segments = (command ?? "").split(/&&|\|\||[;\n|]/).map((x) => x.trim()).filter(Boolean);
|
|
3711
|
+
const first = segments.find((seg) => !/^(cd|pushd|export|source|\.)\b/.test(seg)) ?? segments[0] ?? "";
|
|
3712
|
+
if (!first)
|
|
3713
|
+
return "(none)";
|
|
3714
|
+
const tokens = first.split(/\s+/).filter(Boolean);
|
|
3715
|
+
const head = tokens[0] ?? "";
|
|
3716
|
+
const name = head.includes("/") ? head.split("/").pop() ?? head : head;
|
|
3717
|
+
const second = tokens[1];
|
|
3718
|
+
const more = tokens.length > 2;
|
|
3719
|
+
return `${name}${second ? ` ${second}` : ""}${more ? " \u2026" : ""}`;
|
|
3720
|
+
}
|
|
3721
|
+
function ruleEffect(incidents, changes = [], now = Date.now(), days3 = 30) {
|
|
3722
|
+
const since = now - days3 * DAY;
|
|
3723
|
+
const rows = incidents.filter((i) => i.rule && Date.parse(i.at) >= since);
|
|
3724
|
+
const byRule = new Map;
|
|
3725
|
+
for (const i of rows)
|
|
3726
|
+
byRule.set(i.rule, [...byRule.get(i.rule) ?? [], i]);
|
|
3727
|
+
const dayList = [];
|
|
3728
|
+
for (let t = since;t <= now; t += DAY)
|
|
3729
|
+
dayList.push(dayOf(new Date(t).toISOString()));
|
|
3730
|
+
const rules = [...byRule.entries()].map(([rule, list]) => {
|
|
3731
|
+
const sorted = [...list].sort((a, b) => a.at.localeCompare(b.at));
|
|
3732
|
+
const counts = new Map;
|
|
3733
|
+
for (const i of sorted)
|
|
3734
|
+
counts.set(dayOf(i.at), (counts.get(dayOf(i.at)) ?? 0) + 1);
|
|
3735
|
+
const perDay = dayList.map((day) => ({ day, n: counts.get(day) ?? 0 }));
|
|
3736
|
+
const half = Math.floor(perDay.length / 2);
|
|
3737
|
+
const early = perDay.slice(0, half).reduce((n, d) => n + d.n, 0);
|
|
3738
|
+
const late = perDay.slice(half).reduce((n, d) => n + d.n, 0);
|
|
3739
|
+
const trend = Math.abs(late - early) <= 1 ? "steady" : late > early ? "rising" : "falling";
|
|
3740
|
+
const sig = new Map;
|
|
3741
|
+
for (const i of sorted) {
|
|
3742
|
+
const key = commandSignature(i.command);
|
|
3743
|
+
const cur = sig.get(key) ?? { hits: 0, example: i.command };
|
|
3744
|
+
cur.hits++;
|
|
3745
|
+
sig.set(key, cur);
|
|
3746
|
+
}
|
|
3747
|
+
const clusters = [...sig.entries()].map(([signature, c]) => ({ signature, hits: c.hits, example: c.example })).sort((a, b) => b.hits - a.hits || a.signature.localeCompare(b.signature));
|
|
3748
|
+
const added = changes.filter((c) => c.added.includes(rule) && Date.parse(c.at) >= since).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
3749
|
+
let landed = null;
|
|
3750
|
+
if (added) {
|
|
3751
|
+
const at = Date.parse(added.at);
|
|
3752
|
+
const beforeDays = Math.max(1, (at - since) / DAY);
|
|
3753
|
+
const afterDays = Math.max(1, (now - at) / DAY);
|
|
3754
|
+
landed = {
|
|
3755
|
+
at: added.at,
|
|
3756
|
+
beforePerDay: sorted.filter((i) => Date.parse(i.at) < at).length / beforeDays,
|
|
3757
|
+
afterPerDay: sorted.filter((i) => Date.parse(i.at) >= at).length / afterDays
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
return {
|
|
3761
|
+
rule,
|
|
3762
|
+
total: sorted.length,
|
|
3763
|
+
acked: sorted.filter((i) => i.acked).length,
|
|
3764
|
+
firstAt: sorted[0].at,
|
|
3765
|
+
lastAt: sorted.at(-1).at,
|
|
3766
|
+
perDay,
|
|
3767
|
+
trend,
|
|
3768
|
+
concentration: sorted.length ? (clusters[0]?.hits ?? 0) / sorted.length : 0,
|
|
3769
|
+
clusters: clusters.slice(0, 5),
|
|
3770
|
+
landed
|
|
3771
|
+
};
|
|
3772
|
+
});
|
|
3773
|
+
rules.sort((a, b) => b.total - a.total || a.rule.localeCompare(b.rule));
|
|
3774
|
+
return {
|
|
3775
|
+
rules,
|
|
3776
|
+
totals: {
|
|
3777
|
+
incidents: rows.length,
|
|
3778
|
+
rules: rules.length,
|
|
3779
|
+
acked: rows.filter((i) => i.acked).length,
|
|
3780
|
+
unchanged: rules.filter((r) => r.trend !== "falling" && r.total > 1).length
|
|
3781
|
+
},
|
|
3782
|
+
noChangeHistory: !changes.some((c) => Date.parse(c.at) >= since)
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
// packages/core/src/security.ts
|
|
3786
|
+
var LOCAL = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i;
|
|
3787
|
+
function hostsIn(text) {
|
|
3788
|
+
const out = new Set;
|
|
3789
|
+
for (const m of text.matchAll(/\bhttps?:\/\/(\[[^\]]+\][^/\s"'`)}>,;]*|[^/\s"'`)\]}>,;]+)/gi)) {
|
|
3790
|
+
const hostPort = m[1].replace(/^[^@]*@/, "");
|
|
3791
|
+
const bracket = /^\[([^\]]+)\]/.exec(hostPort);
|
|
3792
|
+
const host = (bracket?.[1] ?? hostPort.split(":")[0]).toLowerCase();
|
|
3793
|
+
if (host)
|
|
3794
|
+
out.add(host);
|
|
3795
|
+
}
|
|
3796
|
+
return [...out].sort();
|
|
3797
|
+
}
|
|
3798
|
+
var INSTALLERS = [
|
|
3799
|
+
{ ecosystem: "npm", re: /^npm\s+(?:i|install|add)\b(.*)$/i },
|
|
3800
|
+
{ ecosystem: "pnpm", re: /^pnpm\s+(?:i|install|add)\b(.*)$/i },
|
|
3801
|
+
{ ecosystem: "yarn", re: /^yarn\s+add\b(.*)$/i },
|
|
3802
|
+
{ ecosystem: "bun", re: /^bun\s+(?:i|install|add)\b(.*)$/i },
|
|
3803
|
+
{ ecosystem: "pip", re: /^pip3?\s+install\b(.*)$/i },
|
|
3804
|
+
{ ecosystem: "cargo", re: /^cargo\s+(?:add|install)\b(.*)$/i },
|
|
3805
|
+
{ ecosystem: "go", re: /^go\s+(?:get|install)\b(.*)$/i },
|
|
3806
|
+
{ ecosystem: "gem", re: /^gem\s+install\b(.*)$/i },
|
|
3807
|
+
{ ecosystem: "brew", re: /^brew\s+install\b(.*)$/i },
|
|
3808
|
+
{ ecosystem: "apt", re: /^(?:sudo\s+)?apt(?:-get)?\s+install\b(.*)$/i }
|
|
3809
|
+
];
|
|
3810
|
+
var PKG_NAME = /^(@[\w.-]+\/)?[a-z0-9][\w.\-/]*([@=<>~^]=?[\w.\-^~*]+)?$/i;
|
|
3811
|
+
function installsIn(text) {
|
|
3812
|
+
const out = [];
|
|
3813
|
+
for (const raw of text.split(/&&|\|\||[;\n|]/)) {
|
|
3814
|
+
const segment = raw.trim();
|
|
3815
|
+
if (!segment)
|
|
3816
|
+
continue;
|
|
3817
|
+
for (const { ecosystem, re } of INSTALLERS) {
|
|
3818
|
+
const m = re.exec(segment);
|
|
3819
|
+
if (!m)
|
|
3820
|
+
continue;
|
|
3821
|
+
const args = [];
|
|
3822
|
+
for (const tok of (m[1] ?? "").split(/\s+/)) {
|
|
3823
|
+
const a = tok.trim();
|
|
3824
|
+
if (!a)
|
|
3825
|
+
continue;
|
|
3826
|
+
if (/^\d*[<>&]/.test(a))
|
|
3827
|
+
break;
|
|
3828
|
+
if (a.startsWith("-"))
|
|
3829
|
+
continue;
|
|
3830
|
+
if (PKG_NAME.test(a))
|
|
3831
|
+
args.push(a);
|
|
3832
|
+
}
|
|
3833
|
+
out.push(...args.length ? args.map((pkg) => ({ ecosystem, pkg })) : [{ ecosystem, pkg: "(from manifest)" }]);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return out;
|
|
3837
|
+
}
|
|
3838
|
+
var SECRETS = [
|
|
3839
|
+
{ what: ".env file", re: /(^|[/\s"'`])\.env(\.[\w-]+)?\b/i },
|
|
3840
|
+
{ what: "SSH private key", re: /(^|[/\s"'`])(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b/i },
|
|
3841
|
+
{ what: "AWS credentials", re: /\.aws\/(credentials|config)\b/i },
|
|
3842
|
+
{ what: "npm token", re: /(^|[/\s"'`])\.npmrc\b/i },
|
|
3843
|
+
{ what: "kubeconfig", re: /\.kube\/config\b/i },
|
|
3844
|
+
{ what: "Google cloud credentials", re: /gcloud\/[\w-]*credential/i },
|
|
3845
|
+
{ what: "PEM or key file", re: /\.(pem|p12|pfx|key)\b/i },
|
|
3846
|
+
{ what: "macOS keychain", re: /\bsecurity\s+find-(generic|internet)-password\b/i },
|
|
3847
|
+
{ what: "netrc", re: /(^|[/\s"'`])\.netrc\b/i }
|
|
3848
|
+
];
|
|
3849
|
+
function secretsIn(text) {
|
|
3850
|
+
return SECRETS.filter((s) => s.re.test(text)).map((s) => s.what);
|
|
3851
|
+
}
|
|
3852
|
+
function securityScan(rows) {
|
|
3853
|
+
const egress = new Map;
|
|
3854
|
+
const installs = new Map;
|
|
3855
|
+
const secrets = new Map;
|
|
3856
|
+
let scanned = 0;
|
|
3857
|
+
for (const r of rows) {
|
|
3858
|
+
if (!r.sessionId)
|
|
3859
|
+
continue;
|
|
3860
|
+
const text = `${r.command ?? ""} ${r.path ?? ""}`.trim();
|
|
3861
|
+
if (!text)
|
|
3862
|
+
continue;
|
|
3863
|
+
scanned++;
|
|
3864
|
+
for (const host of hostsIn(text)) {
|
|
3865
|
+
const e = egress.get(host) ?? { hits: 0, sessions: new Set };
|
|
3866
|
+
e.hits++;
|
|
3867
|
+
e.sessions.add(r.sessionId);
|
|
3868
|
+
egress.set(host, e);
|
|
3869
|
+
}
|
|
3870
|
+
for (const i of installsIn(r.command ?? "")) {
|
|
3871
|
+
const key = `${i.ecosystem} ${i.pkg}`;
|
|
3872
|
+
const cur = installs.get(key) ?? { ...i, hits: 0, sessions: new Set };
|
|
3873
|
+
cur.hits++;
|
|
3874
|
+
cur.sessions.add(r.sessionId);
|
|
3875
|
+
installs.set(key, cur);
|
|
3876
|
+
}
|
|
3877
|
+
for (const what of secretsIn(text)) {
|
|
3878
|
+
const cur = secrets.get(what) ?? { hits: 0, sessions: new Set };
|
|
3879
|
+
cur.hits++;
|
|
3880
|
+
cur.sessions.add(r.sessionId);
|
|
3881
|
+
secrets.set(what, cur);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
const egressList = [...egress.entries()].map(([host, e]) => ({
|
|
3885
|
+
host,
|
|
3886
|
+
hits: e.hits,
|
|
3887
|
+
sessions: e.sessions.size,
|
|
3888
|
+
local: LOCAL.test(host)
|
|
3889
|
+
})).sort((a, b) => Number(a.local) - Number(b.local) || b.hits - a.hits || a.host.localeCompare(b.host));
|
|
3890
|
+
return {
|
|
3891
|
+
egress: egressList,
|
|
3892
|
+
installs: [...installs.values()].map((i) => ({ ecosystem: i.ecosystem, pkg: i.pkg, hits: i.hits, sessions: i.sessions.size })).sort((a, b) => b.hits - a.hits || a.ecosystem.localeCompare(b.ecosystem) || a.pkg.localeCompare(b.pkg)),
|
|
3893
|
+
secrets: [...secrets.entries()].map(([what, s]) => ({ what, hits: s.hits, sessions: s.sessions.size })).sort((a, b) => b.hits - a.hits || a.what.localeCompare(b.what)),
|
|
3894
|
+
totals: {
|
|
3895
|
+
scanned,
|
|
3896
|
+
remoteHosts: egressList.filter((h) => !h.local).length,
|
|
3897
|
+
installs: [...installs.values()].reduce((n, i) => n + i.hits, 0),
|
|
3898
|
+
secrets: [...secrets.values()].reduce((n, s) => n + s.hits, 0)
|
|
3899
|
+
}
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3502
3902
|
// packages/core/src/stall.ts
|
|
3503
3903
|
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
3504
3904
|
function toolResponseErrored(resp) {
|
|
@@ -3730,6 +4130,70 @@ function clusterProjectKey(remoteUrl) {
|
|
|
3730
4130
|
return null;
|
|
3731
4131
|
return `${host}/${m[2]}`;
|
|
3732
4132
|
}
|
|
4133
|
+
// packages/core/src/transitions.ts
|
|
4134
|
+
function transitionGraph(steps, { minWeight = 1 } = {}) {
|
|
4135
|
+
const calls = new Map;
|
|
4136
|
+
const selfLoops = new Map;
|
|
4137
|
+
const edges = new Map;
|
|
4138
|
+
const bySession = new Map;
|
|
4139
|
+
let transitions = 0;
|
|
4140
|
+
for (const s of steps) {
|
|
4141
|
+
if (!s.sessionId || !s.tool)
|
|
4142
|
+
continue;
|
|
4143
|
+
calls.set(s.tool, (calls.get(s.tool) ?? 0) + 1);
|
|
4144
|
+
const prev = bySession.get(s.sessionId);
|
|
4145
|
+
if (prev !== undefined) {
|
|
4146
|
+
const key = `${prev}\x00${s.tool}`;
|
|
4147
|
+
const e = edges.get(key) ?? {
|
|
4148
|
+
from: prev,
|
|
4149
|
+
to: s.tool,
|
|
4150
|
+
weight: 0,
|
|
4151
|
+
sessions: new Set
|
|
4152
|
+
};
|
|
4153
|
+
e.weight++;
|
|
4154
|
+
e.sessions.add(s.sessionId);
|
|
4155
|
+
edges.set(key, e);
|
|
4156
|
+
if (prev === s.tool)
|
|
4157
|
+
selfLoops.set(s.tool, (selfLoops.get(s.tool) ?? 0) + 1);
|
|
4158
|
+
transitions++;
|
|
4159
|
+
}
|
|
4160
|
+
bySession.set(s.sessionId, s.tool);
|
|
4161
|
+
}
|
|
4162
|
+
const edgeList = [...edges.values()].filter((e) => e.weight >= minWeight).map((e) => ({ from: e.from, to: e.to, weight: e.weight, sessions: e.sessions.size })).sort((a, b) => b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
4163
|
+
const nodeList = [...calls.entries()].map(([tool, n]) => ({ tool, calls: n, selfLoops: selfLoops.get(tool) ?? 0 })).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
|
|
4164
|
+
return {
|
|
4165
|
+
nodes: nodeList,
|
|
4166
|
+
edges: edgeList,
|
|
4167
|
+
loops: findLoops(edgeList),
|
|
4168
|
+
sessions: bySession.size,
|
|
4169
|
+
steps: steps.filter((s) => s.sessionId && s.tool).length,
|
|
4170
|
+
transitions
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
function findLoops(edges) {
|
|
4174
|
+
const at = new Map(edges.map((e) => [`${e.from}\x00${e.to}`, e]));
|
|
4175
|
+
const out = [];
|
|
4176
|
+
const seen = new Set;
|
|
4177
|
+
for (const e of edges) {
|
|
4178
|
+
if (e.from === e.to) {
|
|
4179
|
+
out.push({ tools: [e.from], weight: e.weight, sessions: e.sessions });
|
|
4180
|
+
continue;
|
|
4181
|
+
}
|
|
4182
|
+
const back = at.get(`${e.to}\x00${e.from}`);
|
|
4183
|
+
if (!back)
|
|
4184
|
+
continue;
|
|
4185
|
+
const pair = [e.from, e.to].sort().join("\x00");
|
|
4186
|
+
if (seen.has(pair))
|
|
4187
|
+
continue;
|
|
4188
|
+
seen.add(pair);
|
|
4189
|
+
out.push({
|
|
4190
|
+
tools: [e.from, e.to],
|
|
4191
|
+
weight: Math.min(e.weight, back.weight),
|
|
4192
|
+
sessions: Math.max(e.sessions, back.sessions)
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
return out.sort((a, b) => b.weight - a.weight || a.tools.join().localeCompare(b.tools.join()));
|
|
4196
|
+
}
|
|
3733
4197
|
// packages/core/src/waiting.ts
|
|
3734
4198
|
var emptyByKind = () => ({
|
|
3735
4199
|
permission: { episodes: 0, blockedMs: 0 },
|
|
@@ -6028,12 +6492,19 @@ function applyStatus(w, st, ah) {
|
|
|
6028
6492
|
const a = ah?.trim();
|
|
6029
6493
|
w.ahead = a === undefined || a === "" ? -1 : Number(a);
|
|
6030
6494
|
}
|
|
6031
|
-
function applyDrift(w, behind, ancestor, firstParents) {
|
|
6495
|
+
function applyDrift(w, behind, ancestor, firstParents, cherry = null) {
|
|
6032
6496
|
const b = behind?.trim();
|
|
6033
6497
|
w.behind = b === undefined || b === "" ? -1 : Number(b);
|
|
6034
6498
|
const onLine = firstParents?.split(`
|
|
6035
6499
|
`).some((sha) => sha.startsWith(w.head)) ?? true;
|
|
6036
|
-
w.merged = ancestor && !onLine;
|
|
6500
|
+
w.merged = (ancestor || squashed(cherry)) && !onLine;
|
|
6501
|
+
}
|
|
6502
|
+
function squashed(cherry) {
|
|
6503
|
+
if (!cherry)
|
|
6504
|
+
return false;
|
|
6505
|
+
const lines = cherry.split(`
|
|
6506
|
+
`).filter((l) => l.trim());
|
|
6507
|
+
return lines.length > 0 && lines.every((l) => l.startsWith("-"));
|
|
6037
6508
|
}
|
|
6038
6509
|
var FIRST_PARENT_DEPTH = "5000";
|
|
6039
6510
|
var baseOf = (wts) => wts[0]?.main ? wts[0].branch : null;
|
|
@@ -6055,15 +6526,16 @@ async function listWorktreesAsync(root) {
|
|
|
6055
6526
|
const line = base ? await gitAsync(root, ["rev-list", "--first-parent", "-n", FIRST_PARENT_DEPTH, base]) : null;
|
|
6056
6527
|
await Promise.all(wts.map(async (w) => {
|
|
6057
6528
|
const drift = base && !w.main;
|
|
6058
|
-
const [st, ah, be, mg] = await Promise.all([
|
|
6529
|
+
const [st, ah, be, mg, ch] = await Promise.all([
|
|
6059
6530
|
gitAsync(w.path, ["status", "--porcelain", "--untracked-files=no"]),
|
|
6060
6531
|
gitAsync(w.path, ["rev-list", "--count", "@{upstream}..HEAD"]),
|
|
6061
6532
|
drift ? gitAsync(w.path, ["rev-list", "--count", `HEAD..${base}`]) : null,
|
|
6062
|
-
drift ? gitAsync(w.path, ["merge-base", "--is-ancestor", "HEAD", base]) : null
|
|
6533
|
+
drift ? gitAsync(w.path, ["merge-base", "--is-ancestor", "HEAD", base]) : null,
|
|
6534
|
+
drift ? gitAsync(w.path, ["cherry", base, "HEAD"]) : null
|
|
6063
6535
|
]);
|
|
6064
6536
|
applyStatus(w, st, ah);
|
|
6065
6537
|
if (drift)
|
|
6066
|
-
applyDrift(w, be, mg !== null, line);
|
|
6538
|
+
applyDrift(w, be, mg !== null, line, ch);
|
|
6067
6539
|
}));
|
|
6068
6540
|
return wts;
|
|
6069
6541
|
}
|
|
@@ -6533,6 +7005,7 @@ import {
|
|
|
6533
7005
|
readSync,
|
|
6534
7006
|
realpathSync as realpathSync2,
|
|
6535
7007
|
renameSync,
|
|
7008
|
+
rmSync as rmSync2,
|
|
6536
7009
|
statSync,
|
|
6537
7010
|
unlinkSync,
|
|
6538
7011
|
writeFileSync as writeFileSync2
|
|
@@ -7891,8 +8364,46 @@ ${err}
|
|
|
7891
8364
|
const loaded = loadConfigDetailed({ repoRoot, home: this.home });
|
|
7892
8365
|
this.policyCache.set(key, { at: Date.now(), loaded });
|
|
7893
8366
|
this.writePolicyCache(loaded);
|
|
8367
|
+
this.noteRuleChange(key, loaded.config.rules);
|
|
7894
8368
|
return loaded;
|
|
7895
8369
|
}
|
|
8370
|
+
projectIdForRoot(root) {
|
|
8371
|
+
if (!root)
|
|
8372
|
+
return null;
|
|
8373
|
+
const row = this.db.query("SELECT id FROM projects WHERE root = ?").get(root);
|
|
8374
|
+
return row?.id ?? null;
|
|
8375
|
+
}
|
|
8376
|
+
noteRuleChange(key, rules2) {
|
|
8377
|
+
const sig = JSON.stringify(Object.entries(rules2).sort());
|
|
8378
|
+
const metaKey = `rules.sig:${key}`;
|
|
8379
|
+
const prev = this.db.query("SELECT value FROM meta WHERE key = ?").get(metaKey)?.value;
|
|
8380
|
+
if (prev === sig)
|
|
8381
|
+
return;
|
|
8382
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(metaKey, sig);
|
|
8383
|
+
if (prev === undefined)
|
|
8384
|
+
return;
|
|
8385
|
+
const before = new Map(JSON.parse(prev));
|
|
8386
|
+
const after = new Map(Object.entries(rules2));
|
|
8387
|
+
const added = [...after.keys()].filter((r) => !before.has(r)).sort();
|
|
8388
|
+
const removed = [...before.keys()].filter((r) => !after.has(r)).sort();
|
|
8389
|
+
const retuned = [...after.entries()].filter(([r, mode]) => before.has(r) && before.get(r) !== mode).map(([r, mode]) => `${r}=${mode}`).sort();
|
|
8390
|
+
if (!added.length && !removed.length && !retuned.length)
|
|
8391
|
+
return;
|
|
8392
|
+
this.append({
|
|
8393
|
+
ts: new Date().toISOString(),
|
|
8394
|
+
type: "rules.changed",
|
|
8395
|
+
projectId: this.projectIdForRoot(key) ?? "",
|
|
8396
|
+
sessionId: null,
|
|
8397
|
+
payload: {
|
|
8398
|
+
repo: key || null,
|
|
8399
|
+
added,
|
|
8400
|
+
removed,
|
|
8401
|
+
retuned,
|
|
8402
|
+
rules: [...after.keys()].sort(),
|
|
8403
|
+
summary: `rules changed${added.length ? ` +${added.join(",")}` : ""}${removed.length ? ` -${removed.join(",")}` : ""}${retuned.length ? ` ~${retuned.join(",")}` : ""}`
|
|
8404
|
+
}
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
7896
8407
|
writePolicyCache(loaded) {
|
|
7897
8408
|
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
7898
8409
|
try {
|
|
@@ -8928,8 +9439,24 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8928
9439
|
return { ok: false, error: "unknown project" };
|
|
8929
9440
|
const now = Date.now();
|
|
8930
9441
|
const decision = canClaim(this.claimRows(projectId), task, owner, now);
|
|
8931
|
-
if (!decision.ok)
|
|
9442
|
+
if (!decision.ok) {
|
|
9443
|
+
if (decision.heldBy !== owner)
|
|
9444
|
+
this.append({
|
|
9445
|
+
ts: new Date(now).toISOString(),
|
|
9446
|
+
type: "claim.denied",
|
|
9447
|
+
projectId,
|
|
9448
|
+
sessionId,
|
|
9449
|
+
actor: this.actorFor(owner, sessionId),
|
|
9450
|
+
payload: {
|
|
9451
|
+
task,
|
|
9452
|
+
owner,
|
|
9453
|
+
heldBy: decision.heldBy,
|
|
9454
|
+
until: decision.until,
|
|
9455
|
+
summary: `${owner} was refused ${task} \u2014 held by ${decision.heldBy}`
|
|
9456
|
+
}
|
|
9457
|
+
});
|
|
8932
9458
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
9459
|
+
}
|
|
8933
9460
|
const branch = `task/${task}`;
|
|
8934
9461
|
const worktree2 = this.worktreePath(projectId, task);
|
|
8935
9462
|
if (existsSync6(worktree2))
|
|
@@ -9129,7 +9656,34 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9129
9656
|
return newest ? Math.max(0, Date.now() - newest) : null;
|
|
9130
9657
|
}
|
|
9131
9658
|
duCache = new Map;
|
|
9659
|
+
buildCache = new Map;
|
|
9132
9660
|
duInflight = null;
|
|
9661
|
+
async measureBuild(path) {
|
|
9662
|
+
const args = ["-maxdepth", "4", "("];
|
|
9663
|
+
BUILD_DIRS.forEach((d, i) => {
|
|
9664
|
+
if (i)
|
|
9665
|
+
args.push("-o");
|
|
9666
|
+
args.push("-name", d);
|
|
9667
|
+
});
|
|
9668
|
+
args.push(")", "-type", "d", "-prune");
|
|
9669
|
+
const out = [];
|
|
9670
|
+
try {
|
|
9671
|
+
const find = Bun.spawn(["find", path, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
9672
|
+
const found = (await new Response(find.stdout).text()).split(`
|
|
9673
|
+
`).filter(Boolean);
|
|
9674
|
+
await find.exited;
|
|
9675
|
+
for (const dir of found) {
|
|
9676
|
+
const du = Bun.spawn(["du", "-sk", "-x", dir], { stdout: "pipe", stderr: "ignore" });
|
|
9677
|
+
const text = await new Response(du.stdout).text();
|
|
9678
|
+
if (await du.exited === 0) {
|
|
9679
|
+
const kb = Number.parseInt(text.trim().split(/\s+/)[0] ?? "", 10);
|
|
9680
|
+
if (Number.isFinite(kb))
|
|
9681
|
+
out.push({ path: dir, kb });
|
|
9682
|
+
}
|
|
9683
|
+
}
|
|
9684
|
+
} catch {}
|
|
9685
|
+
return out;
|
|
9686
|
+
}
|
|
9133
9687
|
refreshDisk(paths, ttlMs) {
|
|
9134
9688
|
if (this.duInflight)
|
|
9135
9689
|
return;
|
|
@@ -9152,6 +9706,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9152
9706
|
}
|
|
9153
9707
|
} catch {}
|
|
9154
9708
|
this.duCache.set(path, { v, t: Date.now() });
|
|
9709
|
+
this.buildCache.set(path, { dirs: await this.measureBuild(path), t: Date.now() });
|
|
9155
9710
|
}
|
|
9156
9711
|
})().finally(() => {
|
|
9157
9712
|
this.duInflight = null;
|
|
@@ -9211,6 +9766,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9211
9766
|
merged: w.merged,
|
|
9212
9767
|
idleMs: Store.worktreeIdleMs(w.path),
|
|
9213
9768
|
diskKb: this.duCache.get(w.path)?.v ?? null,
|
|
9769
|
+
buildKb: this.buildCache.has(w.path) ? this.buildCache.get(w.path).dirs.reduce((n, d) => n + d.kb, 0) : null,
|
|
9214
9770
|
heldByClaim: held?.task ?? null,
|
|
9215
9771
|
liveSessions: this.sessions().filter((s) => s.cwd?.startsWith(w.path) && !s.endedAt && s.state !== "ended").length
|
|
9216
9772
|
});
|
|
@@ -9491,6 +10047,151 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9491
10047
|
})
|
|
9492
10048
|
};
|
|
9493
10049
|
}
|
|
10050
|
+
transitions(projectId, days3 = 7, minWeight = 1) {
|
|
10051
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10052
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool
|
|
10053
|
+
FROM events
|
|
10054
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
10055
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
10056
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
10057
|
+
return transitionGraph(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "" })), { minWeight });
|
|
10058
|
+
}
|
|
10059
|
+
resourceHolding(projectId, days3 = 3) {
|
|
10060
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10061
|
+
const p = projectId ? " AND c.project_id = ?" : "";
|
|
10062
|
+
const args = projectId ? [projectId] : [];
|
|
10063
|
+
const ended = new Map(this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all().map((r) => [r.id, r.ended_at]));
|
|
10064
|
+
const claims = this.db.query(`SELECT c.task AS name, c.owner, c.project_id, c.expires_at, c.actor_id AS session_id
|
|
10065
|
+
FROM claims c WHERE c.state = 'held' AND c.released_at IS NULL${p}`).all(...args);
|
|
10066
|
+
const resources2 = this.db.query(`SELECT c.name, c.owner, c.project_id, c.expires_at, c.session_id, c.port
|
|
10067
|
+
FROM resources c WHERE c.released = 0${p}`).all(...args);
|
|
10068
|
+
const procs = this.db.query(`SELECT c.name, c.owner, c.project_id, c.session_id, c.port
|
|
10069
|
+
FROM processes c WHERE c.ended_at IS NULL${p}`).all(...args);
|
|
10070
|
+
const denials = this.db.query(`SELECT json_extract(payload,'$.task') AS name, json_extract(payload,'$.owner') AS owner,
|
|
10071
|
+
json_extract(payload,'$.heldBy') AS held_by, ts, project_id
|
|
10072
|
+
FROM events WHERE type = 'claim.denied' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...args);
|
|
10073
|
+
const held = [
|
|
10074
|
+
...claims.map((r) => ({
|
|
10075
|
+
kind: "claim",
|
|
10076
|
+
name: r.name,
|
|
10077
|
+
owner: r.owner,
|
|
10078
|
+
sessionId: r.session_id,
|
|
10079
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10080
|
+
expiresAt: r.expires_at,
|
|
10081
|
+
projectId: r.project_id
|
|
10082
|
+
})),
|
|
10083
|
+
...resources2.map((r) => ({
|
|
10084
|
+
kind: r.port ? "port" : "lease",
|
|
10085
|
+
name: r.port ? String(r.port) : r.name,
|
|
10086
|
+
owner: r.owner ?? "unknown",
|
|
10087
|
+
sessionId: r.session_id,
|
|
10088
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10089
|
+
expiresAt: r.expires_at,
|
|
10090
|
+
projectId: r.project_id
|
|
10091
|
+
})),
|
|
10092
|
+
...procs.map((r) => ({
|
|
10093
|
+
kind: "process",
|
|
10094
|
+
name: r.name ?? (r.port ? `:${r.port}` : "process"),
|
|
10095
|
+
owner: r.owner ?? "unknown",
|
|
10096
|
+
sessionId: r.session_id,
|
|
10097
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10098
|
+
expiresAt: null,
|
|
10099
|
+
projectId: r.project_id
|
|
10100
|
+
}))
|
|
10101
|
+
];
|
|
10102
|
+
const wanted = denials.filter((d) => d.name && d.owner && d.held_by).map((d) => ({
|
|
10103
|
+
kind: "claim",
|
|
10104
|
+
name: d.name,
|
|
10105
|
+
owner: d.owner,
|
|
10106
|
+
heldBy: d.held_by,
|
|
10107
|
+
at: d.ts,
|
|
10108
|
+
projectId: d.project_id
|
|
10109
|
+
}));
|
|
10110
|
+
return resourceGraph(held, wanted);
|
|
10111
|
+
}
|
|
10112
|
+
fileHeat(projectId, days3 = 14) {
|
|
10113
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10114
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
10115
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10116
|
+
FROM events
|
|
10117
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
10118
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10119
|
+
return fileHeat(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "", path: r.path ?? "" })));
|
|
10120
|
+
}
|
|
10121
|
+
security(projectId, days3 = 14) {
|
|
10122
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10123
|
+
const rows = this.db.query(`SELECT session_id, ts, json_extract(payload,'$.tool') AS tool,
|
|
10124
|
+
COALESCE(json_extract(payload,'$.toolInput.command'),
|
|
10125
|
+
json_extract(payload,'$.toolInput.url'), '') AS command,
|
|
10126
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10127
|
+
FROM events
|
|
10128
|
+
WHERE type = 'tool.requested' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10129
|
+
return securityScan(rows.map((r) => ({
|
|
10130
|
+
sessionId: r.session_id ?? "",
|
|
10131
|
+
tool: r.tool ?? "",
|
|
10132
|
+
command: r.command ?? "",
|
|
10133
|
+
path: r.path,
|
|
10134
|
+
at: r.ts
|
|
10135
|
+
})));
|
|
10136
|
+
}
|
|
10137
|
+
ruleEffect(projectId, days3 = 30) {
|
|
10138
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10139
|
+
const rows = this.db.query(`SELECT e.seq, e.ts,
|
|
10140
|
+
json_extract(e.payload,'$.rule') AS rule,
|
|
10141
|
+
COALESCE(json_extract(e.payload,'$.command'), '') AS command,
|
|
10142
|
+
(a.seq IS NOT NULL) AS acked
|
|
10143
|
+
FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10144
|
+
WHERE e.type = 'incident.opened' AND e.ts >= ?${projectId ? " AND e.project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10145
|
+
const changes = this.db.query(`SELECT ts, COALESCE(json_extract(payload,'$.added'), '[]') AS added
|
|
10146
|
+
FROM events WHERE type = 'rules.changed' AND ts >= ?`).all(since);
|
|
10147
|
+
return ruleEffect(rows.map((r) => ({
|
|
10148
|
+
rule: r.rule ?? "",
|
|
10149
|
+
command: r.command ?? "",
|
|
10150
|
+
at: r.ts,
|
|
10151
|
+
acked: Boolean(r.acked)
|
|
10152
|
+
})), changes.map((c) => ({ at: c.ts, added: JSON.parse(c.added) })), Date.now(), days3);
|
|
10153
|
+
}
|
|
10154
|
+
reclaimBuild(path, { dryRun = false } = {}) {
|
|
10155
|
+
const report = this.hygiene();
|
|
10156
|
+
const w = report.worktrees.find((x) => x.path === path);
|
|
10157
|
+
if (!w)
|
|
10158
|
+
return { ok: false, error: `not a tracked worktree: ${path}` };
|
|
10159
|
+
const cached = this.buildCache.get(path);
|
|
10160
|
+
if (!cached)
|
|
10161
|
+
return { ok: false, error: "not measured yet \u2014 try again in a moment" };
|
|
10162
|
+
const plan = reclaimPlan(w, cached.dirs, report.worktrees.map((x) => x.path));
|
|
10163
|
+
if (plan.refusals.length)
|
|
10164
|
+
return { ok: false, error: plan.refusals.join("; "), plan };
|
|
10165
|
+
if (dryRun)
|
|
10166
|
+
return { ok: true, plan, removed: 0, freedKb: 0 };
|
|
10167
|
+
let removed = 0;
|
|
10168
|
+
let freedKb = 0;
|
|
10169
|
+
for (const dir of plan.dirs) {
|
|
10170
|
+
if (!dir.startsWith(`${path}/`))
|
|
10171
|
+
continue;
|
|
10172
|
+
try {
|
|
10173
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
10174
|
+
removed++;
|
|
10175
|
+
freedKb += cached.dirs.find((d) => d.path === dir)?.kb ?? 0;
|
|
10176
|
+
} catch {}
|
|
10177
|
+
}
|
|
10178
|
+
this.duCache.delete(path);
|
|
10179
|
+
this.buildCache.delete(path);
|
|
10180
|
+
this.append({
|
|
10181
|
+
ts: new Date().toISOString(),
|
|
10182
|
+
type: "worktree.reclaimed",
|
|
10183
|
+
projectId: w.projectId,
|
|
10184
|
+
sessionId: null,
|
|
10185
|
+
payload: {
|
|
10186
|
+
path,
|
|
10187
|
+
branch: w.branch,
|
|
10188
|
+
dirs: removed,
|
|
10189
|
+
freedKb,
|
|
10190
|
+
summary: `reclaimed ${Math.round(freedKb / 1024)} MB of build output from ${w.branch ?? path}`
|
|
10191
|
+
}
|
|
10192
|
+
});
|
|
10193
|
+
return { ok: true, plan, removed, freedKb };
|
|
10194
|
+
}
|
|
9494
10195
|
stalls = new Map;
|
|
9495
10196
|
checkStalls() {
|
|
9496
10197
|
const live = this.db.query("SELECT id, project_id FROM sessions WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?").all(new Date(Date.now() - IDLE_MS).toISOString());
|
|
@@ -11120,7 +11821,7 @@ class WorkflowEngine {
|
|
|
11120
11821
|
}
|
|
11121
11822
|
|
|
11122
11823
|
// packages/daemon/src/app.ts
|
|
11123
|
-
var VERSION = "0.
|
|
11824
|
+
var VERSION = "0.12.1";
|
|
11124
11825
|
var WEB_DIR = (() => {
|
|
11125
11826
|
if (process.env.SWARM_WEB_DIR)
|
|
11126
11827
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -11247,6 +11948,18 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11247
11948
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
11248
11949
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11249
11950
|
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") ?? [])));
|
|
11951
|
+
app.get("/v1/rules/effect", (c) => c.json(store.ruleEffect(c.req.query("project") || undefined, Math.max(1, Math.min(365, Number(c.req.query("days") ?? 30) || 30)))));
|
|
11952
|
+
app.post("/v1/hygiene/reclaim", async (c) => {
|
|
11953
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11954
|
+
if (!body.path)
|
|
11955
|
+
return c.json({ ok: false, error: "path required" }, 400);
|
|
11956
|
+
const r = store.reclaimBuild(body.path, { dryRun: Boolean(body.dry) });
|
|
11957
|
+
return c.json(r, r.ok ? 200 : 409);
|
|
11958
|
+
});
|
|
11959
|
+
app.get("/v1/security", (c) => c.json(store.security(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)))));
|
|
11960
|
+
app.get("/v1/heat", (c) => c.json(store.fileHeat(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)))));
|
|
11961
|
+
app.get("/v1/graphs/resources", (c) => c.json(store.resourceHolding(c.req.query("project") || undefined, Math.max(1, Math.min(30, Number(c.req.query("days") ?? 3) || 3)))));
|
|
11962
|
+
app.get("/v1/graphs/transitions", (c) => c.json(store.transitions(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)), Math.max(1, Math.min(50, Number(c.req.query("min") ?? 2) || 2)))));
|
|
11250
11963
|
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)))));
|
|
11251
11964
|
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)))));
|
|
11252
11965
|
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|