@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/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
- " 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 "
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 ROBOT_HEAD = ROBOT.slice(0, 13);
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.11.2";
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)));