@ra3orblade/swarm 0.8.0 → 0.9.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"]);
@@ -218,6 +245,79 @@ function parseCodexRollout(chunk) {
218
245
  return out;
219
246
  }
220
247
 
248
+ // packages/core/src/adapters/gemini/chats.ts
249
+ function partText(content) {
250
+ if (typeof content === "string")
251
+ return content.slice(0, 400);
252
+ const parts = Array.isArray(content) ? content : [content];
253
+ let out = "";
254
+ for (const p of parts) {
255
+ if (typeof p === "string")
256
+ out += p;
257
+ else if (p && typeof p === "object" && typeof p.text === "string")
258
+ out += p.text;
259
+ if (out.length >= 400)
260
+ break;
261
+ }
262
+ return out.slice(0, 400);
263
+ }
264
+ function parseGeminiChat(chunk) {
265
+ const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
266
+ let subagent = false;
267
+ for (const raw of chunk.split(`
268
+ `)) {
269
+ if (!raw.trim())
270
+ continue;
271
+ let d = null;
272
+ try {
273
+ d = JSON.parse(raw);
274
+ } catch {
275
+ continue;
276
+ }
277
+ if (d.$set) {
278
+ const set = d.$set;
279
+ if (typeof set.summary === "string")
280
+ out.title = set.summary;
281
+ continue;
282
+ }
283
+ if (d.sessionId && d.projectHash !== undefined) {
284
+ out.sessionId = d.sessionId;
285
+ if (Array.isArray(d.directories) && typeof d.directories[0] === "string")
286
+ out.cwd = d.directories[0];
287
+ if (typeof d.summary === "string")
288
+ out.title = d.summary;
289
+ if (d.kind === "subagent")
290
+ subagent = true;
291
+ continue;
292
+ }
293
+ if (d.type !== "gemini" || !d.id)
294
+ continue;
295
+ const t = d.tokens ?? {};
296
+ const cacheRead = t.cached ?? 0;
297
+ const turn = {
298
+ id: `${out.sessionId ?? "gemini"}-${d.id}`,
299
+ ts: d.timestamp ?? new Date(0).toISOString(),
300
+ model: d.model ?? out.model ?? "gemini-2.5-pro",
301
+ usage: {
302
+ input: Math.max(0, (t.input ?? 0) - cacheRead),
303
+ output: (t.output ?? 0) + (t.tool ?? 0),
304
+ cacheWrite: 0,
305
+ cacheWrite1h: 0,
306
+ cacheRead,
307
+ thinking: t.thoughts ?? 0
308
+ },
309
+ text: partText(d.content),
310
+ tools: (d.toolCalls ?? []).map((c) => c.name ?? c.displayName ?? c.tool ?? "").filter(Boolean),
311
+ effort: null,
312
+ sidechain: subagent
313
+ };
314
+ if (d.model)
315
+ out.model = d.model;
316
+ out.turns.push(turn);
317
+ }
318
+ return out;
319
+ }
320
+
221
321
  // packages/core/src/adapters/grok/updates.ts
222
322
  function parseGrokUpdates(chunk) {
223
323
  const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
@@ -421,7 +521,9 @@ var AUDIT_TYPES = new Set([
421
521
  "permission.resolved",
422
522
  "incident.opened",
423
523
  "incident.acked",
424
- "run.result"
524
+ "run.result",
525
+ "workflow.started",
526
+ "workflow.finished"
425
527
  ]);
426
528
  var isAuditType = (t) => AUDIT_TYPES.has(t);
427
529
  var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
@@ -594,16 +696,82 @@ function runProfile(name) {
594
696
  return RUN_PROFILES[name] ?? null;
595
697
  }
596
698
  // packages/core/src/config.ts
597
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
699
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
598
700
  import { join as join2 } from "path";
701
+
702
+ // packages/core/src/workflows.ts
703
+ var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
704
+ function isRecord(v) {
705
+ return typeof v === "object" && v !== null && !Array.isArray(v);
706
+ }
707
+ function parseWorkflows(raw) {
708
+ const out = {};
709
+ if (!Array.isArray(raw))
710
+ return out;
711
+ for (const w of raw) {
712
+ if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
713
+ continue;
714
+ if (!Array.isArray(w.steps) || !w.steps.length)
715
+ continue;
716
+ const prompts = isRecord(w.prompts) ? w.prompts : {};
717
+ const steps = [];
718
+ for (const s of w.steps) {
719
+ if (typeof s !== "string" || !s.trim()) {
720
+ steps.length = 0;
721
+ break;
722
+ }
723
+ const t = s.trim();
724
+ if (t === "pr")
725
+ steps.push({ kind: "pr" });
726
+ else if (t.startsWith("gate:")) {
727
+ const gate = t.slice(5);
728
+ if (!NAME_RE.test(gate)) {
729
+ steps.length = 0;
730
+ break;
731
+ }
732
+ steps.push({ kind: "gate", gate });
733
+ } else if (NAME_RE.test(t)) {
734
+ const p = prompts[t];
735
+ steps.push({
736
+ kind: "run",
737
+ name: t,
738
+ prompt: typeof p === "string" && p.trim() ? p.trim() : null
739
+ });
740
+ } else {
741
+ steps.length = 0;
742
+ break;
743
+ }
744
+ }
745
+ if (steps.length)
746
+ out[w.name] = { name: w.name, steps };
747
+ }
748
+ return out;
749
+ }
750
+ function workflowStepPrompt(step, task, ctx) {
751
+ if (step.prompt)
752
+ return step.prompt.replaceAll("{task}", task.id).replaceAll("{title}", task.title ?? "");
753
+ return [
754
+ `Task ${task.id}: ${task.title}`,
755
+ "",
756
+ `You are the "${step.name}" step of the "${ctx.workflow}" workflow. Work only inside this worktree; commit and push as you go.`,
757
+ ctx.remaining.length ? `After you finish, the workflow itself runs: ${ctx.remaining.join(" \u2192 ")}. Do not do those yourself.` : "You are the last step.",
758
+ "When done, call swarm_handoff with what was done and what remains."
759
+ ].join(`
760
+ `);
761
+ }
762
+ function stepLabel(s) {
763
+ return s.kind === "run" ? s.name : s.kind === "gate" ? `gate:${s.gate}` : "pr";
764
+ }
765
+
766
+ // packages/core/src/config.ts
599
767
  var DEFAULT_GATE_TIMEOUT_S = 900;
600
768
  var AUTO_MODES = ["session-end", "stop", "off"];
601
769
  function parseGateDefs(gates) {
602
770
  const out = {};
603
- if (!isRecord(gates))
771
+ if (!isRecord2(gates))
604
772
  return out;
605
773
  for (const [name, v] of Object.entries(gates)) {
606
- if (!isRecord(v))
774
+ if (!isRecord2(v))
607
775
  continue;
608
776
  const builtin = v.builtin === "review" ? "review" : null;
609
777
  const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
@@ -626,6 +794,7 @@ var DEFAULT_CONFIG = {
626
794
  daemon: { port: 7777, auth: "loopback-optional" },
627
795
  tasks: { source: null, labels: [], team: null },
628
796
  gates: { required: [], auto: "session-end", defs: {} },
797
+ workflows: {},
629
798
  budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
630
799
  events: { retain_days: 30 },
631
800
  audit: { retain_days: 0 },
@@ -650,11 +819,11 @@ var DEFAULT_CONFIG = {
650
819
  }
651
820
  };
652
821
  var MODES = ["ask", "deny", "off"];
653
- function isRecord(v) {
822
+ function isRecord2(v) {
654
823
  return typeof v === "object" && v !== null && !Array.isArray(v);
655
824
  }
656
825
  function merge(a, b) {
657
- if (!isRecord(a) || !isRecord(b))
826
+ if (!isRecord2(a) || !isRecord2(b))
658
827
  return b === undefined ? a : b;
659
828
  const out = { ...a };
660
829
  for (const [k, v] of Object.entries(b))
@@ -721,6 +890,7 @@ function validate(c) {
721
890
  warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
722
891
  on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
723
892
  },
893
+ workflows: parseWorkflows(c.workflows),
724
894
  events: {
725
895
  retain_days: days(c.events?.retain_days, 30)
726
896
  },
@@ -760,7 +930,7 @@ function validate(c) {
760
930
  };
761
931
  }
762
932
  function leafPaths(v, prefix = "") {
763
- if (!isRecord(v))
933
+ if (!isRecord2(v))
764
934
  return prefix ? [prefix] : [];
765
935
  const keys = Object.keys(v);
766
936
  if (keys.length === 0)
@@ -770,7 +940,7 @@ function leafPaths(v, prefix = "") {
770
940
  function getPath(v, path) {
771
941
  let cur = v;
772
942
  for (const seg of path.split(".")) {
773
- if (!isRecord(cur))
943
+ if (!isRecord2(cur))
774
944
  return;
775
945
  cur = cur[seg];
776
946
  }
@@ -780,7 +950,7 @@ function setPath(obj, path, value) {
780
950
  const segs = path.split(".");
781
951
  let cur = obj;
782
952
  for (const seg of segs.slice(0, -1)) {
783
- if (!isRecord(cur[seg]))
953
+ if (!isRecord2(cur[seg]))
784
954
  cur[seg] = {};
785
955
  cur = cur[seg];
786
956
  }
@@ -788,7 +958,7 @@ function setPath(obj, path, value) {
788
958
  }
789
959
  var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
790
960
  function readLayer(path) {
791
- return existsSync2(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
961
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
792
962
  }
793
963
  function loadConfigDetailed(opts = {}) {
794
964
  const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
@@ -1274,11 +1444,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
1274
1444
  `) };
1275
1445
  }
1276
1446
  // packages/core/src/gates.ts
1277
- var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
1447
+ var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
1278
1448
  function validateGateRun(input) {
1279
1449
  if (!input.task?.trim())
1280
1450
  return { ok: false, reason: "task is required" };
1281
- if (!NAME_RE.test(input.gate ?? ""))
1451
+ if (!NAME_RE2.test(input.gate ?? ""))
1282
1452
  return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
1283
1453
  if (input.verdict !== "pass" && input.verdict !== "fail")
1284
1454
  return { ok: false, reason: 'verdict must be "pass" or "fail"' };
@@ -1639,6 +1809,35 @@ function parseMemoryQuery(q) {
1639
1809
  }
1640
1810
  return { match: terms.join(" "), kind, task };
1641
1811
  }
1812
+ // packages/core/src/messages.ts
1813
+ var MESSAGE_MAX = 4000;
1814
+ function validateMessage(text) {
1815
+ if (typeof text !== "string" || !text.trim())
1816
+ return { ok: false, reason: "message text is required" };
1817
+ const t = text.trim();
1818
+ if (t.length > MESSAGE_MAX)
1819
+ return { ok: false, reason: `message is over ${MESSAGE_MAX} chars` };
1820
+ return { ok: true, text: t };
1821
+ }
1822
+ function parseTo(to) {
1823
+ if (typeof to !== "string" || !to.trim())
1824
+ return null;
1825
+ const t = to.trim();
1826
+ if (t === "lead")
1827
+ return { kind: "lead" };
1828
+ if (/^[0-9a-f]{8}(-[0-9a-f-]{4,28})?$/i.test(t))
1829
+ return { kind: "session", id: t };
1830
+ return { kind: "task", task: t };
1831
+ }
1832
+ function formatMessages(ms) {
1833
+ if (!ms.length)
1834
+ return null;
1835
+ const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
1836
+ return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
1837
+ ${lines.join(`
1838
+ `)}
1839
+ Reply with swarm_send if a reply is expected.`;
1840
+ }
1642
1841
  // packages/core/src/policy.ts
1643
1842
  import { createHash } from "crypto";
1644
1843
  var HOOK_MARK = "swarm-hook";
@@ -2212,10 +2411,10 @@ function planGc(worktrees, claims) {
2212
2411
  return out;
2213
2412
  }
2214
2413
  // packages/daemon/src/app.ts
2215
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
2414
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
2216
2415
  import { homedir as homedir4 } from "os";
2217
- import { dirname as dirname3, join as join9 } from "path";
2218
- import { fileURLToPath } from "url";
2416
+ import { dirname as dirname4, join as join9 } from "path";
2417
+ import { fileURLToPath as fileURLToPath2 } from "url";
2219
2418
 
2220
2419
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
2221
2420
  var compose = (middleware, onError, onNotFound) => {
@@ -4088,7 +4287,7 @@ class Dispatcher {
4088
4287
  }
4089
4288
 
4090
4289
  // packages/daemon/src/forge.ts
4091
- import { existsSync as existsSync3 } from "fs";
4290
+ import { existsSync as existsSync4 } from "fs";
4092
4291
  import { homedir as homedir2 } from "os";
4093
4292
  import { join as join4 } from "path";
4094
4293
  var EXTRA_BIN_DIRS = [
@@ -4106,7 +4305,7 @@ function findBin(name) {
4106
4305
  return onPath;
4107
4306
  for (const d of EXTRA_BIN_DIRS) {
4108
4307
  const p = join4(d, name);
4109
- if (existsSync3(p))
4308
+ if (existsSync4(p))
4110
4309
  return p;
4111
4310
  }
4112
4311
  return null;
@@ -4805,7 +5004,7 @@ class Runner {
4805
5004
  import { Database } from "bun:sqlite";
4806
5005
  import {
4807
5006
  closeSync,
4808
- existsSync as existsSync5,
5007
+ existsSync as existsSync6,
4809
5008
  mkdirSync as mkdirSync4,
4810
5009
  openSync as openSync3,
4811
5010
  readdirSync,
@@ -4818,11 +5017,11 @@ import {
4818
5017
  writeFileSync as writeFileSync2
4819
5018
  } from "fs";
4820
5019
  import { homedir as homedir3, tmpdir, userInfo } from "os";
4821
- import { basename, dirname as dirname2, join as join8 } from "path";
5020
+ import { basename, dirname as dirname3, join as join8 } from "path";
4822
5021
 
4823
5022
  // 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";
5023
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
5024
+ import { dirname as dirname2, join as join7 } from "path";
4826
5025
  function runBootstrap(plan, opts) {
4827
5026
  const logDir = join7(opts.home, "logs", opts.projectId);
4828
5027
  mkdirSync3(logDir, { recursive: true });
@@ -4830,12 +5029,12 @@ function runBootstrap(plan, opts) {
4830
5029
  const copied = [];
4831
5030
  const skipped = [];
4832
5031
  for (const c of plan.copies) {
4833
- if (!existsSync4(c.from)) {
5032
+ if (!existsSync5(c.from)) {
4834
5033
  skipped.push(c.rel);
4835
5034
  continue;
4836
5035
  }
4837
5036
  try {
4838
- mkdirSync3(dirname(c.to), { recursive: true });
5037
+ mkdirSync3(dirname2(c.to), { recursive: true });
4839
5038
  cpSync(c.from, c.to, { recursive: true, force: true });
4840
5039
  copied.push(c.rel);
4841
5040
  } catch (e) {
@@ -4997,6 +5196,12 @@ CREATE TABLE IF NOT EXISTS messages (
4997
5196
  answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
4998
5197
  );
4999
5198
  CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
5199
+ CREATE TABLE IF NOT EXISTS workflow_runs (
5200
+ id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
5201
+ step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
5202
+ started_at TEXT, updated_at TEXT, ended_at TEXT, actor_kind TEXT, actor_id TEXT
5203
+ );
5204
+ CREATE INDEX IF NOT EXISTS workflow_runs_proj ON workflow_runs(project_id, id);
5000
5205
  CREATE TABLE IF NOT EXISTS claims (
5001
5206
  project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
5002
5207
  acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
@@ -5027,6 +5232,8 @@ class Store {
5027
5232
  this.ensureColumn("projects", "sort_order", "INTEGER");
5028
5233
  this.ensureColumn("projects", "icon", "TEXT");
5029
5234
  this.ensureColumn("projects", "color", "TEXT");
5235
+ this.ensureColumn("messages", "to_kind", "TEXT");
5236
+ this.ensureColumn("messages", "from_session", "TEXT");
5030
5237
  this.migrate();
5031
5238
  this.migrateProjectsJson(join8(home, "projects.json"));
5032
5239
  this.reconcileMovedProjects();
@@ -5076,9 +5283,9 @@ class Store {
5076
5283
  reconcileMovedProjects() {
5077
5284
  const all = this.projects();
5078
5285
  for (const stale of all) {
5079
- if (existsSync5(stale.root))
5286
+ if (existsSync6(stale.root))
5080
5287
  continue;
5081
- const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync5(p.root));
5288
+ const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
5082
5289
  if (live.length !== 1)
5083
5290
  continue;
5084
5291
  this.mergeProject(stale.id, live[0].id);
@@ -5162,7 +5369,7 @@ class Store {
5162
5369
  return actorFrom(owner, sessionId, { user: osUser(), runId });
5163
5370
  }
5164
5371
  migrateProjectsJson(file) {
5165
- if (!existsSync5(file))
5372
+ if (!existsSync6(file))
5166
5373
  return;
5167
5374
  try {
5168
5375
  const list = JSON.parse(readFileSync3(file, "utf8"));
@@ -5177,7 +5384,7 @@ class Store {
5177
5384
  const hit = this.topCache.get(cwd);
5178
5385
  if (hit && Date.now() - hit.t < 1e4)
5179
5386
  return hit.v;
5180
- const v = cwd && existsSync5(cwd) ? gitToplevel(cwd) : null;
5387
+ const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
5181
5388
  this.topCache.set(cwd, { v, t: Date.now() });
5182
5389
  return v;
5183
5390
  }
@@ -5383,7 +5590,7 @@ class Store {
5383
5590
  }));
5384
5591
  }
5385
5592
  sessionContext(cwd) {
5386
- if (!cwd || !existsSync5(cwd))
5593
+ if (!cwd || !existsSync6(cwd))
5387
5594
  return null;
5388
5595
  const toplevel = this.toplevel(cwd);
5389
5596
  const project = this.resolveProject(cwd);
@@ -5523,7 +5730,160 @@ class Store {
5523
5730
  return qs;
5524
5731
  }
5525
5732
  answerContext(sessionId) {
5526
- return formatAnswers(this.inbox(sessionId));
5733
+ const parts = [
5734
+ formatAnswers(this.inbox(sessionId)),
5735
+ formatMessages(this.messageInbox(sessionId))
5736
+ ];
5737
+ const out = parts.filter(Boolean);
5738
+ return out.length ? out.join(`
5739
+ `) : null;
5740
+ }
5741
+ wfInsert(projectId, task, workflow, steps, actor2) {
5742
+ const now = new Date().toISOString();
5743
+ 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)
5744
+ VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor2.kind, actor2.id);
5745
+ this.touch();
5746
+ return Number(r.lastInsertRowid);
5747
+ }
5748
+ wfUpdate(id, patch) {
5749
+ const sets = ["updated_at = ?"];
5750
+ const args = [new Date().toISOString()];
5751
+ if (patch.step !== undefined) {
5752
+ sets.push("step = ?");
5753
+ args.push(patch.step);
5754
+ }
5755
+ if (patch.stepLabel !== undefined) {
5756
+ sets.push("step_label = ?");
5757
+ args.push(patch.stepLabel);
5758
+ }
5759
+ if (patch.state !== undefined) {
5760
+ sets.push("state = ?");
5761
+ args.push(patch.state);
5762
+ }
5763
+ if (patch.detail !== undefined) {
5764
+ sets.push("detail = ?");
5765
+ args.push(patch.detail);
5766
+ }
5767
+ if (patch.runId !== undefined) {
5768
+ sets.push("run_id = ?");
5769
+ args.push(patch.runId);
5770
+ }
5771
+ if (patch.ended) {
5772
+ sets.push("ended_at = ?");
5773
+ args.push(new Date().toISOString());
5774
+ }
5775
+ this.db.query(`UPDATE workflow_runs SET ${sets.join(", ")} WHERE id = ?`).run(...args, id);
5776
+ this.touch();
5777
+ }
5778
+ wfRuns(projectId, limit = 50) {
5779
+ return this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map(rowToWorkflowRun);
5780
+ }
5781
+ wfActive(projectId, task) {
5782
+ 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);
5783
+ return r ? rowToWorkflowRun(r) : null;
5784
+ }
5785
+ wfSweepOrphans() {
5786
+ 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());
5787
+ }
5788
+ send(projectId, input) {
5789
+ if (!this.project(projectId))
5790
+ return { ok: false, error: "unknown project" };
5791
+ const v = validateMessage(input.text);
5792
+ if (!v.ok)
5793
+ return { ok: false, error: v.reason };
5794
+ const to = parseTo(input.to);
5795
+ if (!to)
5796
+ return { ok: false, error: 'to must be a session id, a task, or "lead"' };
5797
+ let sessionId = null;
5798
+ let task = null;
5799
+ if (to.kind === "session") {
5800
+ sessionId = this.knownSession(to.id) ?? this.sessionByPrefix(to.id);
5801
+ if (!sessionId)
5802
+ return { ok: false, error: `unknown session ${to.id}` };
5803
+ } else if (to.kind === "task") {
5804
+ task = to.task;
5805
+ sessionId = this.sessionForTask(projectId, to.task);
5806
+ } else {
5807
+ sessionId = this.leadSession(projectId);
5808
+ }
5809
+ const createdAt = new Date().toISOString();
5810
+ const from = input.from ?? (input.fromSession ? `agent ${input.fromSession.slice(0, 8)}` : null);
5811
+ const r = this.db.query(`INSERT INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
5812
+ VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?)`).run(projectId, sessionId, task, v.text, from, createdAt, to.kind, input.fromSession ?? null);
5813
+ const message = this.message(Number(r.lastInsertRowid));
5814
+ this.append({
5815
+ ts: createdAt,
5816
+ type: "message.sent",
5817
+ projectId,
5818
+ sessionId: input.fromSession ?? null,
5819
+ actor: this.actorFor(input.from ?? null, input.fromSession ?? null),
5820
+ payload: {
5821
+ id: message.id,
5822
+ to: input.to,
5823
+ task,
5824
+ recipient: sessionId,
5825
+ text: v.text.slice(0, 400),
5826
+ summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
5827
+ }
5828
+ });
5829
+ return { ok: true, message };
5830
+ }
5831
+ message(id) {
5832
+ const r = this.db.query("SELECT * FROM messages WHERE id = ? AND kind = 'message'").get(id);
5833
+ return r ? rowToMessage(r) : null;
5834
+ }
5835
+ messages(opts = {}) {
5836
+ const where = ["kind = 'message'"];
5837
+ const args = [];
5838
+ if (opts.projectId) {
5839
+ where.push("project_id = ?");
5840
+ args.push(opts.projectId);
5841
+ }
5842
+ if (opts.sessionId) {
5843
+ where.push("(session_id = ? OR from_session = ?)");
5844
+ args.push(opts.sessionId, opts.sessionId);
5845
+ }
5846
+ if (opts.task) {
5847
+ where.push("task = ?");
5848
+ args.push(opts.task);
5849
+ }
5850
+ args.push(opts.limit ?? 100);
5851
+ return this.db.query(`SELECT * FROM messages WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`).all(...args).map(rowToMessage);
5852
+ }
5853
+ messageInbox(sessionId, opts = {}) {
5854
+ if (!sessionId)
5855
+ return [];
5856
+ const s = this.db.query("SELECT project_id, kind, cwd FROM sessions WHERE id = ?").get(sessionId);
5857
+ if (!s)
5858
+ return [];
5859
+ const task = this.heldClaimsWithWorktree().find((c) => isInside(s.cwd, c.worktree))?.task ?? null;
5860
+ const rows = this.db.query(`SELECT * FROM messages WHERE kind = 'message' AND delivered_at IS NULL AND from_session IS NOT ?
5861
+ AND (session_id = ?
5862
+ OR (to_kind = 'task' AND project_id = ? AND task IS ?)
5863
+ OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
5864
+ ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
5865
+ const ms = rows.map(rowToMessage);
5866
+ if (ms.length && !opts.peek)
5867
+ 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));
5868
+ return ms;
5869
+ }
5870
+ markMessageDelivered(id, sessionId) {
5871
+ 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);
5872
+ }
5873
+ leadSession(projectId) {
5874
+ 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);
5875
+ return r?.id ?? null;
5876
+ }
5877
+ sessionByPrefix(prefix) {
5878
+ const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
5879
+ return rows.length === 1 ? rows[0]?.id ?? null : null;
5880
+ }
5881
+ sessionForTask(projectId, task) {
5882
+ const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
5883
+ if (!claim?.worktree)
5884
+ return null;
5885
+ const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
5886
+ return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
5527
5887
  }
5528
5888
  questionContext(task, projectId) {
5529
5889
  if (!task)
@@ -5600,13 +5960,13 @@ class Store {
5600
5960
  };
5601
5961
  const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
5602
5962
  const worktree2 = claim?.worktree;
5603
- if (!worktree2 || !existsSync5(worktree2))
5963
+ if (!worktree2 || !existsSync6(worktree2))
5604
5964
  return {
5605
5965
  ok: false,
5606
5966
  reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
5607
5967
  };
5608
5968
  const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
5609
- if (!existsSync5(cwd))
5969
+ if (!existsSync6(cwd))
5610
5970
  return { ok: false, reason: `gate cwd ${cwd} does not exist` };
5611
5971
  const key = `${projectId}:${task}:${gate}`;
5612
5972
  if (this.gateJobs.has(key))
@@ -5947,7 +6307,7 @@ ${err}
5947
6307
  error = e.error;
5948
6308
  } else {
5949
6309
  const path = join8(p.root, source);
5950
- if (!existsSync5(path))
6310
+ if (!existsSync6(path))
5951
6311
  return { source, required: this.requiredGates(projectId), tasks: [] };
5952
6312
  const mtime = statSync(path).mtimeMs;
5953
6313
  let md = this.taskCache.get(projectId);
@@ -5996,7 +6356,7 @@ ${err}
5996
6356
  const file = join8(this.home, POLICY_CACHE_FILE);
5997
6357
  try {
5998
6358
  if (!hasLockedRules(loaded)) {
5999
- if (existsSync5(file))
6359
+ if (existsSync6(file))
6000
6360
  unlinkSync(file);
6001
6361
  return;
6002
6362
  }
@@ -6012,13 +6372,13 @@ ${err}
6012
6372
  claudeSettings() {
6013
6373
  const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
6014
6374
  try {
6015
- return existsSync5(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6375
+ return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6016
6376
  } catch {
6017
6377
  return null;
6018
6378
  }
6019
6379
  }
6020
6380
  checkPolicy(cwd, sessionId) {
6021
- const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
6381
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
6022
6382
  const repoRoot = project?.root ?? null;
6023
6383
  const loaded = this.policyFor(repoRoot);
6024
6384
  const settings = this.claudeSettings();
@@ -6043,7 +6403,7 @@ ${err}
6043
6403
  return findings;
6044
6404
  }
6045
6405
  evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
6046
- if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync5(cwd)) {
6406
+ if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
6047
6407
  const project = this.resolveProject(cwd);
6048
6408
  const b = this.budgetFor(project.id);
6049
6409
  if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
@@ -6134,7 +6494,7 @@ ${err}
6134
6494
  return this.openIncident(d, cwd, id, cmd);
6135
6495
  }
6136
6496
  openIncident(d, cwd, sessionId, command) {
6137
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
6497
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
6138
6498
  this.append({
6139
6499
  ts: new Date().toISOString(),
6140
6500
  type: "incident.opened",
@@ -6185,7 +6545,7 @@ ${err}
6185
6545
  }
6186
6546
  }
6187
6547
  const report = dryRunRules(calls, modes, {
6188
- toplevel: (cwd) => cwd && existsSync5(cwd) ? this.toplevel(cwd) : null,
6548
+ toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
6189
6549
  claims: this.heldWorktrees()
6190
6550
  });
6191
6551
  return { ...report, modes };
@@ -6201,7 +6561,7 @@ ${err}
6201
6561
  this.prices = { ...PRICES };
6202
6562
  for (const f of ["pricing.litellm.json", "pricing.json"]) {
6203
6563
  const p = join8(this.home, f);
6204
- if (!existsSync5(p))
6564
+ if (!existsSync6(p))
6205
6565
  continue;
6206
6566
  try {
6207
6567
  const j = JSON.parse(readFileSync3(p, "utf8"));
@@ -6426,10 +6786,10 @@ ${err}
6426
6786
  if (typeof raw2.cwd === "string")
6427
6787
  this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
6428
6788
  const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
6429
- const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
6789
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
6430
6790
  const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
6431
6791
  if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
6432
- if (existsSync5(cwd)) {
6792
+ if (existsSync6(cwd)) {
6433
6793
  this.autoHandoff(e.sessionId, cwd);
6434
6794
  this.autoGate(event, e.sessionId, cwd);
6435
6795
  }
@@ -6475,7 +6835,7 @@ ${err}
6475
6835
  return;
6476
6836
  const p = e.payload;
6477
6837
  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;
6838
+ const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
6479
6839
  if (!row) {
6480
6840
  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
6841
  }
@@ -6547,10 +6907,10 @@ ${err}
6547
6907
  }
6548
6908
  tailSession(sessionId) {
6549
6909
  const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
6550
- if (!s?.transcript_path || !existsSync5(s.transcript_path))
6910
+ if (!s?.transcript_path || !existsSync6(s.transcript_path))
6551
6911
  return 0;
6552
6912
  let n = this.tailFile(s.transcript_path, sessionId, null);
6553
- const subDir = join8(dirname2(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
6913
+ const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
6554
6914
  for (const f of this.subagentFiles(subDir)) {
6555
6915
  n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
6556
6916
  }
@@ -6618,7 +6978,7 @@ ${err}
6618
6978
  return out;
6619
6979
  }
6620
6980
  tailCodex(windowMs = 3 * 24 * 60 * 60000) {
6621
- if (!existsSync5(this.codexRoot()))
6981
+ if (!existsSync6(this.codexRoot()))
6622
6982
  return 0;
6623
6983
  let n = 0;
6624
6984
  for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
@@ -6630,9 +6990,54 @@ ${err}
6630
6990
  return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
6631
6991
  }
6632
6992
  grokSummary = new Map;
6993
+ tailGemini(windowMs = 3 * 24 * 60 * 60000) {
6994
+ const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
6995
+ if (!existsSync6(root))
6996
+ return 0;
6997
+ const since = Date.now() - windowMs;
6998
+ const ls = (p) => {
6999
+ try {
7000
+ return readdirSync(p);
7001
+ } catch {
7002
+ return [];
7003
+ }
7004
+ };
7005
+ let n = 0;
7006
+ const ingestDir = (dir) => {
7007
+ for (const f of ls(dir)) {
7008
+ const path = join8(dir, f);
7009
+ if (!f.endsWith(".jsonl")) {
7010
+ try {
7011
+ if (statSync(path).isDirectory()) {
7012
+ for (const g of ls(path))
7013
+ if (g.endsWith(".jsonl"))
7014
+ ingestFile(join8(path, g));
7015
+ }
7016
+ } catch {}
7017
+ continue;
7018
+ }
7019
+ ingestFile(path);
7020
+ }
7021
+ };
7022
+ const ingestFile = (path) => {
7023
+ try {
7024
+ if (statSync(path).mtimeMs < since)
7025
+ return;
7026
+ } catch {
7027
+ return;
7028
+ }
7029
+ n += this.ingestLog(path, "gemini", parseGeminiChat);
7030
+ };
7031
+ for (const hash of ls(root)) {
7032
+ const chats = join8(root, hash, "chats");
7033
+ if (existsSync6(chats))
7034
+ ingestDir(chats);
7035
+ }
7036
+ return n;
7037
+ }
6633
7038
  tailGrok(windowMs = 3 * 24 * 60 * 60000) {
6634
7039
  const root = this.grokRoot();
6635
- if (!existsSync5(root))
7040
+ if (!existsSync6(root))
6636
7041
  return 0;
6637
7042
  const since = Date.now() - windowMs;
6638
7043
  const ls = (p) => {
@@ -6655,7 +7060,7 @@ ${err}
6655
7060
  const cwdDir = join8(root, enc);
6656
7061
  for (const sid of ls(cwdDir)) {
6657
7062
  const path = join8(cwdDir, sid, "updates.jsonl");
6658
- if (!existsSync5(path))
7063
+ if (!existsSync6(path))
6659
7064
  continue;
6660
7065
  try {
6661
7066
  if (statSync(path).mtimeMs < since)
@@ -6714,9 +7119,9 @@ ${err}
6714
7119
  ensureAgentSession(sid, agent, cwd, mtime) {
6715
7120
  if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
6716
7121
  return;
6717
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
7122
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
6718
7123
  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);
7124
+ 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
7125
  }
6721
7126
  claimRows(projectId) {
6722
7127
  return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
@@ -6762,9 +7167,9 @@ ${err}
6762
7167
  return { ok: false, error: claimRefusalMessage(decision, task) };
6763
7168
  const branch = `task/${task}`;
6764
7169
  const worktree2 = this.worktreePath(projectId, task);
6765
- if (existsSync5(worktree2))
7170
+ if (existsSync6(worktree2))
6766
7171
  return { ok: false, error: `${worktree2} already exists; release ${task} first` };
6767
- mkdirSync4(dirname2(worktree2), { recursive: true });
7172
+ mkdirSync4(dirname3(worktree2), { recursive: true });
6768
7173
  const created = worktreeAdd(p.root, worktree2, branch, baseRef);
6769
7174
  if (!created)
6770
7175
  return { ok: false, error: `git worktree add failed for ${task}` };
@@ -6874,7 +7279,7 @@ ${err}
6874
7279
  for (const c of this.claimRows(p.id)) {
6875
7280
  if (c.state !== "held" || isActive(c, now))
6876
7281
  continue;
6877
- const exists = c.worktree ? existsSync5(c.worktree) : false;
7282
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6878
7283
  const work = exists ? heldWork(c.worktree) : null;
6879
7284
  if (reapAction(c, now, exists, work) !== "keep-orphaned")
6880
7285
  continue;
@@ -6930,7 +7335,7 @@ ${err}
6930
7335
  if (!row)
6931
7336
  return { ok: false, error: `no claim on ${task}` };
6932
7337
  const worktree2 = row.worktree ?? "";
6933
- if (worktree2 && existsSync5(worktree2)) {
7338
+ if (worktree2 && existsSync6(worktree2)) {
6934
7339
  const work = heldWork(worktree2);
6935
7340
  const can = canRelease(work, force);
6936
7341
  if (!can.ok)
@@ -6965,7 +7370,7 @@ ${err}
6965
7370
  continue;
6966
7371
  if (isActive({ ...c, state: "held" }, now))
6967
7372
  continue;
6968
- const exists = c.worktree ? existsSync5(c.worktree) : false;
7373
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6969
7374
  const work = exists ? heldWork(c.worktree) : null;
6970
7375
  const action = reapAction({ ...c, state: "held" }, now, exists, work);
6971
7376
  if (action === "not-expired")
@@ -7064,9 +7469,9 @@ ${err}
7064
7469
  if (!slug || slug === "." || slug === "..")
7065
7470
  return { ok: false, error: "bad worktree name" };
7066
7471
  const path = this.worktreePath(projectId, slug);
7067
- if (existsSync5(path))
7472
+ if (existsSync6(path))
7068
7473
  return { ok: false, error: `${path} already exists` };
7069
- mkdirSync4(dirname2(path), { recursive: true });
7474
+ mkdirSync4(dirname3(path), { recursive: true });
7070
7475
  const br = branch?.trim() || `wt/${slug}`;
7071
7476
  const created = worktreeAdd(p.root, path, br, baseRef);
7072
7477
  if (!created)
@@ -7751,6 +8156,48 @@ ${err}
7751
8156
  seq() {
7752
8157
  return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
7753
8158
  }
8159
+ timelineDetail(hours, projectId) {
8160
+ const from = new Date(Date.now() - Math.min(Math.max(hours, 1), 168) * 3600000).toISOString();
8161
+ const args = [from];
8162
+ let filter = "";
8163
+ if (projectId) {
8164
+ filter = " AND s.project_id = ?";
8165
+ args.push(projectId);
8166
+ }
8167
+ 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
8168
+ WHERE t.ts >= ? AND t.sidechain = 0${filter} ORDER BY t.ts LIMIT 20000`).all(...args);
8169
+ const turns = {};
8170
+ for (const r of rows) {
8171
+ turns[r.sid] ??= [];
8172
+ turns[r.sid]?.push(new Date(r.ts).getTime());
8173
+ }
8174
+ const claims = this.claims().filter((c) => (!projectId || c.projectId === projectId) && c.state !== "released").map((c) => ({
8175
+ projectId: c.projectId,
8176
+ task: c.task,
8177
+ owner: c.owner,
8178
+ state: c.state,
8179
+ acquiredAt: c.acquiredAt,
8180
+ expiresAt: c.expiresAt
8181
+ }));
8182
+ return { turns, claims };
8183
+ }
8184
+ spendSparks() {
8185
+ const from = localDayIso(-13);
8186
+ const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
8187
+ FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
8188
+ const days2 = [];
8189
+ for (let i = 13;i >= 0; i--)
8190
+ days2.push(localDayIso(-i).slice(0, 10));
8191
+ const out = {};
8192
+ for (const r of rows) {
8193
+ out[r.pid] ??= new Array(14).fill(0);
8194
+ const arr = out[r.pid];
8195
+ const i = days2.indexOf(r.day);
8196
+ if (i >= 0)
8197
+ arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
8198
+ }
8199
+ return out;
8200
+ }
7754
8201
  snapshot() {
7755
8202
  const worktrees = {};
7756
8203
  const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
@@ -7761,6 +8208,7 @@ ${err}
7761
8208
  worktrees,
7762
8209
  sessions: this.memoised("sessions", 2000, () => this.sessions()),
7763
8210
  spend: this.memoised("spend", 30000, () => this.spend()),
8211
+ spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
7764
8212
  claims: this.claims(),
7765
8213
  processes: this.memoised("processes", 5000, () => this.processes()),
7766
8214
  incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
@@ -7858,15 +8306,242 @@ function isScratchRoot(root) {
7858
8306
  const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
7859
8307
  return tmp.some((t) => root === t || root.startsWith(`${t}/`));
7860
8308
  }
8309
+ function rowToMessage(r) {
8310
+ return {
8311
+ id: r.id,
8312
+ projectId: r.project_id,
8313
+ task: r.task ?? null,
8314
+ sessionId: r.session_id ?? null,
8315
+ toKind: r.to_kind ?? "session",
8316
+ from: r.asked_by ?? null,
8317
+ fromSession: r.from_session ?? null,
8318
+ text: r.text,
8319
+ createdAt: r.created_at,
8320
+ deliveredAt: r.delivered_at ?? null
8321
+ };
8322
+ }
8323
+ function rowToWorkflowRun(r) {
8324
+ return {
8325
+ id: r.id,
8326
+ projectId: r.project_id,
8327
+ task: r.task,
8328
+ workflow: r.workflow,
8329
+ step: r.step,
8330
+ stepLabel: r.step_label ?? "",
8331
+ steps: JSON.parse(r.steps ?? "[]"),
8332
+ state: r.state,
8333
+ detail: r.detail ?? null,
8334
+ runId: r.run_id ?? null,
8335
+ startedAt: r.started_at,
8336
+ updatedAt: r.updated_at,
8337
+ endedAt: r.ended_at ?? null
8338
+ };
8339
+ }
8340
+ function localDayIso(offsetDays) {
8341
+ const d = new Date;
8342
+ d.setHours(0, 0, 0, 0);
8343
+ d.setDate(d.getDate() + offsetDays);
8344
+ return d.toISOString();
8345
+ }
8346
+
8347
+ // packages/daemon/src/workflow.ts
8348
+ class WorkflowEngine {
8349
+ store;
8350
+ runner;
8351
+ forge;
8352
+ active = new Map;
8353
+ constructor(store, runner, forge2) {
8354
+ this.store = store;
8355
+ this.runner = runner;
8356
+ this.forge = forge2;
8357
+ store.wfSweepOrphans();
8358
+ runner.onEnd((run2) => void this.onRunEnd(run2));
8359
+ }
8360
+ start(projectId, task, workflow, opts = {}) {
8361
+ const def = this.store.config(projectId).workflows[workflow];
8362
+ if (!def) {
8363
+ const known = Object.keys(this.store.config(projectId).workflows);
8364
+ return {
8365
+ ok: false,
8366
+ error: `unknown workflow ${workflow}${known.length ? ` \u2014 this repo declares: ${known.join(", ")}` : " \u2014 declare [[workflows]] in .swarm.toml"}`
8367
+ };
8368
+ }
8369
+ const key = `${projectId}:${task}`;
8370
+ if (this.active.has(key) || this.store.wfActive(projectId, task))
8371
+ return { ok: false, error: `a workflow is already running on ${task}` };
8372
+ const title = this.store.tasks(projectId)?.tasks.find((t) => t.id === task)?.title ?? task;
8373
+ const owner = opts.owner ?? "workflow";
8374
+ const id = this.store.wfInsert(projectId, task, workflow, def.steps.map(stepLabel), this.store.actorFor(owner, opts.sessionId ?? null));
8375
+ const w = { id, projectId, task, title, def, step: 0, runId: null, owner };
8376
+ this.active.set(key, w);
8377
+ this.store.append({
8378
+ ts: new Date().toISOString(),
8379
+ type: "workflow.started",
8380
+ projectId,
8381
+ sessionId: opts.sessionId ?? null,
8382
+ payload: {
8383
+ id,
8384
+ task,
8385
+ workflow,
8386
+ steps: def.steps.map(stepLabel),
8387
+ summary: `workflow ${workflow} on ${task}: ${def.steps.map(stepLabel).join(" \u2192 ")}`
8388
+ }
8389
+ });
8390
+ this.advance(w);
8391
+ return { ok: true, id };
8392
+ }
8393
+ status(projectId) {
8394
+ return this.store.wfRuns(projectId);
8395
+ }
8396
+ stop(projectId, task) {
8397
+ const key = `${projectId}:${task}`;
8398
+ const w = this.active.get(key);
8399
+ if (!w)
8400
+ return { ok: false, error: `no running workflow on ${task}` };
8401
+ if (w.runId)
8402
+ this.runner.stop(w.runId);
8403
+ this.finish(w, "stopped", `stopped at ${this.label(w)}`);
8404
+ return { ok: true };
8405
+ }
8406
+ label(w) {
8407
+ const s = w.def.steps[w.step];
8408
+ return s ? stepLabel(s) : "done";
8409
+ }
8410
+ async advance(w) {
8411
+ while (w.step < w.def.steps.length) {
8412
+ const s = w.def.steps[w.step];
8413
+ this.store.wfUpdate(w.id, { step: w.step, stepLabel: stepLabel(s), runId: null });
8414
+ this.step(w, `step ${w.step + 1}/${w.def.steps.length}: ${stepLabel(s)}`);
8415
+ if (s.kind === "run") {
8416
+ const cfg = this.store.config(w.projectId).dispatch;
8417
+ const remaining = w.def.steps.slice(w.step + 1).map(stepLabel);
8418
+ const r = await this.runner.start({
8419
+ projectId: w.projectId,
8420
+ task: w.task,
8421
+ prompt: workflowStepPrompt(s, { id: w.task, title: w.title }, { workflow: w.def.name, remaining }),
8422
+ owner: w.owner,
8423
+ permissionMode: cfg.permission_mode ?? "acceptEdits",
8424
+ model: cfg.model ?? undefined,
8425
+ maxTurns: cfg.max_turns ?? undefined,
8426
+ profile: cfg.profile ?? undefined
8427
+ });
8428
+ if (!r.ok)
8429
+ return this.fail(w, `could not start ${stepLabel(s)}: ${r.reason}`);
8430
+ w.runId = r.run.id;
8431
+ this.store.wfUpdate(w.id, { runId: r.run.id });
8432
+ return;
8433
+ }
8434
+ if (s.kind === "gate") {
8435
+ const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
8436
+ const run2 = r.runs.find((x) => x.gate === s.gate);
8437
+ if (!run2)
8438
+ return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
8439
+ if (run2.verdict !== "pass")
8440
+ return this.fail(w, `gate ${s.gate} failed \u2014 ${run2.rubric}`);
8441
+ w.step++;
8442
+ continue;
8443
+ }
8444
+ const d = await this.store.prDraftFor(w.projectId, w.task);
8445
+ if (!d.ok)
8446
+ return this.fail(w, `pr: ${d.error}`);
8447
+ const pr = await this.forge.openPR(w.projectId, d.worktree, {
8448
+ title: d.title,
8449
+ body: d.body,
8450
+ isDraft: false
8451
+ });
8452
+ if (!pr.ok)
8453
+ return this.fail(w, `pr: ${pr.error}`);
8454
+ this.store.recordPrOpened(w.projectId, d.task, d.worktree.path, pr.url);
8455
+ this.store.wfUpdate(w.id, { detail: `PR ${pr.url}` });
8456
+ w.step++;
8457
+ }
8458
+ this.finish(w, "done", null);
8459
+ }
8460
+ async onRunEnd(run2) {
8461
+ const w = this.active.get(`${run2.projectId}:${run2.task}`);
8462
+ if (!w || w.runId !== run2.id)
8463
+ return;
8464
+ w.runId = null;
8465
+ if (run2.stopped)
8466
+ return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
8467
+ if (run2.exitCode !== 0 || run2.result?.isError)
8468
+ return this.fail(w, `${this.label(w)} exited ${run2.exitCode}${run2.result?.isError ? " (error)" : ""} \u2014 log: ${run2.log}`);
8469
+ w.step++;
8470
+ this.advance(w);
8471
+ }
8472
+ step(w, summary) {
8473
+ this.store.append({
8474
+ ts: new Date().toISOString(),
8475
+ type: "workflow.step",
8476
+ projectId: w.projectId,
8477
+ sessionId: null,
8478
+ payload: {
8479
+ id: w.id,
8480
+ task: w.task,
8481
+ workflow: w.def.name,
8482
+ step: w.step,
8483
+ label: this.label(w),
8484
+ summary: `workflow ${w.def.name} on ${w.task} \u2014 ${summary}`
8485
+ }
8486
+ });
8487
+ }
8488
+ fail(w, detail) {
8489
+ this.store.wfUpdate(w.id, { state: "failed", detail, ended: true });
8490
+ this.active.delete(`${w.projectId}:${w.task}`);
8491
+ this.store.append({
8492
+ ts: new Date().toISOString(),
8493
+ type: "workflow.finished",
8494
+ projectId: w.projectId,
8495
+ sessionId: null,
8496
+ payload: {
8497
+ id: w.id,
8498
+ task: w.task,
8499
+ workflow: w.def.name,
8500
+ outcome: "failed",
8501
+ detail,
8502
+ summary: `workflow ${w.def.name} on ${w.task} failed at ${this.label(w)}: ${detail.slice(0, 160)}`
8503
+ }
8504
+ });
8505
+ this.store.append({
8506
+ ts: new Date().toISOString(),
8507
+ type: "incident.opened",
8508
+ projectId: w.projectId,
8509
+ sessionId: null,
8510
+ payload: {
8511
+ rule: "workflow_failed",
8512
+ action: "failed",
8513
+ command: `${w.task} \xB7 ${w.def.name} \xB7 ${this.label(w)}`,
8514
+ reason: detail.slice(0, 400)
8515
+ }
8516
+ });
8517
+ }
8518
+ finish(w, state, detail) {
8519
+ this.store.wfUpdate(w.id, { state, ...detail !== null ? { detail } : {}, ended: true });
8520
+ this.active.delete(`${w.projectId}:${w.task}`);
8521
+ this.store.append({
8522
+ ts: new Date().toISOString(),
8523
+ type: "workflow.finished",
8524
+ projectId: w.projectId,
8525
+ sessionId: null,
8526
+ payload: {
8527
+ id: w.id,
8528
+ task: w.task,
8529
+ workflow: w.def.name,
8530
+ outcome: state,
8531
+ summary: `workflow ${w.def.name} on ${w.task}: ${state}${detail ? ` \u2014 ${detail}` : ""}`
8532
+ }
8533
+ });
8534
+ }
8535
+ }
7861
8536
 
7862
8537
  // packages/daemon/src/app.ts
7863
- var VERSION = "0.8.0";
8538
+ var VERSION = "0.9.0";
7864
8539
  var WEB_DIR = (() => {
7865
8540
  if (process.env.SWARM_WEB_DIR)
7866
8541
  return process.env.SWARM_WEB_DIR;
7867
- const here = dirname3(fileURLToPath(import.meta.url));
8542
+ const here = dirname4(fileURLToPath2(import.meta.url));
7868
8543
  const dev = join9(here, "../../web/public");
7869
- return existsSync6(join9(dev, "index.html")) ? dev : join9(here, "../web");
8544
+ return existsSync7(join9(dev, "index.html")) ? dev : join9(here, "../web");
7870
8545
  })();
7871
8546
  var REPLAY_TAIL = 200;
7872
8547
  var wireCache = new WeakMap;
@@ -7880,13 +8555,39 @@ function wireJson(e) {
7880
8555
  }
7881
8556
  function hookRepoRoot(store, raw2) {
7882
8557
  const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
7883
- return cwd && existsSync6(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
8558
+ return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
7884
8559
  }
7885
- function createApp(store = new Store) {
8560
+ function claudeSettings() {
8561
+ try {
8562
+ const p = process.env.CLAUDE_SETTINGS ?? join9(homedir4(), ".claude", "settings.json");
8563
+ return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
8564
+ } catch {
8565
+ return null;
8566
+ }
8567
+ }
8568
+ function diskVersion() {
8569
+ try {
8570
+ const entry = daemonCommand().at(-1);
8571
+ if (!entry || !existsSync7(entry))
8572
+ return null;
8573
+ for (const f of [entry, join9(dirname4(entry), "app.ts")]) {
8574
+ if (!existsSync7(f))
8575
+ continue;
8576
+ const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
8577
+ if (m?.[1])
8578
+ return m[1];
8579
+ }
8580
+ return null;
8581
+ } catch {
8582
+ return null;
8583
+ }
8584
+ }
8585
+ function createApp(store = new Store, hooks2 = {}) {
7886
8586
  const app = new Hono2;
7887
8587
  const forge2 = new ForgeService(store);
7888
8588
  const runner = new Runner(store, store.home);
7889
8589
  const dispatcher = new Dispatcher(store, runner, forge2);
8590
+ const workflows2 = new WorkflowEngine(store, runner, forge2);
7890
8591
  store.onBudgetStop((projectId) => {
7891
8592
  dispatcher.clear(projectId);
7892
8593
  for (const run2 of runner.list(projectId))
@@ -7908,6 +8609,8 @@ function createApp(store = new Store) {
7908
8609
  return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
7909
8610
  });
7910
8611
  app.get("/v1/health", (c) => c.json({
8612
+ disk: diskVersion(),
8613
+ hooksInstalled: hookCoverage(claudeSettings()).complete,
7911
8614
  ok: true,
7912
8615
  version: VERSION,
7913
8616
  schema: store.schemaVersion(),
@@ -7942,13 +8645,13 @@ function createApp(store = new Store) {
7942
8645
  const q = c.req.query("path");
7943
8646
  let dir;
7944
8647
  try {
7945
- dir = realpathSync3(q && existsSync6(q) ? q : homedir4());
8648
+ dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
7946
8649
  } catch {
7947
8650
  dir = homedir4();
7948
8651
  }
7949
8652
  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);
8653
+ const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join9(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
8654
+ const parent = dirname4(dir);
7952
8655
  return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
7953
8656
  } catch (e) {
7954
8657
  return c.json({ error: e.message, path: dir }, 400);
@@ -7998,6 +8701,12 @@ function createApp(store = new Store) {
7998
8701
  const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
7999
8702
  return c.body(formatAudit(rows, format), 200, { "content-type": ct });
8000
8703
  });
8704
+ app.post("/v1/daemon/restart", (c) => {
8705
+ if (!hooks2.restart)
8706
+ return c.json({ error: "not restartable in this environment" }, 501);
8707
+ setTimeout(() => hooks2.restart?.(), 50);
8708
+ return c.json({ ok: true, restarting: true });
8709
+ });
8001
8710
  app.get("/v1/rules/dryrun", (c) => {
8002
8711
  const projectId = c.req.query("project");
8003
8712
  if (!projectId)
@@ -8132,6 +8841,58 @@ function createApp(store = new Store) {
8132
8841
  return c.json(r, r.ok ? 200 : 409);
8133
8842
  });
8134
8843
  app.get("/v1/inbox", (c) => c.json(store.inbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
8844
+ app.get("/v1/workflows", (c) => {
8845
+ const project = c.req.query("project");
8846
+ if (!project)
8847
+ return c.json({ error: "project required" }, 400);
8848
+ return c.json({ defs: store.config(project).workflows, runs: workflows2.status(project) });
8849
+ });
8850
+ app.post("/v1/workflows", async (c) => {
8851
+ const b = await c.req.json().catch(() => ({}));
8852
+ if (!b.projectId || !b.task || !b.workflow)
8853
+ return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
8854
+ const r = workflows2.start(b.projectId, b.task, b.workflow, {
8855
+ ...b.owner ? { owner: b.owner } : {},
8856
+ sessionId: b.sessionId ?? null
8857
+ });
8858
+ return c.json(r, r.ok ? 201 : 409);
8859
+ });
8860
+ app.post("/v1/workflows/stop", async (c) => {
8861
+ const b = await c.req.json().catch(() => ({}));
8862
+ if (!b.projectId || !b.task)
8863
+ return c.json({ ok: false, error: "projectId and task required" }, 400);
8864
+ const r = workflows2.stop(b.projectId, b.task);
8865
+ return c.json(r, r.ok ? 200 : 404);
8866
+ });
8867
+ app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
8868
+ app.get("/v1/messages", (c) => c.json(store.messages({
8869
+ ...c.req.query("project") ? { projectId: c.req.query("project") } : {},
8870
+ ...c.req.query("session") ? { sessionId: c.req.query("session") } : {},
8871
+ ...c.req.query("task") ? { task: c.req.query("task") } : {},
8872
+ limit: Number(c.req.query("limit")) || 100
8873
+ })));
8874
+ app.get("/v1/messages/inbox", (c) => c.json(store.messageInbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
8875
+ app.post("/v1/messages", async (c) => {
8876
+ const b = await c.req.json().catch(() => ({}));
8877
+ if (!b.projectId)
8878
+ return c.json({ ok: false, error: "projectId required" }, 400);
8879
+ const r = store.send(b.projectId, {
8880
+ to: b.to,
8881
+ text: b.text,
8882
+ from: b.from ?? null,
8883
+ fromSession: b.sessionId ?? null
8884
+ });
8885
+ if (!r.ok)
8886
+ return c.json(r, 400);
8887
+ const m = r.message;
8888
+ const run2 = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
8889
+ if (run2 && !run2.endedAt) {
8890
+ const sent = runner.send(run2.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
8891
+ if (sent.ok)
8892
+ store.markMessageDelivered(m.id, run2.sessionId);
8893
+ }
8894
+ return c.json({ ok: true, message: store.message(m.id) }, 201);
8895
+ });
8135
8896
  app.get("/v1/dispatch", (c) => {
8136
8897
  const project = c.req.query("project");
8137
8898
  if (!project)
@@ -8453,7 +9214,7 @@ function createApp(store = new Store) {
8453
9214
  await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
8454
9215
  }
8455
9216
  await stream2.writeSSE({ event: "ping", data: "" });
8456
- await new Promise((resolve) => {
9217
+ await new Promise((resolve2) => {
8457
9218
  const off = store.subscribe((e) => {
8458
9219
  stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
8459
9220
  });
@@ -8461,7 +9222,7 @@ function createApp(store = new Store) {
8461
9222
  stream2.onAbort(() => {
8462
9223
  clearInterval(beat);
8463
9224
  off();
8464
- resolve();
9225
+ resolve2();
8465
9226
  });
8466
9227
  });
8467
9228
  });
@@ -8471,18 +9232,107 @@ function createApp(store = new Store) {
8471
9232
  app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
8472
9233
  const f = c.req.param("file");
8473
9234
  const p = join9(WEB_DIR, f);
8474
- if (!existsSync6(p))
9235
+ if (!existsSync7(p))
8475
9236
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
8476
9237
  return c.body(readFileSync4(p, "utf8"), 200, {
8477
9238
  "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
8478
9239
  });
8479
9240
  });
8480
- return { app, store, forge: forge2, runner, dispatcher };
9241
+ return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
9242
+ }
9243
+
9244
+ // packages/daemon/src/demo.ts
9245
+ var H = 3600000;
9246
+ var iso = (msAgo) => new Date(Date.now() - msAgo).toISOString();
9247
+ function isEmpty(store) {
9248
+ return !store.db.query("SELECT 1 FROM sessions LIMIT 1").get();
9249
+ }
9250
+ function seedDemo(store) {
9251
+ const db = store.db;
9252
+ 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);
9253
+ project("p_demo1", "acme-app", "/work/acme-app", "\uD83D\uDED2", "c3");
9254
+ project("p_demo2", "acme-site", "/work/acme-site", "\uD83C\uDF10", "c5");
9255
+ const session = (id, pid, agent, title, cwd, branch, startedAgo, lastAgo, state, model) => {
9256
+ 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)
9257
+ 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));
9258
+ let t = startedAgo;
9259
+ let i = 0;
9260
+ while (t > lastAgo) {
9261
+ const out = 300 + Math.floor(Math.random() * 4000);
9262
+ const read = 50000 + Math.floor(Math.random() * 900000);
9263
+ 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)
9264
+ 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." : "");
9265
+ t -= (8 + i * 13 % 30) * 60000;
9266
+ i++;
9267
+ }
9268
+ };
9269
+ 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");
9270
+ session("demo-s2", "p_demo1", "codex", "Fix flaky cart tests", "/work/acme-app", "main", 7 * H, 3 * H, "ended", "gpt-5.2-codex");
9271
+ session("demo-s3", "p_demo1", "gemini", "Payment webhook audit", "/work/acme-app", "main", 26 * H, 22 * H, "ended", "gemini-2.5-pro");
9272
+ session("demo-s4", "p_demo2", "grok", "Landing page rewrite", "/work/acme-site", "task/landing", 30 * H, 25 * H, "ended", "grok-4");
9273
+ session("demo-s5", "p_demo2", "claude-code", "SEO metadata sweep", "/work/acme-site", "main", 50 * H, 47 * H, "ended", "claude-sonnet-5");
9274
+ db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
9275
+ 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));
9276
+ db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
9277
+ VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
9278
+ 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)
9279
+ VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate2, verdict, rubric, sid, iso(ago));
9280
+ gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
9281
+ gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
9282
+ gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
9283
+ gate("webhooks", "tests", "pass", "ran `bun test` \u2014 exit 0 in 22s", 22 * H, "demo-s3");
9284
+ const ev = (type, ago, sid, payload) => store.append({
9285
+ ts: iso(ago),
9286
+ type,
9287
+ projectId: "p_demo1",
9288
+ sessionId: sid,
9289
+ payload
9290
+ });
9291
+ ev("incident.opened", 4 * H, "demo-s1", {
9292
+ rule: "pattern_kill",
9293
+ action: "ask",
9294
+ command: "pkill -f vite",
9295
+ reason: "This kills processes by command pattern \u2014 other agents' dev servers match too."
9296
+ });
9297
+ ev("incident.opened", 26 * H, "demo-s3", {
9298
+ rule: "shared_tree",
9299
+ action: "deny",
9300
+ command: "git reset --hard",
9301
+ reason: "Another session (demo-s2) is active in this same checkout."
9302
+ });
9303
+ ev("claim.acquired", 5 * H, "demo-s1", {
9304
+ task: "checkout",
9305
+ owner: "demo-s1",
9306
+ summary: "claim checkout"
9307
+ });
9308
+ ev("pr.opened", 30 * 60000, "demo-s1", {
9309
+ task: "checkout",
9310
+ url: "https://github.com/acme/app/pull/128",
9311
+ summary: "PR #128 opened for checkout"
9312
+ });
9313
+ ev("question.asked", 20 * 60000, "demo-s1", {
9314
+ id: 1,
9315
+ task: "checkout",
9316
+ text: "Coupon codes: keep the legacy endpoint alive for one release, or cut over now?",
9317
+ options: ["Keep one release", "Cut over"],
9318
+ summary: "question #1"
9319
+ });
9320
+ db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, options, asked_by, created_at)
9321
+ 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));
9322
+ db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
9323
+ 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));
9324
+ db.query(`INSERT OR IGNORE INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
9325
+ 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));
9326
+ 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)
9327
+ 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
9328
  }
8482
9329
 
8483
9330
  // packages/daemon/src/bin.ts
8484
9331
  var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
8485
- var { app, store, runner } = createApp();
9332
+ var appHooks = {};
9333
+ var { app, store, runner } = createApp(new Store, appHooks);
9334
+ if (process.env.SWARM_DEMO === "1" && isEmpty(store))
9335
+ seedDemo(store);
8486
9336
  function serve() {
8487
9337
  const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
8488
9338
  try {
@@ -8494,21 +9344,50 @@ function serve() {
8494
9344
  return bind(0);
8495
9345
  }
8496
9346
  }
8497
- var server = serve();
9347
+ var server;
9348
+ var restart = () => {
9349
+ console.error("swarmd: restarting into the version on disk\u2026");
9350
+ try {
9351
+ clearInterval(tailer);
9352
+ clearInterval(wtRefresh);
9353
+ clearInterval(pruner);
9354
+ } catch {}
9355
+ try {
9356
+ server.stop(true);
9357
+ } catch {}
9358
+ clearDaemonInfo();
9359
+ const [cmd, ...args] = daemonCommand();
9360
+ if (cmd)
9361
+ Bun.spawn([cmd, ...args], {
9362
+ stdin: "ignore",
9363
+ stdout: "ignore",
9364
+ stderr: "ignore",
9365
+ env: { ...process.env }
9366
+ }).unref();
9367
+ setTimeout(() => process.exit(0), 100);
9368
+ };
9369
+ appHooks.restart = restart;
9370
+ server = serve();
8498
9371
  var port = server.port ?? DEFAULT_PORT2;
8499
9372
  ensureToken();
8500
9373
  writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
8501
9374
  var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
8502
9375
  var backfillMs = backfillDays * 24 * 60 * 60000;
8503
- store.tailCodex(backfillMs);
8504
- store.tailGrok(backfillMs);
9376
+ var DEMO = process.env.SWARM_DEMO === "1";
9377
+ if (!DEMO) {
9378
+ store.tailCodex(backfillMs);
9379
+ store.tailGrok(backfillMs);
9380
+ store.tailGemini(backfillMs);
9381
+ }
8505
9382
  var tick = 0;
8506
9383
  var tailer = setInterval(() => {
8507
9384
  tick++;
8508
- store.tailActive();
8509
- if (tick % 3 === 0 || store.hasActiveSessions()) {
9385
+ if (!DEMO)
9386
+ store.tailActive();
9387
+ if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
8510
9388
  store.tailCodex();
8511
9389
  store.tailGrok();
9390
+ store.tailGemini();
8512
9391
  }
8513
9392
  store.reapResources();
8514
9393
  store.reapProcesses();