@ra3orblade/swarm 0.9.0 → 0.10.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
@@ -105,6 +105,150 @@ function actorFromColumns(kind, id, session) {
105
105
  a.session = session;
106
106
  return a;
107
107
  }
108
+ // packages/core/src/adapters/aider/history.ts
109
+ var djb2 = (s) => {
110
+ let h = 5381;
111
+ for (let i = 0;i < s.length; i++)
112
+ h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
113
+ return h.toString(36);
114
+ };
115
+ var toks = (s) => {
116
+ if (!s)
117
+ return 0;
118
+ const n = Number.parseFloat(s.replaceAll(",", ""));
119
+ return Math.round(s.trim().endsWith("k") ? n * 1000 : n);
120
+ };
121
+ var HEADER = /^# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
122
+ var MODEL = /^> Model: (\S+) with /;
123
+ var TOKENS = /^(?:> )?Tokens: ([\d.,]+k?) sent(?:, ([\d.,]+k?) cache write)?(?:, ([\d.,]+k?) cache hit)?, ([\d.,]+k?) received\./;
124
+ var COST = /Cost: \$([\d.]+(?:e-?\d+)?) message/;
125
+ var EDIT = /^> Applied edit to (.+)/;
126
+ var COMMIT = /^> Commit [0-9a-f]{6,}/;
127
+ function parseAiderHistory(chunk, seed, carry) {
128
+ const segments = [];
129
+ let cur = carry ? {
130
+ sessionId: carry.sessionId,
131
+ startMs: carry.startMs,
132
+ model: carry.model,
133
+ title: carry.title,
134
+ turns: [],
135
+ c: { ...carry }
136
+ } : null;
137
+ const closeTurn = (t, cost) => {
138
+ if (!cur)
139
+ return;
140
+ t.cost = cost;
141
+ cur.turns.push(t);
142
+ cur.c.turns++;
143
+ cur.c.text = "";
144
+ cur.c.tools = [];
145
+ cur.c.pending = null;
146
+ };
147
+ const flushPending = () => {
148
+ if (cur?.c.pending)
149
+ closeTurn(cur.c.pending, null);
150
+ };
151
+ for (const line of chunk.split(`
152
+ `)) {
153
+ const h = line.match(HEADER);
154
+ if (h) {
155
+ flushPending();
156
+ if (cur)
157
+ segments.push(cur);
158
+ const stamp = h[1] ?? "";
159
+ const startMs = Date.parse(stamp.replace(" ", "T"));
160
+ const sessionId = `aider-${djb2(`${seed}|${stamp}`)}`;
161
+ cur = {
162
+ sessionId,
163
+ startMs,
164
+ model: null,
165
+ title: null,
166
+ turns: [],
167
+ c: {
168
+ sessionId,
169
+ startMs,
170
+ model: null,
171
+ title: null,
172
+ turns: 0,
173
+ text: "",
174
+ tools: [],
175
+ pending: null
176
+ }
177
+ };
178
+ continue;
179
+ }
180
+ if (!cur)
181
+ continue;
182
+ const c = cur.c;
183
+ if (c.pending) {
184
+ const cost = line.match(COST);
185
+ if (cost) {
186
+ closeTurn(c.pending, Number.parseFloat(cost[1] ?? "0"));
187
+ continue;
188
+ }
189
+ flushPending();
190
+ }
191
+ const m = line.match(MODEL);
192
+ if (m) {
193
+ cur.model = (m[1] ?? "").split("/").pop() || null;
194
+ c.model = cur.model;
195
+ continue;
196
+ }
197
+ const tk = line.match(TOKENS);
198
+ if (tk) {
199
+ const cacheRead = toks(tk[3]);
200
+ const turn = {
201
+ id: `${c.sessionId}-t${c.turns}`,
202
+ ts: new Date(c.startMs + c.turns * 1000).toISOString(),
203
+ model: c.model ?? "aider",
204
+ usage: {
205
+ input: Math.max(0, toks(tk[1]) - cacheRead),
206
+ output: toks(tk[4]),
207
+ cacheWrite: toks(tk[2]),
208
+ cacheWrite1h: 0,
209
+ cacheRead,
210
+ thinking: 0
211
+ },
212
+ text: c.text,
213
+ tools: c.tools,
214
+ effort: null,
215
+ sidechain: false
216
+ };
217
+ const cost = line.match(COST);
218
+ if (cost)
219
+ closeTurn(turn, Number.parseFloat(cost[1] ?? "0"));
220
+ else
221
+ c.pending = turn;
222
+ continue;
223
+ }
224
+ if (line.startsWith("#### ")) {
225
+ const t = line.slice(5).trim();
226
+ if (t && !cur.title) {
227
+ cur.title = t.slice(0, 80);
228
+ c.title = cur.title;
229
+ }
230
+ continue;
231
+ }
232
+ if (EDIT.test(line)) {
233
+ c.tools.push("edit");
234
+ continue;
235
+ }
236
+ if (COMMIT.test(line)) {
237
+ c.tools.push("commit");
238
+ continue;
239
+ }
240
+ if (line.startsWith(">"))
241
+ continue;
242
+ if (line.trim() && c.text.length < 400)
243
+ c.text = `${c.text}${c.text ? `
244
+ ` : ""}${line}`.slice(0, 400);
245
+ }
246
+ if (cur)
247
+ segments.push(cur);
248
+ const last = cur ? { ...cur.c } : null;
249
+ return { segments, carry: last };
250
+ }
251
+
108
252
  // packages/core/src/adapters/claude-code/transcript.ts
109
253
  function parseTranscriptChunk(chunk) {
110
254
  const out = {
@@ -385,6 +529,62 @@ function parseGrokUpdates(chunk) {
385
529
  }
386
530
  return out;
387
531
  }
532
+
533
+ // packages/core/src/adapters/opencode/db.ts
534
+ var ocModel = (d) => {
535
+ if (typeof d.model === "string")
536
+ return d.model;
537
+ if (d.model && typeof d.model === "object" && typeof d.model.id === "string")
538
+ return d.model.id;
539
+ return typeof d.modelID === "string" ? d.modelID : null;
540
+ };
541
+ var ocTs = (t, fallbackMs) => {
542
+ if (typeof t === "number")
543
+ return new Date(t).toISOString();
544
+ if (typeof t === "string" && !Number.isNaN(Date.parse(t)))
545
+ return new Date(t).toISOString();
546
+ return new Date(fallbackMs).toISOString();
547
+ };
548
+ function opencodeTurn(sessionId, msgId, data, fallbackMs = 0, sidechain = false) {
549
+ let d;
550
+ try {
551
+ d = JSON.parse(data);
552
+ } catch {
553
+ return null;
554
+ }
555
+ if ((d.type ?? d.role) !== "assistant")
556
+ return null;
557
+ const t = d.tokens ?? {};
558
+ let text = "";
559
+ const tools = [];
560
+ for (const p of Array.isArray(d.content) ? d.content : []) {
561
+ if (p?.type === "text" && typeof p.text === "string" && text.length < 400)
562
+ text = `${text}${p.text}`.slice(0, 400);
563
+ else if (p?.type === "tool") {
564
+ const name = p.tool ?? p.name;
565
+ if (name)
566
+ tools.push(name);
567
+ }
568
+ }
569
+ return {
570
+ id: `${sessionId}-${msgId}`,
571
+ ts: ocTs(d.time?.created, fallbackMs),
572
+ model: ocModel(d) ?? "opencode",
573
+ usage: {
574
+ input: t.input ?? 0,
575
+ output: t.output ?? 0,
576
+ cacheWrite: t.cache?.write ?? 0,
577
+ cacheWrite1h: 0,
578
+ cacheRead: t.cache?.read ?? 0,
579
+ thinking: t.reasoning ?? 0
580
+ },
581
+ text,
582
+ tools,
583
+ effort: null,
584
+ sidechain,
585
+ cost: typeof d.cost === "number" && d.cost > 0 ? d.cost : null
586
+ };
587
+ }
388
588
  // packages/core/src/adapters/claude-code/hooks.ts
389
589
  var HOOK_EVENTS = [
390
590
  "SessionStart",
@@ -796,6 +996,9 @@ var DEFAULT_CONFIG = {
796
996
  gates: { required: [], auto: "session-end", defs: {} },
797
997
  workflows: {},
798
998
  budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
999
+ models: { allow: [] },
1000
+ notify: { webhook: null },
1001
+ team: { url: null, forward: ["ledger", "cost"], interval: 5 },
799
1002
  events: { retain_days: 30 },
800
1003
  audit: { retain_days: 0 },
801
1004
  privacy: DEFAULT_PRIVACY,
@@ -891,6 +1094,26 @@ function validate(c) {
891
1094
  on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
892
1095
  },
893
1096
  workflows: parseWorkflows(c.workflows),
1097
+ notify: {
1098
+ webhook: (() => {
1099
+ const w = c.notify?.webhook;
1100
+ return typeof w === "string" && /^https?:\/\//.test(w.trim()) ? w.trim() : null;
1101
+ })()
1102
+ },
1103
+ models: {
1104
+ allow: Array.isArray(c.models?.allow) ? c.models.allow.filter((m) => typeof m === "string" && m.trim() !== "") : []
1105
+ },
1106
+ team: (() => {
1107
+ const t = c.team ?? {};
1108
+ const url = typeof t.url === "string" && /^https?:\/\//.test(t.url.trim()) ? t.url.trim().replace(/\/+$/, "") : null;
1109
+ const iv = Number(t.interval);
1110
+ const KINDS = ["ledger", "cost", "transcripts"];
1111
+ return {
1112
+ url,
1113
+ forward: Array.isArray(t.forward) ? t.forward.filter((k) => typeof k === "string" && KINDS.includes(k)) : ["ledger", "cost"],
1114
+ interval: Number.isFinite(iv) && iv >= 1 && iv <= 300 ? Math.round(iv) : 5
1115
+ };
1116
+ })(),
894
1117
  events: {
895
1118
  retain_days: days(c.events?.retain_days, 30)
896
1119
  },
@@ -1505,6 +1728,41 @@ function executedGateInput(task, gate, cmd, outcome) {
1505
1728
  evidence: evidenceTail(outcome.output) || null
1506
1729
  };
1507
1730
  }
1731
+ // packages/core/src/graphs.ts
1732
+ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
1733
+ const files = new Map;
1734
+ const sessions = new Map;
1735
+ for (const r of rows) {
1736
+ if (!r.path || !r.sessionId)
1737
+ continue;
1738
+ const f = files.get(r.path) ?? { readers: new Set, writers: new Set };
1739
+ const s = sessions.get(r.sessionId) ?? { files: new Set, writes: 0 };
1740
+ if (writeTools.has(r.tool)) {
1741
+ f.writers.add(r.sessionId);
1742
+ s.writes++;
1743
+ } else
1744
+ f.readers.add(r.sessionId);
1745
+ s.files.add(r.path);
1746
+ files.set(r.path, f);
1747
+ sessions.set(r.sessionId, s);
1748
+ }
1749
+ const out = [...files.entries()].map(([path, f]) => {
1750
+ const writers = [...f.writers].sort();
1751
+ const readers = [...f.readers].filter((id) => !f.writers.has(id)).sort();
1752
+ const touchers = writers.length + readers.length;
1753
+ return { path, readers, writers, contested: touchers >= 2 && writers.length >= 1 };
1754
+ });
1755
+ out.sort((a, b) => Number(b.contested) - Number(a.contested) || b.readers.length + b.writers.length - (a.readers.length + a.writers.length) || a.path.localeCompare(b.path));
1756
+ return {
1757
+ sessions: [...sessions.entries()].map(([id, s]) => ({
1758
+ id,
1759
+ files: s.files.size,
1760
+ writes: s.writes
1761
+ })),
1762
+ files: out,
1763
+ contested: out.filter((f) => f.contested).length
1764
+ };
1765
+ }
1508
1766
  // packages/core/src/ledger.ts
1509
1767
  var DEFAULT_LEASE_MINUTES = 45;
1510
1768
  function isExpired(claim, now) {
@@ -1838,6 +2096,92 @@ ${lines.join(`
1838
2096
  `)}
1839
2097
  Reply with swarm_send if a reply is expected.`;
1840
2098
  }
2099
+ // packages/core/src/outcomes.ts
2100
+ var DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
2101
+ var median = (xs) => {
2102
+ if (!xs.length)
2103
+ return null;
2104
+ const s = [...xs].sort((a, b) => a - b);
2105
+ const mid = Math.floor(s.length / 2);
2106
+ return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
2107
+ };
2108
+ function scorecard(key, rows) {
2109
+ const merged = rows.filter((r) => r.outcome === "merged");
2110
+ const reverted = rows.filter((r) => r.outcome === "reverted");
2111
+ const open = rows.filter((r) => r.outcome === "open");
2112
+ const noPr = rows.filter((r) => r.outcome === "no-pr");
2113
+ const finished = merged.length + reverted.length + noPr.length;
2114
+ const mergedCost = merged.reduce((a, r) => a + r.costUsd, 0);
2115
+ return {
2116
+ key,
2117
+ branches: rows.length,
2118
+ merged: merged.length,
2119
+ reverted: reverted.length,
2120
+ open: open.length,
2121
+ noPr: noPr.length,
2122
+ mergeRate: finished ? merged.length / finished : null,
2123
+ medianLeadHours: median(merged.map((r) => r.leadHours).filter((x) => x != null)),
2124
+ costPerMerge: merged.length ? mergedCost / merged.length : null
2125
+ };
2126
+ }
2127
+ function outcomeReport(sessions, prs, revertedShas) {
2128
+ const byBranch = new Map;
2129
+ for (const s of sessions) {
2130
+ if (!s.branch || DEFAULT_BRANCHES.has(s.branch))
2131
+ continue;
2132
+ const a = byBranch.get(s.branch) ?? [];
2133
+ a.push(s);
2134
+ byBranch.set(s.branch, a);
2135
+ }
2136
+ const prByBranch = new Map;
2137
+ for (const pr of prs) {
2138
+ const prev = prByBranch.get(pr.branch);
2139
+ if (!prev || pr.state === "merged" && prev.state !== "merged" || pr.state === prev.state && pr.number > prev.number)
2140
+ prByBranch.set(pr.branch, pr);
2141
+ }
2142
+ const rows = [...byBranch.entries()].map(([branch, ss]) => {
2143
+ const dominant = [...ss].sort((a, b) => (b.costUsd ?? 0) - (a.costUsd ?? 0) || a.startedAt.localeCompare(b.startedAt))[0];
2144
+ const pr = prByBranch.get(branch) ?? null;
2145
+ const wasReverted = (sha) => {
2146
+ if (!sha)
2147
+ return false;
2148
+ const s = sha.toLowerCase();
2149
+ for (const r of revertedShas)
2150
+ if (s.startsWith(r) || r.startsWith(s))
2151
+ return true;
2152
+ return false;
2153
+ };
2154
+ const outcome = !pr ? "no-pr" : pr.state === "open" ? "open" : wasReverted(pr.mergeSha) ? "reverted" : "merged";
2155
+ const firstStart = ss.map((s) => s.startedAt).sort()[0];
2156
+ const leadHours = outcome === "merged" && pr?.mergedAt ? Math.max(0, (new Date(pr.mergedAt).getTime() - new Date(firstStart).getTime()) / 3600000) : null;
2157
+ return {
2158
+ branch,
2159
+ outcome,
2160
+ prNumber: pr?.number ?? null,
2161
+ title: pr?.title ?? null,
2162
+ url: pr?.url ?? null,
2163
+ mergedAt: pr?.mergedAt ?? null,
2164
+ leadHours,
2165
+ sessions: ss.map((s) => s.id),
2166
+ model: dominant.model,
2167
+ agent: dominant.agent,
2168
+ costUsd: ss.reduce((a, s) => a + (s.costUsd ?? 0), 0)
2169
+ };
2170
+ });
2171
+ rows.sort((a, b) => (b.mergedAt ?? "").localeCompare(a.mergedAt ?? "") || a.branch.localeCompare(b.branch));
2172
+ const group = (key) => {
2173
+ const m = new Map;
2174
+ for (const r of rows) {
2175
+ const k = key(r) ?? "unknown";
2176
+ m.set(k, [...m.get(k) ?? [], r]);
2177
+ }
2178
+ return [...m.entries()].map(([k, rs]) => scorecard(k, rs)).sort((a, b) => b.branches - a.branches);
2179
+ };
2180
+ return { branches: rows, byModel: group((r) => r.model), byAgent: group((r) => r.agent) };
2181
+ }
2182
+ function parseReverts(gitLog) {
2183
+ return new Set([...gitLog.matchAll(/This reverts commit ([0-9a-f]{7,40})/gi)].map((m) => m[1].toLowerCase()));
2184
+ }
1841
2185
  // packages/core/src/policy.ts
1842
2186
  import { createHash } from "crypto";
1843
2187
  var HOOK_MARK = "swarm-hook";
@@ -2178,6 +2522,49 @@ ${lines.join(`
2178
2522
  `)}` : ""}`
2179
2523
  };
2180
2524
  }
2525
+ // packages/core/src/stall.ts
2526
+ var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
2527
+ function toolResponseErrored(resp) {
2528
+ if (typeof resp === "string")
2529
+ return /^\s*error[:\s]/i.test(resp);
2530
+ if (!resp || typeof resp !== "object")
2531
+ return false;
2532
+ const r = resp;
2533
+ if (r.is_error === true || r.isError === true)
2534
+ return true;
2535
+ if (r.success === false)
2536
+ return true;
2537
+ if (r.interrupted === true)
2538
+ return true;
2539
+ if (typeof r.error === "string" && r.error.length > 0)
2540
+ return true;
2541
+ return false;
2542
+ }
2543
+ function detectStall(calls, opts = {}) {
2544
+ const o = { ...STALL_DEFAULTS, ...opts };
2545
+ const tail = calls.slice(-o.window);
2546
+ const last = tail.at(-1);
2547
+ if (!last)
2548
+ return null;
2549
+ let run = 0;
2550
+ let runErrors = 0;
2551
+ for (let i = tail.length - 1;i >= 0; i--) {
2552
+ const c = tail[i];
2553
+ if (!c || c.tool !== last.tool || c.input !== last.input)
2554
+ break;
2555
+ run++;
2556
+ if (c.errored)
2557
+ runErrors++;
2558
+ }
2559
+ if (run >= o.repeat && runErrors >= o.repeatErrors)
2560
+ return { kind: "repeat", reason: `repeating a failing ${last.tool} call \xD7${run}` };
2561
+ let streak = 0;
2562
+ for (let i = tail.length - 1;i >= 0 && tail[i]?.errored; i--)
2563
+ streak++;
2564
+ if (streak >= o.errors)
2565
+ return { kind: "errors", reason: `${streak} tool calls failing in a row` };
2566
+ return null;
2567
+ }
2181
2568
  // packages/core/src/tasks.ts
2182
2569
  var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
2183
2570
  var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
@@ -2337,6 +2724,35 @@ function linearIssuesQuery(teamKey, first = 200) {
2337
2724
  inverseRelations { nodes { type issue { identifier } } }
2338
2725
  } } }`;
2339
2726
  }
2727
+ // packages/core/src/team.ts
2728
+ import { createPublicKey, verify as nodeVerify } from "crypto";
2729
+ function modelAllowed(model, allow) {
2730
+ if (!allow.length)
2731
+ return true;
2732
+ return allow.some((g) => {
2733
+ const re = new RegExp(`^${g.trim().split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`, "i");
2734
+ return re.test(model);
2735
+ });
2736
+ }
2737
+ function verifyPolicySignature(toml, signatureB64, publicKeyB64) {
2738
+ try {
2739
+ return nodeVerify(null, Buffer.from(toml), createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" }), Buffer.from(signatureB64, "base64"));
2740
+ } catch {
2741
+ return false;
2742
+ }
2743
+ }
2744
+ function clusterProjectKey(remoteUrl) {
2745
+ if (!remoteUrl)
2746
+ return null;
2747
+ const url = remoteUrl.trim();
2748
+ const m = url.match(/^https?:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/) ?? url.match(/^(?:ssh:\/\/)?(?:[^@/]+@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/);
2749
+ if (!m?.[1] || !m[2])
2750
+ return null;
2751
+ const host = m[1].toLowerCase();
2752
+ if (host.includes(" ") || !host.includes("."))
2753
+ return null;
2754
+ return `${host}/${m[2]}`;
2755
+ }
2340
2756
  // packages/core/src/worktree.ts
2341
2757
  import { join as join3 } from "path";
2342
2758
  function planBootstrap(cfg, repoRoot, worktree) {
@@ -2413,7 +2829,7 @@ function planGc(worktrees, claims) {
2413
2829
  // packages/daemon/src/app.ts
2414
2830
  import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
2415
2831
  import { homedir as homedir4 } from "os";
2416
- import { dirname as dirname4, join as join9 } from "path";
2832
+ import { dirname as dirname4, join as join10 } from "path";
2417
2833
  import { fileURLToPath as fileURLToPath2 } from "url";
2418
2834
 
2419
2835
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
@@ -4343,6 +4759,64 @@ class ForgeService {
4343
4759
  }
4344
4760
  }));
4345
4761
  }
4762
+ outcomeCache = new Map;
4763
+ async merged(projectId, root) {
4764
+ const hit = this.outcomeCache.get(projectId);
4765
+ if (hit && Date.now() - hit.at < 600000)
4766
+ return hit;
4767
+ let merged = [];
4768
+ const remote = this.remote(root);
4769
+ if (remote?.forge === "github") {
4770
+ const out = await this.run([
4771
+ "gh",
4772
+ "pr",
4773
+ "list",
4774
+ "--state",
4775
+ "merged",
4776
+ "--limit",
4777
+ "200",
4778
+ "--json",
4779
+ "number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
4780
+ ], root);
4781
+ if (out)
4782
+ merged = JSON.parse(out).map((r) => ({
4783
+ branch: String(r.headRefName ?? ""),
4784
+ number: Number(r.number ?? 0),
4785
+ title: String(r.title ?? ""),
4786
+ url: String(r.url ?? ""),
4787
+ createdAt: r.createdAt ?? null,
4788
+ mergedAt: r.mergedAt ?? null,
4789
+ mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
4790
+ }));
4791
+ } else if (remote?.forge === "gitlab") {
4792
+ const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
4793
+ if (out)
4794
+ merged = JSON.parse(out).map((r) => ({
4795
+ branch: String(r.source_branch ?? ""),
4796
+ number: Number(r.iid ?? 0),
4797
+ title: String(r.title ?? ""),
4798
+ url: String(r.web_url ?? ""),
4799
+ createdAt: r.created_at ?? null,
4800
+ mergedAt: r.merged_at ?? null,
4801
+ mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
4802
+ }));
4803
+ }
4804
+ const log = Bun.spawnSync([
4805
+ "git",
4806
+ "-C",
4807
+ root,
4808
+ "log",
4809
+ "--grep",
4810
+ "This reverts commit",
4811
+ "--format=%B",
4812
+ "-n",
4813
+ "300"
4814
+ ]);
4815
+ const reverted = log.exitCode === 0 ? [...parseReverts(new TextDecoder().decode(log.stdout))] : [];
4816
+ const entry = { at: Date.now(), merged, reverted };
4817
+ this.outcomeCache.set(projectId, entry);
4818
+ return entry;
4819
+ }
4346
4820
  remote(root) {
4347
4821
  const r = Bun.spawnSync(["git", "-C", root, "remote", "get-url", "origin"]);
4348
4822
  if (r.exitCode !== 0)
@@ -4575,6 +5049,16 @@ function currentBranch(cwd) {
4575
5049
  branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
4576
5050
  return branchCache.get(cwd)?.v ?? null;
4577
5051
  }
5052
+ var originCache = new Map;
5053
+ function originUrl(root) {
5054
+ const hit = originCache.get(root);
5055
+ const now = Date.now();
5056
+ if (hit && now - hit.t < 300000)
5057
+ return hit.v;
5058
+ const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
5059
+ originCache.set(root, { v, t: now });
5060
+ return v;
5061
+ }
4578
5062
  function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
4579
5063
  const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
4580
5064
  const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
@@ -4706,6 +5190,14 @@ class Runner {
4706
5190
  return { ok: false, reason: "unknown project" };
4707
5191
  if (!input.prompt.trim())
4708
5192
  return { ok: false, reason: "prompt is required" };
5193
+ if (input.model) {
5194
+ const allow = this.store.config(input.projectId).models.allow;
5195
+ if (!modelAllowed(input.model, allow))
5196
+ return {
5197
+ ok: false,
5198
+ reason: `model "${input.model}" is not in [models] allow (${allow.join(", ")})`
5199
+ };
5200
+ }
4709
5201
  if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
4710
5202
  return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
4711
5203
  if (this.get(input.task)?.projectId === input.projectId)
@@ -5004,6 +5496,7 @@ class Runner {
5004
5496
  import { Database } from "bun:sqlite";
5005
5497
  import {
5006
5498
  closeSync,
5499
+ copyFileSync,
5007
5500
  existsSync as existsSync6,
5008
5501
  mkdirSync as mkdirSync4,
5009
5502
  openSync as openSync3,
@@ -5016,7 +5509,7 @@ import {
5016
5509
  unlinkSync,
5017
5510
  writeFileSync as writeFileSync2
5018
5511
  } from "fs";
5019
- import { homedir as homedir3, tmpdir, userInfo } from "os";
5512
+ import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
5020
5513
  import { basename, dirname as dirname3, join as join8 } from "path";
5021
5514
 
5022
5515
  // packages/daemon/src/bootstrap.ts
@@ -5126,14 +5619,14 @@ class TaskSources {
5126
5619
  `)[0] ?? code}`);
5127
5620
  return normalizeGithubIssues(JSON.parse(out));
5128
5621
  }
5129
- async linear(team) {
5622
+ async linear(team2) {
5130
5623
  const key = this.env.LINEAR_API_KEY;
5131
5624
  if (!key)
5132
5625
  throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
5133
5626
  const r = await fetch("https://api.linear.app/graphql", {
5134
5627
  method: "POST",
5135
5628
  headers: { "content-type": "application/json", authorization: key },
5136
- body: JSON.stringify({ query: linearIssuesQuery(team) })
5629
+ body: JSON.stringify({ query: linearIssuesQuery(team2) })
5137
5630
  });
5138
5631
  if (!r.ok)
5139
5632
  throw new Error(`Linear API ${r.status}`);
@@ -5159,11 +5652,12 @@ CREATE INDEX IF NOT EXISTS events_type_seq ON events(type, seq);
5159
5652
  CREATE TABLE IF NOT EXISTS turns (
5160
5653
  id TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, ts TEXT, model TEXT, effort TEXT, sidechain INTEGER,
5161
5654
  input INTEGER, output INTEGER, cache_write INTEGER, cache_write_1h INTEGER, cache_read INTEGER, thinking INTEGER,
5162
- cost_usd REAL, text TEXT, tools TEXT
5655
+ cost_usd REAL, cost_fixed INTEGER DEFAULT 0, text TEXT, tools TEXT
5163
5656
  );
5164
5657
  CREATE INDEX IF NOT EXISTS turns_session ON turns(session_id, ts);
5165
5658
  CREATE INDEX IF NOT EXISTS turns_ts ON turns(ts);
5166
5659
  CREATE TABLE IF NOT EXISTS tails (path TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, offset INTEGER);
5660
+ CREATE TABLE IF NOT EXISTS outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, payload TEXT, created_at TEXT);
5167
5661
  CREATE TABLE IF NOT EXISTS resources (
5168
5662
  name TEXT, project_id TEXT, kind TEXT, owner TEXT, session_id TEXT,
5169
5663
  pid INTEGER, port INTEGER, acquired_at TEXT, expires_at TEXT, released INTEGER DEFAULT 0,
@@ -5229,6 +5723,8 @@ class Store {
5229
5723
  this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
5230
5724
  this.db.exec(SCHEMA);
5231
5725
  this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
5726
+ this.ensureColumn("claims", "team_state", "TEXT");
5727
+ this.ensureColumn("turns", "cost_fixed", "INTEGER DEFAULT 0");
5232
5728
  this.ensureColumn("projects", "sort_order", "INTEGER");
5233
5729
  this.ensureColumn("projects", "icon", "TEXT");
5234
5730
  this.ensureColumn("projects", "color", "TEXT");
@@ -5254,6 +5750,12 @@ class Store {
5254
5750
  setMeta(key, value) {
5255
5751
  this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
5256
5752
  }
5753
+ metaValue(key) {
5754
+ return this.meta(key) || null;
5755
+ }
5756
+ setMetaValue(key, value) {
5757
+ this.setMeta(key, value);
5758
+ }
5257
5759
  slimExistingEvents() {
5258
5760
  if (this.meta("events_slim") === "1")
5259
5761
  return;
@@ -6414,6 +6916,18 @@ ${err}
6414
6916
  };
6415
6917
  return { decision: d, display: input.command ?? input.file_path ?? tool };
6416
6918
  }
6919
+ const tb = this.teamBudgets().find((x) => x.level === "exceeded" && x.on_exceed === "ask" && (x.scope !== "project" || x.key === this.clusterKeyFor(project.id)));
6920
+ if (tb) {
6921
+ const label = tb.scope === "org" ? "the org" : `${tb.scope} ${tb.key}`;
6922
+ return {
6923
+ decision: {
6924
+ action: "ask",
6925
+ rule: "budget",
6926
+ reason: `team ${tb.kind} budget for ${label} is exceeded ($${tb.spent.toFixed(2)} of $${tb.limit}) \u2014 the team's on_exceed = "ask": confirm each change, or have an admin raise it (POST /t1/budgets)`
6927
+ },
6928
+ display: input.command ?? input.file_path ?? tool
6929
+ };
6930
+ }
6417
6931
  }
6418
6932
  const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
6419
6933
  const cmd = tool === "Bash" ? input.command : undefined;
@@ -6581,7 +7095,7 @@ ${err}
6581
7095
  this.reprice();
6582
7096
  }
6583
7097
  reprice() {
6584
- const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns").all();
7098
+ const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns WHERE cost_fixed IS NOT 1").all();
6585
7099
  const up = this.db.query("UPDATE turns SET cost_usd = ? WHERE id = ?");
6586
7100
  const tx = this.db.transaction(() => {
6587
7101
  for (const r of rows)
@@ -6742,12 +7256,69 @@ ${err}
6742
7256
  if (stored.type === "incident.opened")
6743
7257
  this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
6744
7258
  this.projectSession(stored);
7259
+ if (stored.type === "incident.opened") {
7260
+ const webhook = this.policyFor(null).config.notify.webhook;
7261
+ if (webhook) {
7262
+ const p2 = stored.payload ?? {};
7263
+ const project = this.project(stored.projectId)?.name ?? stored.projectId;
7264
+ fetch(webhook, {
7265
+ method: "POST",
7266
+ headers: { "content-type": "application/json" },
7267
+ body: JSON.stringify({
7268
+ text: `Swarm incident \xB7 ${p2.rule ?? "?"} \xB7 ${project}
7269
+ ${p2.command ?? ""}
7270
+ ${p2.reason ?? ""}`.trim(),
7271
+ rule: p2.rule,
7272
+ project,
7273
+ sessionId: stored.sessionId,
7274
+ ts: stored.ts
7275
+ }),
7276
+ signal: AbortSignal.timeout(5000)
7277
+ }).catch(() => {});
7278
+ }
7279
+ }
7280
+ const team2 = this.policyFor(null).config.team;
7281
+ if (team2.url && team2.forward.includes("ledger") && isAuditType(stored.type)) {
7282
+ this.db.query("INSERT INTO outbox (kind, payload, created_at) VALUES ('event', ?, ?)").run(JSON.stringify({
7283
+ seq: stored.seq,
7284
+ ts: stored.ts,
7285
+ type: stored.type,
7286
+ projectId: stored.projectId,
7287
+ sessionId: stored.sessionId,
7288
+ actor: actor2,
7289
+ payload: slim.payload ?? null
7290
+ }), stored.ts);
7291
+ }
6745
7292
  this.touch();
6746
7293
  const wire = toWire(stored);
6747
7294
  for (const l of this.listeners)
6748
7295
  l(wire);
6749
7296
  return stored;
6750
7297
  }
7298
+ outboxPending(limit = 200) {
7299
+ return this.db.query("SELECT seq, kind, payload FROM outbox ORDER BY seq LIMIT ?").all(limit);
7300
+ }
7301
+ outboxAck(upTo) {
7302
+ this.db.query("DELETE FROM outbox WHERE seq <= ?").run(upTo);
7303
+ }
7304
+ outboxStatus() {
7305
+ const r = this.db.query("SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM outbox").get();
7306
+ return { pending: r.n, oldest: r.oldest };
7307
+ }
7308
+ machineIdentity() {
7309
+ let id = this.meta("machine_id");
7310
+ if (!id) {
7311
+ id = crypto.randomUUID();
7312
+ this.setMeta("machine_id", id);
7313
+ }
7314
+ return { id, name: hostname() };
7315
+ }
7316
+ spendRollup(day = new Date().toISOString().slice(0, 10)) {
7317
+ return this.db.query(`SELECT s.project_id AS projectId, COALESCE(s.agent, 'claude-code') AS agent, t.model AS model,
7318
+ SUM(t.cost_usd) AS cost, SUM(t.input + t.cache_write + t.cache_read) AS tokensIn, SUM(t.output) AS tokensOut
7319
+ FROM turns t JOIN sessions s ON s.id = t.session_id
7320
+ WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, agent, t.model`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
7321
+ }
6751
7322
  audit(opts = {}) {
6752
7323
  const where = [`type IN (${AUDIT_TYPES_SQL})`];
6753
7324
  const args = [];
@@ -6875,13 +7446,13 @@ ${err}
6875
7446
  persistTurns(sessionId, agentId, turns) {
6876
7447
  const privacy = this.policyFor(null).config.privacy;
6877
7448
  const res = this.redactions();
6878
- const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
6879
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7449
+ const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, cost_fixed, text, tools)
7450
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6880
7451
  ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
6881
- cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
7452
+ cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, cost_fixed=excluded.cost_fixed, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
6882
7453
  const tx = this.db.transaction((ts) => {
6883
7454
  for (const t of ts) {
6884
- up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
7455
+ up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, t.cost ?? costUsd(t.model, t.usage, this.prices), t.cost != null ? 1 : 0, privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
6885
7456
  }
6886
7457
  });
6887
7458
  if (turns.length)
@@ -7091,6 +7662,160 @@ ${err}
7091
7662
  }
7092
7663
  return n;
7093
7664
  }
7665
+ heldClaimsForSync() {
7666
+ return this.db.query("SELECT project_id, task, acquired_at, expires_at, actor_kind, actor_id, team_state FROM claims WHERE state = 'held'").all().map((r) => ({
7667
+ projectId: r.project_id,
7668
+ task: r.task,
7669
+ acquiredAt: r.acquired_at,
7670
+ expiresAt: r.expires_at,
7671
+ actorKind: r.actor_kind ?? null,
7672
+ actorId: r.actor_id ?? null,
7673
+ teamState: r.team_state ?? null
7674
+ }));
7675
+ }
7676
+ markClaimTeamState(projectId, task, state) {
7677
+ this.db.query("UPDATE claims SET team_state = ? WHERE project_id = ? AND task = ?").run(state, projectId, task);
7678
+ }
7679
+ revokeClaimConflict(projectId, task, holder) {
7680
+ const row = this.db.query("SELECT state FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
7681
+ if (row?.state !== "held")
7682
+ return;
7683
+ const now = new Date().toISOString();
7684
+ this.db.query("UPDATE claims SET state = 'released', released_at = ?, team_state = 'conflict' WHERE project_id = ? AND task = ?").run(now, projectId, task);
7685
+ this.append({
7686
+ ts: now,
7687
+ type: "claim.released",
7688
+ projectId,
7689
+ sessionId: null,
7690
+ payload: { task, summary: `revoked \u2014 the team ledger holds ${task} on ${holder}` }
7691
+ });
7692
+ this.append({
7693
+ ts: now,
7694
+ type: "incident.opened",
7695
+ projectId,
7696
+ sessionId: null,
7697
+ payload: {
7698
+ rule: "claim_conflict",
7699
+ action: "revoked",
7700
+ command: task,
7701
+ reason: `the team daemon holds ${task} for ${holder}; the local claim was revoked \u2014 the worktree is untouched`
7702
+ }
7703
+ });
7704
+ }
7705
+ aiderCarries = new Map;
7706
+ recoverAiderCarry(sessionId) {
7707
+ const s = this.db.query("SELECT started_at, model, title FROM sessions WHERE id = ?").get(sessionId);
7708
+ if (!s)
7709
+ return null;
7710
+ const t = this.db.query("SELECT COUNT(*) AS n FROM turns WHERE session_id = ?").get(sessionId);
7711
+ return {
7712
+ sessionId,
7713
+ startMs: Date.parse(s.started_at) || 0,
7714
+ model: s.model,
7715
+ title: s.title,
7716
+ turns: t.n,
7717
+ text: "",
7718
+ tools: [],
7719
+ pending: null
7720
+ };
7721
+ }
7722
+ tailAider(windowMs = 3 * 24 * 60 * 60000) {
7723
+ const roots = this.db.query("SELECT DISTINCT root FROM projects WHERE root IS NOT NULL AND root != ''").all();
7724
+ let n = 0;
7725
+ for (const { root } of roots) {
7726
+ const path = join8(root, ".aider.chat.history.md");
7727
+ let mtime;
7728
+ try {
7729
+ mtime = statSync(path).mtimeMs;
7730
+ } catch {
7731
+ continue;
7732
+ }
7733
+ if (mtime < Date.now() - windowMs)
7734
+ continue;
7735
+ const row = this.db.query("SELECT offset, session_id FROM tails WHERE path = ?").get(path);
7736
+ const r = this.readFrom(path, row?.offset ?? 0);
7737
+ if (!r)
7738
+ continue;
7739
+ let carry = this.aiderCarries.get(path) ?? null;
7740
+ if (!carry && row?.session_id)
7741
+ carry = this.recoverAiderCarry(row.session_id);
7742
+ const { segments, carry: next } = parseAiderHistory(r.chunk, path, carry);
7743
+ this.aiderCarries.set(path, next);
7744
+ const lastSeg = segments.at(-1);
7745
+ for (const seg of segments) {
7746
+ this.ensureAgentSession(seg.sessionId, "aider", root, seg.startMs || mtime);
7747
+ this.persistTurns(seg.sessionId, null, seg.turns);
7748
+ const live = seg === lastSeg && Date.now() - mtime < 90000;
7749
+ const lastSeen = new Date(seg === lastSeg ? mtime : seg.startMs + seg.turns.length * 1000).toISOString();
7750
+ const lastText = [...seg.turns].reverse().find((t) => t.text)?.text ?? null;
7751
+ this.db.query("UPDATE sessions SET title = COALESCE(title, ?), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(seg.title, seg.model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, seg.sessionId);
7752
+ n += seg.turns.length;
7753
+ }
7754
+ this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, ?, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset, session_id = excluded.session_id").run(path, lastSeg?.sessionId ?? row?.session_id ?? null, r.next);
7755
+ }
7756
+ return n;
7757
+ }
7758
+ ocDbs = new Map;
7759
+ tailOpencode(windowMs = 3 * 24 * 60 * 60000) {
7760
+ const dir = process.env.SWARM_OPENCODE_DIR ?? join8(process.env.XDG_DATA_HOME ?? join8(homedir3(), ".local", "share"), "opencode");
7761
+ let files;
7762
+ try {
7763
+ files = readdirSync(dir).filter((f) => /^opencode[^/]*\.db$/.test(f));
7764
+ } catch {
7765
+ return 0;
7766
+ }
7767
+ let n = 0;
7768
+ for (const f of files) {
7769
+ const path = join8(dir, f);
7770
+ let db = this.ocDbs.get(path);
7771
+ if (!db) {
7772
+ try {
7773
+ db = new Database(path, { readonly: true });
7774
+ } catch {
7775
+ continue;
7776
+ }
7777
+ this.ocDbs.set(path, db);
7778
+ }
7779
+ const row = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path);
7780
+ const lower = Math.max(row?.offset ?? 0, Date.now() - windowMs);
7781
+ let rows;
7782
+ try {
7783
+ rows = db.query(`SELECT m.id, m.session_id, m.time_created, m.time_updated, m.data,
7784
+ s.directory, s.title, s.parent_id
7785
+ FROM message m JOIN session s ON s.id = m.session_id
7786
+ WHERE m.time_updated > ? ORDER BY m.time_updated ASC LIMIT 2000`).all(lower);
7787
+ } catch {
7788
+ continue;
7789
+ }
7790
+ if (!rows.length)
7791
+ continue;
7792
+ let cursor = lower;
7793
+ const bySession = new Map;
7794
+ for (const m of rows) {
7795
+ cursor = Math.max(cursor, m.time_updated ?? 0);
7796
+ const g = bySession.get(m.session_id) ?? { rows: [], last: 0 };
7797
+ g.rows.push(m);
7798
+ g.last = Math.max(g.last, m.time_updated ?? m.time_created ?? 0);
7799
+ bySession.set(m.session_id, g);
7800
+ }
7801
+ for (const [sid, g] of bySession) {
7802
+ const first = g.rows[0];
7803
+ if (!first)
7804
+ continue;
7805
+ this.ensureAgentSession(sid, "opencode", first.directory ?? "", first.time_created ?? g.last);
7806
+ const turns = g.rows.map((m) => opencodeTurn(sid, m.id, m.data, m.time_created ?? 0, first.parent_id != null)).filter((t) => t != null);
7807
+ this.persistTurns(sid, null, turns);
7808
+ const live = Date.now() - g.last < 90000;
7809
+ const lastSeen = new Date(g.last).toISOString();
7810
+ const lastText = [...turns].reverse().find((t) => t.text)?.text ?? null;
7811
+ const model = [...turns].reverse().find((t) => t.model !== "opencode")?.model ?? null;
7812
+ this.db.query("UPDATE sessions SET title = COALESCE(?, title), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(first.title, model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, sid);
7813
+ n += turns.length;
7814
+ }
7815
+ this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, NULL, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset").run(path, cursor);
7816
+ }
7817
+ return n;
7818
+ }
7094
7819
  ingestLog(path, agent, parse, cwdHint, titleHint) {
7095
7820
  const off = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path) ?? { offset: 0 };
7096
7821
  const r = this.readFrom(path, off.offset);
@@ -7272,6 +7997,77 @@ ${err}
7272
7997
  expiresAt: r.expires_at
7273
7998
  }));
7274
7999
  }
8000
+ collisions(projectId) {
8001
+ const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
8002
+ const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
8003
+ WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [cutoff, projectId] : [cutoff]);
8004
+ if (!live.length)
8005
+ return { sessions: [], files: [], contested: 0 };
8006
+ const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool, json_extract(payload,'$.toolInput.file_path') AS path
8007
+ FROM events WHERE type = 'tool.requested' AND session_id IN (${live.map(() => "?").join(",")})
8008
+ AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL`).all(...live.map((s) => s.id));
8009
+ const g = collisionGraph(rows.map((r) => ({ sessionId: r.session_id, tool: r.tool ?? "", path: r.path ?? "" })));
8010
+ const meta = new Map(live.map((s) => [s.id, s]));
8011
+ return {
8012
+ ...g,
8013
+ sessions: g.sessions.map((s) => {
8014
+ const m = meta.get(s.id);
8015
+ return {
8016
+ ...s,
8017
+ title: m?.title ?? null,
8018
+ agent: m?.agent ?? "claude-code",
8019
+ projectId: m?.project_id ?? null
8020
+ };
8021
+ })
8022
+ };
8023
+ }
8024
+ stalls = new Map;
8025
+ checkStalls() {
8026
+ 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());
8027
+ const liveIds = new Set(live.map((s) => s.id));
8028
+ for (const id of [...this.stalls.keys()])
8029
+ if (!liveIds.has(id))
8030
+ this.stalls.delete(id);
8031
+ let flagged = 0;
8032
+ for (const s of live) {
8033
+ const rows = this.db.query("SELECT payload FROM events WHERE session_id = ? AND type = 'tool.completed' ORDER BY seq DESC LIMIT 12").all(s.id);
8034
+ const calls = rows.reverse().map((r) => {
8035
+ let p = {};
8036
+ try {
8037
+ p = JSON.parse(r.payload || "{}");
8038
+ } catch {}
8039
+ return {
8040
+ tool: typeof p.tool === "string" ? p.tool : "?",
8041
+ input: JSON.stringify(p.toolInput ?? null),
8042
+ errored: toolResponseErrored(p.toolResponse),
8043
+ ts: ""
8044
+ };
8045
+ });
8046
+ const stall2 = detectStall(calls);
8047
+ if (!stall2) {
8048
+ this.stalls.delete(s.id);
8049
+ continue;
8050
+ }
8051
+ flagged++;
8052
+ const prev = this.stalls.get(s.id);
8053
+ this.stalls.set(s.id, stall2);
8054
+ if (prev?.kind === stall2.kind)
8055
+ continue;
8056
+ this.append({
8057
+ ts: new Date().toISOString(),
8058
+ type: "session.stuck",
8059
+ projectId: s.project_id,
8060
+ sessionId: s.id,
8061
+ payload: {
8062
+ kind: stall2.kind,
8063
+ reason: stall2.reason,
8064
+ summary: `session looks stuck \u2014 ${stall2.reason}`
8065
+ }
8066
+ });
8067
+ this.touch();
8068
+ }
8069
+ return flagged;
8070
+ }
7275
8071
  sweepOrphans() {
7276
8072
  const now = Date.now();
7277
8073
  let n = 0;
@@ -7649,6 +8445,7 @@ ${err}
7649
8445
  lastType: r.last_type,
7650
8446
  lastText: r.last_text ?? null,
7651
8447
  state,
8448
+ stuck: state === "active" || state === "waiting" ? this.stalls.get(r.id)?.reason ?? null : null,
7652
8449
  toolCalls: r.tool_calls,
7653
8450
  subagents: r.subagents,
7654
8451
  turns: r.turns,
@@ -7713,8 +8510,120 @@ ${err}
7713
8510
  fn(p.id, b.status);
7714
8511
  this.touch();
7715
8512
  }
8513
+ for (const b of this.teamBudgets()) {
8514
+ if (b.level === "ok")
8515
+ continue;
8516
+ const mapKey = `team:${b.scope}:${b.key}`;
8517
+ const seen = `${day}:${b.level}`;
8518
+ const affected = b.scope === "project" ? this.projects().filter((p) => this.clusterKeyFor(p.id) === b.key) : this.projects();
8519
+ if (this.budgetNotified.get(mapKey) !== seen) {
8520
+ this.budgetNotified.set(mapKey, seen);
8521
+ const label = b.scope === "org" ? "the org" : `${b.scope} ${b.key}`;
8522
+ this.append({
8523
+ ts: new Date().toISOString(),
8524
+ type: "incident.opened",
8525
+ projectId: affected[0]?.id ?? "p_unknown",
8526
+ sessionId: null,
8527
+ payload: {
8528
+ rule: "budget",
8529
+ action: b.level === "exceeded" ? b.on_exceed : "warn",
8530
+ command: `team ${b.kind ?? ""} budget \xB7 ${label}`,
8531
+ reason: b.level === "exceeded" ? `${label} spent $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling set on the team daemon. ${b.on_exceed === "stop" ? "Spawned runs were stopped." : b.on_exceed === "ask" ? "Every Bash/Edit/Write now asks first." : "An admin can raise it via POST /t1/budgets."}` : `${label} is at $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling \u2014 approaching the team's limit`
8532
+ }
8533
+ });
8534
+ this.touch();
8535
+ }
8536
+ if (b.level === "exceeded" && b.on_exceed === "stop")
8537
+ for (const p of affected)
8538
+ for (const fn of this.budgetListeners)
8539
+ fn(p.id, {
8540
+ level: "exceeded",
8541
+ kind: b.kind === "daily" ? "daily" : "weekly",
8542
+ spent: b.spent,
8543
+ limit: b.limit,
8544
+ pct: b.limit ? b.spent / b.limit : 1,
8545
+ daily: { spent: b.spent, limit: b.limit, pct: 1 },
8546
+ weekly: { spent: 0, limit: null, pct: 0 }
8547
+ });
8548
+ }
7716
8549
  return out;
7717
8550
  }
8551
+ teamBudgets() {
8552
+ try {
8553
+ return JSON.parse(this.metaValue("team_budget") ?? "[]");
8554
+ } catch {
8555
+ return [];
8556
+ }
8557
+ }
8558
+ backupTo(destDir) {
8559
+ mkdirSync4(destDir, { recursive: true });
8560
+ const files = [];
8561
+ const dbDest = join8(destDir, "swarm.db");
8562
+ if (existsSync6(dbDest))
8563
+ unlinkSync(dbDest);
8564
+ this.db.exec(`VACUUM INTO '${dbDest.replaceAll("'", "''")}'`);
8565
+ files.push("swarm.db");
8566
+ for (const f of [
8567
+ "config.toml",
8568
+ "policy.toml",
8569
+ "policy.sig.json",
8570
+ "token",
8571
+ "pricing.json",
8572
+ "pricing.litellm.json",
8573
+ "team-token"
8574
+ ]) {
8575
+ const src = join8(this.home, f);
8576
+ if (!existsSync6(src))
8577
+ continue;
8578
+ copyFileSync(src, join8(destDir, f));
8579
+ files.push(f);
8580
+ }
8581
+ return { dest: destDir, files };
8582
+ }
8583
+ clusterKeyCache = new Map;
8584
+ clusterKeyFor(projectId) {
8585
+ const hit = this.clusterKeyCache.get(projectId);
8586
+ if (hit)
8587
+ return hit;
8588
+ const root = this.db.query("SELECT root FROM projects WHERE id = ?").get(projectId)?.root;
8589
+ const key = root && clusterProjectKey(originUrl(root)) || `local:${projectId}`;
8590
+ this.clusterKeyCache.set(projectId, key);
8591
+ return key;
8592
+ }
8593
+ taskSpendRollup(day = new Date().toISOString().slice(0, 10)) {
8594
+ return this.db.query(`SELECT s.project_id AS projectId, c.task AS task, SUM(t.cost_usd) AS cost
8595
+ FROM turns t JOIN sessions s ON s.id = t.session_id
8596
+ JOIN claims c ON c.project_id = s.project_id AND c.worktree != '' AND (s.cwd = c.worktree OR s.cwd LIKE c.worktree || '/%')
8597
+ WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, c.task`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
8598
+ }
8599
+ modelFlagged = new Set;
8600
+ checkModels() {
8601
+ const since = new Date(Date.now() - IDLE_MS).toISOString();
8602
+ const rows = this.db.query("SELECT id, project_id, model FROM sessions WHERE model IS NOT NULL AND model != '' AND state != 'ended' AND last_seen_at > ?").all(since);
8603
+ let n = 0;
8604
+ for (const s of rows) {
8605
+ if (this.modelFlagged.has(s.id))
8606
+ continue;
8607
+ const allow = this.config(s.project_id).models.allow;
8608
+ if (!allow.length || modelAllowed(s.model, allow))
8609
+ continue;
8610
+ this.modelFlagged.add(s.id);
8611
+ n++;
8612
+ this.append({
8613
+ ts: new Date().toISOString(),
8614
+ type: "incident.opened",
8615
+ projectId: s.project_id,
8616
+ sessionId: s.id,
8617
+ payload: {
8618
+ rule: "model_allowlist",
8619
+ action: "observed",
8620
+ command: s.model,
8621
+ reason: `session runs on "${s.model}", outside [models] allow (${allow.join(", ")}) \u2014 nothing was interrupted; spawned runs on this model are refused`
8622
+ }
8623
+ });
8624
+ }
8625
+ return n;
8626
+ }
7718
8627
  spend() {
7719
8628
  const dayStart = new Date;
7720
8629
  dayStart.setHours(0, 0, 0, 0);
@@ -8344,6 +9253,192 @@ function localDayIso(offsetDays) {
8344
9253
  return d.toISOString();
8345
9254
  }
8346
9255
 
9256
+ // packages/daemon/src/team.ts
9257
+ import { writeFileSync as writeFileSync3 } from "fs";
9258
+ import { join as join9 } from "path";
9259
+ var SPEND_EVERY_MS = 60000;
9260
+ var MAX_BACKOFF_MS = 300000;
9261
+ var POLICY_EVERY_MS = 300000;
9262
+
9263
+ class TeamForwarder {
9264
+ store;
9265
+ version;
9266
+ lastTry = 0;
9267
+ backoffMs = 0;
9268
+ lastSpend = 0;
9269
+ constructor(store, version) {
9270
+ this.store = store;
9271
+ this.version = version;
9272
+ }
9273
+ projectKey(projectId) {
9274
+ return this.store.clusterKeyFor(projectId);
9275
+ }
9276
+ status() {
9277
+ const team2 = this.store.policyFor(null).config.team;
9278
+ const box = this.store.outboxStatus();
9279
+ return {
9280
+ configured: team2.url != null,
9281
+ url: team2.url,
9282
+ forward: team2.forward,
9283
+ pending: box.pending,
9284
+ oldest: box.oldest,
9285
+ lastAckAt: this.store.metaValue("team_last_ack") ?? null,
9286
+ lastError: this.store.metaValue("team_last_error") ?? null,
9287
+ machine: this.store.machineIdentity(),
9288
+ authed: this.store.metaValue("team_machine_token") != null
9289
+ };
9290
+ }
9291
+ async tick(now = Date.now()) {
9292
+ const team2 = this.store.policyFor(null).config.team;
9293
+ if (!team2.url)
9294
+ return 0;
9295
+ if (now - this.lastTry < team2.interval * 1000 + this.backoffMs)
9296
+ return 0;
9297
+ this.lastTry = now;
9298
+ const records = [];
9299
+ const events = team2.forward.includes("ledger") ? this.store.outboxPending() : [];
9300
+ for (const e of events) {
9301
+ const body = JSON.parse(e.payload);
9302
+ if (typeof body.projectId === "string")
9303
+ body.projectKey = this.projectKey(body.projectId);
9304
+ records.push({ seq: e.seq, kind: e.kind, body });
9305
+ }
9306
+ let spendRows = 0;
9307
+ if (team2.forward.includes("cost") && now - this.lastSpend > SPEND_EVERY_MS) {
9308
+ const day = new Date(now).toISOString().slice(0, 10);
9309
+ for (const r of this.store.spendRollup(day)) {
9310
+ records.push({
9311
+ seq: 0,
9312
+ kind: "spend",
9313
+ body: { day, ...r, projectKey: this.projectKey(r.projectId) }
9314
+ });
9315
+ spendRows++;
9316
+ }
9317
+ for (const r of this.store.taskSpendRollup(day)) {
9318
+ records.push({
9319
+ seq: 0,
9320
+ kind: "spend_task",
9321
+ body: { day, ...r, projectKey: this.projectKey(r.projectId) }
9322
+ });
9323
+ spendRows++;
9324
+ }
9325
+ }
9326
+ const held = this.store.heldClaimsForSync();
9327
+ if (!records.length && !held.length)
9328
+ return 0;
9329
+ const machine = { ...this.store.machineIdentity(), version: this.version };
9330
+ const token = this.store.metaValue("team_machine_token");
9331
+ const headers = {
9332
+ "content-type": "application/json",
9333
+ ...token ? { authorization: `Bearer ${token}` } : {}
9334
+ };
9335
+ const base = this.store.policyFor(null).config.team.url;
9336
+ try {
9337
+ if (records.length) {
9338
+ const req = { machine, records };
9339
+ const res = await fetch(`${base}/t1/ingest`, {
9340
+ method: "POST",
9341
+ headers,
9342
+ body: JSON.stringify(req),
9343
+ signal: AbortSignal.timeout(1e4)
9344
+ });
9345
+ if (!res.ok)
9346
+ throw new Error(`ingest ${res.status}`);
9347
+ const reply = await res.json();
9348
+ if (reply.ack > 0)
9349
+ this.store.outboxAck(reply.ack);
9350
+ if (spendRows)
9351
+ this.lastSpend = now;
9352
+ }
9353
+ if (held.length) {
9354
+ const claims = held.map((c) => ({
9355
+ projectKey: this.projectKey(c.projectId),
9356
+ task: c.task,
9357
+ acquiredAt: c.acquiredAt,
9358
+ expiresAt: c.expiresAt,
9359
+ actor: c.actorKind && c.actorId ? { kind: c.actorKind, id: c.actorId } : undefined
9360
+ }));
9361
+ const res = await fetch(`${base}/t1/claims`, {
9362
+ method: "POST",
9363
+ headers,
9364
+ body: JSON.stringify({ machine, claims }),
9365
+ signal: AbortSignal.timeout(1e4)
9366
+ });
9367
+ if (!res.ok)
9368
+ throw new Error(`claims ${res.status}`);
9369
+ const reply = await res.json();
9370
+ for (const r of reply.results) {
9371
+ const local = held.find((c) => c.task === r.task && this.projectKey(c.projectId) === r.projectKey);
9372
+ if (!local)
9373
+ continue;
9374
+ if (r.status === "ok") {
9375
+ if (local.teamState !== "registered")
9376
+ this.store.markClaimTeamState(local.projectId, local.task, "registered");
9377
+ } else
9378
+ this.store.revokeClaimConflict(local.projectId, local.task, r.holder);
9379
+ }
9380
+ }
9381
+ try {
9382
+ const res = await fetch(`${base}/t1/budget`, {
9383
+ headers,
9384
+ signal: AbortSignal.timeout(1e4)
9385
+ });
9386
+ if (res.ok) {
9387
+ const { budgets } = await res.json();
9388
+ this.store.setMetaValue("team_budget", JSON.stringify(budgets ?? []));
9389
+ }
9390
+ } catch {}
9391
+ this.backoffMs = 0;
9392
+ this.store.setMetaValue("team_last_ack", new Date(now).toISOString());
9393
+ this.store.setMetaValue("team_last_error", "");
9394
+ await this.syncPolicy(base, headers, now);
9395
+ return records.length + held.length;
9396
+ } catch (e) {
9397
+ this.backoffMs = Math.min(this.backoffMs ? this.backoffMs * 2 : 5000, MAX_BACKOFF_MS);
9398
+ this.store.setMetaValue("team_last_error", e.message);
9399
+ return 0;
9400
+ }
9401
+ }
9402
+ lastPolicy = 0;
9403
+ async syncPolicy(base, headers, now) {
9404
+ if (now - this.lastPolicy < POLICY_EVERY_MS)
9405
+ return;
9406
+ this.lastPolicy = now;
9407
+ try {
9408
+ const res = await fetch(`${base}/t1/policy`, {
9409
+ headers,
9410
+ signal: AbortSignal.timeout(1e4)
9411
+ });
9412
+ if (!res.ok)
9413
+ return;
9414
+ const { policy: policy2 } = await res.json();
9415
+ if (!policy2)
9416
+ return;
9417
+ let pinned = this.store.metaValue("team_policy_pubkey");
9418
+ if (!pinned) {
9419
+ pinned = policy2.publicKey;
9420
+ this.store.setMetaValue("team_policy_pubkey", pinned);
9421
+ }
9422
+ if (!verifyPolicySignature(policy2.toml, policy2.signature, pinned)) {
9423
+ this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
9424
+ return;
9425
+ }
9426
+ const file = join9(this.store.home, "policy.toml");
9427
+ const prev = this.store.metaValue("team_policy_sig");
9428
+ if (prev === policy2.signature)
9429
+ return;
9430
+ writeFileSync3(file, policy2.toml, { mode: 384 });
9431
+ writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
9432
+ signature: policy2.signature,
9433
+ publicKey: pinned,
9434
+ fetchedAt: new Date(now).toISOString(),
9435
+ url: base
9436
+ }), { mode: 384 });
9437
+ this.store.setMetaValue("team_policy_sig", policy2.signature);
9438
+ } catch {}
9439
+ }
9440
+ }
9441
+
8347
9442
  // packages/daemon/src/workflow.ts
8348
9443
  class WorkflowEngine {
8349
9444
  store;
@@ -8535,13 +9630,13 @@ class WorkflowEngine {
8535
9630
  }
8536
9631
 
8537
9632
  // packages/daemon/src/app.ts
8538
- var VERSION = "0.9.0";
9633
+ var VERSION = "0.10.0";
8539
9634
  var WEB_DIR = (() => {
8540
9635
  if (process.env.SWARM_WEB_DIR)
8541
9636
  return process.env.SWARM_WEB_DIR;
8542
9637
  const here = dirname4(fileURLToPath2(import.meta.url));
8543
- const dev = join9(here, "../../web/public");
8544
- return existsSync7(join9(dev, "index.html")) ? dev : join9(here, "../web");
9638
+ const dev = join10(here, "../../web/public");
9639
+ return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
8545
9640
  })();
8546
9641
  var REPLAY_TAIL = 200;
8547
9642
  var wireCache = new WeakMap;
@@ -8559,7 +9654,7 @@ function hookRepoRoot(store, raw2) {
8559
9654
  }
8560
9655
  function claudeSettings() {
8561
9656
  try {
8562
- const p = process.env.CLAUDE_SETTINGS ?? join9(homedir4(), ".claude", "settings.json");
9657
+ const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
8563
9658
  return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
8564
9659
  } catch {
8565
9660
  return null;
@@ -8570,7 +9665,7 @@ function diskVersion() {
8570
9665
  const entry = daemonCommand().at(-1);
8571
9666
  if (!entry || !existsSync7(entry))
8572
9667
  return null;
8573
- for (const f of [entry, join9(dirname4(entry), "app.ts")]) {
9668
+ for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
8574
9669
  if (!existsSync7(f))
8575
9670
  continue;
8576
9671
  const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
@@ -8588,6 +9683,7 @@ function createApp(store = new Store, hooks2 = {}) {
8588
9683
  const runner = new Runner(store, store.home);
8589
9684
  const dispatcher = new Dispatcher(store, runner, forge2);
8590
9685
  const workflows2 = new WorkflowEngine(store, runner, forge2);
9686
+ const team2 = new TeamForwarder(store, VERSION);
8591
9687
  store.onBudgetStop((projectId) => {
8592
9688
  dispatcher.clear(projectId);
8593
9689
  for (const run2 of runner.list(projectId))
@@ -8650,7 +9746,7 @@ function createApp(store = new Store, hooks2 = {}) {
8650
9746
  dir = homedir4();
8651
9747
  }
8652
9748
  try {
8653
- const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join9(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
9749
+ const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join10(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
8654
9750
  const parent = dirname4(dir);
8655
9751
  return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
8656
9752
  } catch (e) {
@@ -8659,10 +9755,44 @@ function createApp(store = new Store, hooks2 = {}) {
8659
9755
  });
8660
9756
  app.get("/v1/state", (c) => c.json(store.snapshot()));
8661
9757
  app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
9758
+ app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
8662
9759
  app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
8663
9760
  open: c.req.query("open") === "1",
8664
9761
  projectId: c.req.query("project") || undefined
8665
9762
  })));
9763
+ app.get("/v1/outcomes", async (c) => {
9764
+ const project = c.req.query("project") || undefined;
9765
+ const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
9766
+ id: s.id,
9767
+ branch: s.branch,
9768
+ model: s.model,
9769
+ agent: s.agent,
9770
+ costUsd: s.costUsd,
9771
+ startedAt: s.startedAt
9772
+ }));
9773
+ const prs = [];
9774
+ const reverted = new Set;
9775
+ for (const p of store.projects().filter((x) => !project || x.id === project)) {
9776
+ const o = await forge2.merged(p.id, p.root);
9777
+ for (const m of o.merged)
9778
+ prs.push({ ...m, state: "merged" });
9779
+ for (const sha of o.reverted)
9780
+ reverted.add(sha);
9781
+ }
9782
+ for (const pr of forge2.prs())
9783
+ if (!project || pr.projectId === project)
9784
+ prs.push({
9785
+ branch: pr.branch,
9786
+ number: pr.number,
9787
+ state: "open",
9788
+ title: pr.title,
9789
+ url: pr.url,
9790
+ createdAt: pr.createdAt,
9791
+ mergedAt: null,
9792
+ mergeSha: null
9793
+ });
9794
+ return c.json(outcomeReport(sessions, prs, reverted));
9795
+ });
8666
9796
  app.get("/v1/memory", (c) => {
8667
9797
  const q = c.req.query("q") ?? "";
8668
9798
  const kind = c.req.query("kind");
@@ -8676,6 +9806,26 @@ function createApp(store = new Store, hooks2 = {}) {
8676
9806
  })
8677
9807
  });
8678
9808
  });
9809
+ app.post("/v1/backup", async (c) => {
9810
+ const b = await c.req.json().catch(() => ({}));
9811
+ if (typeof b.dest !== "string" || !b.dest.startsWith("/"))
9812
+ return c.json({ error: "dest must be an absolute path" }, 400);
9813
+ try {
9814
+ return c.json(store.backupTo(b.dest));
9815
+ } catch (e) {
9816
+ return c.json({ error: e.message }, 500);
9817
+ }
9818
+ });
9819
+ app.get("/v1/team", (c) => c.json(team2.status()));
9820
+ app.post("/v1/team/credentials", async (c) => {
9821
+ const b = await c.req.json().catch(() => ({}));
9822
+ if (typeof b.token !== "string" || !b.token)
9823
+ return c.json({ error: "token required" }, 400);
9824
+ store.setMetaValue("team_machine_token", b.token);
9825
+ if (typeof b.policyPublicKey === "string" && b.policyPublicKey)
9826
+ store.setMetaValue("team_policy_pubkey", b.policyPublicKey);
9827
+ return c.json({ ok: true, machine: store.machineIdentity() });
9828
+ });
8679
9829
  app.get("/v1/policy", (c) => {
8680
9830
  const id = c.req.query("project");
8681
9831
  const p = id ? store.project(id) : null;
@@ -9227,18 +10377,18 @@ function createApp(store = new Store, hooks2 = {}) {
9227
10377
  });
9228
10378
  });
9229
10379
  });
9230
- app.get("/", (c) => c.html(readFileSync4(join9(WEB_DIR, "index.html"), "utf8")));
10380
+ app.get("/", (c) => c.html(readFileSync4(join10(WEB_DIR, "index.html"), "utf8")));
9231
10381
  const MIME = { js: "text/javascript", css: "text/css" };
9232
10382
  app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
9233
10383
  const f = c.req.param("file");
9234
- const p = join9(WEB_DIR, f);
10384
+ const p = join10(WEB_DIR, f);
9235
10385
  if (!existsSync7(p))
9236
10386
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
9237
10387
  return c.body(readFileSync4(p, "utf8"), 200, {
9238
10388
  "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
9239
10389
  });
9240
10390
  });
9241
- return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
10391
+ return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
9242
10392
  }
9243
10393
 
9244
10394
  // packages/daemon/src/demo.ts
@@ -9330,7 +10480,7 @@ function seedDemo(store) {
9330
10480
  // packages/daemon/src/bin.ts
9331
10481
  var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
9332
10482
  var appHooks = {};
9333
- var { app, store, runner } = createApp(new Store, appHooks);
10483
+ var { app, store, runner, team: team2 } = createApp(new Store, appHooks);
9334
10484
  if (process.env.SWARM_DEMO === "1" && isEmpty(store))
9335
10485
  seedDemo(store);
9336
10486
  function serve() {
@@ -9378,6 +10528,8 @@ if (!DEMO) {
9378
10528
  store.tailCodex(backfillMs);
9379
10529
  store.tailGrok(backfillMs);
9380
10530
  store.tailGemini(backfillMs);
10531
+ store.tailAider(backfillMs);
10532
+ store.tailOpencode(backfillMs);
9381
10533
  }
9382
10534
  var tick = 0;
9383
10535
  var tailer = setInterval(() => {
@@ -9388,6 +10540,8 @@ var tailer = setInterval(() => {
9388
10540
  store.tailCodex();
9389
10541
  store.tailGrok();
9390
10542
  store.tailGemini();
10543
+ store.tailAider();
10544
+ store.tailOpencode();
9391
10545
  }
9392
10546
  store.reapResources();
9393
10547
  store.reapProcesses();
@@ -9395,6 +10549,11 @@ var tailer = setInterval(() => {
9395
10549
  store.sweepOrphans();
9396
10550
  if (tick % 6 === 0)
9397
10551
  store.checkBudgets();
10552
+ if (tick % 12 === 0)
10553
+ store.checkModels();
10554
+ if (tick % 2 === 0)
10555
+ store.checkStalls();
10556
+ team2.tick();
9398
10557
  }, 5000);
9399
10558
  store.refreshAllWorktrees();
9400
10559
  var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);