@ra3orblade/swarm 0.8.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
@@ -3,9 +3,33 @@
3
3
 
4
4
  // packages/client/src/daemon.ts
5
5
  import { randomBytes } from "crypto";
6
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6
+ import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
9
+
10
+ // packages/client/src/bins.ts
11
+ import { existsSync } from "fs";
12
+ import { dirname, resolve } from "path";
13
+ import { fileURLToPath } from "url";
14
+ var SRC = {
15
+ swarm: "cli",
16
+ swarmd: "daemon",
17
+ "swarm-hook": "hook",
18
+ "swarm-mcp": "mcp"
19
+ };
20
+ function resolveBin(name, from = import.meta.url) {
21
+ const here = dirname(fileURLToPath(from));
22
+ const candidates = [
23
+ resolve(here, `../../${SRC[name]}/src/bin.ts`),
24
+ resolve(here, `${name}.js`)
25
+ ];
26
+ for (const c of candidates)
27
+ if (existsSync(c))
28
+ return ["bun", c];
29
+ return [name];
30
+ }
31
+
32
+ // packages/client/src/daemon.ts
9
33
  function swarmHome() {
10
34
  return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
11
35
  }
@@ -41,6 +65,9 @@ function ensureToken(home = swarmHome()) {
41
65
  function clearDaemonInfo() {
42
66
  rmSync(infoFile(), { force: true });
43
67
  }
68
+ function daemonCommand() {
69
+ return resolveBin("swarmd");
70
+ }
44
71
  // packages/core/src/actor.ts
45
72
  var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
46
73
  var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
@@ -78,6 +105,150 @@ function actorFromColumns(kind, id, session) {
78
105
  a.session = session;
79
106
  return a;
80
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
+
81
252
  // packages/core/src/adapters/claude-code/transcript.ts
82
253
  function parseTranscriptChunk(chunk) {
83
254
  const out = {
@@ -218,6 +389,79 @@ function parseCodexRollout(chunk) {
218
389
  return out;
219
390
  }
220
391
 
392
+ // packages/core/src/adapters/gemini/chats.ts
393
+ function partText(content) {
394
+ if (typeof content === "string")
395
+ return content.slice(0, 400);
396
+ const parts = Array.isArray(content) ? content : [content];
397
+ let out = "";
398
+ for (const p of parts) {
399
+ if (typeof p === "string")
400
+ out += p;
401
+ else if (p && typeof p === "object" && typeof p.text === "string")
402
+ out += p.text;
403
+ if (out.length >= 400)
404
+ break;
405
+ }
406
+ return out.slice(0, 400);
407
+ }
408
+ function parseGeminiChat(chunk) {
409
+ const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
410
+ let subagent = false;
411
+ for (const raw of chunk.split(`
412
+ `)) {
413
+ if (!raw.trim())
414
+ continue;
415
+ let d = null;
416
+ try {
417
+ d = JSON.parse(raw);
418
+ } catch {
419
+ continue;
420
+ }
421
+ if (d.$set) {
422
+ const set = d.$set;
423
+ if (typeof set.summary === "string")
424
+ out.title = set.summary;
425
+ continue;
426
+ }
427
+ if (d.sessionId && d.projectHash !== undefined) {
428
+ out.sessionId = d.sessionId;
429
+ if (Array.isArray(d.directories) && typeof d.directories[0] === "string")
430
+ out.cwd = d.directories[0];
431
+ if (typeof d.summary === "string")
432
+ out.title = d.summary;
433
+ if (d.kind === "subagent")
434
+ subagent = true;
435
+ continue;
436
+ }
437
+ if (d.type !== "gemini" || !d.id)
438
+ continue;
439
+ const t = d.tokens ?? {};
440
+ const cacheRead = t.cached ?? 0;
441
+ const turn = {
442
+ id: `${out.sessionId ?? "gemini"}-${d.id}`,
443
+ ts: d.timestamp ?? new Date(0).toISOString(),
444
+ model: d.model ?? out.model ?? "gemini-2.5-pro",
445
+ usage: {
446
+ input: Math.max(0, (t.input ?? 0) - cacheRead),
447
+ output: (t.output ?? 0) + (t.tool ?? 0),
448
+ cacheWrite: 0,
449
+ cacheWrite1h: 0,
450
+ cacheRead,
451
+ thinking: t.thoughts ?? 0
452
+ },
453
+ text: partText(d.content),
454
+ tools: (d.toolCalls ?? []).map((c) => c.name ?? c.displayName ?? c.tool ?? "").filter(Boolean),
455
+ effort: null,
456
+ sidechain: subagent
457
+ };
458
+ if (d.model)
459
+ out.model = d.model;
460
+ out.turns.push(turn);
461
+ }
462
+ return out;
463
+ }
464
+
221
465
  // packages/core/src/adapters/grok/updates.ts
222
466
  function parseGrokUpdates(chunk) {
223
467
  const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
@@ -285,6 +529,62 @@ function parseGrokUpdates(chunk) {
285
529
  }
286
530
  return out;
287
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
+ }
288
588
  // packages/core/src/adapters/claude-code/hooks.ts
289
589
  var HOOK_EVENTS = [
290
590
  "SessionStart",
@@ -421,7 +721,9 @@ var AUDIT_TYPES = new Set([
421
721
  "permission.resolved",
422
722
  "incident.opened",
423
723
  "incident.acked",
424
- "run.result"
724
+ "run.result",
725
+ "workflow.started",
726
+ "workflow.finished"
425
727
  ]);
426
728
  var isAuditType = (t) => AUDIT_TYPES.has(t);
427
729
  var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
@@ -594,16 +896,82 @@ function runProfile(name) {
594
896
  return RUN_PROFILES[name] ?? null;
595
897
  }
596
898
  // packages/core/src/config.ts
597
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
899
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
598
900
  import { join as join2 } from "path";
901
+
902
+ // packages/core/src/workflows.ts
903
+ var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
904
+ function isRecord(v) {
905
+ return typeof v === "object" && v !== null && !Array.isArray(v);
906
+ }
907
+ function parseWorkflows(raw) {
908
+ const out = {};
909
+ if (!Array.isArray(raw))
910
+ return out;
911
+ for (const w of raw) {
912
+ if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
913
+ continue;
914
+ if (!Array.isArray(w.steps) || !w.steps.length)
915
+ continue;
916
+ const prompts = isRecord(w.prompts) ? w.prompts : {};
917
+ const steps = [];
918
+ for (const s of w.steps) {
919
+ if (typeof s !== "string" || !s.trim()) {
920
+ steps.length = 0;
921
+ break;
922
+ }
923
+ const t = s.trim();
924
+ if (t === "pr")
925
+ steps.push({ kind: "pr" });
926
+ else if (t.startsWith("gate:")) {
927
+ const gate = t.slice(5);
928
+ if (!NAME_RE.test(gate)) {
929
+ steps.length = 0;
930
+ break;
931
+ }
932
+ steps.push({ kind: "gate", gate });
933
+ } else if (NAME_RE.test(t)) {
934
+ const p = prompts[t];
935
+ steps.push({
936
+ kind: "run",
937
+ name: t,
938
+ prompt: typeof p === "string" && p.trim() ? p.trim() : null
939
+ });
940
+ } else {
941
+ steps.length = 0;
942
+ break;
943
+ }
944
+ }
945
+ if (steps.length)
946
+ out[w.name] = { name: w.name, steps };
947
+ }
948
+ return out;
949
+ }
950
+ function workflowStepPrompt(step, task, ctx) {
951
+ if (step.prompt)
952
+ return step.prompt.replaceAll("{task}", task.id).replaceAll("{title}", task.title ?? "");
953
+ return [
954
+ `Task ${task.id}: ${task.title}`,
955
+ "",
956
+ `You are the "${step.name}" step of the "${ctx.workflow}" workflow. Work only inside this worktree; commit and push as you go.`,
957
+ ctx.remaining.length ? `After you finish, the workflow itself runs: ${ctx.remaining.join(" \u2192 ")}. Do not do those yourself.` : "You are the last step.",
958
+ "When done, call swarm_handoff with what was done and what remains."
959
+ ].join(`
960
+ `);
961
+ }
962
+ function stepLabel(s) {
963
+ return s.kind === "run" ? s.name : s.kind === "gate" ? `gate:${s.gate}` : "pr";
964
+ }
965
+
966
+ // packages/core/src/config.ts
599
967
  var DEFAULT_GATE_TIMEOUT_S = 900;
600
968
  var AUTO_MODES = ["session-end", "stop", "off"];
601
969
  function parseGateDefs(gates) {
602
970
  const out = {};
603
- if (!isRecord(gates))
971
+ if (!isRecord2(gates))
604
972
  return out;
605
973
  for (const [name, v] of Object.entries(gates)) {
606
- if (!isRecord(v))
974
+ if (!isRecord2(v))
607
975
  continue;
608
976
  const builtin = v.builtin === "review" ? "review" : null;
609
977
  const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
@@ -626,7 +994,11 @@ var DEFAULT_CONFIG = {
626
994
  daemon: { port: 7777, auth: "loopback-optional" },
627
995
  tasks: { source: null, labels: [], team: null },
628
996
  gates: { required: [], auto: "session-end", defs: {} },
997
+ workflows: {},
629
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 },
630
1002
  events: { retain_days: 30 },
631
1003
  audit: { retain_days: 0 },
632
1004
  privacy: DEFAULT_PRIVACY,
@@ -650,11 +1022,11 @@ var DEFAULT_CONFIG = {
650
1022
  }
651
1023
  };
652
1024
  var MODES = ["ask", "deny", "off"];
653
- function isRecord(v) {
1025
+ function isRecord2(v) {
654
1026
  return typeof v === "object" && v !== null && !Array.isArray(v);
655
1027
  }
656
1028
  function merge(a, b) {
657
- if (!isRecord(a) || !isRecord(b))
1029
+ if (!isRecord2(a) || !isRecord2(b))
658
1030
  return b === undefined ? a : b;
659
1031
  const out = { ...a };
660
1032
  for (const [k, v] of Object.entries(b))
@@ -721,6 +1093,27 @@ function validate(c) {
721
1093
  warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
722
1094
  on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
723
1095
  },
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
+ })(),
724
1117
  events: {
725
1118
  retain_days: days(c.events?.retain_days, 30)
726
1119
  },
@@ -760,7 +1153,7 @@ function validate(c) {
760
1153
  };
761
1154
  }
762
1155
  function leafPaths(v, prefix = "") {
763
- if (!isRecord(v))
1156
+ if (!isRecord2(v))
764
1157
  return prefix ? [prefix] : [];
765
1158
  const keys = Object.keys(v);
766
1159
  if (keys.length === 0)
@@ -770,7 +1163,7 @@ function leafPaths(v, prefix = "") {
770
1163
  function getPath(v, path) {
771
1164
  let cur = v;
772
1165
  for (const seg of path.split(".")) {
773
- if (!isRecord(cur))
1166
+ if (!isRecord2(cur))
774
1167
  return;
775
1168
  cur = cur[seg];
776
1169
  }
@@ -780,7 +1173,7 @@ function setPath(obj, path, value) {
780
1173
  const segs = path.split(".");
781
1174
  let cur = obj;
782
1175
  for (const seg of segs.slice(0, -1)) {
783
- if (!isRecord(cur[seg]))
1176
+ if (!isRecord2(cur[seg]))
784
1177
  cur[seg] = {};
785
1178
  cur = cur[seg];
786
1179
  }
@@ -788,7 +1181,7 @@ function setPath(obj, path, value) {
788
1181
  }
789
1182
  var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
790
1183
  function readLayer(path) {
791
- return existsSync2(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
1184
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
792
1185
  }
793
1186
  function loadConfigDetailed(opts = {}) {
794
1187
  const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
@@ -1274,11 +1667,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
1274
1667
  `) };
1275
1668
  }
1276
1669
  // packages/core/src/gates.ts
1277
- var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
1670
+ var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
1278
1671
  function validateGateRun(input) {
1279
1672
  if (!input.task?.trim())
1280
1673
  return { ok: false, reason: "task is required" };
1281
- if (!NAME_RE.test(input.gate ?? ""))
1674
+ if (!NAME_RE2.test(input.gate ?? ""))
1282
1675
  return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
1283
1676
  if (input.verdict !== "pass" && input.verdict !== "fail")
1284
1677
  return { ok: false, reason: 'verdict must be "pass" or "fail"' };
@@ -1335,6 +1728,41 @@ function executedGateInput(task, gate, cmd, outcome) {
1335
1728
  evidence: evidenceTail(outcome.output) || null
1336
1729
  };
1337
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
+ }
1338
1766
  // packages/core/src/ledger.ts
1339
1767
  var DEFAULT_LEASE_MINUTES = 45;
1340
1768
  function isExpired(claim, now) {
@@ -1639,6 +2067,121 @@ function parseMemoryQuery(q) {
1639
2067
  }
1640
2068
  return { match: terms.join(" "), kind, task };
1641
2069
  }
2070
+ // packages/core/src/messages.ts
2071
+ var MESSAGE_MAX = 4000;
2072
+ function validateMessage(text) {
2073
+ if (typeof text !== "string" || !text.trim())
2074
+ return { ok: false, reason: "message text is required" };
2075
+ const t = text.trim();
2076
+ if (t.length > MESSAGE_MAX)
2077
+ return { ok: false, reason: `message is over ${MESSAGE_MAX} chars` };
2078
+ return { ok: true, text: t };
2079
+ }
2080
+ function parseTo(to) {
2081
+ if (typeof to !== "string" || !to.trim())
2082
+ return null;
2083
+ const t = to.trim();
2084
+ if (t === "lead")
2085
+ return { kind: "lead" };
2086
+ if (/^[0-9a-f]{8}(-[0-9a-f-]{4,28})?$/i.test(t))
2087
+ return { kind: "session", id: t };
2088
+ return { kind: "task", task: t };
2089
+ }
2090
+ function formatMessages(ms) {
2091
+ if (!ms.length)
2092
+ return null;
2093
+ const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
2094
+ return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
2095
+ ${lines.join(`
2096
+ `)}
2097
+ Reply with swarm_send if a reply is expected.`;
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
+ }
1642
2185
  // packages/core/src/policy.ts
1643
2186
  import { createHash } from "crypto";
1644
2187
  var HOOK_MARK = "swarm-hook";
@@ -1979,6 +2522,49 @@ ${lines.join(`
1979
2522
  `)}` : ""}`
1980
2523
  };
1981
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
+ }
1982
2568
  // packages/core/src/tasks.ts
1983
2569
  var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
1984
2570
  var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
@@ -2138,6 +2724,35 @@ function linearIssuesQuery(teamKey, first = 200) {
2138
2724
  inverseRelations { nodes { type issue { identifier } } }
2139
2725
  } } }`;
2140
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
+ }
2141
2756
  // packages/core/src/worktree.ts
2142
2757
  import { join as join3 } from "path";
2143
2758
  function planBootstrap(cfg, repoRoot, worktree) {
@@ -2212,10 +2827,10 @@ function planGc(worktrees, claims) {
2212
2827
  return out;
2213
2828
  }
2214
2829
  // packages/daemon/src/app.ts
2215
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
2830
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
2216
2831
  import { homedir as homedir4 } from "os";
2217
- import { dirname as dirname3, join as join9 } from "path";
2218
- import { fileURLToPath } from "url";
2832
+ import { dirname as dirname4, join as join10 } from "path";
2833
+ import { fileURLToPath as fileURLToPath2 } from "url";
2219
2834
 
2220
2835
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
2221
2836
  var compose = (middleware, onError, onNotFound) => {
@@ -4088,7 +4703,7 @@ class Dispatcher {
4088
4703
  }
4089
4704
 
4090
4705
  // packages/daemon/src/forge.ts
4091
- import { existsSync as existsSync3 } from "fs";
4706
+ import { existsSync as existsSync4 } from "fs";
4092
4707
  import { homedir as homedir2 } from "os";
4093
4708
  import { join as join4 } from "path";
4094
4709
  var EXTRA_BIN_DIRS = [
@@ -4106,7 +4721,7 @@ function findBin(name) {
4106
4721
  return onPath;
4107
4722
  for (const d of EXTRA_BIN_DIRS) {
4108
4723
  const p = join4(d, name);
4109
- if (existsSync3(p))
4724
+ if (existsSync4(p))
4110
4725
  return p;
4111
4726
  }
4112
4727
  return null;
@@ -4144,6 +4759,64 @@ class ForgeService {
4144
4759
  }
4145
4760
  }));
4146
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
+ }
4147
4820
  remote(root) {
4148
4821
  const r = Bun.spawnSync(["git", "-C", root, "remote", "get-url", "origin"]);
4149
4822
  if (r.exitCode !== 0)
@@ -4376,6 +5049,16 @@ function currentBranch(cwd) {
4376
5049
  branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
4377
5050
  return branchCache.get(cwd)?.v ?? null;
4378
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
+ }
4379
5062
  function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
4380
5063
  const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
4381
5064
  const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
@@ -4507,6 +5190,14 @@ class Runner {
4507
5190
  return { ok: false, reason: "unknown project" };
4508
5191
  if (!input.prompt.trim())
4509
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
+ }
4510
5201
  if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
4511
5202
  return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
4512
5203
  if (this.get(input.task)?.projectId === input.projectId)
@@ -4805,7 +5496,8 @@ class Runner {
4805
5496
  import { Database } from "bun:sqlite";
4806
5497
  import {
4807
5498
  closeSync,
4808
- existsSync as existsSync5,
5499
+ copyFileSync,
5500
+ existsSync as existsSync6,
4809
5501
  mkdirSync as mkdirSync4,
4810
5502
  openSync as openSync3,
4811
5503
  readdirSync,
@@ -4817,12 +5509,12 @@ import {
4817
5509
  unlinkSync,
4818
5510
  writeFileSync as writeFileSync2
4819
5511
  } from "fs";
4820
- import { homedir as homedir3, tmpdir, userInfo } from "os";
4821
- import { basename, dirname as dirname2, join as join8 } from "path";
5512
+ import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
5513
+ import { basename, dirname as dirname3, join as join8 } from "path";
4822
5514
 
4823
5515
  // packages/daemon/src/bootstrap.ts
4824
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
4825
- import { dirname, join as join7 } from "path";
5516
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
5517
+ import { dirname as dirname2, join as join7 } from "path";
4826
5518
  function runBootstrap(plan, opts) {
4827
5519
  const logDir = join7(opts.home, "logs", opts.projectId);
4828
5520
  mkdirSync3(logDir, { recursive: true });
@@ -4830,12 +5522,12 @@ function runBootstrap(plan, opts) {
4830
5522
  const copied = [];
4831
5523
  const skipped = [];
4832
5524
  for (const c of plan.copies) {
4833
- if (!existsSync4(c.from)) {
5525
+ if (!existsSync5(c.from)) {
4834
5526
  skipped.push(c.rel);
4835
5527
  continue;
4836
5528
  }
4837
5529
  try {
4838
- mkdirSync3(dirname(c.to), { recursive: true });
5530
+ mkdirSync3(dirname2(c.to), { recursive: true });
4839
5531
  cpSync(c.from, c.to, { recursive: true, force: true });
4840
5532
  copied.push(c.rel);
4841
5533
  } catch (e) {
@@ -4927,14 +5619,14 @@ class TaskSources {
4927
5619
  `)[0] ?? code}`);
4928
5620
  return normalizeGithubIssues(JSON.parse(out));
4929
5621
  }
4930
- async linear(team) {
5622
+ async linear(team2) {
4931
5623
  const key = this.env.LINEAR_API_KEY;
4932
5624
  if (!key)
4933
5625
  throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
4934
5626
  const r = await fetch("https://api.linear.app/graphql", {
4935
5627
  method: "POST",
4936
5628
  headers: { "content-type": "application/json", authorization: key },
4937
- body: JSON.stringify({ query: linearIssuesQuery(team) })
5629
+ body: JSON.stringify({ query: linearIssuesQuery(team2) })
4938
5630
  });
4939
5631
  if (!r.ok)
4940
5632
  throw new Error(`Linear API ${r.status}`);
@@ -4960,11 +5652,12 @@ CREATE INDEX IF NOT EXISTS events_type_seq ON events(type, seq);
4960
5652
  CREATE TABLE IF NOT EXISTS turns (
4961
5653
  id TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, ts TEXT, model TEXT, effort TEXT, sidechain INTEGER,
4962
5654
  input INTEGER, output INTEGER, cache_write INTEGER, cache_write_1h INTEGER, cache_read INTEGER, thinking INTEGER,
4963
- cost_usd REAL, text TEXT, tools TEXT
5655
+ cost_usd REAL, cost_fixed INTEGER DEFAULT 0, text TEXT, tools TEXT
4964
5656
  );
4965
5657
  CREATE INDEX IF NOT EXISTS turns_session ON turns(session_id, ts);
4966
5658
  CREATE INDEX IF NOT EXISTS turns_ts ON turns(ts);
4967
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);
4968
5661
  CREATE TABLE IF NOT EXISTS resources (
4969
5662
  name TEXT, project_id TEXT, kind TEXT, owner TEXT, session_id TEXT,
4970
5663
  pid INTEGER, port INTEGER, acquired_at TEXT, expires_at TEXT, released INTEGER DEFAULT 0,
@@ -4997,6 +5690,12 @@ CREATE TABLE IF NOT EXISTS messages (
4997
5690
  answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
4998
5691
  );
4999
5692
  CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
5693
+ CREATE TABLE IF NOT EXISTS workflow_runs (
5694
+ id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
5695
+ step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
5696
+ started_at TEXT, updated_at TEXT, ended_at TEXT, actor_kind TEXT, actor_id TEXT
5697
+ );
5698
+ CREATE INDEX IF NOT EXISTS workflow_runs_proj ON workflow_runs(project_id, id);
5000
5699
  CREATE TABLE IF NOT EXISTS claims (
5001
5700
  project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
5002
5701
  acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
@@ -5024,9 +5723,13 @@ class Store {
5024
5723
  this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
5025
5724
  this.db.exec(SCHEMA);
5026
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");
5027
5728
  this.ensureColumn("projects", "sort_order", "INTEGER");
5028
5729
  this.ensureColumn("projects", "icon", "TEXT");
5029
5730
  this.ensureColumn("projects", "color", "TEXT");
5731
+ this.ensureColumn("messages", "to_kind", "TEXT");
5732
+ this.ensureColumn("messages", "from_session", "TEXT");
5030
5733
  this.migrate();
5031
5734
  this.migrateProjectsJson(join8(home, "projects.json"));
5032
5735
  this.reconcileMovedProjects();
@@ -5047,6 +5750,12 @@ class Store {
5047
5750
  setMeta(key, value) {
5048
5751
  this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
5049
5752
  }
5753
+ metaValue(key) {
5754
+ return this.meta(key) || null;
5755
+ }
5756
+ setMetaValue(key, value) {
5757
+ this.setMeta(key, value);
5758
+ }
5050
5759
  slimExistingEvents() {
5051
5760
  if (this.meta("events_slim") === "1")
5052
5761
  return;
@@ -5076,9 +5785,9 @@ class Store {
5076
5785
  reconcileMovedProjects() {
5077
5786
  const all = this.projects();
5078
5787
  for (const stale of all) {
5079
- if (existsSync5(stale.root))
5788
+ if (existsSync6(stale.root))
5080
5789
  continue;
5081
- const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync5(p.root));
5790
+ const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
5082
5791
  if (live.length !== 1)
5083
5792
  continue;
5084
5793
  this.mergeProject(stale.id, live[0].id);
@@ -5162,7 +5871,7 @@ class Store {
5162
5871
  return actorFrom(owner, sessionId, { user: osUser(), runId });
5163
5872
  }
5164
5873
  migrateProjectsJson(file) {
5165
- if (!existsSync5(file))
5874
+ if (!existsSync6(file))
5166
5875
  return;
5167
5876
  try {
5168
5877
  const list = JSON.parse(readFileSync3(file, "utf8"));
@@ -5177,7 +5886,7 @@ class Store {
5177
5886
  const hit = this.topCache.get(cwd);
5178
5887
  if (hit && Date.now() - hit.t < 1e4)
5179
5888
  return hit.v;
5180
- const v = cwd && existsSync5(cwd) ? gitToplevel(cwd) : null;
5889
+ const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
5181
5890
  this.topCache.set(cwd, { v, t: Date.now() });
5182
5891
  return v;
5183
5892
  }
@@ -5383,7 +6092,7 @@ class Store {
5383
6092
  }));
5384
6093
  }
5385
6094
  sessionContext(cwd) {
5386
- if (!cwd || !existsSync5(cwd))
6095
+ if (!cwd || !existsSync6(cwd))
5387
6096
  return null;
5388
6097
  const toplevel = this.toplevel(cwd);
5389
6098
  const project = this.resolveProject(cwd);
@@ -5523,7 +6232,160 @@ class Store {
5523
6232
  return qs;
5524
6233
  }
5525
6234
  answerContext(sessionId) {
5526
- return formatAnswers(this.inbox(sessionId));
6235
+ const parts = [
6236
+ formatAnswers(this.inbox(sessionId)),
6237
+ formatMessages(this.messageInbox(sessionId))
6238
+ ];
6239
+ const out = parts.filter(Boolean);
6240
+ return out.length ? out.join(`
6241
+ `) : null;
6242
+ }
6243
+ wfInsert(projectId, task, workflow, steps, actor2) {
6244
+ const now = new Date().toISOString();
6245
+ const r = this.db.query(`INSERT INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, started_at, updated_at, actor_kind, actor_id)
6246
+ VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor2.kind, actor2.id);
6247
+ this.touch();
6248
+ return Number(r.lastInsertRowid);
6249
+ }
6250
+ wfUpdate(id, patch) {
6251
+ const sets = ["updated_at = ?"];
6252
+ const args = [new Date().toISOString()];
6253
+ if (patch.step !== undefined) {
6254
+ sets.push("step = ?");
6255
+ args.push(patch.step);
6256
+ }
6257
+ if (patch.stepLabel !== undefined) {
6258
+ sets.push("step_label = ?");
6259
+ args.push(patch.stepLabel);
6260
+ }
6261
+ if (patch.state !== undefined) {
6262
+ sets.push("state = ?");
6263
+ args.push(patch.state);
6264
+ }
6265
+ if (patch.detail !== undefined) {
6266
+ sets.push("detail = ?");
6267
+ args.push(patch.detail);
6268
+ }
6269
+ if (patch.runId !== undefined) {
6270
+ sets.push("run_id = ?");
6271
+ args.push(patch.runId);
6272
+ }
6273
+ if (patch.ended) {
6274
+ sets.push("ended_at = ?");
6275
+ args.push(new Date().toISOString());
6276
+ }
6277
+ this.db.query(`UPDATE workflow_runs SET ${sets.join(", ")} WHERE id = ?`).run(...args, id);
6278
+ this.touch();
6279
+ }
6280
+ wfRuns(projectId, limit = 50) {
6281
+ return this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map(rowToWorkflowRun);
6282
+ }
6283
+ wfActive(projectId, task) {
6284
+ const r = this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? AND task = ? AND state = 'running' ORDER BY id DESC LIMIT 1").get(projectId, task);
6285
+ return r ? rowToWorkflowRun(r) : null;
6286
+ }
6287
+ wfSweepOrphans() {
6288
+ this.db.query("UPDATE workflow_runs SET state = 'stopped', detail = COALESCE(detail, 'daemon restarted mid-workflow'), ended_at = ? WHERE state = 'running'").run(new Date().toISOString());
6289
+ }
6290
+ send(projectId, input) {
6291
+ if (!this.project(projectId))
6292
+ return { ok: false, error: "unknown project" };
6293
+ const v = validateMessage(input.text);
6294
+ if (!v.ok)
6295
+ return { ok: false, error: v.reason };
6296
+ const to = parseTo(input.to);
6297
+ if (!to)
6298
+ return { ok: false, error: 'to must be a session id, a task, or "lead"' };
6299
+ let sessionId = null;
6300
+ let task = null;
6301
+ if (to.kind === "session") {
6302
+ sessionId = this.knownSession(to.id) ?? this.sessionByPrefix(to.id);
6303
+ if (!sessionId)
6304
+ return { ok: false, error: `unknown session ${to.id}` };
6305
+ } else if (to.kind === "task") {
6306
+ task = to.task;
6307
+ sessionId = this.sessionForTask(projectId, to.task);
6308
+ } else {
6309
+ sessionId = this.leadSession(projectId);
6310
+ }
6311
+ const createdAt = new Date().toISOString();
6312
+ const from = input.from ?? (input.fromSession ? `agent ${input.fromSession.slice(0, 8)}` : null);
6313
+ const r = this.db.query(`INSERT INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
6314
+ VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?)`).run(projectId, sessionId, task, v.text, from, createdAt, to.kind, input.fromSession ?? null);
6315
+ const message = this.message(Number(r.lastInsertRowid));
6316
+ this.append({
6317
+ ts: createdAt,
6318
+ type: "message.sent",
6319
+ projectId,
6320
+ sessionId: input.fromSession ?? null,
6321
+ actor: this.actorFor(input.from ?? null, input.fromSession ?? null),
6322
+ payload: {
6323
+ id: message.id,
6324
+ to: input.to,
6325
+ task,
6326
+ recipient: sessionId,
6327
+ text: v.text.slice(0, 400),
6328
+ summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
6329
+ }
6330
+ });
6331
+ return { ok: true, message };
6332
+ }
6333
+ message(id) {
6334
+ const r = this.db.query("SELECT * FROM messages WHERE id = ? AND kind = 'message'").get(id);
6335
+ return r ? rowToMessage(r) : null;
6336
+ }
6337
+ messages(opts = {}) {
6338
+ const where = ["kind = 'message'"];
6339
+ const args = [];
6340
+ if (opts.projectId) {
6341
+ where.push("project_id = ?");
6342
+ args.push(opts.projectId);
6343
+ }
6344
+ if (opts.sessionId) {
6345
+ where.push("(session_id = ? OR from_session = ?)");
6346
+ args.push(opts.sessionId, opts.sessionId);
6347
+ }
6348
+ if (opts.task) {
6349
+ where.push("task = ?");
6350
+ args.push(opts.task);
6351
+ }
6352
+ args.push(opts.limit ?? 100);
6353
+ return this.db.query(`SELECT * FROM messages WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`).all(...args).map(rowToMessage);
6354
+ }
6355
+ messageInbox(sessionId, opts = {}) {
6356
+ if (!sessionId)
6357
+ return [];
6358
+ const s = this.db.query("SELECT project_id, kind, cwd FROM sessions WHERE id = ?").get(sessionId);
6359
+ if (!s)
6360
+ return [];
6361
+ const task = this.heldClaimsWithWorktree().find((c) => isInside(s.cwd, c.worktree))?.task ?? null;
6362
+ const rows = this.db.query(`SELECT * FROM messages WHERE kind = 'message' AND delivered_at IS NULL AND from_session IS NOT ?
6363
+ AND (session_id = ?
6364
+ OR (to_kind = 'task' AND project_id = ? AND task IS ?)
6365
+ OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
6366
+ ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
6367
+ const ms = rows.map(rowToMessage);
6368
+ if (ms.length && !opts.peek)
6369
+ this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms.map((m) => m.id));
6370
+ return ms;
6371
+ }
6372
+ markMessageDelivered(id, sessionId) {
6373
+ this.db.query("UPDATE messages SET delivered_at = ?, session_id = COALESCE(?, session_id) WHERE id = ? AND delivered_at IS NULL").run(new Date().toISOString(), sessionId, id);
6374
+ }
6375
+ leadSession(projectId) {
6376
+ const r = this.db.query("SELECT id FROM sessions WHERE project_id = ? AND kind = 'interactive' AND state != 'ended' ORDER BY last_seen_at DESC LIMIT 1").get(projectId);
6377
+ return r?.id ?? null;
6378
+ }
6379
+ sessionByPrefix(prefix) {
6380
+ const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
6381
+ return rows.length === 1 ? rows[0]?.id ?? null : null;
6382
+ }
6383
+ sessionForTask(projectId, task) {
6384
+ const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
6385
+ if (!claim?.worktree)
6386
+ return null;
6387
+ const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
6388
+ return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
5527
6389
  }
5528
6390
  questionContext(task, projectId) {
5529
6391
  if (!task)
@@ -5600,13 +6462,13 @@ class Store {
5600
6462
  };
5601
6463
  const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
5602
6464
  const worktree2 = claim?.worktree;
5603
- if (!worktree2 || !existsSync5(worktree2))
6465
+ if (!worktree2 || !existsSync6(worktree2))
5604
6466
  return {
5605
6467
  ok: false,
5606
6468
  reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
5607
6469
  };
5608
6470
  const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
5609
- if (!existsSync5(cwd))
6471
+ if (!existsSync6(cwd))
5610
6472
  return { ok: false, reason: `gate cwd ${cwd} does not exist` };
5611
6473
  const key = `${projectId}:${task}:${gate}`;
5612
6474
  if (this.gateJobs.has(key))
@@ -5947,7 +6809,7 @@ ${err}
5947
6809
  error = e.error;
5948
6810
  } else {
5949
6811
  const path = join8(p.root, source);
5950
- if (!existsSync5(path))
6812
+ if (!existsSync6(path))
5951
6813
  return { source, required: this.requiredGates(projectId), tasks: [] };
5952
6814
  const mtime = statSync(path).mtimeMs;
5953
6815
  let md = this.taskCache.get(projectId);
@@ -5996,7 +6858,7 @@ ${err}
5996
6858
  const file = join8(this.home, POLICY_CACHE_FILE);
5997
6859
  try {
5998
6860
  if (!hasLockedRules(loaded)) {
5999
- if (existsSync5(file))
6861
+ if (existsSync6(file))
6000
6862
  unlinkSync(file);
6001
6863
  return;
6002
6864
  }
@@ -6012,13 +6874,13 @@ ${err}
6012
6874
  claudeSettings() {
6013
6875
  const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
6014
6876
  try {
6015
- return existsSync5(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6877
+ return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6016
6878
  } catch {
6017
6879
  return null;
6018
6880
  }
6019
6881
  }
6020
6882
  checkPolicy(cwd, sessionId) {
6021
- const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
6883
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
6022
6884
  const repoRoot = project?.root ?? null;
6023
6885
  const loaded = this.policyFor(repoRoot);
6024
6886
  const settings = this.claudeSettings();
@@ -6043,7 +6905,7 @@ ${err}
6043
6905
  return findings;
6044
6906
  }
6045
6907
  evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
6046
- if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync5(cwd)) {
6908
+ if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
6047
6909
  const project = this.resolveProject(cwd);
6048
6910
  const b = this.budgetFor(project.id);
6049
6911
  if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
@@ -6054,6 +6916,18 @@ ${err}
6054
6916
  };
6055
6917
  return { decision: d, display: input.command ?? input.file_path ?? tool };
6056
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
+ }
6057
6931
  }
6058
6932
  const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
6059
6933
  const cmd = tool === "Bash" ? input.command : undefined;
@@ -6134,7 +7008,7 @@ ${err}
6134
7008
  return this.openIncident(d, cwd, id, cmd);
6135
7009
  }
6136
7010
  openIncident(d, cwd, sessionId, command) {
6137
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
7011
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
6138
7012
  this.append({
6139
7013
  ts: new Date().toISOString(),
6140
7014
  type: "incident.opened",
@@ -6185,7 +7059,7 @@ ${err}
6185
7059
  }
6186
7060
  }
6187
7061
  const report = dryRunRules(calls, modes, {
6188
- toplevel: (cwd) => cwd && existsSync5(cwd) ? this.toplevel(cwd) : null,
7062
+ toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
6189
7063
  claims: this.heldWorktrees()
6190
7064
  });
6191
7065
  return { ...report, modes };
@@ -6201,7 +7075,7 @@ ${err}
6201
7075
  this.prices = { ...PRICES };
6202
7076
  for (const f of ["pricing.litellm.json", "pricing.json"]) {
6203
7077
  const p = join8(this.home, f);
6204
- if (!existsSync5(p))
7078
+ if (!existsSync6(p))
6205
7079
  continue;
6206
7080
  try {
6207
7081
  const j = JSON.parse(readFileSync3(p, "utf8"));
@@ -6221,7 +7095,7 @@ ${err}
6221
7095
  this.reprice();
6222
7096
  }
6223
7097
  reprice() {
6224
- 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();
6225
7099
  const up = this.db.query("UPDATE turns SET cost_usd = ? WHERE id = ?");
6226
7100
  const tx = this.db.transaction(() => {
6227
7101
  for (const r of rows)
@@ -6382,13 +7256,70 @@ ${err}
6382
7256
  if (stored.type === "incident.opened")
6383
7257
  this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
6384
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
+ }
6385
7292
  this.touch();
6386
7293
  const wire = toWire(stored);
6387
7294
  for (const l of this.listeners)
6388
7295
  l(wire);
6389
7296
  return stored;
6390
7297
  }
6391
- audit(opts = {}) {
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
+ }
7322
+ audit(opts = {}) {
6392
7323
  const where = [`type IN (${AUDIT_TYPES_SQL})`];
6393
7324
  const args = [];
6394
7325
  if (opts.since) {
@@ -6426,10 +7357,10 @@ ${err}
6426
7357
  if (typeof raw2.cwd === "string")
6427
7358
  this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
6428
7359
  const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
6429
- const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
7360
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
6430
7361
  const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
6431
7362
  if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
6432
- if (existsSync5(cwd)) {
7363
+ if (existsSync6(cwd)) {
6433
7364
  this.autoHandoff(e.sessionId, cwd);
6434
7365
  this.autoGate(event, e.sessionId, cwd);
6435
7366
  }
@@ -6475,7 +7406,7 @@ ${err}
6475
7406
  return;
6476
7407
  const p = e.payload;
6477
7408
  const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
6478
- const branch = p.cwd && existsSync5(p.cwd) ? currentBranch(p.cwd) : null;
7409
+ const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
6479
7410
  if (!row) {
6480
7411
  this.db.query("INSERT INTO sessions (id, project_id, kind, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, 'active')").run(e.sessionId, e.projectId, p.cwd ?? "", branch, e.ts, e.ts, p.summary ?? e.type, e.type);
6481
7412
  }
@@ -6515,13 +7446,13 @@ ${err}
6515
7446
  persistTurns(sessionId, agentId, turns) {
6516
7447
  const privacy = this.policyFor(null).config.privacy;
6517
7448
  const res = this.redactions();
6518
- 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)
6519
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6520
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,
6521
- 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`);
6522
7453
  const tx = this.db.transaction((ts) => {
6523
7454
  for (const t of ts) {
6524
- 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));
6525
7456
  }
6526
7457
  });
6527
7458
  if (turns.length)
@@ -6547,10 +7478,10 @@ ${err}
6547
7478
  }
6548
7479
  tailSession(sessionId) {
6549
7480
  const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
6550
- if (!s?.transcript_path || !existsSync5(s.transcript_path))
7481
+ if (!s?.transcript_path || !existsSync6(s.transcript_path))
6551
7482
  return 0;
6552
7483
  let n = this.tailFile(s.transcript_path, sessionId, null);
6553
- const subDir = join8(dirname2(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
7484
+ const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
6554
7485
  for (const f of this.subagentFiles(subDir)) {
6555
7486
  n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
6556
7487
  }
@@ -6618,7 +7549,7 @@ ${err}
6618
7549
  return out;
6619
7550
  }
6620
7551
  tailCodex(windowMs = 3 * 24 * 60 * 60000) {
6621
- if (!existsSync5(this.codexRoot()))
7552
+ if (!existsSync6(this.codexRoot()))
6622
7553
  return 0;
6623
7554
  let n = 0;
6624
7555
  for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
@@ -6630,9 +7561,54 @@ ${err}
6630
7561
  return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
6631
7562
  }
6632
7563
  grokSummary = new Map;
7564
+ tailGemini(windowMs = 3 * 24 * 60 * 60000) {
7565
+ const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
7566
+ if (!existsSync6(root))
7567
+ return 0;
7568
+ const since = Date.now() - windowMs;
7569
+ const ls = (p) => {
7570
+ try {
7571
+ return readdirSync(p);
7572
+ } catch {
7573
+ return [];
7574
+ }
7575
+ };
7576
+ let n = 0;
7577
+ const ingestDir = (dir) => {
7578
+ for (const f of ls(dir)) {
7579
+ const path = join8(dir, f);
7580
+ if (!f.endsWith(".jsonl")) {
7581
+ try {
7582
+ if (statSync(path).isDirectory()) {
7583
+ for (const g of ls(path))
7584
+ if (g.endsWith(".jsonl"))
7585
+ ingestFile(join8(path, g));
7586
+ }
7587
+ } catch {}
7588
+ continue;
7589
+ }
7590
+ ingestFile(path);
7591
+ }
7592
+ };
7593
+ const ingestFile = (path) => {
7594
+ try {
7595
+ if (statSync(path).mtimeMs < since)
7596
+ return;
7597
+ } catch {
7598
+ return;
7599
+ }
7600
+ n += this.ingestLog(path, "gemini", parseGeminiChat);
7601
+ };
7602
+ for (const hash of ls(root)) {
7603
+ const chats = join8(root, hash, "chats");
7604
+ if (existsSync6(chats))
7605
+ ingestDir(chats);
7606
+ }
7607
+ return n;
7608
+ }
6633
7609
  tailGrok(windowMs = 3 * 24 * 60 * 60000) {
6634
7610
  const root = this.grokRoot();
6635
- if (!existsSync5(root))
7611
+ if (!existsSync6(root))
6636
7612
  return 0;
6637
7613
  const since = Date.now() - windowMs;
6638
7614
  const ls = (p) => {
@@ -6655,7 +7631,7 @@ ${err}
6655
7631
  const cwdDir = join8(root, enc);
6656
7632
  for (const sid of ls(cwdDir)) {
6657
7633
  const path = join8(cwdDir, sid, "updates.jsonl");
6658
- if (!existsSync5(path))
7634
+ if (!existsSync6(path))
6659
7635
  continue;
6660
7636
  try {
6661
7637
  if (statSync(path).mtimeMs < since)
@@ -6686,6 +7662,160 @@ ${err}
6686
7662
  }
6687
7663
  return n;
6688
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
+ }
6689
7819
  ingestLog(path, agent, parse, cwdHint, titleHint) {
6690
7820
  const off = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path) ?? { offset: 0 };
6691
7821
  const r = this.readFrom(path, off.offset);
@@ -6714,9 +7844,9 @@ ${err}
6714
7844
  ensureAgentSession(sid, agent, cwd, mtime) {
6715
7845
  if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
6716
7846
  return;
6717
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
7847
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
6718
7848
  const ts = new Date(mtime).toISOString();
6719
- this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd && existsSync5(cwd) ? currentBranch(cwd) : null, ts, ts);
7849
+ this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd && existsSync6(cwd) ? currentBranch(cwd) : null, ts, ts);
6720
7850
  }
6721
7851
  claimRows(projectId) {
6722
7852
  return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
@@ -6762,9 +7892,9 @@ ${err}
6762
7892
  return { ok: false, error: claimRefusalMessage(decision, task) };
6763
7893
  const branch = `task/${task}`;
6764
7894
  const worktree2 = this.worktreePath(projectId, task);
6765
- if (existsSync5(worktree2))
7895
+ if (existsSync6(worktree2))
6766
7896
  return { ok: false, error: `${worktree2} already exists; release ${task} first` };
6767
- mkdirSync4(dirname2(worktree2), { recursive: true });
7897
+ mkdirSync4(dirname3(worktree2), { recursive: true });
6768
7898
  const created = worktreeAdd(p.root, worktree2, branch, baseRef);
6769
7899
  if (!created)
6770
7900
  return { ok: false, error: `git worktree add failed for ${task}` };
@@ -6867,6 +7997,77 @@ ${err}
6867
7997
  expiresAt: r.expires_at
6868
7998
  }));
6869
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
+ }
6870
8071
  sweepOrphans() {
6871
8072
  const now = Date.now();
6872
8073
  let n = 0;
@@ -6874,7 +8075,7 @@ ${err}
6874
8075
  for (const c of this.claimRows(p.id)) {
6875
8076
  if (c.state !== "held" || isActive(c, now))
6876
8077
  continue;
6877
- const exists = c.worktree ? existsSync5(c.worktree) : false;
8078
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6878
8079
  const work = exists ? heldWork(c.worktree) : null;
6879
8080
  if (reapAction(c, now, exists, work) !== "keep-orphaned")
6880
8081
  continue;
@@ -6930,7 +8131,7 @@ ${err}
6930
8131
  if (!row)
6931
8132
  return { ok: false, error: `no claim on ${task}` };
6932
8133
  const worktree2 = row.worktree ?? "";
6933
- if (worktree2 && existsSync5(worktree2)) {
8134
+ if (worktree2 && existsSync6(worktree2)) {
6934
8135
  const work = heldWork(worktree2);
6935
8136
  const can = canRelease(work, force);
6936
8137
  if (!can.ok)
@@ -6965,7 +8166,7 @@ ${err}
6965
8166
  continue;
6966
8167
  if (isActive({ ...c, state: "held" }, now))
6967
8168
  continue;
6968
- const exists = c.worktree ? existsSync5(c.worktree) : false;
8169
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6969
8170
  const work = exists ? heldWork(c.worktree) : null;
6970
8171
  const action = reapAction({ ...c, state: "held" }, now, exists, work);
6971
8172
  if (action === "not-expired")
@@ -7064,9 +8265,9 @@ ${err}
7064
8265
  if (!slug || slug === "." || slug === "..")
7065
8266
  return { ok: false, error: "bad worktree name" };
7066
8267
  const path = this.worktreePath(projectId, slug);
7067
- if (existsSync5(path))
8268
+ if (existsSync6(path))
7068
8269
  return { ok: false, error: `${path} already exists` };
7069
- mkdirSync4(dirname2(path), { recursive: true });
8270
+ mkdirSync4(dirname3(path), { recursive: true });
7070
8271
  const br = branch?.trim() || `wt/${slug}`;
7071
8272
  const created = worktreeAdd(p.root, path, br, baseRef);
7072
8273
  if (!created)
@@ -7244,6 +8445,7 @@ ${err}
7244
8445
  lastType: r.last_type,
7245
8446
  lastText: r.last_text ?? null,
7246
8447
  state,
8448
+ stuck: state === "active" || state === "waiting" ? this.stalls.get(r.id)?.reason ?? null : null,
7247
8449
  toolCalls: r.tool_calls,
7248
8450
  subagents: r.subagents,
7249
8451
  turns: r.turns,
@@ -7308,8 +8510,120 @@ ${err}
7308
8510
  fn(p.id, b.status);
7309
8511
  this.touch();
7310
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
+ }
7311
8549
  return out;
7312
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
+ }
7313
8627
  spend() {
7314
8628
  const dayStart = new Date;
7315
8629
  dayStart.setHours(0, 0, 0, 0);
@@ -7751,6 +9065,48 @@ ${err}
7751
9065
  seq() {
7752
9066
  return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
7753
9067
  }
9068
+ timelineDetail(hours, projectId) {
9069
+ const from = new Date(Date.now() - Math.min(Math.max(hours, 1), 168) * 3600000).toISOString();
9070
+ const args = [from];
9071
+ let filter = "";
9072
+ if (projectId) {
9073
+ filter = " AND s.project_id = ?";
9074
+ args.push(projectId);
9075
+ }
9076
+ const rows = this.db.query(`SELECT t.session_id AS sid, t.ts FROM turns t JOIN sessions s ON s.id = t.session_id
9077
+ WHERE t.ts >= ? AND t.sidechain = 0${filter} ORDER BY t.ts LIMIT 20000`).all(...args);
9078
+ const turns = {};
9079
+ for (const r of rows) {
9080
+ turns[r.sid] ??= [];
9081
+ turns[r.sid]?.push(new Date(r.ts).getTime());
9082
+ }
9083
+ const claims = this.claims().filter((c) => (!projectId || c.projectId === projectId) && c.state !== "released").map((c) => ({
9084
+ projectId: c.projectId,
9085
+ task: c.task,
9086
+ owner: c.owner,
9087
+ state: c.state,
9088
+ acquiredAt: c.acquiredAt,
9089
+ expiresAt: c.expiresAt
9090
+ }));
9091
+ return { turns, claims };
9092
+ }
9093
+ spendSparks() {
9094
+ const from = localDayIso(-13);
9095
+ const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
9096
+ FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
9097
+ const days2 = [];
9098
+ for (let i = 13;i >= 0; i--)
9099
+ days2.push(localDayIso(-i).slice(0, 10));
9100
+ const out = {};
9101
+ for (const r of rows) {
9102
+ out[r.pid] ??= new Array(14).fill(0);
9103
+ const arr = out[r.pid];
9104
+ const i = days2.indexOf(r.day);
9105
+ if (i >= 0)
9106
+ arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
9107
+ }
9108
+ return out;
9109
+ }
7754
9110
  snapshot() {
7755
9111
  const worktrees = {};
7756
9112
  const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
@@ -7761,6 +9117,7 @@ ${err}
7761
9117
  worktrees,
7762
9118
  sessions: this.memoised("sessions", 2000, () => this.sessions()),
7763
9119
  spend: this.memoised("spend", 30000, () => this.spend()),
9120
+ spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
7764
9121
  claims: this.claims(),
7765
9122
  processes: this.memoised("processes", 5000, () => this.processes()),
7766
9123
  incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
@@ -7858,15 +9215,428 @@ function isScratchRoot(root) {
7858
9215
  const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
7859
9216
  return tmp.some((t) => root === t || root.startsWith(`${t}/`));
7860
9217
  }
9218
+ function rowToMessage(r) {
9219
+ return {
9220
+ id: r.id,
9221
+ projectId: r.project_id,
9222
+ task: r.task ?? null,
9223
+ sessionId: r.session_id ?? null,
9224
+ toKind: r.to_kind ?? "session",
9225
+ from: r.asked_by ?? null,
9226
+ fromSession: r.from_session ?? null,
9227
+ text: r.text,
9228
+ createdAt: r.created_at,
9229
+ deliveredAt: r.delivered_at ?? null
9230
+ };
9231
+ }
9232
+ function rowToWorkflowRun(r) {
9233
+ return {
9234
+ id: r.id,
9235
+ projectId: r.project_id,
9236
+ task: r.task,
9237
+ workflow: r.workflow,
9238
+ step: r.step,
9239
+ stepLabel: r.step_label ?? "",
9240
+ steps: JSON.parse(r.steps ?? "[]"),
9241
+ state: r.state,
9242
+ detail: r.detail ?? null,
9243
+ runId: r.run_id ?? null,
9244
+ startedAt: r.started_at,
9245
+ updatedAt: r.updated_at,
9246
+ endedAt: r.ended_at ?? null
9247
+ };
9248
+ }
9249
+ function localDayIso(offsetDays) {
9250
+ const d = new Date;
9251
+ d.setHours(0, 0, 0, 0);
9252
+ d.setDate(d.getDate() + offsetDays);
9253
+ return d.toISOString();
9254
+ }
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
+
9442
+ // packages/daemon/src/workflow.ts
9443
+ class WorkflowEngine {
9444
+ store;
9445
+ runner;
9446
+ forge;
9447
+ active = new Map;
9448
+ constructor(store, runner, forge2) {
9449
+ this.store = store;
9450
+ this.runner = runner;
9451
+ this.forge = forge2;
9452
+ store.wfSweepOrphans();
9453
+ runner.onEnd((run2) => void this.onRunEnd(run2));
9454
+ }
9455
+ start(projectId, task, workflow, opts = {}) {
9456
+ const def = this.store.config(projectId).workflows[workflow];
9457
+ if (!def) {
9458
+ const known = Object.keys(this.store.config(projectId).workflows);
9459
+ return {
9460
+ ok: false,
9461
+ error: `unknown workflow ${workflow}${known.length ? ` \u2014 this repo declares: ${known.join(", ")}` : " \u2014 declare [[workflows]] in .swarm.toml"}`
9462
+ };
9463
+ }
9464
+ const key = `${projectId}:${task}`;
9465
+ if (this.active.has(key) || this.store.wfActive(projectId, task))
9466
+ return { ok: false, error: `a workflow is already running on ${task}` };
9467
+ const title = this.store.tasks(projectId)?.tasks.find((t) => t.id === task)?.title ?? task;
9468
+ const owner = opts.owner ?? "workflow";
9469
+ const id = this.store.wfInsert(projectId, task, workflow, def.steps.map(stepLabel), this.store.actorFor(owner, opts.sessionId ?? null));
9470
+ const w = { id, projectId, task, title, def, step: 0, runId: null, owner };
9471
+ this.active.set(key, w);
9472
+ this.store.append({
9473
+ ts: new Date().toISOString(),
9474
+ type: "workflow.started",
9475
+ projectId,
9476
+ sessionId: opts.sessionId ?? null,
9477
+ payload: {
9478
+ id,
9479
+ task,
9480
+ workflow,
9481
+ steps: def.steps.map(stepLabel),
9482
+ summary: `workflow ${workflow} on ${task}: ${def.steps.map(stepLabel).join(" \u2192 ")}`
9483
+ }
9484
+ });
9485
+ this.advance(w);
9486
+ return { ok: true, id };
9487
+ }
9488
+ status(projectId) {
9489
+ return this.store.wfRuns(projectId);
9490
+ }
9491
+ stop(projectId, task) {
9492
+ const key = `${projectId}:${task}`;
9493
+ const w = this.active.get(key);
9494
+ if (!w)
9495
+ return { ok: false, error: `no running workflow on ${task}` };
9496
+ if (w.runId)
9497
+ this.runner.stop(w.runId);
9498
+ this.finish(w, "stopped", `stopped at ${this.label(w)}`);
9499
+ return { ok: true };
9500
+ }
9501
+ label(w) {
9502
+ const s = w.def.steps[w.step];
9503
+ return s ? stepLabel(s) : "done";
9504
+ }
9505
+ async advance(w) {
9506
+ while (w.step < w.def.steps.length) {
9507
+ const s = w.def.steps[w.step];
9508
+ this.store.wfUpdate(w.id, { step: w.step, stepLabel: stepLabel(s), runId: null });
9509
+ this.step(w, `step ${w.step + 1}/${w.def.steps.length}: ${stepLabel(s)}`);
9510
+ if (s.kind === "run") {
9511
+ const cfg = this.store.config(w.projectId).dispatch;
9512
+ const remaining = w.def.steps.slice(w.step + 1).map(stepLabel);
9513
+ const r = await this.runner.start({
9514
+ projectId: w.projectId,
9515
+ task: w.task,
9516
+ prompt: workflowStepPrompt(s, { id: w.task, title: w.title }, { workflow: w.def.name, remaining }),
9517
+ owner: w.owner,
9518
+ permissionMode: cfg.permission_mode ?? "acceptEdits",
9519
+ model: cfg.model ?? undefined,
9520
+ maxTurns: cfg.max_turns ?? undefined,
9521
+ profile: cfg.profile ?? undefined
9522
+ });
9523
+ if (!r.ok)
9524
+ return this.fail(w, `could not start ${stepLabel(s)}: ${r.reason}`);
9525
+ w.runId = r.run.id;
9526
+ this.store.wfUpdate(w.id, { runId: r.run.id });
9527
+ return;
9528
+ }
9529
+ if (s.kind === "gate") {
9530
+ const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
9531
+ const run2 = r.runs.find((x) => x.gate === s.gate);
9532
+ if (!run2)
9533
+ return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
9534
+ if (run2.verdict !== "pass")
9535
+ return this.fail(w, `gate ${s.gate} failed \u2014 ${run2.rubric}`);
9536
+ w.step++;
9537
+ continue;
9538
+ }
9539
+ const d = await this.store.prDraftFor(w.projectId, w.task);
9540
+ if (!d.ok)
9541
+ return this.fail(w, `pr: ${d.error}`);
9542
+ const pr = await this.forge.openPR(w.projectId, d.worktree, {
9543
+ title: d.title,
9544
+ body: d.body,
9545
+ isDraft: false
9546
+ });
9547
+ if (!pr.ok)
9548
+ return this.fail(w, `pr: ${pr.error}`);
9549
+ this.store.recordPrOpened(w.projectId, d.task, d.worktree.path, pr.url);
9550
+ this.store.wfUpdate(w.id, { detail: `PR ${pr.url}` });
9551
+ w.step++;
9552
+ }
9553
+ this.finish(w, "done", null);
9554
+ }
9555
+ async onRunEnd(run2) {
9556
+ const w = this.active.get(`${run2.projectId}:${run2.task}`);
9557
+ if (!w || w.runId !== run2.id)
9558
+ return;
9559
+ w.runId = null;
9560
+ if (run2.stopped)
9561
+ return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
9562
+ if (run2.exitCode !== 0 || run2.result?.isError)
9563
+ return this.fail(w, `${this.label(w)} exited ${run2.exitCode}${run2.result?.isError ? " (error)" : ""} \u2014 log: ${run2.log}`);
9564
+ w.step++;
9565
+ this.advance(w);
9566
+ }
9567
+ step(w, summary) {
9568
+ this.store.append({
9569
+ ts: new Date().toISOString(),
9570
+ type: "workflow.step",
9571
+ projectId: w.projectId,
9572
+ sessionId: null,
9573
+ payload: {
9574
+ id: w.id,
9575
+ task: w.task,
9576
+ workflow: w.def.name,
9577
+ step: w.step,
9578
+ label: this.label(w),
9579
+ summary: `workflow ${w.def.name} on ${w.task} \u2014 ${summary}`
9580
+ }
9581
+ });
9582
+ }
9583
+ fail(w, detail) {
9584
+ this.store.wfUpdate(w.id, { state: "failed", detail, ended: true });
9585
+ this.active.delete(`${w.projectId}:${w.task}`);
9586
+ this.store.append({
9587
+ ts: new Date().toISOString(),
9588
+ type: "workflow.finished",
9589
+ projectId: w.projectId,
9590
+ sessionId: null,
9591
+ payload: {
9592
+ id: w.id,
9593
+ task: w.task,
9594
+ workflow: w.def.name,
9595
+ outcome: "failed",
9596
+ detail,
9597
+ summary: `workflow ${w.def.name} on ${w.task} failed at ${this.label(w)}: ${detail.slice(0, 160)}`
9598
+ }
9599
+ });
9600
+ this.store.append({
9601
+ ts: new Date().toISOString(),
9602
+ type: "incident.opened",
9603
+ projectId: w.projectId,
9604
+ sessionId: null,
9605
+ payload: {
9606
+ rule: "workflow_failed",
9607
+ action: "failed",
9608
+ command: `${w.task} \xB7 ${w.def.name} \xB7 ${this.label(w)}`,
9609
+ reason: detail.slice(0, 400)
9610
+ }
9611
+ });
9612
+ }
9613
+ finish(w, state, detail) {
9614
+ this.store.wfUpdate(w.id, { state, ...detail !== null ? { detail } : {}, ended: true });
9615
+ this.active.delete(`${w.projectId}:${w.task}`);
9616
+ this.store.append({
9617
+ ts: new Date().toISOString(),
9618
+ type: "workflow.finished",
9619
+ projectId: w.projectId,
9620
+ sessionId: null,
9621
+ payload: {
9622
+ id: w.id,
9623
+ task: w.task,
9624
+ workflow: w.def.name,
9625
+ outcome: state,
9626
+ summary: `workflow ${w.def.name} on ${w.task}: ${state}${detail ? ` \u2014 ${detail}` : ""}`
9627
+ }
9628
+ });
9629
+ }
9630
+ }
7861
9631
 
7862
9632
  // packages/daemon/src/app.ts
7863
- var VERSION = "0.8.0";
9633
+ var VERSION = "0.10.0";
7864
9634
  var WEB_DIR = (() => {
7865
9635
  if (process.env.SWARM_WEB_DIR)
7866
9636
  return process.env.SWARM_WEB_DIR;
7867
- const here = dirname3(fileURLToPath(import.meta.url));
7868
- const dev = join9(here, "../../web/public");
7869
- return existsSync6(join9(dev, "index.html")) ? dev : join9(here, "../web");
9637
+ const here = dirname4(fileURLToPath2(import.meta.url));
9638
+ const dev = join10(here, "../../web/public");
9639
+ return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
7870
9640
  })();
7871
9641
  var REPLAY_TAIL = 200;
7872
9642
  var wireCache = new WeakMap;
@@ -7880,13 +9650,40 @@ function wireJson(e) {
7880
9650
  }
7881
9651
  function hookRepoRoot(store, raw2) {
7882
9652
  const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
7883
- return cwd && existsSync6(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
9653
+ return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
9654
+ }
9655
+ function claudeSettings() {
9656
+ try {
9657
+ const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
9658
+ return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
9659
+ } catch {
9660
+ return null;
9661
+ }
9662
+ }
9663
+ function diskVersion() {
9664
+ try {
9665
+ const entry = daemonCommand().at(-1);
9666
+ if (!entry || !existsSync7(entry))
9667
+ return null;
9668
+ for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
9669
+ if (!existsSync7(f))
9670
+ continue;
9671
+ const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
9672
+ if (m?.[1])
9673
+ return m[1];
9674
+ }
9675
+ return null;
9676
+ } catch {
9677
+ return null;
9678
+ }
7884
9679
  }
7885
- function createApp(store = new Store) {
9680
+ function createApp(store = new Store, hooks2 = {}) {
7886
9681
  const app = new Hono2;
7887
9682
  const forge2 = new ForgeService(store);
7888
9683
  const runner = new Runner(store, store.home);
7889
9684
  const dispatcher = new Dispatcher(store, runner, forge2);
9685
+ const workflows2 = new WorkflowEngine(store, runner, forge2);
9686
+ const team2 = new TeamForwarder(store, VERSION);
7890
9687
  store.onBudgetStop((projectId) => {
7891
9688
  dispatcher.clear(projectId);
7892
9689
  for (const run2 of runner.list(projectId))
@@ -7908,6 +9705,8 @@ function createApp(store = new Store) {
7908
9705
  return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
7909
9706
  });
7910
9707
  app.get("/v1/health", (c) => c.json({
9708
+ disk: diskVersion(),
9709
+ hooksInstalled: hookCoverage(claudeSettings()).complete,
7911
9710
  ok: true,
7912
9711
  version: VERSION,
7913
9712
  schema: store.schemaVersion(),
@@ -7942,13 +9741,13 @@ function createApp(store = new Store) {
7942
9741
  const q = c.req.query("path");
7943
9742
  let dir;
7944
9743
  try {
7945
- dir = realpathSync3(q && existsSync6(q) ? q : homedir4());
9744
+ dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
7946
9745
  } catch {
7947
9746
  dir = homedir4();
7948
9747
  }
7949
9748
  try {
7950
- const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync6(join9(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
7951
- const parent = dirname3(dir);
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));
9750
+ const parent = dirname4(dir);
7952
9751
  return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
7953
9752
  } catch (e) {
7954
9753
  return c.json({ error: e.message, path: dir }, 400);
@@ -7956,10 +9755,44 @@ function createApp(store = new Store) {
7956
9755
  });
7957
9756
  app.get("/v1/state", (c) => c.json(store.snapshot()));
7958
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)));
7959
9759
  app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
7960
9760
  open: c.req.query("open") === "1",
7961
9761
  projectId: c.req.query("project") || undefined
7962
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
+ });
7963
9796
  app.get("/v1/memory", (c) => {
7964
9797
  const q = c.req.query("q") ?? "";
7965
9798
  const kind = c.req.query("kind");
@@ -7973,6 +9806,26 @@ function createApp(store = new Store) {
7973
9806
  })
7974
9807
  });
7975
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
+ });
7976
9829
  app.get("/v1/policy", (c) => {
7977
9830
  const id = c.req.query("project");
7978
9831
  const p = id ? store.project(id) : null;
@@ -7998,6 +9851,12 @@ function createApp(store = new Store) {
7998
9851
  const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
7999
9852
  return c.body(formatAudit(rows, format), 200, { "content-type": ct });
8000
9853
  });
9854
+ app.post("/v1/daemon/restart", (c) => {
9855
+ if (!hooks2.restart)
9856
+ return c.json({ error: "not restartable in this environment" }, 501);
9857
+ setTimeout(() => hooks2.restart?.(), 50);
9858
+ return c.json({ ok: true, restarting: true });
9859
+ });
8001
9860
  app.get("/v1/rules/dryrun", (c) => {
8002
9861
  const projectId = c.req.query("project");
8003
9862
  if (!projectId)
@@ -8132,6 +9991,58 @@ function createApp(store = new Store) {
8132
9991
  return c.json(r, r.ok ? 200 : 409);
8133
9992
  });
8134
9993
  app.get("/v1/inbox", (c) => c.json(store.inbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
9994
+ app.get("/v1/workflows", (c) => {
9995
+ const project = c.req.query("project");
9996
+ if (!project)
9997
+ return c.json({ error: "project required" }, 400);
9998
+ return c.json({ defs: store.config(project).workflows, runs: workflows2.status(project) });
9999
+ });
10000
+ app.post("/v1/workflows", async (c) => {
10001
+ const b = await c.req.json().catch(() => ({}));
10002
+ if (!b.projectId || !b.task || !b.workflow)
10003
+ return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
10004
+ const r = workflows2.start(b.projectId, b.task, b.workflow, {
10005
+ ...b.owner ? { owner: b.owner } : {},
10006
+ sessionId: b.sessionId ?? null
10007
+ });
10008
+ return c.json(r, r.ok ? 201 : 409);
10009
+ });
10010
+ app.post("/v1/workflows/stop", async (c) => {
10011
+ const b = await c.req.json().catch(() => ({}));
10012
+ if (!b.projectId || !b.task)
10013
+ return c.json({ ok: false, error: "projectId and task required" }, 400);
10014
+ const r = workflows2.stop(b.projectId, b.task);
10015
+ return c.json(r, r.ok ? 200 : 404);
10016
+ });
10017
+ app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
10018
+ app.get("/v1/messages", (c) => c.json(store.messages({
10019
+ ...c.req.query("project") ? { projectId: c.req.query("project") } : {},
10020
+ ...c.req.query("session") ? { sessionId: c.req.query("session") } : {},
10021
+ ...c.req.query("task") ? { task: c.req.query("task") } : {},
10022
+ limit: Number(c.req.query("limit")) || 100
10023
+ })));
10024
+ app.get("/v1/messages/inbox", (c) => c.json(store.messageInbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
10025
+ app.post("/v1/messages", async (c) => {
10026
+ const b = await c.req.json().catch(() => ({}));
10027
+ if (!b.projectId)
10028
+ return c.json({ ok: false, error: "projectId required" }, 400);
10029
+ const r = store.send(b.projectId, {
10030
+ to: b.to,
10031
+ text: b.text,
10032
+ from: b.from ?? null,
10033
+ fromSession: b.sessionId ?? null
10034
+ });
10035
+ if (!r.ok)
10036
+ return c.json(r, 400);
10037
+ const m = r.message;
10038
+ const run2 = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
10039
+ if (run2 && !run2.endedAt) {
10040
+ const sent = runner.send(run2.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
10041
+ if (sent.ok)
10042
+ store.markMessageDelivered(m.id, run2.sessionId);
10043
+ }
10044
+ return c.json({ ok: true, message: store.message(m.id) }, 201);
10045
+ });
8135
10046
  app.get("/v1/dispatch", (c) => {
8136
10047
  const project = c.req.query("project");
8137
10048
  if (!project)
@@ -8453,7 +10364,7 @@ function createApp(store = new Store) {
8453
10364
  await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
8454
10365
  }
8455
10366
  await stream2.writeSSE({ event: "ping", data: "" });
8456
- await new Promise((resolve) => {
10367
+ await new Promise((resolve2) => {
8457
10368
  const off = store.subscribe((e) => {
8458
10369
  stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
8459
10370
  });
@@ -8461,28 +10372,117 @@ function createApp(store = new Store) {
8461
10372
  stream2.onAbort(() => {
8462
10373
  clearInterval(beat);
8463
10374
  off();
8464
- resolve();
10375
+ resolve2();
8465
10376
  });
8466
10377
  });
8467
10378
  });
8468
10379
  });
8469
- 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")));
8470
10381
  const MIME = { js: "text/javascript", css: "text/css" };
8471
10382
  app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
8472
10383
  const f = c.req.param("file");
8473
- const p = join9(WEB_DIR, f);
8474
- if (!existsSync6(p))
10384
+ const p = join10(WEB_DIR, f);
10385
+ if (!existsSync7(p))
8475
10386
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
8476
10387
  return c.body(readFileSync4(p, "utf8"), 200, {
8477
10388
  "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
8478
10389
  });
8479
10390
  });
8480
- return { app, store, forge: forge2, runner, dispatcher };
10391
+ return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
10392
+ }
10393
+
10394
+ // packages/daemon/src/demo.ts
10395
+ var H = 3600000;
10396
+ var iso = (msAgo) => new Date(Date.now() - msAgo).toISOString();
10397
+ function isEmpty(store) {
10398
+ return !store.db.query("SELECT 1 FROM sessions LIMIT 1").get();
10399
+ }
10400
+ function seedDemo(store) {
10401
+ const db = store.db;
10402
+ const project = (id, name, root, icon, color) => db.query("INSERT OR IGNORE INTO projects (id, root, common_dir, name, discovered, created_at, icon, color) VALUES (?, ?, ?, ?, 0, ?, ?, ?)").run(id, root, `${root}/.git`, name, iso(90 * 24 * H), icon, color);
10403
+ project("p_demo1", "acme-app", "/work/acme-app", "\uD83D\uDED2", "c3");
10404
+ project("p_demo2", "acme-site", "/work/acme-site", "\uD83C\uDF10", "c5");
10405
+ const session = (id, pid, agent, title, cwd, branch, startedAgo, lastAgo, state, model) => {
10406
+ db.query(`INSERT OR IGNORE INTO sessions (id, project_id, kind, agent, cwd, branch, title, model, started_at, last_seen_at, last, last_type, last_text, state, tool_calls)
10407
+ VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, ?, ?, 'tool.completed', ?, ?, ?)`).run(id, pid, agent, cwd, branch, title, model, iso(startedAgo), iso(lastAgo), state === "active" ? "Bash bun test" : "session ended", state === "active" ? "Running the suite before handing off." : "Done \u2014 PR opened, gates green.", state, 40 + Math.floor(Math.random() * 200));
10408
+ let t = startedAgo;
10409
+ let i = 0;
10410
+ while (t > lastAgo) {
10411
+ const out = 300 + Math.floor(Math.random() * 4000);
10412
+ const read = 50000 + Math.floor(Math.random() * 900000);
10413
+ db.query(`INSERT OR IGNORE 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)
10414
+ VALUES (?, ?, NULL, ?, ?, NULL, 0, ?, ?, ?, 0, ?, ?, ?, ?, '["Bash","Edit"]')`).run(`${id}-t${i}`, id, iso(t), model, 800 + i * 97 % 2000, out, 12000, read, i % 3 === 0 ? 900 : 0, 0.02 + out / 1e6 * 15 + read / 1e6 * 0.3, i % 4 === 0 ? "Tests are green; tightening the error path next." : "");
10415
+ t -= (8 + i * 13 % 30) * 60000;
10416
+ i++;
10417
+ }
10418
+ };
10419
+ session("demo-s1", "p_demo1", "claude-code", "Checkout flow refactor", "/work/acme-app-wt/checkout", "task/checkout", 5 * H, 2 * 60000, "active", "claude-fable-5");
10420
+ session("demo-s2", "p_demo1", "codex", "Fix flaky cart tests", "/work/acme-app", "main", 7 * H, 3 * H, "ended", "gpt-5.2-codex");
10421
+ session("demo-s3", "p_demo1", "gemini", "Payment webhook audit", "/work/acme-app", "main", 26 * H, 22 * H, "ended", "gemini-2.5-pro");
10422
+ session("demo-s4", "p_demo2", "grok", "Landing page rewrite", "/work/acme-site", "task/landing", 30 * H, 25 * H, "ended", "grok-4");
10423
+ session("demo-s5", "p_demo2", "claude-code", "SEO metadata sweep", "/work/acme-site", "main", 50 * H, 47 * H, "ended", "claude-sonnet-5");
10424
+ db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
10425
+ VALUES ('p_demo1', 'checkout', 'demo-s1', '/work/acme-app-wt/checkout', 'task/checkout', ?, ?, NULL, 'held', 'agent', 'demo-s1')`).run(iso(5 * H), iso(-30 * 60000));
10426
+ db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
10427
+ VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
10428
+ const gate = (task, gate2, verdict, rubric, ago, sid) => db.query(`INSERT OR IGNORE INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
10429
+ VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate2, verdict, rubric, sid, iso(ago));
10430
+ gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
10431
+ gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
10432
+ gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
10433
+ gate("webhooks", "tests", "pass", "ran `bun test` \u2014 exit 0 in 22s", 22 * H, "demo-s3");
10434
+ const ev = (type, ago, sid, payload) => store.append({
10435
+ ts: iso(ago),
10436
+ type,
10437
+ projectId: "p_demo1",
10438
+ sessionId: sid,
10439
+ payload
10440
+ });
10441
+ ev("incident.opened", 4 * H, "demo-s1", {
10442
+ rule: "pattern_kill",
10443
+ action: "ask",
10444
+ command: "pkill -f vite",
10445
+ reason: "This kills processes by command pattern \u2014 other agents' dev servers match too."
10446
+ });
10447
+ ev("incident.opened", 26 * H, "demo-s3", {
10448
+ rule: "shared_tree",
10449
+ action: "deny",
10450
+ command: "git reset --hard",
10451
+ reason: "Another session (demo-s2) is active in this same checkout."
10452
+ });
10453
+ ev("claim.acquired", 5 * H, "demo-s1", {
10454
+ task: "checkout",
10455
+ owner: "demo-s1",
10456
+ summary: "claim checkout"
10457
+ });
10458
+ ev("pr.opened", 30 * 60000, "demo-s1", {
10459
+ task: "checkout",
10460
+ url: "https://github.com/acme/app/pull/128",
10461
+ summary: "PR #128 opened for checkout"
10462
+ });
10463
+ ev("question.asked", 20 * 60000, "demo-s1", {
10464
+ id: 1,
10465
+ task: "checkout",
10466
+ text: "Coupon codes: keep the legacy endpoint alive for one release, or cut over now?",
10467
+ options: ["Keep one release", "Cut over"],
10468
+ summary: "question #1"
10469
+ });
10470
+ db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, options, asked_by, created_at)
10471
+ VALUES ('p_demo1', 'demo-s1', 'checkout', 'question', 'Coupon codes: keep the legacy endpoint alive for one release, or cut over now?', '["Keep one release","Cut over"]', 'demo-s1', ?)`).run(iso(20 * 60000));
10472
+ db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
10473
+ VALUES ('p_demo1', 'demo-s1', 'checkout', 'message', 'Cart tests are green again \u2014 rebasing on main is safe now.', 'agent demo-s2', ?, 'task', 'demo-s2')`).run(iso(50 * 60000));
10474
+ db.query(`INSERT OR IGNORE INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
10475
+ VALUES ('p_demo1', 'webhooks', 'Signature validation + retries done', 'Dead-letter queue wiring', '["src/webhooks.ts","src/queue.ts"]', 'bun test \u2014 118 pass', 'auto:demo-s3', 'demo-s3', ?, 'daemon', 'daemon')`).run(iso(22 * H));
10476
+ db.query(`INSERT OR IGNORE INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, detail, started_at, updated_at, ended_at, actor_kind, actor_id)
10477
+ VALUES ('p_demo1', 'checkout', 'ship', 2, 'gate:review', '["implement","gate:tests","gate:review","pr"]', 'running', NULL, ?, ?, NULL, 'human', 'demo')`).run(iso(2 * H), iso(10 * 60000));
8481
10478
  }
8482
10479
 
8483
10480
  // packages/daemon/src/bin.ts
8484
10481
  var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
8485
- var { app, store, runner } = createApp();
10482
+ var appHooks = {};
10483
+ var { app, store, runner, team: team2 } = createApp(new Store, appHooks);
10484
+ if (process.env.SWARM_DEMO === "1" && isEmpty(store))
10485
+ seedDemo(store);
8486
10486
  function serve() {
8487
10487
  const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
8488
10488
  try {
@@ -8494,21 +10494,54 @@ function serve() {
8494
10494
  return bind(0);
8495
10495
  }
8496
10496
  }
8497
- var server = serve();
10497
+ var server;
10498
+ var restart = () => {
10499
+ console.error("swarmd: restarting into the version on disk\u2026");
10500
+ try {
10501
+ clearInterval(tailer);
10502
+ clearInterval(wtRefresh);
10503
+ clearInterval(pruner);
10504
+ } catch {}
10505
+ try {
10506
+ server.stop(true);
10507
+ } catch {}
10508
+ clearDaemonInfo();
10509
+ const [cmd, ...args] = daemonCommand();
10510
+ if (cmd)
10511
+ Bun.spawn([cmd, ...args], {
10512
+ stdin: "ignore",
10513
+ stdout: "ignore",
10514
+ stderr: "ignore",
10515
+ env: { ...process.env }
10516
+ }).unref();
10517
+ setTimeout(() => process.exit(0), 100);
10518
+ };
10519
+ appHooks.restart = restart;
10520
+ server = serve();
8498
10521
  var port = server.port ?? DEFAULT_PORT2;
8499
10522
  ensureToken();
8500
10523
  writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
8501
10524
  var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
8502
10525
  var backfillMs = backfillDays * 24 * 60 * 60000;
8503
- store.tailCodex(backfillMs);
8504
- store.tailGrok(backfillMs);
10526
+ var DEMO = process.env.SWARM_DEMO === "1";
10527
+ if (!DEMO) {
10528
+ store.tailCodex(backfillMs);
10529
+ store.tailGrok(backfillMs);
10530
+ store.tailGemini(backfillMs);
10531
+ store.tailAider(backfillMs);
10532
+ store.tailOpencode(backfillMs);
10533
+ }
8505
10534
  var tick = 0;
8506
10535
  var tailer = setInterval(() => {
8507
10536
  tick++;
8508
- store.tailActive();
8509
- if (tick % 3 === 0 || store.hasActiveSessions()) {
10537
+ if (!DEMO)
10538
+ store.tailActive();
10539
+ if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
8510
10540
  store.tailCodex();
8511
10541
  store.tailGrok();
10542
+ store.tailGemini();
10543
+ store.tailAider();
10544
+ store.tailOpencode();
8512
10545
  }
8513
10546
  store.reapResources();
8514
10547
  store.reapProcesses();
@@ -8516,6 +10549,11 @@ var tailer = setInterval(() => {
8516
10549
  store.sweepOrphans();
8517
10550
  if (tick % 6 === 0)
8518
10551
  store.checkBudgets();
10552
+ if (tick % 12 === 0)
10553
+ store.checkModels();
10554
+ if (tick % 2 === 0)
10555
+ store.checkStalls();
10556
+ team2.tick();
8519
10557
  }, 5000);
8520
10558
  store.refreshAllWorktrees();
8521
10559
  var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);