@ra3orblade/swarm 0.11.2 → 0.12.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/dist/swarm-hook.js +90 -29
- package/dist/swarm.js +90 -29
- package/dist/swarmd.js +692 -31
- package/package.json +1 -1
- package/web/app.js +525 -63
- package/web/index.html +63 -8
- package/web/release-notes.js +1 -1
- package/web/viz.js +42 -5
package/dist/swarmd.js
CHANGED
|
@@ -731,36 +731,95 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
|
|
|
731
731
|
}
|
|
732
732
|
// packages/core/src/art.ts
|
|
733
733
|
var ROBOT = [
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"
|
|
741
|
-
"
|
|
742
|
-
"
|
|
743
|
-
"
|
|
744
|
-
"
|
|
745
|
-
"
|
|
746
|
-
"
|
|
747
|
-
"
|
|
748
|
-
"
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"
|
|
753
|
-
"
|
|
754
|
-
"
|
|
755
|
-
"
|
|
756
|
-
"
|
|
757
|
-
"
|
|
758
|
-
"
|
|
759
|
-
"
|
|
760
|
-
"
|
|
761
|
-
"
|
|
734
|
+
" MM MM ",
|
|
735
|
+
" LL LL ",
|
|
736
|
+
" LL LL ",
|
|
737
|
+
" LM MM ",
|
|
738
|
+
" MD MS ",
|
|
739
|
+
" LE LE ",
|
|
740
|
+
" ME ME ",
|
|
741
|
+
" MS MS ",
|
|
742
|
+
" KK KK ",
|
|
743
|
+
" SSDDDSDDDDDDDDDDDDDDDDDDDDDDDDDDDDS ",
|
|
744
|
+
" SSDSDDDDDDDDDDDDDDDDDDDDDDDDDDDDDSS ",
|
|
745
|
+
" SSDOOOOOOOOOOOOOOOOOOOOOOOOOOOOOKSS ",
|
|
746
|
+
" SSOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMOSS ",
|
|
747
|
+
" DDKMMLLLMLLMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
748
|
+
" DDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
749
|
+
" SSSSODDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSSSS ",
|
|
750
|
+
" SSSSODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSSSS ",
|
|
751
|
+
" SMMMMODDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSMMMS ",
|
|
752
|
+
" DMSSSODDOMMMMOOOOOOMMMMMMMMMOOOOOOMMMMODDOSSSED ",
|
|
753
|
+
" DSSSSODDOMMMMODDDDDMMMMMMMMMODDDDDMMMMODDOSSSSK ",
|
|
754
|
+
" DSMMSODDOMMMMDDDDDDMMMMMMMMMKDDDDDMMMMODDOSMMSK ",
|
|
755
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDOSSSSK ",
|
|
756
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSK ",
|
|
757
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSK ",
|
|
758
|
+
" DSSSKODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSD ",
|
|
759
|
+
" OKKKKODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOKKKKO ",
|
|
760
|
+
" KKKKODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOOKKK ",
|
|
761
|
+
" KKKOODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOOKKK ",
|
|
762
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
763
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
764
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
765
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
766
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
767
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
768
|
+
" DDDOOOOOOOOOOOOOOOOOOOOOOOOOOOOOKDD ",
|
|
769
|
+
" DDDDDDDDDDDDDDDDDDDDDDDDDDDDDKDDDDD ",
|
|
770
|
+
" DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD ",
|
|
771
|
+
" OOOOOOOOOOO ",
|
|
772
|
+
" OODDDDDKKOO ",
|
|
773
|
+
" DSMMMMMMSDD ",
|
|
774
|
+
" OOOOOOOOOOO ",
|
|
775
|
+
" KDSMMMMMESSKK ",
|
|
776
|
+
" MMMMMMSSKOOKDSMMMMMSSSDKOODSEMMMMMM ",
|
|
777
|
+
" SSSSDO MMMMMMSSKKOOOOOOOOOOOOOOOOOKSSSMMMMMM ODSSSS ",
|
|
778
|
+
" EEESSSSOOLLLLMMMMMESSSSSSSSSSSSSSSSSSSSMMMMMLLLLOOSSSSMME ",
|
|
779
|
+
" SMMSSSSKOEELLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLEEOOSSSSEMS ",
|
|
780
|
+
" DSSSSSSSOOSEEKSMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSOEEDOOSSSSSSSK ",
|
|
781
|
+
" DSSSSSSDOOSEOSKEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMEOSDEDOODSSSSSSK ",
|
|
782
|
+
" KSSSSSSKOODSSOSEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEESOEEDOOKSSSSSSK ",
|
|
783
|
+
" DKSSSSKKOODSMEEMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMSEMSDOOKKSSSSDK ",
|
|
784
|
+
" OKDDKDKKOOKSMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSKOOKKKKKKKO ",
|
|
785
|
+
" OOOOOKKKOOKSMMMMESSSSSSSSSSSSSMMMMEEEEEESSSSEMMMMSKOOKKKOOOOO ",
|
|
786
|
+
" OOOOOOKKOOKSMMMSSKKKKKKKKKKKKKKMMMDKOOOOOOOODDMMMSKOOKKOOOOOO ",
|
|
787
|
+
" OKSSKOOOOOKSMMMKKMLLLLLLLLLLLMKMMMDDSSSSSSSSDDMMMSKOOOOODSSKO ",
|
|
788
|
+
" SDOKKK KSMMMKDLLLLLLLLLLLLLKMMMKDKKKDDKKKDKMMMSK KKKKDS ",
|
|
789
|
+
" DMLLMKO KSMMMKDLLLLLLLLLLLLLKMMMDDDDDDDDDSKDMMMSK ODMLLMD ",
|
|
790
|
+
" DELLMSD KSMMMKKMLLLLLLLLLLLLKMMMDKKKKKKKKKDDMMMSK DSMLLEO ",
|
|
791
|
+
" KSKKKKDO KSMMMEDDDDKKDDDKKKDKSMMMDDDDDDDDDSDDMMMSK ODDKKKSK ",
|
|
792
|
+
" EMLLMDO KSMMMMLLLLLLLLLLLLLLLMMMDOOOOOOOOODDMMMSK ODMLLME ",
|
|
793
|
+
" KMLLMSD KSMMMMMMMMMMMMMMMMMMMMMMDDSSDDSSSSDDMMMSK SSMLLEK ",
|
|
794
|
+
" KSKKKKSO KSMMMMMOOMMOOMKOEMOOMMMMDOOOOOOOOODDMMMSK OSKKKKSO ",
|
|
795
|
+
" SMLLMDO KSMMMMMKKMMKKMDKEMKKMMMMKDDDDSSSSDDDMMMSK ODMLLMS ",
|
|
796
|
+
" SMMMMSD KSMMMMMEEMMEEMEEMMEEMMMMDSOOOOOOOODDMMSSK DSMMMMD ",
|
|
797
|
+
" OOOOOOO KSSMMMMMMMMMMMMMMMMMMMMMDDDDDDDDDDDDMSSSK OOOOOOO ",
|
|
798
|
+
" KSMLLLMMSKO KSSSSMMMMMMMMMMMMMMMMMMMSOOOOOOOOOOSSSSSK OKSMLLLLMSO ",
|
|
799
|
+
" DSMMMEEESKK KSSKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSOSSK KDSMMMMMESD ",
|
|
800
|
+
" DSMMMMMESKK KSOOKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSOOKSK KKSMMMMMMSD ",
|
|
801
|
+
" DSMMMMMESKK OKSKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSDSKO KDSMMMMMMSD ",
|
|
802
|
+
" KDDDDDDDDKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKDDDDDDDDO ",
|
|
803
|
+
" OOOOOOOOOOO OOOOOOOOO OOOOOOOOO OOOOOOOOOOO ",
|
|
804
|
+
"DSDO OSSO ODKKKKKKK OKKKKKKKO KEDO ODEK",
|
|
805
|
+
"SMDO OSMD KDSSSSSSK KDSSSSSKO SMDO ODMS",
|
|
806
|
+
"DEKO OOED OOKKKOOO OOOKKOOO SSOO OKMS",
|
|
807
|
+
"DEKO OOED SEMMMMSK OSMMMMED SSKO OKES",
|
|
808
|
+
"DSSO OESK SEMMMMSK OSMMMMED KSSO OSSK",
|
|
809
|
+
"KSMS EEDO OOKKKKKO OOKKKKKO OSEE EMSO",
|
|
810
|
+
" DSS SSK OSEEESDO ODSEEESO KSS SSD ",
|
|
811
|
+
" OOO OO SEMMMMSK OSMMMMED OO OOO ",
|
|
812
|
+
" KSSSSSDO OKSSESSK ",
|
|
813
|
+
" DKKDDDDDDKKDK KDKDDDDDDDKDK ",
|
|
814
|
+
" SSMLLMMMMMMLMSK DSSMMMMMMMLLMSS ",
|
|
815
|
+
" SMLMMMMMMMMLLED SEMMMMMMMMMMLMS ",
|
|
816
|
+
" DMMMMMMMMMMMMMMD SMMMMMMMMMMMMMSK ",
|
|
817
|
+
" ODSSSSSSSSSSSSSSDO ODSSSSSSSSSSSSSSKO ",
|
|
818
|
+
" OOOOOOOOOOOOOOOOO OOOOOOOOOOOOOOOOO ",
|
|
819
|
+
" OKSSSSSSSSSSSSSSK KSSSSSSSSSSSSSSKO ",
|
|
820
|
+
" OKDDDDDDDDDDDDDDK KDDDDDDDDDDDDDDOO "
|
|
762
821
|
];
|
|
763
|
-
var
|
|
822
|
+
var HEAD = ROBOT.slice(0, 37);
|
|
764
823
|
// packages/core/src/audit.ts
|
|
765
824
|
var AUDIT_TYPES = new Set([
|
|
766
825
|
"session.started",
|
|
@@ -771,6 +830,8 @@ var AUDIT_TYPES = new Set([
|
|
|
771
830
|
"claim.released",
|
|
772
831
|
"claim.expired",
|
|
773
832
|
"claim.orphaned",
|
|
833
|
+
"claim.denied",
|
|
834
|
+
"rules.changed",
|
|
774
835
|
"worktree.created",
|
|
775
836
|
"worktree.removed",
|
|
776
837
|
"worktree.bootstrapped",
|
|
@@ -2143,6 +2204,82 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
|
2143
2204
|
contested: out.filter((f) => f.contested).length
|
|
2144
2205
|
};
|
|
2145
2206
|
}
|
|
2207
|
+
// packages/core/src/heat.ts
|
|
2208
|
+
var HEAT_DEFAULTS = {
|
|
2209
|
+
floor: 2,
|
|
2210
|
+
candidateSessions: 2,
|
|
2211
|
+
candidateRereads: 3,
|
|
2212
|
+
candidateWriteShare: 0.2,
|
|
2213
|
+
top: 40
|
|
2214
|
+
};
|
|
2215
|
+
function dirOf(path) {
|
|
2216
|
+
const i = path.lastIndexOf("/");
|
|
2217
|
+
return i <= 0 ? i === 0 ? "/" : "." : path.slice(0, i);
|
|
2218
|
+
}
|
|
2219
|
+
function fileHeat(rows, opts = {}, writeTools = WRITE_TOOLS) {
|
|
2220
|
+
const o = { ...HEAT_DEFAULTS, ...opts };
|
|
2221
|
+
const files = new Map;
|
|
2222
|
+
for (const r of rows) {
|
|
2223
|
+
if (!r.path || !r.sessionId)
|
|
2224
|
+
continue;
|
|
2225
|
+
const f = files.get(r.path) ?? { touches: 0, reads: 0, writes: 0, perSession: new Map };
|
|
2226
|
+
f.touches++;
|
|
2227
|
+
if (writeTools.has(r.tool))
|
|
2228
|
+
f.writes++;
|
|
2229
|
+
else
|
|
2230
|
+
f.reads++;
|
|
2231
|
+
f.perSession.set(r.sessionId, (f.perSession.get(r.sessionId) ?? 0) + 1);
|
|
2232
|
+
files.set(r.path, f);
|
|
2233
|
+
}
|
|
2234
|
+
const all = [...files.entries()].map(([path, f]) => {
|
|
2235
|
+
const rereads = [...f.perSession.values()].reduce((n, c) => n + (c - 1), 0);
|
|
2236
|
+
const writeShare = f.touches ? f.writes / f.touches : 0;
|
|
2237
|
+
return {
|
|
2238
|
+
path,
|
|
2239
|
+
touches: f.touches,
|
|
2240
|
+
sessions: f.perSession.size,
|
|
2241
|
+
reads: f.reads,
|
|
2242
|
+
writes: f.writes,
|
|
2243
|
+
rereads,
|
|
2244
|
+
candidate: f.perSession.size >= o.candidateSessions && rereads >= o.candidateRereads && writeShare <= o.candidateWriteShare
|
|
2245
|
+
};
|
|
2246
|
+
});
|
|
2247
|
+
const byTouches = (a, b) => b.touches - a.touches || b.sessions - a.sessions || a.path.localeCompare(b.path);
|
|
2248
|
+
const dirs = new Map;
|
|
2249
|
+
for (const [path, f] of files) {
|
|
2250
|
+
const d = dirOf(path);
|
|
2251
|
+
const cur = dirs.get(d) ?? {
|
|
2252
|
+
touches: 0,
|
|
2253
|
+
writes: 0,
|
|
2254
|
+
sessions: new Set,
|
|
2255
|
+
files: new Set
|
|
2256
|
+
};
|
|
2257
|
+
cur.touches += f.touches;
|
|
2258
|
+
cur.writes += f.writes;
|
|
2259
|
+
cur.files.add(path);
|
|
2260
|
+
for (const s of f.perSession.keys())
|
|
2261
|
+
cur.sessions.add(s);
|
|
2262
|
+
dirs.set(d, cur);
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
files: all.filter((f) => f.touches >= o.floor).sort(byTouches).slice(0, o.top),
|
|
2266
|
+
dirs: [...dirs.entries()].map(([dir, d]) => ({
|
|
2267
|
+
dir,
|
|
2268
|
+
touches: d.touches,
|
|
2269
|
+
sessions: d.sessions.size,
|
|
2270
|
+
files: d.files.size,
|
|
2271
|
+
writes: d.writes
|
|
2272
|
+
})).sort((a, b) => b.touches - a.touches || a.dir.localeCompare(b.dir)).slice(0, o.top),
|
|
2273
|
+
candidates: all.filter((f) => f.candidate).sort((a, b) => b.rereads - a.rereads || byTouches(a, b)).slice(0, o.top),
|
|
2274
|
+
totals: {
|
|
2275
|
+
files: all.length,
|
|
2276
|
+
touches: all.reduce((n, f) => n + f.touches, 0),
|
|
2277
|
+
sessions: new Set(rows.filter((r) => r.path && r.sessionId).map((r) => r.sessionId)).size,
|
|
2278
|
+
rereads: all.reduce((n, f) => n + f.rereads, 0),
|
|
2279
|
+
cold: all.filter((f) => f.touches === 1).length
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2146
2283
|
// packages/core/src/worktree.ts
|
|
2147
2284
|
import { join as join3 } from "path";
|
|
2148
2285
|
function planBootstrap(cfg, repoRoot, worktree) {
|
|
@@ -3307,6 +3444,107 @@ function formatOpenQuestions(qs) {
|
|
|
3307
3444
|
return null;
|
|
3308
3445
|
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`;
|
|
3309
3446
|
}
|
|
3447
|
+
// packages/core/src/resourcegraph.ts
|
|
3448
|
+
var nodeId = (kind, name, projectId) => `${kind}:${projectId ?? ""}:${name}`;
|
|
3449
|
+
function resourceGraph(held, wanted = [], now = Date.now()) {
|
|
3450
|
+
const resources = new Map;
|
|
3451
|
+
const holders = new Map;
|
|
3452
|
+
for (const h of held) {
|
|
3453
|
+
if (!h.name || !h.owner)
|
|
3454
|
+
continue;
|
|
3455
|
+
const id = nodeId(h.kind, h.name, h.projectId);
|
|
3456
|
+
const expired = h.expiresAt ? Date.parse(h.expiresAt) < now : false;
|
|
3457
|
+
const orphaned = Boolean(h.sessionEndedAt) || expired;
|
|
3458
|
+
if (!resources.has(id))
|
|
3459
|
+
resources.set(id, {
|
|
3460
|
+
id,
|
|
3461
|
+
kind: h.kind,
|
|
3462
|
+
name: h.name,
|
|
3463
|
+
holder: h.owner,
|
|
3464
|
+
orphaned,
|
|
3465
|
+
wanted: [],
|
|
3466
|
+
projectId: h.projectId ?? null
|
|
3467
|
+
});
|
|
3468
|
+
const o = holders.get(h.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3469
|
+
o.holds++;
|
|
3470
|
+
if (!orphaned)
|
|
3471
|
+
o.live++;
|
|
3472
|
+
holders.set(h.owner, o);
|
|
3473
|
+
}
|
|
3474
|
+
for (const w of wanted) {
|
|
3475
|
+
if (!w.name || !w.owner)
|
|
3476
|
+
continue;
|
|
3477
|
+
const r = resources.get(nodeId(w.kind, w.name, w.projectId));
|
|
3478
|
+
if (!r || r.holder === w.owner)
|
|
3479
|
+
continue;
|
|
3480
|
+
if (!r.wanted.includes(w.owner))
|
|
3481
|
+
r.wanted.push(w.owner);
|
|
3482
|
+
const o = holders.get(w.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3483
|
+
o.wants++;
|
|
3484
|
+
holders.set(w.owner, o);
|
|
3485
|
+
}
|
|
3486
|
+
for (const r of resources.values())
|
|
3487
|
+
r.wanted.sort();
|
|
3488
|
+
const edges = [];
|
|
3489
|
+
for (const r of [...resources.values()].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
3490
|
+
if (r.holder)
|
|
3491
|
+
edges.push({ from: r.holder, to: r.id, kind: "holds" });
|
|
3492
|
+
for (const w of r.wanted)
|
|
3493
|
+
edges.push({ from: w, to: r.id, kind: "wants" });
|
|
3494
|
+
}
|
|
3495
|
+
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));
|
|
3496
|
+
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));
|
|
3497
|
+
return {
|
|
3498
|
+
holders: holderList,
|
|
3499
|
+
resources: resourceList,
|
|
3500
|
+
edges,
|
|
3501
|
+
contention: findContention(resourceList),
|
|
3502
|
+
totals: {
|
|
3503
|
+
held: resourceList.length,
|
|
3504
|
+
orphaned: resourceList.filter((r) => r.orphaned).length,
|
|
3505
|
+
contested: resourceList.filter((r) => r.wanted.length > 0).length
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
function findContention(resources) {
|
|
3510
|
+
const next = new Map;
|
|
3511
|
+
for (const r of resources) {
|
|
3512
|
+
if (!r.holder)
|
|
3513
|
+
continue;
|
|
3514
|
+
for (const w of r.wanted) {
|
|
3515
|
+
const list = next.get(w) ?? [];
|
|
3516
|
+
list.push({ to: r.holder, via: r.name });
|
|
3517
|
+
next.set(w, list);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
for (const list of next.values())
|
|
3521
|
+
list.sort((a, b) => a.to.localeCompare(b.to) || a.via.localeCompare(b.via));
|
|
3522
|
+
const found = new Map;
|
|
3523
|
+
const walk = (start, at, owners, vias, depth) => {
|
|
3524
|
+
if (depth > 6)
|
|
3525
|
+
return;
|
|
3526
|
+
for (const step of next.get(at) ?? []) {
|
|
3527
|
+
if (step.to === start) {
|
|
3528
|
+
const ring = [...owners];
|
|
3529
|
+
const res = [...vias, step.via];
|
|
3530
|
+
const pivot = ring.indexOf([...ring].sort()[0]);
|
|
3531
|
+
const key = [...ring.slice(pivot), ...ring.slice(0, pivot)].join(">");
|
|
3532
|
+
if (!found.has(key))
|
|
3533
|
+
found.set(key, {
|
|
3534
|
+
owners: [...ring.slice(pivot), ...ring.slice(0, pivot)],
|
|
3535
|
+
resources: [...res.slice(pivot), ...res.slice(0, pivot)]
|
|
3536
|
+
});
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
if (owners.includes(step.to))
|
|
3540
|
+
continue;
|
|
3541
|
+
walk(start, step.to, [...owners, step.to], [...vias, step.via], depth + 1);
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
for (const owner of [...next.keys()].sort())
|
|
3545
|
+
walk(owner, owner, [owner], [], 0);
|
|
3546
|
+
return [...found.values()].sort((a, b) => a.owners.length - b.owners.length || a.owners.join().localeCompare(b.owners.join()));
|
|
3547
|
+
}
|
|
3310
3548
|
// packages/core/src/resources.ts
|
|
3311
3549
|
var DEFAULT_RESOURCE_LEASE_MINUTES = 60;
|
|
3312
3550
|
function isTrackedPid(pid) {
|
|
@@ -3440,6 +3678,202 @@ ${lines.join(`
|
|
|
3440
3678
|
`)}` : ""}`
|
|
3441
3679
|
};
|
|
3442
3680
|
}
|
|
3681
|
+
// packages/core/src/ruleeffect.ts
|
|
3682
|
+
var DAY = 86400000;
|
|
3683
|
+
var dayOf = (iso) => iso.slice(0, 10);
|
|
3684
|
+
function commandSignature(command) {
|
|
3685
|
+
const segments = (command ?? "").split(/&&|\|\||[;\n|]/).map((x) => x.trim()).filter(Boolean);
|
|
3686
|
+
const first = segments.find((seg) => !/^(cd|pushd|export|source|\.)\b/.test(seg)) ?? segments[0] ?? "";
|
|
3687
|
+
if (!first)
|
|
3688
|
+
return "(none)";
|
|
3689
|
+
const tokens = first.split(/\s+/).filter(Boolean);
|
|
3690
|
+
const head = tokens[0] ?? "";
|
|
3691
|
+
const name = head.includes("/") ? head.split("/").pop() ?? head : head;
|
|
3692
|
+
const second = tokens[1];
|
|
3693
|
+
const more = tokens.length > 2;
|
|
3694
|
+
return `${name}${second ? ` ${second}` : ""}${more ? " \u2026" : ""}`;
|
|
3695
|
+
}
|
|
3696
|
+
function ruleEffect(incidents, changes = [], now = Date.now(), days3 = 30) {
|
|
3697
|
+
const since = now - days3 * DAY;
|
|
3698
|
+
const rows = incidents.filter((i) => i.rule && Date.parse(i.at) >= since);
|
|
3699
|
+
const byRule = new Map;
|
|
3700
|
+
for (const i of rows)
|
|
3701
|
+
byRule.set(i.rule, [...byRule.get(i.rule) ?? [], i]);
|
|
3702
|
+
const dayList = [];
|
|
3703
|
+
for (let t = since;t <= now; t += DAY)
|
|
3704
|
+
dayList.push(dayOf(new Date(t).toISOString()));
|
|
3705
|
+
const rules = [...byRule.entries()].map(([rule, list]) => {
|
|
3706
|
+
const sorted = [...list].sort((a, b) => a.at.localeCompare(b.at));
|
|
3707
|
+
const counts = new Map;
|
|
3708
|
+
for (const i of sorted)
|
|
3709
|
+
counts.set(dayOf(i.at), (counts.get(dayOf(i.at)) ?? 0) + 1);
|
|
3710
|
+
const perDay = dayList.map((day) => ({ day, n: counts.get(day) ?? 0 }));
|
|
3711
|
+
const half = Math.floor(perDay.length / 2);
|
|
3712
|
+
const early = perDay.slice(0, half).reduce((n, d) => n + d.n, 0);
|
|
3713
|
+
const late = perDay.slice(half).reduce((n, d) => n + d.n, 0);
|
|
3714
|
+
const trend = Math.abs(late - early) <= 1 ? "steady" : late > early ? "rising" : "falling";
|
|
3715
|
+
const sig = new Map;
|
|
3716
|
+
for (const i of sorted) {
|
|
3717
|
+
const key = commandSignature(i.command);
|
|
3718
|
+
const cur = sig.get(key) ?? { hits: 0, example: i.command };
|
|
3719
|
+
cur.hits++;
|
|
3720
|
+
sig.set(key, cur);
|
|
3721
|
+
}
|
|
3722
|
+
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));
|
|
3723
|
+
const added = changes.filter((c) => c.added.includes(rule) && Date.parse(c.at) >= since).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
3724
|
+
let landed = null;
|
|
3725
|
+
if (added) {
|
|
3726
|
+
const at = Date.parse(added.at);
|
|
3727
|
+
const beforeDays = Math.max(1, (at - since) / DAY);
|
|
3728
|
+
const afterDays = Math.max(1, (now - at) / DAY);
|
|
3729
|
+
landed = {
|
|
3730
|
+
at: added.at,
|
|
3731
|
+
beforePerDay: sorted.filter((i) => Date.parse(i.at) < at).length / beforeDays,
|
|
3732
|
+
afterPerDay: sorted.filter((i) => Date.parse(i.at) >= at).length / afterDays
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
return {
|
|
3736
|
+
rule,
|
|
3737
|
+
total: sorted.length,
|
|
3738
|
+
acked: sorted.filter((i) => i.acked).length,
|
|
3739
|
+
firstAt: sorted[0].at,
|
|
3740
|
+
lastAt: sorted.at(-1).at,
|
|
3741
|
+
perDay,
|
|
3742
|
+
trend,
|
|
3743
|
+
concentration: sorted.length ? (clusters[0]?.hits ?? 0) / sorted.length : 0,
|
|
3744
|
+
clusters: clusters.slice(0, 5),
|
|
3745
|
+
landed
|
|
3746
|
+
};
|
|
3747
|
+
});
|
|
3748
|
+
rules.sort((a, b) => b.total - a.total || a.rule.localeCompare(b.rule));
|
|
3749
|
+
return {
|
|
3750
|
+
rules,
|
|
3751
|
+
totals: {
|
|
3752
|
+
incidents: rows.length,
|
|
3753
|
+
rules: rules.length,
|
|
3754
|
+
acked: rows.filter((i) => i.acked).length,
|
|
3755
|
+
unchanged: rules.filter((r) => r.trend !== "falling" && r.total > 1).length
|
|
3756
|
+
},
|
|
3757
|
+
noChangeHistory: !changes.some((c) => Date.parse(c.at) >= since)
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
// packages/core/src/security.ts
|
|
3761
|
+
var LOCAL = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i;
|
|
3762
|
+
function hostsIn(text) {
|
|
3763
|
+
const out = new Set;
|
|
3764
|
+
for (const m of text.matchAll(/\bhttps?:\/\/(\[[^\]]+\][^/\s"'`)}>,;]*|[^/\s"'`)\]}>,;]+)/gi)) {
|
|
3765
|
+
const hostPort = m[1].replace(/^[^@]*@/, "");
|
|
3766
|
+
const bracket = /^\[([^\]]+)\]/.exec(hostPort);
|
|
3767
|
+
const host = (bracket?.[1] ?? hostPort.split(":")[0]).toLowerCase();
|
|
3768
|
+
if (host)
|
|
3769
|
+
out.add(host);
|
|
3770
|
+
}
|
|
3771
|
+
return [...out].sort();
|
|
3772
|
+
}
|
|
3773
|
+
var INSTALLERS = [
|
|
3774
|
+
{ ecosystem: "npm", re: /^npm\s+(?:i|install|add)\b(.*)$/i },
|
|
3775
|
+
{ ecosystem: "pnpm", re: /^pnpm\s+(?:i|install|add)\b(.*)$/i },
|
|
3776
|
+
{ ecosystem: "yarn", re: /^yarn\s+add\b(.*)$/i },
|
|
3777
|
+
{ ecosystem: "bun", re: /^bun\s+(?:i|install|add)\b(.*)$/i },
|
|
3778
|
+
{ ecosystem: "pip", re: /^pip3?\s+install\b(.*)$/i },
|
|
3779
|
+
{ ecosystem: "cargo", re: /^cargo\s+(?:add|install)\b(.*)$/i },
|
|
3780
|
+
{ ecosystem: "go", re: /^go\s+(?:get|install)\b(.*)$/i },
|
|
3781
|
+
{ ecosystem: "gem", re: /^gem\s+install\b(.*)$/i },
|
|
3782
|
+
{ ecosystem: "brew", re: /^brew\s+install\b(.*)$/i },
|
|
3783
|
+
{ ecosystem: "apt", re: /^(?:sudo\s+)?apt(?:-get)?\s+install\b(.*)$/i }
|
|
3784
|
+
];
|
|
3785
|
+
var PKG_NAME = /^(@[\w.-]+\/)?[a-z0-9][\w.\-/]*([@=<>~^]=?[\w.\-^~*]+)?$/i;
|
|
3786
|
+
function installsIn(text) {
|
|
3787
|
+
const out = [];
|
|
3788
|
+
for (const raw of text.split(/&&|\|\||[;\n|]/)) {
|
|
3789
|
+
const segment = raw.trim();
|
|
3790
|
+
if (!segment)
|
|
3791
|
+
continue;
|
|
3792
|
+
for (const { ecosystem, re } of INSTALLERS) {
|
|
3793
|
+
const m = re.exec(segment);
|
|
3794
|
+
if (!m)
|
|
3795
|
+
continue;
|
|
3796
|
+
const args = [];
|
|
3797
|
+
for (const tok of (m[1] ?? "").split(/\s+/)) {
|
|
3798
|
+
const a = tok.trim();
|
|
3799
|
+
if (!a)
|
|
3800
|
+
continue;
|
|
3801
|
+
if (/^\d*[<>&]/.test(a))
|
|
3802
|
+
break;
|
|
3803
|
+
if (a.startsWith("-"))
|
|
3804
|
+
continue;
|
|
3805
|
+
if (PKG_NAME.test(a))
|
|
3806
|
+
args.push(a);
|
|
3807
|
+
}
|
|
3808
|
+
out.push(...args.length ? args.map((pkg) => ({ ecosystem, pkg })) : [{ ecosystem, pkg: "(from manifest)" }]);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return out;
|
|
3812
|
+
}
|
|
3813
|
+
var SECRETS = [
|
|
3814
|
+
{ what: ".env file", re: /(^|[/\s"'`])\.env(\.[\w-]+)?\b/i },
|
|
3815
|
+
{ what: "SSH private key", re: /(^|[/\s"'`])(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b/i },
|
|
3816
|
+
{ what: "AWS credentials", re: /\.aws\/(credentials|config)\b/i },
|
|
3817
|
+
{ what: "npm token", re: /(^|[/\s"'`])\.npmrc\b/i },
|
|
3818
|
+
{ what: "kubeconfig", re: /\.kube\/config\b/i },
|
|
3819
|
+
{ what: "Google cloud credentials", re: /gcloud\/[\w-]*credential/i },
|
|
3820
|
+
{ what: "PEM or key file", re: /\.(pem|p12|pfx|key)\b/i },
|
|
3821
|
+
{ what: "macOS keychain", re: /\bsecurity\s+find-(generic|internet)-password\b/i },
|
|
3822
|
+
{ what: "netrc", re: /(^|[/\s"'`])\.netrc\b/i }
|
|
3823
|
+
];
|
|
3824
|
+
function secretsIn(text) {
|
|
3825
|
+
return SECRETS.filter((s) => s.re.test(text)).map((s) => s.what);
|
|
3826
|
+
}
|
|
3827
|
+
function securityScan(rows) {
|
|
3828
|
+
const egress = new Map;
|
|
3829
|
+
const installs = new Map;
|
|
3830
|
+
const secrets = new Map;
|
|
3831
|
+
let scanned = 0;
|
|
3832
|
+
for (const r of rows) {
|
|
3833
|
+
if (!r.sessionId)
|
|
3834
|
+
continue;
|
|
3835
|
+
const text = `${r.command ?? ""} ${r.path ?? ""}`.trim();
|
|
3836
|
+
if (!text)
|
|
3837
|
+
continue;
|
|
3838
|
+
scanned++;
|
|
3839
|
+
for (const host of hostsIn(text)) {
|
|
3840
|
+
const e = egress.get(host) ?? { hits: 0, sessions: new Set };
|
|
3841
|
+
e.hits++;
|
|
3842
|
+
e.sessions.add(r.sessionId);
|
|
3843
|
+
egress.set(host, e);
|
|
3844
|
+
}
|
|
3845
|
+
for (const i of installsIn(r.command ?? "")) {
|
|
3846
|
+
const key = `${i.ecosystem} ${i.pkg}`;
|
|
3847
|
+
const cur = installs.get(key) ?? { ...i, hits: 0, sessions: new Set };
|
|
3848
|
+
cur.hits++;
|
|
3849
|
+
cur.sessions.add(r.sessionId);
|
|
3850
|
+
installs.set(key, cur);
|
|
3851
|
+
}
|
|
3852
|
+
for (const what of secretsIn(text)) {
|
|
3853
|
+
const cur = secrets.get(what) ?? { hits: 0, sessions: new Set };
|
|
3854
|
+
cur.hits++;
|
|
3855
|
+
cur.sessions.add(r.sessionId);
|
|
3856
|
+
secrets.set(what, cur);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const egressList = [...egress.entries()].map(([host, e]) => ({
|
|
3860
|
+
host,
|
|
3861
|
+
hits: e.hits,
|
|
3862
|
+
sessions: e.sessions.size,
|
|
3863
|
+
local: LOCAL.test(host)
|
|
3864
|
+
})).sort((a, b) => Number(a.local) - Number(b.local) || b.hits - a.hits || a.host.localeCompare(b.host));
|
|
3865
|
+
return {
|
|
3866
|
+
egress: egressList,
|
|
3867
|
+
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)),
|
|
3868
|
+
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)),
|
|
3869
|
+
totals: {
|
|
3870
|
+
scanned,
|
|
3871
|
+
remoteHosts: egressList.filter((h) => !h.local).length,
|
|
3872
|
+
installs: [...installs.values()].reduce((n, i) => n + i.hits, 0),
|
|
3873
|
+
secrets: [...secrets.values()].reduce((n, s) => n + s.hits, 0)
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3443
3877
|
// packages/core/src/stall.ts
|
|
3444
3878
|
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
3445
3879
|
function toolResponseErrored(resp) {
|
|
@@ -3671,6 +4105,70 @@ function clusterProjectKey(remoteUrl) {
|
|
|
3671
4105
|
return null;
|
|
3672
4106
|
return `${host}/${m[2]}`;
|
|
3673
4107
|
}
|
|
4108
|
+
// packages/core/src/transitions.ts
|
|
4109
|
+
function transitionGraph(steps, { minWeight = 1 } = {}) {
|
|
4110
|
+
const calls = new Map;
|
|
4111
|
+
const selfLoops = new Map;
|
|
4112
|
+
const edges = new Map;
|
|
4113
|
+
const bySession = new Map;
|
|
4114
|
+
let transitions = 0;
|
|
4115
|
+
for (const s of steps) {
|
|
4116
|
+
if (!s.sessionId || !s.tool)
|
|
4117
|
+
continue;
|
|
4118
|
+
calls.set(s.tool, (calls.get(s.tool) ?? 0) + 1);
|
|
4119
|
+
const prev = bySession.get(s.sessionId);
|
|
4120
|
+
if (prev !== undefined) {
|
|
4121
|
+
const key = `${prev}\x00${s.tool}`;
|
|
4122
|
+
const e = edges.get(key) ?? {
|
|
4123
|
+
from: prev,
|
|
4124
|
+
to: s.tool,
|
|
4125
|
+
weight: 0,
|
|
4126
|
+
sessions: new Set
|
|
4127
|
+
};
|
|
4128
|
+
e.weight++;
|
|
4129
|
+
e.sessions.add(s.sessionId);
|
|
4130
|
+
edges.set(key, e);
|
|
4131
|
+
if (prev === s.tool)
|
|
4132
|
+
selfLoops.set(s.tool, (selfLoops.get(s.tool) ?? 0) + 1);
|
|
4133
|
+
transitions++;
|
|
4134
|
+
}
|
|
4135
|
+
bySession.set(s.sessionId, s.tool);
|
|
4136
|
+
}
|
|
4137
|
+
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));
|
|
4138
|
+
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));
|
|
4139
|
+
return {
|
|
4140
|
+
nodes: nodeList,
|
|
4141
|
+
edges: edgeList,
|
|
4142
|
+
loops: findLoops(edgeList),
|
|
4143
|
+
sessions: bySession.size,
|
|
4144
|
+
steps: steps.filter((s) => s.sessionId && s.tool).length,
|
|
4145
|
+
transitions
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function findLoops(edges) {
|
|
4149
|
+
const at = new Map(edges.map((e) => [`${e.from}\x00${e.to}`, e]));
|
|
4150
|
+
const out = [];
|
|
4151
|
+
const seen = new Set;
|
|
4152
|
+
for (const e of edges) {
|
|
4153
|
+
if (e.from === e.to) {
|
|
4154
|
+
out.push({ tools: [e.from], weight: e.weight, sessions: e.sessions });
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
const back = at.get(`${e.to}\x00${e.from}`);
|
|
4158
|
+
if (!back)
|
|
4159
|
+
continue;
|
|
4160
|
+
const pair = [e.from, e.to].sort().join("\x00");
|
|
4161
|
+
if (seen.has(pair))
|
|
4162
|
+
continue;
|
|
4163
|
+
seen.add(pair);
|
|
4164
|
+
out.push({
|
|
4165
|
+
tools: [e.from, e.to],
|
|
4166
|
+
weight: Math.min(e.weight, back.weight),
|
|
4167
|
+
sessions: Math.max(e.sessions, back.sessions)
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return out.sort((a, b) => b.weight - a.weight || a.tools.join().localeCompare(b.tools.join()));
|
|
4171
|
+
}
|
|
3674
4172
|
// packages/core/src/waiting.ts
|
|
3675
4173
|
var emptyByKind = () => ({
|
|
3676
4174
|
permission: { episodes: 0, blockedMs: 0 },
|
|
@@ -7832,8 +8330,46 @@ ${err}
|
|
|
7832
8330
|
const loaded = loadConfigDetailed({ repoRoot, home: this.home });
|
|
7833
8331
|
this.policyCache.set(key, { at: Date.now(), loaded });
|
|
7834
8332
|
this.writePolicyCache(loaded);
|
|
8333
|
+
this.noteRuleChange(key, loaded.config.rules);
|
|
7835
8334
|
return loaded;
|
|
7836
8335
|
}
|
|
8336
|
+
projectIdForRoot(root) {
|
|
8337
|
+
if (!root)
|
|
8338
|
+
return null;
|
|
8339
|
+
const row = this.db.query("SELECT id FROM projects WHERE root = ?").get(root);
|
|
8340
|
+
return row?.id ?? null;
|
|
8341
|
+
}
|
|
8342
|
+
noteRuleChange(key, rules2) {
|
|
8343
|
+
const sig = JSON.stringify(Object.entries(rules2).sort());
|
|
8344
|
+
const metaKey = `rules.sig:${key}`;
|
|
8345
|
+
const prev = this.db.query("SELECT value FROM meta WHERE key = ?").get(metaKey)?.value;
|
|
8346
|
+
if (prev === sig)
|
|
8347
|
+
return;
|
|
8348
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(metaKey, sig);
|
|
8349
|
+
if (prev === undefined)
|
|
8350
|
+
return;
|
|
8351
|
+
const before = new Map(JSON.parse(prev));
|
|
8352
|
+
const after = new Map(Object.entries(rules2));
|
|
8353
|
+
const added = [...after.keys()].filter((r) => !before.has(r)).sort();
|
|
8354
|
+
const removed = [...before.keys()].filter((r) => !after.has(r)).sort();
|
|
8355
|
+
const retuned = [...after.entries()].filter(([r, mode]) => before.has(r) && before.get(r) !== mode).map(([r, mode]) => `${r}=${mode}`).sort();
|
|
8356
|
+
if (!added.length && !removed.length && !retuned.length)
|
|
8357
|
+
return;
|
|
8358
|
+
this.append({
|
|
8359
|
+
ts: new Date().toISOString(),
|
|
8360
|
+
type: "rules.changed",
|
|
8361
|
+
projectId: this.projectIdForRoot(key) ?? "",
|
|
8362
|
+
sessionId: null,
|
|
8363
|
+
payload: {
|
|
8364
|
+
repo: key || null,
|
|
8365
|
+
added,
|
|
8366
|
+
removed,
|
|
8367
|
+
retuned,
|
|
8368
|
+
rules: [...after.keys()].sort(),
|
|
8369
|
+
summary: `rules changed${added.length ? ` +${added.join(",")}` : ""}${removed.length ? ` -${removed.join(",")}` : ""}${retuned.length ? ` ~${retuned.join(",")}` : ""}`
|
|
8370
|
+
}
|
|
8371
|
+
});
|
|
8372
|
+
}
|
|
7837
8373
|
writePolicyCache(loaded) {
|
|
7838
8374
|
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
7839
8375
|
try {
|
|
@@ -8869,8 +9405,24 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8869
9405
|
return { ok: false, error: "unknown project" };
|
|
8870
9406
|
const now = Date.now();
|
|
8871
9407
|
const decision = canClaim(this.claimRows(projectId), task, owner, now);
|
|
8872
|
-
if (!decision.ok)
|
|
9408
|
+
if (!decision.ok) {
|
|
9409
|
+
if (decision.heldBy !== owner)
|
|
9410
|
+
this.append({
|
|
9411
|
+
ts: new Date(now).toISOString(),
|
|
9412
|
+
type: "claim.denied",
|
|
9413
|
+
projectId,
|
|
9414
|
+
sessionId,
|
|
9415
|
+
actor: this.actorFor(owner, sessionId),
|
|
9416
|
+
payload: {
|
|
9417
|
+
task,
|
|
9418
|
+
owner,
|
|
9419
|
+
heldBy: decision.heldBy,
|
|
9420
|
+
until: decision.until,
|
|
9421
|
+
summary: `${owner} was refused ${task} \u2014 held by ${decision.heldBy}`
|
|
9422
|
+
}
|
|
9423
|
+
});
|
|
8873
9424
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
9425
|
+
}
|
|
8874
9426
|
const branch = `task/${task}`;
|
|
8875
9427
|
const worktree2 = this.worktreePath(projectId, task);
|
|
8876
9428
|
if (existsSync6(worktree2))
|
|
@@ -9432,6 +9984,110 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9432
9984
|
})
|
|
9433
9985
|
};
|
|
9434
9986
|
}
|
|
9987
|
+
transitions(projectId, days3 = 7, minWeight = 1) {
|
|
9988
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9989
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool
|
|
9990
|
+
FROM events
|
|
9991
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
9992
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
9993
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
9994
|
+
return transitionGraph(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "" })), { minWeight });
|
|
9995
|
+
}
|
|
9996
|
+
resourceHolding(projectId, days3 = 3) {
|
|
9997
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9998
|
+
const p = projectId ? " AND c.project_id = ?" : "";
|
|
9999
|
+
const args = projectId ? [projectId] : [];
|
|
10000
|
+
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]));
|
|
10001
|
+
const claims = this.db.query(`SELECT c.task AS name, c.owner, c.project_id, c.expires_at, c.actor_id AS session_id
|
|
10002
|
+
FROM claims c WHERE c.state = 'held' AND c.released_at IS NULL${p}`).all(...args);
|
|
10003
|
+
const resources2 = this.db.query(`SELECT c.name, c.owner, c.project_id, c.expires_at, c.session_id, c.port
|
|
10004
|
+
FROM resources c WHERE c.released = 0${p}`).all(...args);
|
|
10005
|
+
const procs = this.db.query(`SELECT c.name, c.owner, c.project_id, c.session_id, c.port
|
|
10006
|
+
FROM processes c WHERE c.ended_at IS NULL${p}`).all(...args);
|
|
10007
|
+
const denials = this.db.query(`SELECT json_extract(payload,'$.task') AS name, json_extract(payload,'$.owner') AS owner,
|
|
10008
|
+
json_extract(payload,'$.heldBy') AS held_by, ts, project_id
|
|
10009
|
+
FROM events WHERE type = 'claim.denied' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...args);
|
|
10010
|
+
const held = [
|
|
10011
|
+
...claims.map((r) => ({
|
|
10012
|
+
kind: "claim",
|
|
10013
|
+
name: r.name,
|
|
10014
|
+
owner: r.owner,
|
|
10015
|
+
sessionId: r.session_id,
|
|
10016
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10017
|
+
expiresAt: r.expires_at,
|
|
10018
|
+
projectId: r.project_id
|
|
10019
|
+
})),
|
|
10020
|
+
...resources2.map((r) => ({
|
|
10021
|
+
kind: r.port ? "port" : "lease",
|
|
10022
|
+
name: r.port ? String(r.port) : r.name,
|
|
10023
|
+
owner: r.owner ?? "unknown",
|
|
10024
|
+
sessionId: r.session_id,
|
|
10025
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10026
|
+
expiresAt: r.expires_at,
|
|
10027
|
+
projectId: r.project_id
|
|
10028
|
+
})),
|
|
10029
|
+
...procs.map((r) => ({
|
|
10030
|
+
kind: "process",
|
|
10031
|
+
name: r.name ?? (r.port ? `:${r.port}` : "process"),
|
|
10032
|
+
owner: r.owner ?? "unknown",
|
|
10033
|
+
sessionId: r.session_id,
|
|
10034
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10035
|
+
expiresAt: null,
|
|
10036
|
+
projectId: r.project_id
|
|
10037
|
+
}))
|
|
10038
|
+
];
|
|
10039
|
+
const wanted = denials.filter((d) => d.name && d.owner && d.held_by).map((d) => ({
|
|
10040
|
+
kind: "claim",
|
|
10041
|
+
name: d.name,
|
|
10042
|
+
owner: d.owner,
|
|
10043
|
+
heldBy: d.held_by,
|
|
10044
|
+
at: d.ts,
|
|
10045
|
+
projectId: d.project_id
|
|
10046
|
+
}));
|
|
10047
|
+
return resourceGraph(held, wanted);
|
|
10048
|
+
}
|
|
10049
|
+
fileHeat(projectId, days3 = 14) {
|
|
10050
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10051
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
10052
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10053
|
+
FROM events
|
|
10054
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
10055
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10056
|
+
return fileHeat(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "", path: r.path ?? "" })));
|
|
10057
|
+
}
|
|
10058
|
+
security(projectId, days3 = 14) {
|
|
10059
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10060
|
+
const rows = this.db.query(`SELECT session_id, ts, json_extract(payload,'$.tool') AS tool,
|
|
10061
|
+
COALESCE(json_extract(payload,'$.toolInput.command'),
|
|
10062
|
+
json_extract(payload,'$.toolInput.url'), '') AS command,
|
|
10063
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10064
|
+
FROM events
|
|
10065
|
+
WHERE type = 'tool.requested' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10066
|
+
return securityScan(rows.map((r) => ({
|
|
10067
|
+
sessionId: r.session_id ?? "",
|
|
10068
|
+
tool: r.tool ?? "",
|
|
10069
|
+
command: r.command ?? "",
|
|
10070
|
+
path: r.path,
|
|
10071
|
+
at: r.ts
|
|
10072
|
+
})));
|
|
10073
|
+
}
|
|
10074
|
+
ruleEffect(projectId, days3 = 30) {
|
|
10075
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10076
|
+
const rows = this.db.query(`SELECT e.seq, e.ts,
|
|
10077
|
+
json_extract(e.payload,'$.rule') AS rule,
|
|
10078
|
+
COALESCE(json_extract(e.payload,'$.command'), '') AS command,
|
|
10079
|
+
(a.seq IS NOT NULL) AS acked
|
|
10080
|
+
FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10081
|
+
WHERE e.type = 'incident.opened' AND e.ts >= ?${projectId ? " AND e.project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10082
|
+
const changes = this.db.query(`SELECT ts, COALESCE(json_extract(payload,'$.added'), '[]') AS added
|
|
10083
|
+
FROM events WHERE type = 'rules.changed' AND ts >= ?`).all(since);
|
|
10084
|
+
return ruleEffect(rows.map((r) => ({
|
|
10085
|
+
rule: r.rule ?? "",
|
|
10086
|
+
command: r.command ?? "",
|
|
10087
|
+
at: r.ts,
|
|
10088
|
+
acked: Boolean(r.acked)
|
|
10089
|
+
})), changes.map((c) => ({ at: c.ts, added: JSON.parse(c.added) })), Date.now(), days3);
|
|
10090
|
+
}
|
|
9435
10091
|
stalls = new Map;
|
|
9436
10092
|
checkStalls() {
|
|
9437
10093
|
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());
|
|
@@ -11061,7 +11717,7 @@ class WorkflowEngine {
|
|
|
11061
11717
|
}
|
|
11062
11718
|
|
|
11063
11719
|
// packages/daemon/src/app.ts
|
|
11064
|
-
var VERSION = "0.
|
|
11720
|
+
var VERSION = "0.12.0";
|
|
11065
11721
|
var WEB_DIR = (() => {
|
|
11066
11722
|
if (process.env.SWARM_WEB_DIR)
|
|
11067
11723
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -11188,6 +11844,11 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11188
11844
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
11189
11845
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11190
11846
|
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") ?? [])));
|
|
11847
|
+
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)))));
|
|
11848
|
+
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)))));
|
|
11849
|
+
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)))));
|
|
11850
|
+
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)))));
|
|
11851
|
+
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)))));
|
|
11191
11852
|
app.get("/v1/context", (c) => c.json(store.context(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11192
11853
|
app.get("/v1/mcp/health", (c) => c.json(store.mcpHealth(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11193
11854
|
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|