@ra3orblade/swarm 0.7.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
@@ -2,9 +2,34 @@
2
2
  // @bun
3
3
 
4
4
  // packages/client/src/daemon.ts
5
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
5
+ import { randomBytes } from "crypto";
6
+ import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6
7
  import { homedir } from "os";
7
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
8
33
  function swarmHome() {
9
34
  return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
10
35
  }
@@ -18,9 +43,68 @@ function writeDaemonInfo(info) {
18
43
  `);
19
44
  return full;
20
45
  }
46
+ var tokenFile = (home = swarmHome()) => join(home, "token");
47
+ function readToken(home = swarmHome()) {
48
+ try {
49
+ const t = readFileSync(tokenFile(home), "utf8").trim();
50
+ return /^[a-f0-9]{64}$/.test(t) ? t : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function ensureToken(home = swarmHome()) {
56
+ const cur = readToken(home);
57
+ if (cur)
58
+ return cur;
59
+ mkdirSync(home, { recursive: true });
60
+ const t = randomBytes(32).toString("hex");
61
+ writeFileSync(tokenFile(home), `${t}
62
+ `, { mode: 384 });
63
+ return t;
64
+ }
21
65
  function clearDaemonInfo() {
22
66
  rmSync(infoFile(), { force: true });
23
67
  }
68
+ function daemonCommand() {
69
+ return resolveBin("swarmd");
70
+ }
71
+ // packages/core/src/actor.ts
72
+ var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
73
+ var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
74
+ function actorFrom(owner, sessionId, opts = {}) {
75
+ const o = (owner ?? "").trim();
76
+ const sid = sessionId?.trim() || undefined;
77
+ if (opts.runId)
78
+ return { kind: "run", id: opts.runId, session: sid };
79
+ if (o.startsWith("auto:"))
80
+ return { kind: "daemon", id: "daemon", session: sid };
81
+ if (DAEMON_ALIASES.has(o))
82
+ return { kind: "daemon", id: "daemon" };
83
+ if (o === "agent" || o.startsWith("agent:") || o.startsWith("session:"))
84
+ return {
85
+ kind: "agent",
86
+ id: sid ?? o.replace(/^(agent|session):/, "") ?? "agent",
87
+ session: sid
88
+ };
89
+ if (!o || HUMAN_ALIASES.has(o)) {
90
+ if (!o && sid)
91
+ return { kind: "agent", id: sid, session: sid };
92
+ return { kind: "human", id: opts.user?.trim() || "me" };
93
+ }
94
+ if (sid && o === sid)
95
+ return { kind: "agent", id: sid, session: sid };
96
+ return { kind: "human", id: o };
97
+ }
98
+ function actorFromColumns(kind, id, session) {
99
+ if (!kind || !id)
100
+ return null;
101
+ if (!["human", "agent", "run", "daemon"].includes(kind))
102
+ return null;
103
+ const a = { kind, id };
104
+ if (session && kind !== "human" && kind !== "daemon")
105
+ a.session = session;
106
+ return a;
107
+ }
24
108
  // packages/core/src/adapters/claude-code/transcript.ts
25
109
  function parseTranscriptChunk(chunk) {
26
110
  const out = {
@@ -161,6 +245,79 @@ function parseCodexRollout(chunk) {
161
245
  return out;
162
246
  }
163
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
+
164
321
  // packages/core/src/adapters/grok/updates.ts
165
322
  function parseGrokUpdates(chunk) {
166
323
  const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
@@ -229,6 +386,18 @@ function parseGrokUpdates(chunk) {
229
386
  return out;
230
387
  }
231
388
  // packages/core/src/adapters/claude-code/hooks.ts
389
+ var HOOK_EVENTS = [
390
+ "SessionStart",
391
+ "UserPromptSubmit",
392
+ "PreToolUse",
393
+ "PostToolUse",
394
+ "SubagentStart",
395
+ "SubagentStop",
396
+ "Stop",
397
+ "SessionEnd",
398
+ "Notification",
399
+ "PreCompact"
400
+ ];
232
401
  var MAP = {
233
402
  SessionStart: "session.started",
234
403
  UserPromptSubmit: "prompt.submitted",
@@ -322,6 +491,157 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
322
491
  payload.prompt = raw.prompt;
323
492
  return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
324
493
  }
494
+ // packages/core/src/audit.ts
495
+ var AUDIT_TYPES = new Set([
496
+ "session.started",
497
+ "session.ended",
498
+ "tool.denied",
499
+ "claim.acquired",
500
+ "claim.renewed",
501
+ "claim.released",
502
+ "claim.expired",
503
+ "claim.orphaned",
504
+ "worktree.created",
505
+ "worktree.removed",
506
+ "worktree.bootstrapped",
507
+ "pr.opened",
508
+ "question.asked",
509
+ "question.answered",
510
+ "dispatch.queued",
511
+ "dispatch.started",
512
+ "dispatch.finished",
513
+ "resource.acquired",
514
+ "resource.released",
515
+ "resource.reaped",
516
+ "process.started",
517
+ "process.exited",
518
+ "gate.recorded",
519
+ "handoff.recorded",
520
+ "permission.requested",
521
+ "permission.resolved",
522
+ "incident.opened",
523
+ "incident.acked",
524
+ "run.result",
525
+ "workflow.started",
526
+ "workflow.finished"
527
+ ]);
528
+ var isAuditType = (t) => AUDIT_TYPES.has(t);
529
+ var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
530
+ var DEFAULT_PRIVACY = {
531
+ store_prompts: true,
532
+ store_reasoning: true,
533
+ redact: []
534
+ };
535
+ var BUILTIN_REDACT = [
536
+ /\b(sk|pk|rk|ghp|gho|ghu|ghs|xoxb|xoxp|AKIA)[A-Za-z0-9_-]{16,}\b/g,
537
+ /\bBearer\s+[A-Za-z0-9._-]{20,}/g
538
+ ];
539
+ function compileRedactions(patterns) {
540
+ const out = [...BUILTIN_REDACT];
541
+ for (const p of patterns) {
542
+ try {
543
+ out.push(new RegExp(p, "g"));
544
+ } catch {}
545
+ }
546
+ return out;
547
+ }
548
+ function redactValue(v, res) {
549
+ if (!res.length)
550
+ return v;
551
+ if (typeof v === "string")
552
+ return redactString(v, res);
553
+ if (Array.isArray(v)) {
554
+ let changed = false;
555
+ const out = v.map((x) => {
556
+ const r = redactValue(x, res);
557
+ if (r !== x)
558
+ changed = true;
559
+ return r;
560
+ });
561
+ return changed ? out : v;
562
+ }
563
+ if (v && typeof v === "object") {
564
+ let changed = false;
565
+ const out = {};
566
+ for (const [k, x] of Object.entries(v)) {
567
+ const r = redactValue(x, res);
568
+ if (r !== x)
569
+ changed = true;
570
+ out[k] = r;
571
+ }
572
+ return changed ? out : v;
573
+ }
574
+ return v;
575
+ }
576
+ function redactString(s, res) {
577
+ let out = s;
578
+ for (const re of res) {
579
+ re.lastIndex = 0;
580
+ if (re.test(out)) {
581
+ re.lastIndex = 0;
582
+ out = out.replace(re, "[redacted]");
583
+ }
584
+ }
585
+ return out;
586
+ }
587
+ var AUDIT_COLUMNS = [
588
+ "seq",
589
+ "ts",
590
+ "type",
591
+ "projectId",
592
+ "sessionId",
593
+ "actorKind",
594
+ "actorId",
595
+ "summary",
596
+ "payload"
597
+ ];
598
+ function auditRow(e) {
599
+ const p = e.payload ?? {};
600
+ const summary = typeof p.summary === "string" ? p.summary : typeof p.reason === "string" ? p.reason : typeof p.command === "string" ? p.command : typeof p.task === "string" ? String(p.task) : "";
601
+ return {
602
+ seq: e.seq ?? 0,
603
+ ts: e.ts,
604
+ type: e.type,
605
+ projectId: e.projectId,
606
+ sessionId: e.sessionId,
607
+ actorKind: e.actor?.kind ?? null,
608
+ actorId: e.actor?.id ?? null,
609
+ summary: summary.slice(0, 400),
610
+ payload: e.payload ?? null
611
+ };
612
+ }
613
+ var csvCell = (v) => {
614
+ const s = v == null ? "" : typeof v === "string" ? v : JSON.stringify(v);
615
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
616
+ };
617
+ function formatAudit(rows, format) {
618
+ if (format === "json")
619
+ return JSON.stringify(rows);
620
+ if (format === "csv") {
621
+ const lines = [
622
+ AUDIT_COLUMNS.join(","),
623
+ ...rows.map((r) => AUDIT_COLUMNS.map((c) => csvCell(r[c])).join(","))
624
+ ];
625
+ return `${lines.join(`
626
+ `)}
627
+ `;
628
+ }
629
+ return `${rows.map((r) => JSON.stringify(r)).join(`
630
+ `)}${rows.length ? `
631
+ ` : ""}`;
632
+ }
633
+ function sinceToIso(since, now = Date.now()) {
634
+ if (!since)
635
+ return null;
636
+ const m = /^(\d+)([dhm])$/.exec(since.trim());
637
+ if (m) {
638
+ const n = Number(m[1]);
639
+ const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
640
+ return new Date(now - n * ms).toISOString();
641
+ }
642
+ const t = Date.parse(since);
643
+ return Number.isNaN(t) ? null : new Date(t).toISOString();
644
+ }
325
645
  // packages/core/src/budget.ts
326
646
  function budgetStatus(spent, cfg) {
327
647
  const part = (s, l) => ({
@@ -376,33 +696,109 @@ function runProfile(name) {
376
696
  return RUN_PROFILES[name] ?? null;
377
697
  }
378
698
  // packages/core/src/config.ts
379
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
699
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
380
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
381
767
  var DEFAULT_GATE_TIMEOUT_S = 900;
382
768
  var AUTO_MODES = ["session-end", "stop", "off"];
383
769
  function parseGateDefs(gates) {
384
770
  const out = {};
385
- if (!isRecord(gates))
771
+ if (!isRecord2(gates))
386
772
  return out;
387
773
  for (const [name, v] of Object.entries(gates)) {
388
- if (!isRecord(v) || typeof v.cmd !== "string" || !v.cmd.trim())
774
+ if (!isRecord2(v))
775
+ continue;
776
+ const builtin = v.builtin === "review" ? "review" : null;
777
+ const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
778
+ if (!cmd && !builtin)
389
779
  continue;
390
780
  if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
391
781
  continue;
392
782
  const t = Number(v.timeout);
393
783
  out[name] = {
394
- cmd: v.cmd.trim(),
395
- timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : DEFAULT_GATE_TIMEOUT_S,
396
- cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null
784
+ cmd: builtin ? "" : cmd,
785
+ timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
786
+ cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
787
+ builtin,
788
+ model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
397
789
  };
398
790
  }
399
791
  return out;
400
792
  }
401
793
  var DEFAULT_CONFIG = {
402
- daemon: { port: 7777 },
794
+ daemon: { port: 7777, auth: "loopback-optional" },
403
795
  tasks: { source: null, labels: [], team: null },
404
796
  gates: { required: [], auto: "session-end", defs: {} },
797
+ workflows: {},
405
798
  budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
799
+ events: { retain_days: 30 },
800
+ audit: { retain_days: 0 },
801
+ privacy: DEFAULT_PRIVACY,
406
802
  dispatch: {
407
803
  max_parallel: 2,
408
804
  permission_mode: null,
@@ -423,11 +819,11 @@ var DEFAULT_CONFIG = {
423
819
  }
424
820
  };
425
821
  var MODES = ["ask", "deny", "off"];
426
- function isRecord(v) {
822
+ function isRecord2(v) {
427
823
  return typeof v === "object" && v !== null && !Array.isArray(v);
428
824
  }
429
825
  function merge(a, b) {
430
- if (!isRecord(a) || !isRecord(b))
826
+ if (!isRecord2(a) || !isRecord2(b))
431
827
  return b === undefined ? a : b;
432
828
  const out = { ...a };
433
829
  for (const [k, v] of Object.entries(b))
@@ -450,6 +846,10 @@ function isRepoRelative(f) {
450
846
  return false;
451
847
  return !t.split(/[/\\]/).some((seg) => seg === "..");
452
848
  }
849
+ var days = (v, fallback) => {
850
+ const n = Number(v);
851
+ return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
852
+ };
453
853
  function validate(c) {
454
854
  const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
455
855
  const port = Number(c.daemon?.port);
@@ -470,7 +870,10 @@ function validate(c) {
470
870
  const auto = rawGates?.auto;
471
871
  return {
472
872
  ...c,
473
- daemon: { port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777 },
873
+ daemon: {
874
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
875
+ auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
876
+ },
474
877
  tasks: {
475
878
  source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
476
879
  labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
@@ -487,6 +890,18 @@ function validate(c) {
487
890
  warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
488
891
  on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
489
892
  },
893
+ workflows: parseWorkflows(c.workflows),
894
+ events: {
895
+ retain_days: days(c.events?.retain_days, 30)
896
+ },
897
+ audit: {
898
+ retain_days: days(c.audit?.retain_days, 0)
899
+ },
900
+ privacy: {
901
+ store_prompts: c.privacy?.store_prompts !== false,
902
+ store_reasoning: c.privacy?.store_reasoning !== false,
903
+ redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
904
+ },
490
905
  dispatch: {
491
906
  max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
492
907
  permission_mode: str(d.permission_mode),
@@ -514,18 +929,84 @@ function validate(c) {
514
929
  }
515
930
  };
516
931
  }
517
- function loadConfig(opts = {}) {
932
+ function leafPaths(v, prefix = "") {
933
+ if (!isRecord2(v))
934
+ return prefix ? [prefix] : [];
935
+ const keys = Object.keys(v);
936
+ if (keys.length === 0)
937
+ return prefix ? [prefix] : [];
938
+ return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
939
+ }
940
+ function getPath(v, path) {
941
+ let cur = v;
942
+ for (const seg of path.split(".")) {
943
+ if (!isRecord2(cur))
944
+ return;
945
+ cur = cur[seg];
946
+ }
947
+ return cur;
948
+ }
949
+ function setPath(obj, path, value) {
950
+ const segs = path.split(".");
951
+ let cur = obj;
952
+ for (const seg of segs.slice(0, -1)) {
953
+ if (!isRecord2(cur[seg]))
954
+ cur[seg] = {};
955
+ cur = cur[seg];
956
+ }
957
+ cur[segs[segs.length - 1]] = value;
958
+ }
959
+ var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
960
+ function readLayer(path) {
961
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
962
+ }
963
+ function loadConfigDetailed(opts = {}) {
518
964
  const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
965
+ const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
966
+ const policyRaw = readLayer(policyPath);
967
+ const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
968
+ const policy = { ...policyRaw ?? {} };
969
+ delete policy.locked;
970
+ const layers = [
971
+ ["policy", policyRaw ? policy : null],
972
+ ["global", readLayer(join2(home, "config.toml"))],
973
+ ["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
974
+ ];
975
+ const provenance = {};
976
+ for (const p of leafPaths(DEFAULT_CONFIG))
977
+ provenance[p] = "default";
978
+ const overridden = [];
519
979
  let cfg = DEFAULT_CONFIG;
520
- const globalPath = join2(home, "config.toml");
521
- if (existsSync2(globalPath))
522
- cfg = merge(cfg, parseToml(readFileSync2(globalPath, "utf8"), globalPath));
523
- if (opts.repoRoot) {
524
- const repoPath = join2(opts.repoRoot, ".swarm.toml");
525
- if (existsSync2(repoPath))
526
- cfg = merge(cfg, parseToml(readFileSync2(repoPath, "utf8"), repoPath));
980
+ for (const [layer, raw] of layers) {
981
+ if (!raw)
982
+ continue;
983
+ for (const p of leafPaths(raw)) {
984
+ const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
985
+ if (lock)
986
+ overridden.push({ key: p, layer, attempted: getPath(raw, p) });
987
+ else
988
+ provenance[p] = layer;
989
+ }
990
+ cfg = merge(cfg, raw);
991
+ }
992
+ if (overridden.length) {
993
+ const out = structuredClone(cfg);
994
+ for (const { key } of overridden) {
995
+ const fromPolicy = getPath(policy, key);
996
+ setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
997
+ provenance[key] = fromPolicy === undefined ? "default" : "policy";
998
+ }
999
+ cfg = out;
527
1000
  }
528
- return validate(cfg);
1001
+ return {
1002
+ config: validate(cfg),
1003
+ provenance,
1004
+ overridden,
1005
+ policy: { path: policyRaw ? policyPath : null, locked }
1006
+ };
1007
+ }
1008
+ function loadConfig(opts = {}) {
1009
+ return loadConfigDetailed(opts).config;
529
1010
  }
530
1011
  // packages/core/src/dispatch.ts
531
1012
  function planDispatch(tasks, wanted, opts) {
@@ -963,11 +1444,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
963
1444
  `) };
964
1445
  }
965
1446
  // packages/core/src/gates.ts
966
- 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;
967
1448
  function validateGateRun(input) {
968
1449
  if (!input.task?.trim())
969
1450
  return { ok: false, reason: "task is required" };
970
- if (!NAME_RE.test(input.gate ?? ""))
1451
+ if (!NAME_RE2.test(input.gate ?? ""))
971
1452
  return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
972
1453
  if (input.verdict !== "pass" && input.verdict !== "fail")
973
1454
  return { ok: false, reason: 'verdict must be "pass" or "fail"' };
@@ -1328,6 +1809,121 @@ function parseMemoryQuery(q) {
1328
1809
  }
1329
1810
  return { match: terms.join(" "), kind, task };
1330
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
+ }
1841
+ // packages/core/src/policy.ts
1842
+ import { createHash } from "crypto";
1843
+ var HOOK_MARK = "swarm-hook";
1844
+ var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
1845
+ var MIN_HOOK_TIMEOUT_S = 5;
1846
+ function hookCoverage(settings) {
1847
+ const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
1848
+ const missing = [];
1849
+ const short = [];
1850
+ for (const ev of HOOK_EVENTS) {
1851
+ const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
1852
+ const ours = groups.flatMap((g) => {
1853
+ const list = g?.hooks;
1854
+ return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
1855
+ });
1856
+ if (!ours.length)
1857
+ missing.push(ev);
1858
+ else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
1859
+ short.push(ev);
1860
+ }
1861
+ return { missing, short, complete: !missing.length && !short.length };
1862
+ }
1863
+ var hasLockedRules = (loaded) => loaded.policy.locked.some((k) => k === "rules" || k.startsWith("rules."));
1864
+ function policyFindings(input) {
1865
+ const out = [];
1866
+ const repo = input.repoRoot ?? "";
1867
+ for (const o of input.loaded.overridden)
1868
+ out.push({
1869
+ key: `override:${repo}:${o.layer}:${o.key}`,
1870
+ subject: `${o.layer === "repo" ? ".swarm.toml" : "config.toml"} ${o.key}`,
1871
+ reason: `locked by policy; ${o.layer} config tried to set ${JSON.stringify(o.attempted)}`
1872
+ });
1873
+ const cov = input.coverage;
1874
+ if (cov && !cov.complete) {
1875
+ if (cov.missing.length)
1876
+ out.push({
1877
+ key: `hooks:missing:${cov.missing.join(",")}`,
1878
+ subject: `hooks ${cov.missing.join(", ")}`,
1879
+ reason: "swarm hook entry removed from settings.json \u2014 run: swarm install"
1880
+ });
1881
+ if (cov.short.length)
1882
+ out.push({
1883
+ key: `hooks:short:${cov.short.join(",")}`,
1884
+ subject: `hooks ${cov.short.join(", ")}`,
1885
+ reason: `hook timeout below ${MIN_HOOK_TIMEOUT_S}s \u2014 run: swarm install`
1886
+ });
1887
+ }
1888
+ if (input.guardOff && hasLockedRules(input.loaded))
1889
+ out.push({
1890
+ key: "guard:off",
1891
+ subject: "SWARM_GUARD=off",
1892
+ reason: "policy locks rules; SWARM_GUARD=off is ignored for locked rules"
1893
+ });
1894
+ return out;
1895
+ }
1896
+ var POLICY_CACHE_VERSION = 1;
1897
+ var POLICY_CACHE_FILE = "policy.cache.json";
1898
+ var RULE_KEYS = [
1899
+ "shared_tree",
1900
+ "destructive_git",
1901
+ "pattern_kill",
1902
+ "protected_ports",
1903
+ "no_foreign_worktree",
1904
+ "claim_required_to_write"
1905
+ ];
1906
+ var lockedKey = (locked, key) => locked.some((l) => l === "rules" || key === l || key.startsWith(`${l}.`));
1907
+ function offlineModes(loaded) {
1908
+ const locked = loaded.policy.locked;
1909
+ const out = { ...DEFAULT_MODES, protected: { ports: [] } };
1910
+ for (const k of RULE_KEYS)
1911
+ out[k] = lockedKey(locked, `rules.${k}`) ? loaded.config.rules[k] : "off";
1912
+ if (lockedKey(locked, "rules.protected.ports"))
1913
+ out.protected = { ports: [...loaded.config.rules.protected.ports] };
1914
+ return out;
1915
+ }
1916
+ var digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
1917
+ function buildPolicyCache(loaded, sessions, worktrees, now = new Date) {
1918
+ const body = {
1919
+ version: POLICY_CACHE_VERSION,
1920
+ writtenAt: now.toISOString(),
1921
+ modes: offlineModes(loaded),
1922
+ sessions,
1923
+ worktrees
1924
+ };
1925
+ return { ...body, sha256: digest(body) };
1926
+ }
1331
1927
  // packages/core/src/pricing.ts
1332
1928
  var PRICES = {
1333
1929
  "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
@@ -1474,6 +2070,114 @@ function acquireRefusalMessage(holder) {
1474
2070
  const via = isTrackedPid(holder.pid) ? `pid ${holder.pid}` : holder.expiresAt ? `lease until ${holder.expiresAt}` : "unbounded";
1475
2071
  return `Resource "${holder.name}" is held by ${holder.owner} (${via}).` + ` Pick another name, coordinate with the holder, or wait for release/reap.`;
1476
2072
  }
2073
+ // packages/core/src/review.ts
2074
+ var REVIEW_RUBRIC = "review: no blocker/major findings \u2014 correctness bugs, data loss, security, broken invariants (never kill by pattern, never touch a worktree you don't hold, repo-agnostic), missing tests for changed behaviour";
2075
+ var REVIEW_PATCH_MAX = 120000;
2076
+ function reviewPrompt(input) {
2077
+ const patch = input.patch.length > REVIEW_PATCH_MAX ? `${input.patch.slice(0, REVIEW_PATCH_MAX)}
2078
+
2079
+ [\u2026 patch truncated at ${REVIEW_PATCH_MAX} chars; read the files for the rest]` : input.patch;
2080
+ return [
2081
+ `You are the review gate for task ${input.task}${input.title ? ` \u2014 ${input.title}` : ""}${input.branch ? ` (branch ${input.branch})` : ""}.`,
2082
+ "You are read-only: you may Read, Grep and Glob files in this worktree to understand context. Do not edit anything.",
2083
+ "",
2084
+ "Judge the diff below against this rubric and nothing else:",
2085
+ `- ${REVIEW_RUBRIC}`,
2086
+ "- A finding is a concrete defect with a file and, when possible, a line \u2014 not style, not preference.",
2087
+ "- Severity: blocker (must not merge), major (should not merge), minor (worth fixing), nit.",
2088
+ '- verdict is "fail" if and only if there is at least one blocker or major finding.',
2089
+ "",
2090
+ "Respond with ONLY a JSON object, no prose, no code fence:",
2091
+ '{"verdict":"pass"|"fail","summary":"one sentence","findings":[{"file":"path","line":123,"severity":"blocker|major|minor|nit","summary":"what is wrong and why"}]}',
2092
+ "",
2093
+ "Files changed:",
2094
+ input.stat.trim() || "(no stat)",
2095
+ "",
2096
+ "Diff:",
2097
+ patch.trim() || "(empty diff)"
2098
+ ].join(`
2099
+ `);
2100
+ }
2101
+ function reviewArgs(prompt, opts = {}) {
2102
+ const args = [
2103
+ "-p",
2104
+ prompt,
2105
+ "--output-format",
2106
+ "json",
2107
+ "--allowedTools",
2108
+ "Read",
2109
+ "Grep",
2110
+ "Glob",
2111
+ "LS",
2112
+ "--disallowedTools",
2113
+ "Edit",
2114
+ "Write",
2115
+ "MultiEdit",
2116
+ "NotebookEdit",
2117
+ "Bash",
2118
+ "WebFetch",
2119
+ "WebSearch",
2120
+ "--permission-mode",
2121
+ "dontAsk"
2122
+ ];
2123
+ if (opts.model)
2124
+ args.push("--model", opts.model);
2125
+ return args;
2126
+ }
2127
+ function parseReviewVerdict(stdout) {
2128
+ let text = stdout.trim();
2129
+ try {
2130
+ const env = JSON.parse(text);
2131
+ if (env && typeof env === "object" && typeof env.result === "string")
2132
+ text = env.result.trim();
2133
+ } catch {}
2134
+ text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
2135
+ const start = text.indexOf("{");
2136
+ const end = text.lastIndexOf("}");
2137
+ if (start < 0 || end <= start)
2138
+ return null;
2139
+ let obj;
2140
+ try {
2141
+ obj = JSON.parse(text.slice(start, end + 1));
2142
+ } catch {
2143
+ return null;
2144
+ }
2145
+ if (!obj || typeof obj !== "object")
2146
+ return null;
2147
+ const o = obj;
2148
+ const findings = Array.isArray(o.findings) ? o.findings.filter((f) => !!f && typeof f === "object").map((f) => ({
2149
+ file: String(f.file ?? "").slice(0, 300),
2150
+ line: Number.isInteger(f.line) ? f.line : null,
2151
+ severity: ["blocker", "major", "minor", "nit"].includes(String(f.severity)) ? String(f.severity) : "minor",
2152
+ summary: String(f.summary ?? "").slice(0, 600)
2153
+ })).filter((f) => f.summary) : [];
2154
+ const serious = findings.some((f) => f.severity === "blocker" || f.severity === "major");
2155
+ const verdict = serious ? "fail" : o.verdict === "fail" ? "fail" : "pass";
2156
+ return { verdict, summary: String(o.summary ?? "").slice(0, 400), findings };
2157
+ }
2158
+ function reviewGateInput(task, gate, outcome) {
2159
+ if (outcome.kind === "error")
2160
+ return {
2161
+ task,
2162
+ gate,
2163
+ verdict: "fail",
2164
+ rubric: REVIEW_RUBRIC,
2165
+ evidence: `reviewer did not answer: ${outcome.reason}${outcome.output ? `
2166
+ ${outcome.output.slice(-1500)}` : ""}`
2167
+ };
2168
+ const v = outcome.verdict;
2169
+ const lines = v.findings.map((f) => `- [${f.severity}] ${f.file}${f.line ? `:${f.line}` : ""} \u2014 ${f.summary}`);
2170
+ const secs = (outcome.durationMs / 1000).toFixed(0);
2171
+ return {
2172
+ task,
2173
+ gate,
2174
+ verdict: v.verdict,
2175
+ rubric: REVIEW_RUBRIC,
2176
+ evidence: `${v.summary || (v.verdict === "pass" ? "no blocking findings" : "blocking findings")} (${v.findings.length} finding${v.findings.length === 1 ? "" : "s"}, ${secs}s)${lines.length ? `
2177
+ ${lines.join(`
2178
+ `)}` : ""}`
2179
+ };
2180
+ }
1477
2181
  // packages/core/src/tasks.ts
1478
2182
  var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
1479
2183
  var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
@@ -1707,10 +2411,10 @@ function planGc(worktrees, claims) {
1707
2411
  return out;
1708
2412
  }
1709
2413
  // packages/daemon/src/app.ts
1710
- 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";
1711
2415
  import { homedir as homedir4 } from "os";
1712
- import { dirname as dirname3, join as join9 } from "path";
1713
- import { fileURLToPath } from "url";
2416
+ import { dirname as dirname4, join as join9 } from "path";
2417
+ import { fileURLToPath as fileURLToPath2 } from "url";
1714
2418
 
1715
2419
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
1716
2420
  var compose = (middleware, onError, onNotFound) => {
@@ -1918,7 +2622,7 @@ var tryDecode = (str, decoder) => {
1918
2622
  }
1919
2623
  };
1920
2624
  var tryDecodeURI = (str) => tryDecode(str, decodeURI);
1921
- var getPath = (request) => {
2625
+ var getPath2 = (request) => {
1922
2626
  const url = request.url;
1923
2627
  const start = url.indexOf("/", url.indexOf(":") + 4);
1924
2628
  let i = start;
@@ -1937,7 +2641,7 @@ var getPath = (request) => {
1937
2641
  return url.slice(start, i);
1938
2642
  };
1939
2643
  var getPathNoStrict = (request) => {
1940
- const result = getPath(request);
2644
+ const result = getPath2(request);
1941
2645
  return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
1942
2646
  };
1943
2647
  var mergePath = (base, sub, ...rest) => {
@@ -2459,7 +3163,7 @@ var Hono = class _Hono {
2459
3163
  };
2460
3164
  const { strict, ...optionsWithoutStrict } = options;
2461
3165
  Object.assign(this, optionsWithoutStrict);
2462
- this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
3166
+ this.getPath = strict ?? true ? options.getPath ?? getPath2 : getPathNoStrict;
2463
3167
  }
2464
3168
  #clone() {
2465
3169
  const clone = new _Hono({
@@ -3583,7 +4287,7 @@ class Dispatcher {
3583
4287
  }
3584
4288
 
3585
4289
  // packages/daemon/src/forge.ts
3586
- import { existsSync as existsSync3 } from "fs";
4290
+ import { existsSync as existsSync4 } from "fs";
3587
4291
  import { homedir as homedir2 } from "os";
3588
4292
  import { join as join4 } from "path";
3589
4293
  var EXTRA_BIN_DIRS = [
@@ -3601,7 +4305,7 @@ function findBin(name) {
3601
4305
  return onPath;
3602
4306
  for (const d of EXTRA_BIN_DIRS) {
3603
4307
  const p = join4(d, name);
3604
- if (existsSync3(p))
4308
+ if (existsSync4(p))
3605
4309
  return p;
3606
4310
  }
3607
4311
  return null;
@@ -4300,7 +5004,7 @@ class Runner {
4300
5004
  import { Database } from "bun:sqlite";
4301
5005
  import {
4302
5006
  closeSync,
4303
- existsSync as existsSync5,
5007
+ existsSync as existsSync6,
4304
5008
  mkdirSync as mkdirSync4,
4305
5009
  openSync as openSync3,
4306
5010
  readdirSync,
@@ -4309,14 +5013,15 @@ import {
4309
5013
  realpathSync as realpathSync2,
4310
5014
  renameSync,
4311
5015
  statSync,
5016
+ unlinkSync,
4312
5017
  writeFileSync as writeFileSync2
4313
5018
  } from "fs";
4314
- import { homedir as homedir3 } from "os";
4315
- import { basename, dirname as dirname2, join as join8 } from "path";
5019
+ import { homedir as homedir3, tmpdir, userInfo } from "os";
5020
+ import { basename, dirname as dirname3, join as join8 } from "path";
4316
5021
 
4317
5022
  // packages/daemon/src/bootstrap.ts
4318
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
4319
- 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";
4320
5025
  function runBootstrap(plan, opts) {
4321
5026
  const logDir = join7(opts.home, "logs", opts.projectId);
4322
5027
  mkdirSync3(logDir, { recursive: true });
@@ -4324,12 +5029,12 @@ function runBootstrap(plan, opts) {
4324
5029
  const copied = [];
4325
5030
  const skipped = [];
4326
5031
  for (const c of plan.copies) {
4327
- if (!existsSync4(c.from)) {
5032
+ if (!existsSync5(c.from)) {
4328
5033
  skipped.push(c.rel);
4329
5034
  continue;
4330
5035
  }
4331
5036
  try {
4332
- mkdirSync3(dirname(c.to), { recursive: true });
5037
+ mkdirSync3(dirname2(c.to), { recursive: true });
4333
5038
  cpSync(c.from, c.to, { recursive: true, force: true });
4334
5039
  copied.push(c.rel);
4335
5040
  } catch (e) {
@@ -4491,6 +5196,12 @@ CREATE TABLE IF NOT EXISTS messages (
4491
5196
  answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
4492
5197
  );
4493
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);
4494
5205
  CREATE TABLE IF NOT EXISTS claims (
4495
5206
  project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
4496
5207
  acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
@@ -4519,6 +5230,11 @@ class Store {
4519
5230
  this.db.exec(SCHEMA);
4520
5231
  this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
4521
5232
  this.ensureColumn("projects", "sort_order", "INTEGER");
5233
+ this.ensureColumn("projects", "icon", "TEXT");
5234
+ this.ensureColumn("projects", "color", "TEXT");
5235
+ this.ensureColumn("messages", "to_kind", "TEXT");
5236
+ this.ensureColumn("messages", "from_session", "TEXT");
5237
+ this.migrate();
4522
5238
  this.migrateProjectsJson(join8(home, "projects.json"));
4523
5239
  this.reconcileMovedProjects();
4524
5240
  this.slimExistingEvents();
@@ -4567,9 +5283,9 @@ class Store {
4567
5283
  reconcileMovedProjects() {
4568
5284
  const all = this.projects();
4569
5285
  for (const stale of all) {
4570
- if (existsSync5(stale.root))
5286
+ if (existsSync6(stale.root))
4571
5287
  continue;
4572
- 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));
4573
5289
  if (live.length !== 1)
4574
5290
  continue;
4575
5291
  this.mergeProject(stale.id, live[0].id);
@@ -4601,8 +5317,59 @@ class Store {
4601
5317
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
4602
5318
  }
4603
5319
  }
5320
+ static SCHEMA_VERSION = 1;
5321
+ schemaVersion() {
5322
+ return Number(this.meta("schema_version") ?? 0);
5323
+ }
5324
+ migrate() {
5325
+ const steps = [
5326
+ (db) => {
5327
+ for (const t of [
5328
+ "events",
5329
+ "claims",
5330
+ "resources",
5331
+ "processes",
5332
+ "handoffs",
5333
+ "gates",
5334
+ "incident_acks",
5335
+ "sessions"
5336
+ ]) {
5337
+ this.ensureColumn(t, "actor_kind", "TEXT");
5338
+ this.ensureColumn(t, "actor_id", "TEXT");
5339
+ }
5340
+ const user = osUser();
5341
+ const fill = (table, ownerCol, sessionCol, key) => {
5342
+ const rows = db.query(`SELECT rowid AS rid, ${ownerCol ?? "NULL"} AS owner, ${sessionCol ?? "NULL"} AS sid FROM ${table} WHERE actor_kind IS NULL`).all();
5343
+ const upd = db.query(`UPDATE ${table} SET actor_kind = ?, actor_id = ? WHERE rowid = ?`);
5344
+ for (const r of rows) {
5345
+ const a = actorFrom(r.owner, r.sid, { user });
5346
+ upd.run(a.kind, a.id, r.rid);
5347
+ }
5348
+ return `${key}:${rows.length}`;
5349
+ };
5350
+ fill("claims", "owner", null, "claims");
5351
+ fill("resources", "owner", "session_id", "resources");
5352
+ fill("processes", "owner", "session_id", "processes");
5353
+ fill("handoffs", "by", "session_id", "handoffs");
5354
+ fill("gates", "NULL", "session_id", "gates");
5355
+ fill("incident_acks", "'dashboard'", null, "acks");
5356
+ fill("sessions", "NULL", "id", "sessions");
5357
+ fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
5358
+ }
5359
+ ];
5360
+ for (let v = this.schemaVersion();v < steps.length; v++) {
5361
+ const step = steps[v];
5362
+ this.db.transaction(() => {
5363
+ step(this.db);
5364
+ this.setMeta("schema_version", String(v + 1));
5365
+ })();
5366
+ }
5367
+ }
5368
+ actorFor(owner, sessionId, runId) {
5369
+ return actorFrom(owner, sessionId, { user: osUser(), runId });
5370
+ }
4604
5371
  migrateProjectsJson(file) {
4605
- if (!existsSync5(file))
5372
+ if (!existsSync6(file))
4606
5373
  return;
4607
5374
  try {
4608
5375
  const list = JSON.parse(readFileSync3(file, "utf8"));
@@ -4617,11 +5384,12 @@ class Store {
4617
5384
  const hit = this.topCache.get(cwd);
4618
5385
  if (hit && Date.now() - hit.t < 1e4)
4619
5386
  return hit.v;
4620
- const v = cwd && existsSync5(cwd) ? gitToplevel(cwd) : null;
5387
+ const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
4621
5388
  this.topCache.set(cwd, { v, t: Date.now() });
4622
5389
  return v;
4623
5390
  }
4624
- rulesCache = new Map;
5391
+ policyCache = new Map;
5392
+ policySeen = new Set;
4625
5393
  preregisterSpawnedSession(id, projectId, cwd, task) {
4626
5394
  const now = new Date().toISOString();
4627
5395
  this.db.query(`INSERT INTO sessions (id, project_id, kind, cwd, started_at, last_seen_at, last, last_type, state, title)
@@ -4650,8 +5418,8 @@ class Store {
4650
5418
  createdAt: new Date().toISOString()
4651
5419
  };
4652
5420
  const sessionId = this.knownSession(h.sessionId);
4653
- const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
4654
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt);
5421
+ const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
5422
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt, ...actorCols(this.actorFor(handoff.by, sessionId)));
4655
5423
  this.remember(handoffDoc(projectId, Number(ins.lastInsertRowid), handoff, sessionId));
4656
5424
  this.append({
4657
5425
  ts: handoff.createdAt,
@@ -4701,8 +5469,8 @@ class Store {
4701
5469
  if (!h)
4702
5470
  return null;
4703
5471
  this.db.query("DELETE FROM handoffs WHERE project_id = ? AND task = ? AND session_id = ? AND by LIKE 'auto%'").run(held.projectId, held.task, sessionId);
4704
- const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
4705
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt);
5472
+ const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
5473
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt, ...actorCols(this.actorFor(h.by, sessionId)));
4706
5474
  this.remember(handoffDoc(held.projectId, Number(ins.lastInsertRowid), h, sessionId));
4707
5475
  this.touch();
4708
5476
  return h;
@@ -4822,7 +5590,7 @@ class Store {
4822
5590
  }));
4823
5591
  }
4824
5592
  sessionContext(cwd) {
4825
- if (!cwd || !existsSync5(cwd))
5593
+ if (!cwd || !existsSync6(cwd))
4826
5594
  return null;
4827
5595
  const toplevel = this.toplevel(cwd);
4828
5596
  const project = this.resolveProject(cwd);
@@ -4962,7 +5730,160 @@ class Store {
4962
5730
  return qs;
4963
5731
  }
4964
5732
  answerContext(sessionId) {
4965
- 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;
4966
5887
  }
4967
5888
  questionContext(task, projectId) {
4968
5889
  if (!task)
@@ -5039,13 +5960,13 @@ class Store {
5039
5960
  };
5040
5961
  const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
5041
5962
  const worktree2 = claim?.worktree;
5042
- if (!worktree2 || !existsSync5(worktree2))
5963
+ if (!worktree2 || !existsSync6(worktree2))
5043
5964
  return {
5044
5965
  ok: false,
5045
5966
  reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
5046
5967
  };
5047
5968
  const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
5048
- if (!existsSync5(cwd))
5969
+ if (!existsSync6(cwd))
5049
5970
  return { ok: false, reason: `gate cwd ${cwd} does not exist` };
5050
5971
  const key = `${projectId}:${task}:${gate}`;
5051
5972
  if (this.gateJobs.has(key))
@@ -5054,6 +5975,8 @@ class Store {
5054
5975
  const logDir = join8(this.home, "logs", projectId);
5055
5976
  mkdirSync4(logDir, { recursive: true });
5056
5977
  const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
5978
+ if (def.builtin === "review")
5979
+ return this.runReviewGate(projectId, task, gate, def, { worktree: worktree2, cwd, key, log }, opts);
5057
5980
  writeFileSync2(log, `$ ${def.cmd}
5058
5981
  # cwd ${cwd} \xB7 ${new Date().toISOString()}
5059
5982
  `);
@@ -5133,6 +6056,134 @@ class Store {
5133
6056
  this.gateJobs.set(key, done);
5134
6057
  return { ok: true, pid: proc.pid, log, done };
5135
6058
  }
6059
+ runReviewGate(projectId, task, gate, def, where, opts) {
6060
+ const bin = findBin("claude");
6061
+ if (!bin)
6062
+ return { ok: false, reason: "claude CLI not found \u2014 the review gate needs Claude Code" };
6063
+ const p = this.project(projectId);
6064
+ if (!p)
6065
+ return { ok: false, reason: "unknown project" };
6066
+ const started = Date.now();
6067
+ const record = (input) => {
6068
+ const run2 = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
6069
+ return run2.ok ? run2.run : null;
6070
+ };
6071
+ const done = (async () => {
6072
+ let diffText = "";
6073
+ let stat = "";
6074
+ try {
6075
+ const diff = await worktreeDiff(p.root, where.worktree);
6076
+ stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
6077
+ `);
6078
+ diffText = await worktreePatch(where.worktree, diff.base);
6079
+ } catch (e) {
6080
+ return record(reviewGateInput(task, gate, {
6081
+ kind: "error",
6082
+ reason: `diff failed: ${e.message}`,
6083
+ durationMs: Date.now() - started
6084
+ }));
6085
+ }
6086
+ if (!diffText.trim())
6087
+ return record(reviewGateInput(task, gate, {
6088
+ kind: "verdict",
6089
+ durationMs: Date.now() - started,
6090
+ verdict: { verdict: "pass", summary: "nothing to review \u2014 empty diff", findings: [] }
6091
+ }));
6092
+ const taskRow = this.tasks(projectId)?.tasks.find((t) => t.id === task) ?? null;
6093
+ const w = this.findWorktree(projectId, where.worktree);
6094
+ const prompt = reviewPrompt({
6095
+ task,
6096
+ title: taskRow?.title ?? null,
6097
+ branch: w?.branch ?? null,
6098
+ stat,
6099
+ patch: diffText
6100
+ });
6101
+ writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
6102
+ # cwd ${where.cwd} \xB7 ${new Date().toISOString()}
6103
+ `);
6104
+ let proc;
6105
+ try {
6106
+ proc = Bun.spawn([bin, ...reviewArgs(prompt, { model: def.model })], {
6107
+ cwd: where.cwd,
6108
+ stdin: "ignore",
6109
+ stdout: "pipe",
6110
+ stderr: "pipe",
6111
+ env: {
6112
+ ...process.env,
6113
+ SWARM_WORKTREE: where.worktree,
6114
+ SWARM_TASK: task,
6115
+ SWARM_GATE: gate,
6116
+ CLAUDE_CODE_DISABLE_AUTOUPDATE: "1"
6117
+ }
6118
+ });
6119
+ } catch (e) {
6120
+ return record(reviewGateInput(task, gate, {
6121
+ kind: "error",
6122
+ reason: e.message,
6123
+ durationMs: Date.now() - started
6124
+ }));
6125
+ }
6126
+ const reg = this.registerProcess({
6127
+ pid: proc.pid,
6128
+ projectId,
6129
+ sessionId: opts.sessionId ?? null,
6130
+ kind: "gate",
6131
+ name: `gate:${task}:${gate}`,
6132
+ cwd: where.cwd,
6133
+ cmd: "claude -p (review)",
6134
+ owner: opts.owner ?? "daemon",
6135
+ log: where.log
6136
+ });
6137
+ let timedOut = false;
6138
+ const timer = setTimeout(() => {
6139
+ timedOut = true;
6140
+ try {
6141
+ proc.kill("SIGTERM");
6142
+ setTimeout(() => {
6143
+ try {
6144
+ proc.kill("SIGKILL");
6145
+ } catch {}
6146
+ }, 5000).unref();
6147
+ } catch {}
6148
+ }, def.timeout * 1000);
6149
+ const [out, err] = await Promise.all([
6150
+ new Response(proc.stdout).text(),
6151
+ new Response(proc.stderr).text()
6152
+ ]);
6153
+ const code = await proc.exited;
6154
+ clearTimeout(timer);
6155
+ try {
6156
+ writeFileSync2(where.log, `${readFileSync3(where.log, "utf8")}${out}
6157
+ ${err}
6158
+ # exit ${timedOut ? "timeout" : code} \xB7 ${((Date.now() - started) / 1000).toFixed(0)}s
6159
+ `);
6160
+ } catch {}
6161
+ if (reg.ok)
6162
+ this.processes(projectId);
6163
+ const durationMs = Date.now() - started;
6164
+ if (timedOut)
6165
+ return record(reviewGateInput(task, gate, {
6166
+ kind: "error",
6167
+ reason: `timed out after ${def.timeout}s`,
6168
+ durationMs,
6169
+ output: err
6170
+ }));
6171
+ const verdict = parseReviewVerdict(out);
6172
+ if (!verdict)
6173
+ return record(reviewGateInput(task, gate, {
6174
+ kind: "error",
6175
+ reason: code === 0 ? "no JSON verdict in the reply" : `claude exited ${code}`,
6176
+ durationMs,
6177
+ output: err || out
6178
+ }));
6179
+ return record(reviewGateInput(task, gate, { kind: "verdict", verdict, durationMs }));
6180
+ })().finally(() => {
6181
+ this.gateJobs.delete(where.key);
6182
+ this.touch();
6183
+ });
6184
+ this.gateJobs.set(where.key, done);
6185
+ return { ok: true, pid: 0, log: where.log, done };
6186
+ }
5136
6187
  async runGates(projectId, task, gates2, opts = {}) {
5137
6188
  const cfg = this.gateDefs(projectId);
5138
6189
  const names = gates2?.length ? gates2 : (cfg?.required ?? []).filter((g) => cfg?.defs[g]);
@@ -5202,8 +6253,8 @@ class Store {
5202
6253
  return v;
5203
6254
  const createdAt = new Date().toISOString();
5204
6255
  const sessionId = this.knownSession(input.sessionId);
5205
- const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at)
5206
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt);
6256
+ const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
6257
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
5207
6258
  const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
5208
6259
  this.remember(gateDoc(projectId, run2.id, run2, sessionId));
5209
6260
  this.append({
@@ -5256,7 +6307,7 @@ class Store {
5256
6307
  error = e.error;
5257
6308
  } else {
5258
6309
  const path = join8(p.root, source);
5259
- if (!existsSync5(path))
6310
+ if (!existsSync6(path))
5260
6311
  return { source, required: this.requiredGates(projectId), tasks: [] };
5261
6312
  const mtime = statSync(path).mtimeMs;
5262
6313
  let md = this.taskCache.get(projectId);
@@ -5289,16 +6340,70 @@ class Store {
5289
6340
  return { source, required, tasks: board, error };
5290
6341
  }
5291
6342
  rulesFor(repoRoot) {
6343
+ return this.policyFor(repoRoot).config.rules;
6344
+ }
6345
+ policyFor(repoRoot) {
5292
6346
  const key = repoRoot ?? "";
5293
- const hit = this.rulesCache.get(key);
6347
+ const hit = this.policyCache.get(key);
5294
6348
  if (hit && Date.now() - hit.at < 30000)
5295
- return hit.rules;
5296
- const rules2 = loadConfig({ repoRoot, home: this.home }).rules;
5297
- this.rulesCache.set(key, { at: Date.now(), rules: rules2 });
5298
- return rules2;
6349
+ return hit.loaded;
6350
+ const loaded = loadConfigDetailed({ repoRoot, home: this.home });
6351
+ this.policyCache.set(key, { at: Date.now(), loaded });
6352
+ this.writePolicyCache(loaded);
6353
+ return loaded;
6354
+ }
6355
+ writePolicyCache(loaded) {
6356
+ const file = join8(this.home, POLICY_CACHE_FILE);
6357
+ try {
6358
+ if (!hasLockedRules(loaded)) {
6359
+ if (existsSync6(file))
6360
+ unlinkSync(file);
6361
+ return;
6362
+ }
6363
+ const cache = buildPolicyCache(loaded, this.liveSessions(), this.heldWorktrees());
6364
+ writeFileSync2(file, JSON.stringify(cache), { mode: 384 });
6365
+ } catch (e) {
6366
+ console.error(`swarm: policy cache: ${e.message}`);
6367
+ }
6368
+ }
6369
+ guardDisabled(repoRoot) {
6370
+ return process.env.SWARM_GUARD === "off" && !hasLockedRules(this.policyFor(repoRoot));
6371
+ }
6372
+ claudeSettings() {
6373
+ const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
6374
+ try {
6375
+ return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6376
+ } catch {
6377
+ return null;
6378
+ }
6379
+ }
6380
+ checkPolicy(cwd, sessionId) {
6381
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
6382
+ const repoRoot = project?.root ?? null;
6383
+ const loaded = this.policyFor(repoRoot);
6384
+ const settings = this.claudeSettings();
6385
+ const findings = policyFindings({
6386
+ loaded,
6387
+ coverage: settings === null ? null : hookCoverage(settings),
6388
+ guardOff: process.env.SWARM_GUARD === "off",
6389
+ repoRoot
6390
+ });
6391
+ for (const f of findings) {
6392
+ if (this.policySeen.has(f.key))
6393
+ continue;
6394
+ this.policySeen.add(f.key);
6395
+ this.append({
6396
+ ts: new Date().toISOString(),
6397
+ type: "incident.opened",
6398
+ projectId: project?.id ?? "p_unknown",
6399
+ sessionId,
6400
+ payload: { rule: "policy", action: "tampered", command: f.subject, reason: f.reason }
6401
+ });
6402
+ }
6403
+ return findings;
5299
6404
  }
5300
6405
  evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
5301
- if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync5(cwd)) {
6406
+ if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
5302
6407
  const project = this.resolveProject(cwd);
5303
6408
  const b = this.budgetFor(project.id);
5304
6409
  if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
@@ -5389,7 +6494,7 @@ class Store {
5389
6494
  return this.openIncident(d, cwd, id, cmd);
5390
6495
  }
5391
6496
  openIncident(d, cwd, sessionId, command) {
5392
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
6497
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
5393
6498
  this.append({
5394
6499
  ts: new Date().toISOString(),
5395
6500
  type: "incident.opened",
@@ -5440,7 +6545,7 @@ class Store {
5440
6545
  }
5441
6546
  }
5442
6547
  const report = dryRunRules(calls, modes, {
5443
- toplevel: (cwd) => cwd && existsSync5(cwd) ? this.toplevel(cwd) : null,
6548
+ toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
5444
6549
  claims: this.heldWorktrees()
5445
6550
  });
5446
6551
  return { ...report, modes };
@@ -5456,7 +6561,7 @@ class Store {
5456
6561
  this.prices = { ...PRICES };
5457
6562
  for (const f of ["pricing.litellm.json", "pricing.json"]) {
5458
6563
  const p = join8(this.home, f);
5459
- if (!existsSync5(p))
6564
+ if (!existsSync6(p))
5460
6565
  continue;
5461
6566
  try {
5462
6567
  const j = JSON.parse(readFileSync3(p, "utf8"));
@@ -5498,6 +6603,8 @@ class Store {
5498
6603
  name: r.name,
5499
6604
  discovered: Boolean(r.discovered),
5500
6605
  order: typeof r.sort_order === "number" ? r.sort_order : null,
6606
+ icon: r.icon ?? null,
6607
+ color: r.color ?? null,
5501
6608
  createdAt: r.created_at
5502
6609
  }));
5503
6610
  }
@@ -5512,6 +6619,8 @@ class Store {
5512
6619
  name: r.name,
5513
6620
  discovered: Boolean(r.discovered),
5514
6621
  order: typeof r.sort_order === "number" ? r.sort_order : null,
6622
+ icon: r.icon ?? null,
6623
+ color: r.color ?? null,
5515
6624
  createdAt: r.created_at
5516
6625
  };
5517
6626
  }
@@ -5549,6 +6658,8 @@ class Store {
5549
6658
  ...ident,
5550
6659
  discovered: !explicit,
5551
6660
  order: null,
6661
+ icon: null,
6662
+ color: null,
5552
6663
  createdAt: new Date().toISOString()
5553
6664
  };
5554
6665
  if (name)
@@ -5569,8 +6680,24 @@ class Store {
5569
6680
  return;
5570
6681
  if (patch.pinned !== undefined)
5571
6682
  this.db.query("UPDATE projects SET discovered = ? WHERE id = ?").run(patch.pinned ? 0 : 1, id);
5572
- if (patch.name)
5573
- this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name, id);
6683
+ if (patch.name?.trim())
6684
+ this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name.trim(), id);
6685
+ if (patch.icon !== undefined) {
6686
+ const icon = (patch.icon ?? "").trim();
6687
+ const isImage = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(icon);
6688
+ if (!isImage && [...icon].length > 4)
6689
+ return;
6690
+ if (isImage && icon.length > 24000)
6691
+ return;
6692
+ this.db.query("UPDATE projects SET icon = ? WHERE id = ?").run(icon || null, id);
6693
+ }
6694
+ if (patch.color !== undefined) {
6695
+ const color = (patch.color ?? "").trim();
6696
+ if (color && !/^c[1-7]$/.test(color))
6697
+ return;
6698
+ this.db.query("UPDATE projects SET color = ? WHERE id = ?").run(color || null, id);
6699
+ }
6700
+ this.touch();
5574
6701
  return this.project(id);
5575
6702
  }
5576
6703
  reorderProjects(ids) {
@@ -5589,10 +6716,29 @@ class Store {
5589
6716
  this.touch();
5590
6717
  return this.db.query("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
5591
6718
  }
5592
- append(e) {
6719
+ redactions() {
6720
+ const cfg = this.policyFor(null).config.privacy;
6721
+ const key = cfg.redact.join("\x00");
6722
+ if (this.redactCache?.key !== key)
6723
+ this.redactCache = { key, res: compileRedactions(cfg.redact) };
6724
+ return this.redactCache.res;
6725
+ }
6726
+ redactCache = null;
6727
+ append(e0) {
6728
+ const privacy = this.policyFor(null).config.privacy;
6729
+ let e = e0;
6730
+ if (!privacy.store_prompts && e.type === "prompt.submitted" && e.payload && typeof e.payload === "object") {
6731
+ const { prompt: _p, ...rest } = e.payload;
6732
+ e = { ...e, payload: { ...rest, prompt: "[not stored]" } };
6733
+ }
6734
+ const res = this.redactions();
6735
+ if (res.length)
6736
+ e = { ...e, payload: redactValue(e.payload, res), raw: redactValue(e.raw, res) };
5593
6737
  const slim = slimForStorage(e);
5594
- const r = this.db.query("INSERT INTO events (ts, type, project_id, session_id, payload, raw) VALUES (?, ?, ?, ?, ?, ?)").run(e.ts, e.type, e.projectId, e.sessionId, JSON.stringify(slim.payload ?? null), slim.raw === undefined ? null : JSON.stringify(slim.raw));
5595
- const stored = { ...e, seq: Number(r.lastInsertRowid) };
6738
+ const p = e.payload ?? {};
6739
+ const actor2 = e.actor ?? this.actorFor(typeof p.owner === "string" ? p.owner : typeof p.by === "string" ? p.by : null, e.sessionId);
6740
+ const r = this.db.query("INSERT INTO events (ts, type, project_id, session_id, payload, raw, actor_kind, actor_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.ts, e.type, e.projectId, e.sessionId, JSON.stringify(slim.payload ?? null), slim.raw === undefined ? null : JSON.stringify(slim.raw), actor2.kind, actor2.id);
6741
+ const stored = { ...e, actor: actor2, seq: Number(r.lastInsertRowid) };
5596
6742
  if (stored.type === "incident.opened")
5597
6743
  this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
5598
6744
  this.projectSession(stored);
@@ -5602,9 +6748,34 @@ class Store {
5602
6748
  l(wire);
5603
6749
  return stored;
5604
6750
  }
5605
- prune(days = 30) {
5606
- const cutoff = new Date(Date.now() - days * 86400000).toISOString();
5607
- const n = this.db.query("DELETE FROM events WHERE ts < ? AND type != 'incident.opened'").run(cutoff).changes;
6751
+ audit(opts = {}) {
6752
+ const where = [`type IN (${AUDIT_TYPES_SQL})`];
6753
+ const args = [];
6754
+ if (opts.since) {
6755
+ where.push("ts >= ?");
6756
+ args.push(opts.since);
6757
+ }
6758
+ if (opts.projectId) {
6759
+ where.push("project_id = ?");
6760
+ args.push(opts.projectId);
6761
+ }
6762
+ if (opts.type && isAuditType(opts.type)) {
6763
+ where.push("type = ?");
6764
+ args.push(opts.type);
6765
+ }
6766
+ const limit = Math.min(Math.max(opts.limit ?? 1e4, 1), 1e5);
6767
+ const rows = this.db.query(`SELECT * FROM (SELECT ${WIRE_COLS} FROM events WHERE ${where.join(" AND ")} ORDER BY seq DESC LIMIT ?) ORDER BY seq`).all(...args, limit);
6768
+ return rows.map((r) => auditRow(wireRowToEvent(r)));
6769
+ }
6770
+ prune(days2) {
6771
+ const cfg = this.policyFor(null).config;
6772
+ const chatter = days2 ?? cfg.events.retain_days;
6773
+ const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
6774
+ let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
6775
+ if (cfg.audit.retain_days > 0) {
6776
+ const acut = new Date(Date.now() - cfg.audit.retain_days * 86400000).toISOString();
6777
+ n += this.db.query(`DELETE FROM events WHERE ts < ? AND type IN (${AUDIT_TYPES_SQL}) AND type != 'incident.opened'`).run(acut).changes;
6778
+ }
5608
6779
  const old = new Date(Date.now() - 7 * 86400000).toISOString();
5609
6780
  this.db.query("UPDATE events SET raw = NULL WHERE ts < ? AND raw IS NOT NULL").run(old);
5610
6781
  if (n > 0)
@@ -5615,10 +6786,10 @@ class Store {
5615
6786
  if (typeof raw2.cwd === "string")
5616
6787
  this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
5617
6788
  const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
5618
- const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
6789
+ const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
5619
6790
  const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
5620
6791
  if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
5621
- if (existsSync5(cwd)) {
6792
+ if (existsSync6(cwd)) {
5622
6793
  this.autoHandoff(e.sessionId, cwd);
5623
6794
  this.autoGate(event, e.sessionId, cwd);
5624
6795
  }
@@ -5664,7 +6835,7 @@ class Store {
5664
6835
  return;
5665
6836
  const p = e.payload;
5666
6837
  const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
5667
- const branch = p.cwd && existsSync5(p.cwd) ? currentBranch(p.cwd) : null;
6838
+ const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
5668
6839
  if (!row) {
5669
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);
5670
6841
  }
@@ -5702,13 +6873,15 @@ class Store {
5702
6873
  }
5703
6874
  }
5704
6875
  persistTurns(sessionId, agentId, turns) {
6876
+ const privacy = this.policyFor(null).config.privacy;
6877
+ const res = this.redactions();
5705
6878
  const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
5706
6879
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5707
6880
  ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
5708
6881
  cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
5709
6882
  const tx = this.db.transaction((ts) => {
5710
6883
  for (const t of ts) {
5711
- 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), t.text, JSON.stringify(t.tools));
6884
+ up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
5712
6885
  }
5713
6886
  });
5714
6887
  if (turns.length)
@@ -5734,10 +6907,10 @@ class Store {
5734
6907
  }
5735
6908
  tailSession(sessionId) {
5736
6909
  const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
5737
- if (!s?.transcript_path || !existsSync5(s.transcript_path))
6910
+ if (!s?.transcript_path || !existsSync6(s.transcript_path))
5738
6911
  return 0;
5739
6912
  let n = this.tailFile(s.transcript_path, sessionId, null);
5740
- 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");
5741
6914
  for (const f of this.subagentFiles(subDir)) {
5742
6915
  n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
5743
6916
  }
@@ -5805,7 +6978,7 @@ class Store {
5805
6978
  return out;
5806
6979
  }
5807
6980
  tailCodex(windowMs = 3 * 24 * 60 * 60000) {
5808
- if (!existsSync5(this.codexRoot()))
6981
+ if (!existsSync6(this.codexRoot()))
5809
6982
  return 0;
5810
6983
  let n = 0;
5811
6984
  for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
@@ -5817,9 +6990,54 @@ class Store {
5817
6990
  return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
5818
6991
  }
5819
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
+ }
5820
7038
  tailGrok(windowMs = 3 * 24 * 60 * 60000) {
5821
7039
  const root = this.grokRoot();
5822
- if (!existsSync5(root))
7040
+ if (!existsSync6(root))
5823
7041
  return 0;
5824
7042
  const since = Date.now() - windowMs;
5825
7043
  const ls = (p) => {
@@ -5842,7 +7060,7 @@ class Store {
5842
7060
  const cwdDir = join8(root, enc);
5843
7061
  for (const sid of ls(cwdDir)) {
5844
7062
  const path = join8(cwdDir, sid, "updates.jsonl");
5845
- if (!existsSync5(path))
7063
+ if (!existsSync6(path))
5846
7064
  continue;
5847
7065
  try {
5848
7066
  if (statSync(path).mtimeMs < since)
@@ -5901,9 +7119,9 @@ class Store {
5901
7119
  ensureAgentSession(sid, agent, cwd, mtime) {
5902
7120
  if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
5903
7121
  return;
5904
- const project = cwd && existsSync5(cwd) ? this.resolveProject(cwd) : null;
7122
+ const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
5905
7123
  const ts = new Date(mtime).toISOString();
5906
- 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);
5907
7125
  }
5908
7126
  claimRows(projectId) {
5909
7127
  return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
@@ -5939,7 +7157,7 @@ class Store {
5939
7157
  const p = this.project(projectId);
5940
7158
  return join8(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
5941
7159
  }
5942
- claim(projectId, task, owner, baseRef = "HEAD") {
7160
+ claim(projectId, task, owner, baseRef = "HEAD", sessionId = null) {
5943
7161
  const p = this.project(projectId);
5944
7162
  if (!p)
5945
7163
  return { ok: false, error: "unknown project" };
@@ -5949,24 +7167,26 @@ class Store {
5949
7167
  return { ok: false, error: claimRefusalMessage(decision, task) };
5950
7168
  const branch = `task/${task}`;
5951
7169
  const worktree2 = this.worktreePath(projectId, task);
5952
- if (existsSync5(worktree2))
7170
+ if (existsSync6(worktree2))
5953
7171
  return { ok: false, error: `${worktree2} already exists; release ${task} first` };
5954
- mkdirSync4(dirname2(worktree2), { recursive: true });
7172
+ mkdirSync4(dirname3(worktree2), { recursive: true });
5955
7173
  const created = worktreeAdd(p.root, worktree2, branch, baseRef);
5956
7174
  if (!created)
5957
7175
  return { ok: false, error: `git worktree add failed for ${task}` };
5958
7176
  this.invalidateWorktrees(projectId);
5959
7177
  const expiresAt = nextExpiry(now);
5960
7178
  const acquiredAt = new Date(now).toISOString();
5961
- this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state)
5962
- VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held')
7179
+ this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
7180
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held', ?, ?)
5963
7181
  ON CONFLICT(project_id, task) DO UPDATE SET owner=excluded.owner, worktree=excluded.worktree, branch=excluded.branch,
5964
- acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held'`).run(projectId, task, owner, created, branch, acquiredAt, expiresAt);
7182
+ acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held',
7183
+ actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(projectId, task, owner, created, branch, acquiredAt, expiresAt, ...actorCols(this.actorFor(owner, sessionId)));
5965
7184
  this.append({
5966
7185
  ts: acquiredAt,
5967
7186
  type: "claim.acquired",
5968
7187
  projectId,
5969
- sessionId: null,
7188
+ sessionId,
7189
+ actor: this.actorFor(owner, sessionId),
5970
7190
  payload: { task, owner, worktree: created, branch, summary: `claim ${task} by ${owner}` }
5971
7191
  });
5972
7192
  const bootstrap = this.bootstrapWorktree(projectId, task, p.root, created);
@@ -6059,7 +7279,7 @@ class Store {
6059
7279
  for (const c of this.claimRows(p.id)) {
6060
7280
  if (c.state !== "held" || isActive(c, now))
6061
7281
  continue;
6062
- const exists = c.worktree ? existsSync5(c.worktree) : false;
7282
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6063
7283
  const work = exists ? heldWork(c.worktree) : null;
6064
7284
  if (reapAction(c, now, exists, work) !== "keep-orphaned")
6065
7285
  continue;
@@ -6115,7 +7335,7 @@ class Store {
6115
7335
  if (!row)
6116
7336
  return { ok: false, error: `no claim on ${task}` };
6117
7337
  const worktree2 = row.worktree ?? "";
6118
- if (worktree2 && existsSync5(worktree2)) {
7338
+ if (worktree2 && existsSync6(worktree2)) {
6119
7339
  const work = heldWork(worktree2);
6120
7340
  const can = canRelease(work, force);
6121
7341
  if (!can.ok)
@@ -6150,7 +7370,7 @@ class Store {
6150
7370
  continue;
6151
7371
  if (isActive({ ...c, state: "held" }, now))
6152
7372
  continue;
6153
- const exists = c.worktree ? existsSync5(c.worktree) : false;
7373
+ const exists = c.worktree ? existsSync6(c.worktree) : false;
6154
7374
  const work = exists ? heldWork(c.worktree) : null;
6155
7375
  const action = reapAction({ ...c, state: "held" }, now, exists, work);
6156
7376
  if (action === "not-expired")
@@ -6249,9 +7469,9 @@ class Store {
6249
7469
  if (!slug || slug === "." || slug === "..")
6250
7470
  return { ok: false, error: "bad worktree name" };
6251
7471
  const path = this.worktreePath(projectId, slug);
6252
- if (existsSync5(path))
7472
+ if (existsSync6(path))
6253
7473
  return { ok: false, error: `${path} already exists` };
6254
- mkdirSync4(dirname2(path), { recursive: true });
7474
+ mkdirSync4(dirname3(path), { recursive: true });
6255
7475
  const br = branch?.trim() || `wt/${slug}`;
6256
7476
  const created = worktreeAdd(p.root, path, br, baseRef);
6257
7477
  if (!created)
@@ -6642,19 +7862,19 @@ class Store {
6642
7862
  WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
6643
7863
  return r.n;
6644
7864
  }
6645
- ackIncident(seq) {
7865
+ ackIncident(seq, by) {
6646
7866
  const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
6647
7867
  if (!row)
6648
7868
  return false;
6649
- this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at) VALUES (?, ?)").run(seq, new Date().toISOString());
7869
+ this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id) VALUES (?, ?, ?, ?)").run(seq, new Date().toISOString(), ...actorCols(this.actorFor(by ?? "dashboard")));
6650
7870
  this.touch();
6651
7871
  return true;
6652
7872
  }
6653
- ackAllIncidents(projectId) {
7873
+ ackAllIncidents(projectId, by) {
6654
7874
  const at = new Date().toISOString();
6655
- const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at)
6656
- SELECT e.seq, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
6657
- WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...projectId ? [projectId] : []);
7875
+ const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id)
7876
+ SELECT e.seq, ?, ?, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
7877
+ WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...actorCols(this.actorFor(by ?? "dashboard")), ...projectId ? [projectId] : []);
6658
7878
  this.touch();
6659
7879
  return Number(r.changes);
6660
7880
  }
@@ -6821,8 +8041,8 @@ class Store {
6821
8041
  endedAt: null
6822
8042
  };
6823
8043
  this.db.query("UPDATE processes SET ended_at = ? WHERE ended_at IS NULL AND project_id = ? AND name = ?").run(p.startedAt, p.projectId, p.name);
6824
- this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at)
6825
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt);
8044
+ this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at, actor_kind, actor_id)
8045
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt, ...actorCols(this.actorFor(p.owner, p.sessionId)));
6826
8046
  this.append({
6827
8047
  ts: p.startedAt,
6828
8048
  type: "process.started",
@@ -6886,11 +8106,12 @@ class Store {
6886
8106
  expiresAt,
6887
8107
  released: false
6888
8108
  };
6889
- this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released)
6890
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
8109
+ this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released, actor_kind, actor_id)
8110
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
6891
8111
  ON CONFLICT(name, project_id) DO UPDATE SET
6892
8112
  kind=excluded.kind, owner=excluded.owner, session_id=excluded.session_id, pid=excluded.pid,
6893
- port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0`).run(resource.name, key, resource.kind, resource.owner, resource.sessionId, resource.pid, resource.port, resource.acquiredAt, resource.expiresAt);
8113
+ port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0,
8114
+ actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(resource.name, key, resource.kind, resource.owner, resource.sessionId, resource.pid, resource.port, resource.acquiredAt, resource.expiresAt, ...actorCols(this.actorFor(resource.owner, resource.sessionId)));
6894
8115
  this.append({
6895
8116
  ts: resource.acquiredAt,
6896
8117
  type: "resource.acquired",
@@ -6935,9 +8156,51 @@ class Store {
6935
8156
  seq() {
6936
8157
  return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
6937
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
+ }
6938
8201
  snapshot() {
6939
8202
  const worktrees = {};
6940
- const projects = this.projects();
8203
+ const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
6941
8204
  for (const p of projects)
6942
8205
  worktrees[p.id] = this.worktrees(p.id);
6943
8206
  return {
@@ -6945,6 +8208,7 @@ class Store {
6945
8208
  worktrees,
6946
8209
  sessions: this.memoised("sessions", 2000, () => this.sessions()),
6947
8210
  spend: this.memoised("spend", 30000, () => this.spend()),
8211
+ spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
6948
8212
  claims: this.claims(),
6949
8213
  processes: this.memoised("processes", 5000, () => this.processes()),
6950
8214
  incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
@@ -6955,7 +8219,7 @@ class Store {
6955
8219
  };
6956
8220
  }
6957
8221
  }
6958
- var WIRE_COLS = "seq, ts, type, project_id, session_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
8222
+ var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
6959
8223
  var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
6960
8224
  var TOOL_INPUT_MAX = 2048;
6961
8225
  var TOOL_RESPONSE_MAX = 4096;
@@ -7001,7 +8265,7 @@ function toWire(e) {
7001
8265
  }
7002
8266
  function wireRowToEvent(r) {
7003
8267
  const p = JSON.parse(r.payload ?? "null");
7004
- return {
8268
+ const e = {
7005
8269
  seq: r.seq,
7006
8270
  ts: r.ts,
7007
8271
  type: r.type,
@@ -7009,6 +8273,10 @@ function wireRowToEvent(r) {
7009
8273
  sessionId: r.session_id ?? null,
7010
8274
  payload: p
7011
8275
  };
8276
+ const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
8277
+ if (a)
8278
+ e.actor = a;
8279
+ return e;
7012
8280
  }
7013
8281
  function rowToEvent(r) {
7014
8282
  const e = {
@@ -7021,17 +8289,259 @@ function rowToEvent(r) {
7021
8289
  };
7022
8290
  if (r.raw)
7023
8291
  e.raw = JSON.parse(r.raw);
8292
+ const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
8293
+ if (a)
8294
+ e.actor = a;
7024
8295
  return e;
7025
8296
  }
8297
+ function osUser() {
8298
+ try {
8299
+ return userInfo().username || process.env.USER || "me";
8300
+ } catch {
8301
+ return process.env.USER || "me";
8302
+ }
8303
+ }
8304
+ var actorCols = (a) => [a.kind, a.id];
8305
+ function isScratchRoot(root) {
8306
+ const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
8307
+ return tmp.some((t) => root === t || root.startsWith(`${t}/`));
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
+ }
7026
8536
 
7027
8537
  // packages/daemon/src/app.ts
7028
- var VERSION = "0.7.0";
8538
+ var VERSION = "0.9.0";
7029
8539
  var WEB_DIR = (() => {
7030
8540
  if (process.env.SWARM_WEB_DIR)
7031
8541
  return process.env.SWARM_WEB_DIR;
7032
- const here = dirname3(fileURLToPath(import.meta.url));
8542
+ const here = dirname4(fileURLToPath2(import.meta.url));
7033
8543
  const dev = join9(here, "../../web/public");
7034
- return existsSync6(join9(dev, "index.html")) ? dev : join9(here, "../web");
8544
+ return existsSync7(join9(dev, "index.html")) ? dev : join9(here, "../web");
7035
8545
  })();
7036
8546
  var REPLAY_TAIL = 200;
7037
8547
  var wireCache = new WeakMap;
@@ -7043,17 +8553,69 @@ function wireJson(e) {
7043
8553
  }
7044
8554
  return s;
7045
8555
  }
7046
- function createApp(store = new Store) {
8556
+ function hookRepoRoot(store, raw2) {
8557
+ const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
8558
+ return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
8559
+ }
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 = {}) {
7047
8586
  const app = new Hono2;
7048
8587
  const forge2 = new ForgeService(store);
7049
8588
  const runner = new Runner(store, store.home);
7050
8589
  const dispatcher = new Dispatcher(store, runner, forge2);
8590
+ const workflows2 = new WorkflowEngine(store, runner, forge2);
7051
8591
  store.onBudgetStop((projectId) => {
7052
8592
  dispatcher.clear(projectId);
7053
8593
  for (const run2 of runner.list(projectId))
7054
8594
  runner.stop(run2.id);
7055
8595
  });
7056
- app.get("/v1/health", (c) => c.json({ ok: true, version: VERSION }));
8596
+ app.use("/v1/*", async (c, next) => {
8597
+ if (c.req.path === "/v1/health")
8598
+ return next();
8599
+ const token = readToken(store.home);
8600
+ const given = c.req.header("authorization")?.replace(/^Bearer\s+/i, "") ?? c.req.query("token");
8601
+ if (token && given && given === token)
8602
+ return next();
8603
+ if (given)
8604
+ return c.json({ error: "unauthorized: wrong daemon token" }, 401);
8605
+ const ip = c.env?.requestIP?.(c.req.raw)?.address;
8606
+ const loopback = !ip || ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
8607
+ if (loopback && store.policyFor(null).config.daemon.auth !== "required")
8608
+ return next();
8609
+ return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
8610
+ });
8611
+ app.get("/v1/health", (c) => c.json({
8612
+ disk: diskVersion(),
8613
+ hooksInstalled: hookCoverage(claudeSettings()).complete,
8614
+ ok: true,
8615
+ version: VERSION,
8616
+ schema: store.schemaVersion(),
8617
+ auth: store.policyFor(null).config.daemon.auth
8618
+ }));
7057
8619
  app.get("/v1/projects", (c) => c.json(store.snapshot().projects));
7058
8620
  app.post("/v1/projects", async (c) => {
7059
8621
  const { path, name } = await c.req.json();
@@ -7072,22 +8634,24 @@ function createApp(store = new Store) {
7072
8634
  return c.json(store.reorderProjects(ids));
7073
8635
  });
7074
8636
  app.patch("/v1/projects/:id", async (c) => {
7075
- const { pinned, name } = await c.req.json().catch(() => ({}));
7076
- const p = store.updateProject(c.req.param("id"), { pinned, name });
7077
- return p ? c.json(p) : c.json({ error: "not found" }, 404);
8637
+ const { pinned, name, icon, color } = await c.req.json().catch(() => ({}));
8638
+ if (!store.project(c.req.param("id")))
8639
+ return c.json({ error: "not found" }, 404);
8640
+ const p = store.updateProject(c.req.param("id"), { pinned, name, icon, color });
8641
+ return p ? c.json(p) : c.json({ error: "icon is at most 4 characters; color is c1\u2026c7" }, 400);
7078
8642
  });
7079
8643
  app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
7080
8644
  app.get("/v1/fs/ls", (c) => {
7081
8645
  const q = c.req.query("path");
7082
8646
  let dir;
7083
8647
  try {
7084
- dir = realpathSync3(q && existsSync6(q) ? q : homedir4());
8648
+ dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
7085
8649
  } catch {
7086
8650
  dir = homedir4();
7087
8651
  }
7088
8652
  try {
7089
- 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));
7090
- 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);
7091
8655
  return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
7092
8656
  } catch (e) {
7093
8657
  return c.json({ error: e.message, path: dir }, 400);
@@ -7112,6 +8676,37 @@ function createApp(store = new Store) {
7112
8676
  })
7113
8677
  });
7114
8678
  });
8679
+ app.get("/v1/policy", (c) => {
8680
+ const id = c.req.query("project");
8681
+ const p = id ? store.project(id) : null;
8682
+ if (id && !p)
8683
+ return c.json({ error: "unknown project" }, 404);
8684
+ const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
8685
+ return c.json({ ...policy2, provenance, overridden });
8686
+ });
8687
+ app.get("/v1/audit", (c) => {
8688
+ const since = sinceToIso(c.req.query("since"));
8689
+ if (c.req.query("since") && !since)
8690
+ return c.json({ error: "since: use 30d / 12h / 90m or an ISO date" }, 400);
8691
+ const pid = c.req.query("project") || null;
8692
+ if (pid && !store.project(pid))
8693
+ return c.json({ error: "unknown project" }, 404);
8694
+ const rows = store.audit({
8695
+ since,
8696
+ projectId: pid,
8697
+ type: c.req.query("type") || null,
8698
+ limit: Number(c.req.query("limit")) || undefined
8699
+ });
8700
+ const format = c.req.query("format") === "csv" ? "csv" : c.req.query("format") === "jsonl" ? "jsonl" : "json";
8701
+ const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
8702
+ return c.body(formatAudit(rows, format), 200, { "content-type": ct });
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
+ });
7115
8710
  app.get("/v1/rules/dryrun", (c) => {
7116
8711
  const projectId = c.req.query("project");
7117
8712
  if (!projectId)
@@ -7125,11 +8720,11 @@ function createApp(store = new Store) {
7125
8720
  });
7126
8721
  app.post("/v1/incidents/ack", async (c) => {
7127
8722
  const body = await c.req.json().catch(() => ({}));
7128
- return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined) });
8723
+ return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined, body.by) });
7129
8724
  });
7130
8725
  app.post("/v1/incidents/:seq/ack", (c) => {
7131
8726
  const seq = Number(c.req.param("seq"));
7132
- if (!Number.isInteger(seq) || !store.ackIncident(seq))
8727
+ if (!Number.isInteger(seq) || !store.ackIncident(seq, c.req.query("by")))
7133
8728
  return c.json({ ok: false, error: "no such incident" }, 404);
7134
8729
  return c.json({ ok: true });
7135
8730
  });
@@ -7246,6 +8841,58 @@ function createApp(store = new Store) {
7246
8841
  return c.json(r, r.ok ? 200 : 409);
7247
8842
  });
7248
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
+ });
7249
8896
  app.get("/v1/dispatch", (c) => {
7250
8897
  const project = c.req.query("project");
7251
8898
  if (!project)
@@ -7377,7 +9024,7 @@ function createApp(store = new Store) {
7377
9024
  const b = await c.req.json();
7378
9025
  if (!b.projectId || !b.task)
7379
9026
  return c.json({ error: "projectId and task required" }, 400);
7380
- const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef);
9027
+ const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef, b.sessionId ?? null);
7381
9028
  return c.json(r, r.ok ? 201 : 409);
7382
9029
  });
7383
9030
  app.post("/v1/claims/renew", async (c) => {
@@ -7524,6 +9171,7 @@ function createApp(store = new Store) {
7524
9171
  const raw2 = await c.req.json().catch(() => ({}));
7525
9172
  store.ingestHook(event, raw2);
7526
9173
  if (event === "SessionStart" && typeof raw2.cwd === "string") {
9174
+ store.checkPolicy(raw2.cwd, typeof raw2.session_id === "string" ? raw2.session_id : null);
7527
9175
  const ctx = store.sessionContext(raw2.cwd);
7528
9176
  if (ctx)
7529
9177
  return c.json({
@@ -7533,7 +9181,7 @@ function createApp(store = new Store) {
7533
9181
  }
7534
9182
  const sid = typeof raw2.session_id === "string" ? raw2.session_id : null;
7535
9183
  const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
7536
- if (event === "PreToolUse" && process.env.SWARM_GUARD !== "off") {
9184
+ if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw2))) {
7537
9185
  const guard = store.guardHook(raw2);
7538
9186
  if (guard) {
7539
9187
  return c.json({
@@ -7566,7 +9214,7 @@ function createApp(store = new Store) {
7566
9214
  await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
7567
9215
  }
7568
9216
  await stream2.writeSSE({ event: "ping", data: "" });
7569
- await new Promise((resolve) => {
9217
+ await new Promise((resolve2) => {
7570
9218
  const off = store.subscribe((e) => {
7571
9219
  stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
7572
9220
  });
@@ -7574,7 +9222,7 @@ function createApp(store = new Store) {
7574
9222
  stream2.onAbort(() => {
7575
9223
  clearInterval(beat);
7576
9224
  off();
7577
- resolve();
9225
+ resolve2();
7578
9226
  });
7579
9227
  });
7580
9228
  });
@@ -7584,18 +9232,107 @@ function createApp(store = new Store) {
7584
9232
  app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
7585
9233
  const f = c.req.param("file");
7586
9234
  const p = join9(WEB_DIR, f);
7587
- if (!existsSync6(p))
9235
+ if (!existsSync7(p))
7588
9236
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
7589
9237
  return c.body(readFileSync4(p, "utf8"), 200, {
7590
9238
  "content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
7591
9239
  });
7592
9240
  });
7593
- 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));
7594
9328
  }
7595
9329
 
7596
9330
  // packages/daemon/src/bin.ts
7597
9331
  var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
7598
- 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);
7599
9336
  function serve() {
7600
9337
  const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
7601
9338
  try {
@@ -7607,20 +9344,50 @@ function serve() {
7607
9344
  return bind(0);
7608
9345
  }
7609
9346
  }
7610
- 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();
7611
9371
  var port = server.port ?? DEFAULT_PORT2;
9372
+ ensureToken();
7612
9373
  writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
7613
9374
  var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
7614
9375
  var backfillMs = backfillDays * 24 * 60 * 60000;
7615
- store.tailCodex(backfillMs);
7616
- 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
+ }
7617
9382
  var tick = 0;
7618
9383
  var tailer = setInterval(() => {
7619
9384
  tick++;
7620
- store.tailActive();
7621
- if (tick % 3 === 0 || store.hasActiveSessions()) {
9385
+ if (!DEMO)
9386
+ store.tailActive();
9387
+ if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
7622
9388
  store.tailCodex();
7623
9389
  store.tailGrok();
9390
+ store.tailGemini();
7624
9391
  }
7625
9392
  store.reapResources();
7626
9393
  store.reapProcesses();