@ra3orblade/swarm 0.11.3 → 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
@@ -830,6 +830,8 @@ var AUDIT_TYPES = new Set([
830
830
  "claim.released",
831
831
  "claim.expired",
832
832
  "claim.orphaned",
833
+ "claim.denied",
834
+ "rules.changed",
833
835
  "worktree.created",
834
836
  "worktree.removed",
835
837
  "worktree.bootstrapped",
@@ -2202,6 +2204,82 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
2202
2204
  contested: out.filter((f) => f.contested).length
2203
2205
  };
2204
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
+ }
2205
2283
  // packages/core/src/worktree.ts
2206
2284
  import { join as join3 } from "path";
2207
2285
  function planBootstrap(cfg, repoRoot, worktree) {
@@ -3366,6 +3444,107 @@ function formatOpenQuestions(qs) {
3366
3444
  return null;
3367
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`;
3368
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
+ }
3369
3548
  // packages/core/src/resources.ts
3370
3549
  var DEFAULT_RESOURCE_LEASE_MINUTES = 60;
3371
3550
  function isTrackedPid(pid) {
@@ -3499,6 +3678,202 @@ ${lines.join(`
3499
3678
  `)}` : ""}`
3500
3679
  };
3501
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
+ }
3502
3877
  // packages/core/src/stall.ts
3503
3878
  var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
3504
3879
  function toolResponseErrored(resp) {
@@ -3730,6 +4105,70 @@ function clusterProjectKey(remoteUrl) {
3730
4105
  return null;
3731
4106
  return `${host}/${m[2]}`;
3732
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
+ }
3733
4172
  // packages/core/src/waiting.ts
3734
4173
  var emptyByKind = () => ({
3735
4174
  permission: { episodes: 0, blockedMs: 0 },
@@ -7891,8 +8330,46 @@ ${err}
7891
8330
  const loaded = loadConfigDetailed({ repoRoot, home: this.home });
7892
8331
  this.policyCache.set(key, { at: Date.now(), loaded });
7893
8332
  this.writePolicyCache(loaded);
8333
+ this.noteRuleChange(key, loaded.config.rules);
7894
8334
  return loaded;
7895
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
+ }
7896
8373
  writePolicyCache(loaded) {
7897
8374
  const file = join8(this.home, POLICY_CACHE_FILE);
7898
8375
  try {
@@ -8928,8 +9405,24 @@ ${p2.reason ?? ""}`.trim(),
8928
9405
  return { ok: false, error: "unknown project" };
8929
9406
  const now = Date.now();
8930
9407
  const decision = canClaim(this.claimRows(projectId), task, owner, now);
8931
- 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
+ });
8932
9424
  return { ok: false, error: claimRefusalMessage(decision, task) };
9425
+ }
8933
9426
  const branch = `task/${task}`;
8934
9427
  const worktree2 = this.worktreePath(projectId, task);
8935
9428
  if (existsSync6(worktree2))
@@ -9491,6 +9984,110 @@ ${p2.reason ?? ""}`.trim(),
9491
9984
  })
9492
9985
  };
9493
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
+ }
9494
10091
  stalls = new Map;
9495
10092
  checkStalls() {
9496
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());
@@ -11120,7 +11717,7 @@ class WorkflowEngine {
11120
11717
  }
11121
11718
 
11122
11719
  // packages/daemon/src/app.ts
11123
- var VERSION = "0.11.3";
11720
+ var VERSION = "0.12.0";
11124
11721
  var WEB_DIR = (() => {
11125
11722
  if (process.env.SWARM_WEB_DIR)
11126
11723
  return process.env.SWARM_WEB_DIR;
@@ -11247,6 +11844,11 @@ function createApp(store = new Store, hooks2 = {}) {
11247
11844
  app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
11248
11845
  app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
11249
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)))));
11250
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)))));
11251
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)))));
11252
11854
  app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ra3orblade/swarm",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Local-first control plane for AI-agent development: watch every Claude Code / Codex / Grok session on your machine, ledger tasks and worktrees, enforce rules as hook denials.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",