@ra3orblade/swarm 0.7.0 → 0.8.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,6 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // packages/client/src/daemon.ts
5
+ import { randomBytes } from "crypto";
5
6
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6
7
  import { homedir } from "os";
7
8
  import { join } from "path";
@@ -18,9 +19,65 @@ function writeDaemonInfo(info) {
18
19
  `);
19
20
  return full;
20
21
  }
22
+ var tokenFile = (home = swarmHome()) => join(home, "token");
23
+ function readToken(home = swarmHome()) {
24
+ try {
25
+ const t = readFileSync(tokenFile(home), "utf8").trim();
26
+ return /^[a-f0-9]{64}$/.test(t) ? t : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function ensureToken(home = swarmHome()) {
32
+ const cur = readToken(home);
33
+ if (cur)
34
+ return cur;
35
+ mkdirSync(home, { recursive: true });
36
+ const t = randomBytes(32).toString("hex");
37
+ writeFileSync(tokenFile(home), `${t}
38
+ `, { mode: 384 });
39
+ return t;
40
+ }
21
41
  function clearDaemonInfo() {
22
42
  rmSync(infoFile(), { force: true });
23
43
  }
44
+ // packages/core/src/actor.ts
45
+ var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
46
+ var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
47
+ function actorFrom(owner, sessionId, opts = {}) {
48
+ const o = (owner ?? "").trim();
49
+ const sid = sessionId?.trim() || undefined;
50
+ if (opts.runId)
51
+ return { kind: "run", id: opts.runId, session: sid };
52
+ if (o.startsWith("auto:"))
53
+ return { kind: "daemon", id: "daemon", session: sid };
54
+ if (DAEMON_ALIASES.has(o))
55
+ return { kind: "daemon", id: "daemon" };
56
+ if (o === "agent" || o.startsWith("agent:") || o.startsWith("session:"))
57
+ return {
58
+ kind: "agent",
59
+ id: sid ?? o.replace(/^(agent|session):/, "") ?? "agent",
60
+ session: sid
61
+ };
62
+ if (!o || HUMAN_ALIASES.has(o)) {
63
+ if (!o && sid)
64
+ return { kind: "agent", id: sid, session: sid };
65
+ return { kind: "human", id: opts.user?.trim() || "me" };
66
+ }
67
+ if (sid && o === sid)
68
+ return { kind: "agent", id: sid, session: sid };
69
+ return { kind: "human", id: o };
70
+ }
71
+ function actorFromColumns(kind, id, session) {
72
+ if (!kind || !id)
73
+ return null;
74
+ if (!["human", "agent", "run", "daemon"].includes(kind))
75
+ return null;
76
+ const a = { kind, id };
77
+ if (session && kind !== "human" && kind !== "daemon")
78
+ a.session = session;
79
+ return a;
80
+ }
24
81
  // packages/core/src/adapters/claude-code/transcript.ts
25
82
  function parseTranscriptChunk(chunk) {
26
83
  const out = {
@@ -229,6 +286,18 @@ function parseGrokUpdates(chunk) {
229
286
  return out;
230
287
  }
231
288
  // packages/core/src/adapters/claude-code/hooks.ts
289
+ var HOOK_EVENTS = [
290
+ "SessionStart",
291
+ "UserPromptSubmit",
292
+ "PreToolUse",
293
+ "PostToolUse",
294
+ "SubagentStart",
295
+ "SubagentStop",
296
+ "Stop",
297
+ "SessionEnd",
298
+ "Notification",
299
+ "PreCompact"
300
+ ];
232
301
  var MAP = {
233
302
  SessionStart: "session.started",
234
303
  UserPromptSubmit: "prompt.submitted",
@@ -322,6 +391,155 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
322
391
  payload.prompt = raw.prompt;
323
392
  return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
324
393
  }
394
+ // packages/core/src/audit.ts
395
+ var AUDIT_TYPES = new Set([
396
+ "session.started",
397
+ "session.ended",
398
+ "tool.denied",
399
+ "claim.acquired",
400
+ "claim.renewed",
401
+ "claim.released",
402
+ "claim.expired",
403
+ "claim.orphaned",
404
+ "worktree.created",
405
+ "worktree.removed",
406
+ "worktree.bootstrapped",
407
+ "pr.opened",
408
+ "question.asked",
409
+ "question.answered",
410
+ "dispatch.queued",
411
+ "dispatch.started",
412
+ "dispatch.finished",
413
+ "resource.acquired",
414
+ "resource.released",
415
+ "resource.reaped",
416
+ "process.started",
417
+ "process.exited",
418
+ "gate.recorded",
419
+ "handoff.recorded",
420
+ "permission.requested",
421
+ "permission.resolved",
422
+ "incident.opened",
423
+ "incident.acked",
424
+ "run.result"
425
+ ]);
426
+ var isAuditType = (t) => AUDIT_TYPES.has(t);
427
+ var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
428
+ var DEFAULT_PRIVACY = {
429
+ store_prompts: true,
430
+ store_reasoning: true,
431
+ redact: []
432
+ };
433
+ var BUILTIN_REDACT = [
434
+ /\b(sk|pk|rk|ghp|gho|ghu|ghs|xoxb|xoxp|AKIA)[A-Za-z0-9_-]{16,}\b/g,
435
+ /\bBearer\s+[A-Za-z0-9._-]{20,}/g
436
+ ];
437
+ function compileRedactions(patterns) {
438
+ const out = [...BUILTIN_REDACT];
439
+ for (const p of patterns) {
440
+ try {
441
+ out.push(new RegExp(p, "g"));
442
+ } catch {}
443
+ }
444
+ return out;
445
+ }
446
+ function redactValue(v, res) {
447
+ if (!res.length)
448
+ return v;
449
+ if (typeof v === "string")
450
+ return redactString(v, res);
451
+ if (Array.isArray(v)) {
452
+ let changed = false;
453
+ const out = v.map((x) => {
454
+ const r = redactValue(x, res);
455
+ if (r !== x)
456
+ changed = true;
457
+ return r;
458
+ });
459
+ return changed ? out : v;
460
+ }
461
+ if (v && typeof v === "object") {
462
+ let changed = false;
463
+ const out = {};
464
+ for (const [k, x] of Object.entries(v)) {
465
+ const r = redactValue(x, res);
466
+ if (r !== x)
467
+ changed = true;
468
+ out[k] = r;
469
+ }
470
+ return changed ? out : v;
471
+ }
472
+ return v;
473
+ }
474
+ function redactString(s, res) {
475
+ let out = s;
476
+ for (const re of res) {
477
+ re.lastIndex = 0;
478
+ if (re.test(out)) {
479
+ re.lastIndex = 0;
480
+ out = out.replace(re, "[redacted]");
481
+ }
482
+ }
483
+ return out;
484
+ }
485
+ var AUDIT_COLUMNS = [
486
+ "seq",
487
+ "ts",
488
+ "type",
489
+ "projectId",
490
+ "sessionId",
491
+ "actorKind",
492
+ "actorId",
493
+ "summary",
494
+ "payload"
495
+ ];
496
+ function auditRow(e) {
497
+ const p = e.payload ?? {};
498
+ 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) : "";
499
+ return {
500
+ seq: e.seq ?? 0,
501
+ ts: e.ts,
502
+ type: e.type,
503
+ projectId: e.projectId,
504
+ sessionId: e.sessionId,
505
+ actorKind: e.actor?.kind ?? null,
506
+ actorId: e.actor?.id ?? null,
507
+ summary: summary.slice(0, 400),
508
+ payload: e.payload ?? null
509
+ };
510
+ }
511
+ var csvCell = (v) => {
512
+ const s = v == null ? "" : typeof v === "string" ? v : JSON.stringify(v);
513
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
514
+ };
515
+ function formatAudit(rows, format) {
516
+ if (format === "json")
517
+ return JSON.stringify(rows);
518
+ if (format === "csv") {
519
+ const lines = [
520
+ AUDIT_COLUMNS.join(","),
521
+ ...rows.map((r) => AUDIT_COLUMNS.map((c) => csvCell(r[c])).join(","))
522
+ ];
523
+ return `${lines.join(`
524
+ `)}
525
+ `;
526
+ }
527
+ return `${rows.map((r) => JSON.stringify(r)).join(`
528
+ `)}${rows.length ? `
529
+ ` : ""}`;
530
+ }
531
+ function sinceToIso(since, now = Date.now()) {
532
+ if (!since)
533
+ return null;
534
+ const m = /^(\d+)([dhm])$/.exec(since.trim());
535
+ if (m) {
536
+ const n = Number(m[1]);
537
+ const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
538
+ return new Date(now - n * ms).toISOString();
539
+ }
540
+ const t = Date.parse(since);
541
+ return Number.isNaN(t) ? null : new Date(t).toISOString();
542
+ }
325
543
  // packages/core/src/budget.ts
326
544
  function budgetStatus(spent, cfg) {
327
545
  const part = (s, l) => ({
@@ -385,24 +603,33 @@ function parseGateDefs(gates) {
385
603
  if (!isRecord(gates))
386
604
  return out;
387
605
  for (const [name, v] of Object.entries(gates)) {
388
- if (!isRecord(v) || typeof v.cmd !== "string" || !v.cmd.trim())
606
+ if (!isRecord(v))
607
+ continue;
608
+ const builtin = v.builtin === "review" ? "review" : null;
609
+ const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
610
+ if (!cmd && !builtin)
389
611
  continue;
390
612
  if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
391
613
  continue;
392
614
  const t = Number(v.timeout);
393
615
  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
616
+ cmd: builtin ? "" : cmd,
617
+ timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
618
+ cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
619
+ builtin,
620
+ model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
397
621
  };
398
622
  }
399
623
  return out;
400
624
  }
401
625
  var DEFAULT_CONFIG = {
402
- daemon: { port: 7777 },
626
+ daemon: { port: 7777, auth: "loopback-optional" },
403
627
  tasks: { source: null, labels: [], team: null },
404
628
  gates: { required: [], auto: "session-end", defs: {} },
405
629
  budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
630
+ events: { retain_days: 30 },
631
+ audit: { retain_days: 0 },
632
+ privacy: DEFAULT_PRIVACY,
406
633
  dispatch: {
407
634
  max_parallel: 2,
408
635
  permission_mode: null,
@@ -450,6 +677,10 @@ function isRepoRelative(f) {
450
677
  return false;
451
678
  return !t.split(/[/\\]/).some((seg) => seg === "..");
452
679
  }
680
+ var days = (v, fallback) => {
681
+ const n = Number(v);
682
+ return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
683
+ };
453
684
  function validate(c) {
454
685
  const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
455
686
  const port = Number(c.daemon?.port);
@@ -470,7 +701,10 @@ function validate(c) {
470
701
  const auto = rawGates?.auto;
471
702
  return {
472
703
  ...c,
473
- daemon: { port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777 },
704
+ daemon: {
705
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
706
+ auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
707
+ },
474
708
  tasks: {
475
709
  source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
476
710
  labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
@@ -487,6 +721,17 @@ function validate(c) {
487
721
  warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
488
722
  on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
489
723
  },
724
+ events: {
725
+ retain_days: days(c.events?.retain_days, 30)
726
+ },
727
+ audit: {
728
+ retain_days: days(c.audit?.retain_days, 0)
729
+ },
730
+ privacy: {
731
+ store_prompts: c.privacy?.store_prompts !== false,
732
+ store_reasoning: c.privacy?.store_reasoning !== false,
733
+ redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
734
+ },
490
735
  dispatch: {
491
736
  max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
492
737
  permission_mode: str(d.permission_mode),
@@ -514,18 +759,84 @@ function validate(c) {
514
759
  }
515
760
  };
516
761
  }
517
- function loadConfig(opts = {}) {
762
+ function leafPaths(v, prefix = "") {
763
+ if (!isRecord(v))
764
+ return prefix ? [prefix] : [];
765
+ const keys = Object.keys(v);
766
+ if (keys.length === 0)
767
+ return prefix ? [prefix] : [];
768
+ return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
769
+ }
770
+ function getPath(v, path) {
771
+ let cur = v;
772
+ for (const seg of path.split(".")) {
773
+ if (!isRecord(cur))
774
+ return;
775
+ cur = cur[seg];
776
+ }
777
+ return cur;
778
+ }
779
+ function setPath(obj, path, value) {
780
+ const segs = path.split(".");
781
+ let cur = obj;
782
+ for (const seg of segs.slice(0, -1)) {
783
+ if (!isRecord(cur[seg]))
784
+ cur[seg] = {};
785
+ cur = cur[seg];
786
+ }
787
+ cur[segs[segs.length - 1]] = value;
788
+ }
789
+ var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
790
+ function readLayer(path) {
791
+ return existsSync2(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
792
+ }
793
+ function loadConfigDetailed(opts = {}) {
518
794
  const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
795
+ const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
796
+ const policyRaw = readLayer(policyPath);
797
+ const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
798
+ const policy = { ...policyRaw ?? {} };
799
+ delete policy.locked;
800
+ const layers = [
801
+ ["policy", policyRaw ? policy : null],
802
+ ["global", readLayer(join2(home, "config.toml"))],
803
+ ["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
804
+ ];
805
+ const provenance = {};
806
+ for (const p of leafPaths(DEFAULT_CONFIG))
807
+ provenance[p] = "default";
808
+ const overridden = [];
519
809
  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));
810
+ for (const [layer, raw] of layers) {
811
+ if (!raw)
812
+ continue;
813
+ for (const p of leafPaths(raw)) {
814
+ const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
815
+ if (lock)
816
+ overridden.push({ key: p, layer, attempted: getPath(raw, p) });
817
+ else
818
+ provenance[p] = layer;
819
+ }
820
+ cfg = merge(cfg, raw);
821
+ }
822
+ if (overridden.length) {
823
+ const out = structuredClone(cfg);
824
+ for (const { key } of overridden) {
825
+ const fromPolicy = getPath(policy, key);
826
+ setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
827
+ provenance[key] = fromPolicy === undefined ? "default" : "policy";
828
+ }
829
+ cfg = out;
527
830
  }
528
- return validate(cfg);
831
+ return {
832
+ config: validate(cfg),
833
+ provenance,
834
+ overridden,
835
+ policy: { path: policyRaw ? policyPath : null, locked }
836
+ };
837
+ }
838
+ function loadConfig(opts = {}) {
839
+ return loadConfigDetailed(opts).config;
529
840
  }
530
841
  // packages/core/src/dispatch.ts
531
842
  function planDispatch(tasks, wanted, opts) {
@@ -1328,6 +1639,92 @@ function parseMemoryQuery(q) {
1328
1639
  }
1329
1640
  return { match: terms.join(" "), kind, task };
1330
1641
  }
1642
+ // packages/core/src/policy.ts
1643
+ import { createHash } from "crypto";
1644
+ var HOOK_MARK = "swarm-hook";
1645
+ var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
1646
+ var MIN_HOOK_TIMEOUT_S = 5;
1647
+ function hookCoverage(settings) {
1648
+ const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
1649
+ const missing = [];
1650
+ const short = [];
1651
+ for (const ev of HOOK_EVENTS) {
1652
+ const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
1653
+ const ours = groups.flatMap((g) => {
1654
+ const list = g?.hooks;
1655
+ return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
1656
+ });
1657
+ if (!ours.length)
1658
+ missing.push(ev);
1659
+ else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
1660
+ short.push(ev);
1661
+ }
1662
+ return { missing, short, complete: !missing.length && !short.length };
1663
+ }
1664
+ var hasLockedRules = (loaded) => loaded.policy.locked.some((k) => k === "rules" || k.startsWith("rules."));
1665
+ function policyFindings(input) {
1666
+ const out = [];
1667
+ const repo = input.repoRoot ?? "";
1668
+ for (const o of input.loaded.overridden)
1669
+ out.push({
1670
+ key: `override:${repo}:${o.layer}:${o.key}`,
1671
+ subject: `${o.layer === "repo" ? ".swarm.toml" : "config.toml"} ${o.key}`,
1672
+ reason: `locked by policy; ${o.layer} config tried to set ${JSON.stringify(o.attempted)}`
1673
+ });
1674
+ const cov = input.coverage;
1675
+ if (cov && !cov.complete) {
1676
+ if (cov.missing.length)
1677
+ out.push({
1678
+ key: `hooks:missing:${cov.missing.join(",")}`,
1679
+ subject: `hooks ${cov.missing.join(", ")}`,
1680
+ reason: "swarm hook entry removed from settings.json \u2014 run: swarm install"
1681
+ });
1682
+ if (cov.short.length)
1683
+ out.push({
1684
+ key: `hooks:short:${cov.short.join(",")}`,
1685
+ subject: `hooks ${cov.short.join(", ")}`,
1686
+ reason: `hook timeout below ${MIN_HOOK_TIMEOUT_S}s \u2014 run: swarm install`
1687
+ });
1688
+ }
1689
+ if (input.guardOff && hasLockedRules(input.loaded))
1690
+ out.push({
1691
+ key: "guard:off",
1692
+ subject: "SWARM_GUARD=off",
1693
+ reason: "policy locks rules; SWARM_GUARD=off is ignored for locked rules"
1694
+ });
1695
+ return out;
1696
+ }
1697
+ var POLICY_CACHE_VERSION = 1;
1698
+ var POLICY_CACHE_FILE = "policy.cache.json";
1699
+ var RULE_KEYS = [
1700
+ "shared_tree",
1701
+ "destructive_git",
1702
+ "pattern_kill",
1703
+ "protected_ports",
1704
+ "no_foreign_worktree",
1705
+ "claim_required_to_write"
1706
+ ];
1707
+ var lockedKey = (locked, key) => locked.some((l) => l === "rules" || key === l || key.startsWith(`${l}.`));
1708
+ function offlineModes(loaded) {
1709
+ const locked = loaded.policy.locked;
1710
+ const out = { ...DEFAULT_MODES, protected: { ports: [] } };
1711
+ for (const k of RULE_KEYS)
1712
+ out[k] = lockedKey(locked, `rules.${k}`) ? loaded.config.rules[k] : "off";
1713
+ if (lockedKey(locked, "rules.protected.ports"))
1714
+ out.protected = { ports: [...loaded.config.rules.protected.ports] };
1715
+ return out;
1716
+ }
1717
+ var digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
1718
+ function buildPolicyCache(loaded, sessions, worktrees, now = new Date) {
1719
+ const body = {
1720
+ version: POLICY_CACHE_VERSION,
1721
+ writtenAt: now.toISOString(),
1722
+ modes: offlineModes(loaded),
1723
+ sessions,
1724
+ worktrees
1725
+ };
1726
+ return { ...body, sha256: digest(body) };
1727
+ }
1331
1728
  // packages/core/src/pricing.ts
1332
1729
  var PRICES = {
1333
1730
  "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
@@ -1474,6 +1871,114 @@ function acquireRefusalMessage(holder) {
1474
1871
  const via = isTrackedPid(holder.pid) ? `pid ${holder.pid}` : holder.expiresAt ? `lease until ${holder.expiresAt}` : "unbounded";
1475
1872
  return `Resource "${holder.name}" is held by ${holder.owner} (${via}).` + ` Pick another name, coordinate with the holder, or wait for release/reap.`;
1476
1873
  }
1874
+ // packages/core/src/review.ts
1875
+ 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";
1876
+ var REVIEW_PATCH_MAX = 120000;
1877
+ function reviewPrompt(input) {
1878
+ const patch = input.patch.length > REVIEW_PATCH_MAX ? `${input.patch.slice(0, REVIEW_PATCH_MAX)}
1879
+
1880
+ [\u2026 patch truncated at ${REVIEW_PATCH_MAX} chars; read the files for the rest]` : input.patch;
1881
+ return [
1882
+ `You are the review gate for task ${input.task}${input.title ? ` \u2014 ${input.title}` : ""}${input.branch ? ` (branch ${input.branch})` : ""}.`,
1883
+ "You are read-only: you may Read, Grep and Glob files in this worktree to understand context. Do not edit anything.",
1884
+ "",
1885
+ "Judge the diff below against this rubric and nothing else:",
1886
+ `- ${REVIEW_RUBRIC}`,
1887
+ "- A finding is a concrete defect with a file and, when possible, a line \u2014 not style, not preference.",
1888
+ "- Severity: blocker (must not merge), major (should not merge), minor (worth fixing), nit.",
1889
+ '- verdict is "fail" if and only if there is at least one blocker or major finding.',
1890
+ "",
1891
+ "Respond with ONLY a JSON object, no prose, no code fence:",
1892
+ '{"verdict":"pass"|"fail","summary":"one sentence","findings":[{"file":"path","line":123,"severity":"blocker|major|minor|nit","summary":"what is wrong and why"}]}',
1893
+ "",
1894
+ "Files changed:",
1895
+ input.stat.trim() || "(no stat)",
1896
+ "",
1897
+ "Diff:",
1898
+ patch.trim() || "(empty diff)"
1899
+ ].join(`
1900
+ `);
1901
+ }
1902
+ function reviewArgs(prompt, opts = {}) {
1903
+ const args = [
1904
+ "-p",
1905
+ prompt,
1906
+ "--output-format",
1907
+ "json",
1908
+ "--allowedTools",
1909
+ "Read",
1910
+ "Grep",
1911
+ "Glob",
1912
+ "LS",
1913
+ "--disallowedTools",
1914
+ "Edit",
1915
+ "Write",
1916
+ "MultiEdit",
1917
+ "NotebookEdit",
1918
+ "Bash",
1919
+ "WebFetch",
1920
+ "WebSearch",
1921
+ "--permission-mode",
1922
+ "dontAsk"
1923
+ ];
1924
+ if (opts.model)
1925
+ args.push("--model", opts.model);
1926
+ return args;
1927
+ }
1928
+ function parseReviewVerdict(stdout) {
1929
+ let text = stdout.trim();
1930
+ try {
1931
+ const env = JSON.parse(text);
1932
+ if (env && typeof env === "object" && typeof env.result === "string")
1933
+ text = env.result.trim();
1934
+ } catch {}
1935
+ text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1936
+ const start = text.indexOf("{");
1937
+ const end = text.lastIndexOf("}");
1938
+ if (start < 0 || end <= start)
1939
+ return null;
1940
+ let obj;
1941
+ try {
1942
+ obj = JSON.parse(text.slice(start, end + 1));
1943
+ } catch {
1944
+ return null;
1945
+ }
1946
+ if (!obj || typeof obj !== "object")
1947
+ return null;
1948
+ const o = obj;
1949
+ const findings = Array.isArray(o.findings) ? o.findings.filter((f) => !!f && typeof f === "object").map((f) => ({
1950
+ file: String(f.file ?? "").slice(0, 300),
1951
+ line: Number.isInteger(f.line) ? f.line : null,
1952
+ severity: ["blocker", "major", "minor", "nit"].includes(String(f.severity)) ? String(f.severity) : "minor",
1953
+ summary: String(f.summary ?? "").slice(0, 600)
1954
+ })).filter((f) => f.summary) : [];
1955
+ const serious = findings.some((f) => f.severity === "blocker" || f.severity === "major");
1956
+ const verdict = serious ? "fail" : o.verdict === "fail" ? "fail" : "pass";
1957
+ return { verdict, summary: String(o.summary ?? "").slice(0, 400), findings };
1958
+ }
1959
+ function reviewGateInput(task, gate, outcome) {
1960
+ if (outcome.kind === "error")
1961
+ return {
1962
+ task,
1963
+ gate,
1964
+ verdict: "fail",
1965
+ rubric: REVIEW_RUBRIC,
1966
+ evidence: `reviewer did not answer: ${outcome.reason}${outcome.output ? `
1967
+ ${outcome.output.slice(-1500)}` : ""}`
1968
+ };
1969
+ const v = outcome.verdict;
1970
+ const lines = v.findings.map((f) => `- [${f.severity}] ${f.file}${f.line ? `:${f.line}` : ""} \u2014 ${f.summary}`);
1971
+ const secs = (outcome.durationMs / 1000).toFixed(0);
1972
+ return {
1973
+ task,
1974
+ gate,
1975
+ verdict: v.verdict,
1976
+ rubric: REVIEW_RUBRIC,
1977
+ evidence: `${v.summary || (v.verdict === "pass" ? "no blocking findings" : "blocking findings")} (${v.findings.length} finding${v.findings.length === 1 ? "" : "s"}, ${secs}s)${lines.length ? `
1978
+ ${lines.join(`
1979
+ `)}` : ""}`
1980
+ };
1981
+ }
1477
1982
  // packages/core/src/tasks.ts
1478
1983
  var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
1479
1984
  var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
@@ -1918,7 +2423,7 @@ var tryDecode = (str, decoder) => {
1918
2423
  }
1919
2424
  };
1920
2425
  var tryDecodeURI = (str) => tryDecode(str, decodeURI);
1921
- var getPath = (request) => {
2426
+ var getPath2 = (request) => {
1922
2427
  const url = request.url;
1923
2428
  const start = url.indexOf("/", url.indexOf(":") + 4);
1924
2429
  let i = start;
@@ -1937,7 +2442,7 @@ var getPath = (request) => {
1937
2442
  return url.slice(start, i);
1938
2443
  };
1939
2444
  var getPathNoStrict = (request) => {
1940
- const result = getPath(request);
2445
+ const result = getPath2(request);
1941
2446
  return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
1942
2447
  };
1943
2448
  var mergePath = (base, sub, ...rest) => {
@@ -2459,7 +2964,7 @@ var Hono = class _Hono {
2459
2964
  };
2460
2965
  const { strict, ...optionsWithoutStrict } = options;
2461
2966
  Object.assign(this, optionsWithoutStrict);
2462
- this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
2967
+ this.getPath = strict ?? true ? options.getPath ?? getPath2 : getPathNoStrict;
2463
2968
  }
2464
2969
  #clone() {
2465
2970
  const clone = new _Hono({
@@ -4309,9 +4814,10 @@ import {
4309
4814
  realpathSync as realpathSync2,
4310
4815
  renameSync,
4311
4816
  statSync,
4817
+ unlinkSync,
4312
4818
  writeFileSync as writeFileSync2
4313
4819
  } from "fs";
4314
- import { homedir as homedir3 } from "os";
4820
+ import { homedir as homedir3, tmpdir, userInfo } from "os";
4315
4821
  import { basename, dirname as dirname2, join as join8 } from "path";
4316
4822
 
4317
4823
  // packages/daemon/src/bootstrap.ts
@@ -4519,6 +5025,9 @@ class Store {
4519
5025
  this.db.exec(SCHEMA);
4520
5026
  this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
4521
5027
  this.ensureColumn("projects", "sort_order", "INTEGER");
5028
+ this.ensureColumn("projects", "icon", "TEXT");
5029
+ this.ensureColumn("projects", "color", "TEXT");
5030
+ this.migrate();
4522
5031
  this.migrateProjectsJson(join8(home, "projects.json"));
4523
5032
  this.reconcileMovedProjects();
4524
5033
  this.slimExistingEvents();
@@ -4601,6 +5110,57 @@ class Store {
4601
5110
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
4602
5111
  }
4603
5112
  }
5113
+ static SCHEMA_VERSION = 1;
5114
+ schemaVersion() {
5115
+ return Number(this.meta("schema_version") ?? 0);
5116
+ }
5117
+ migrate() {
5118
+ const steps = [
5119
+ (db) => {
5120
+ for (const t of [
5121
+ "events",
5122
+ "claims",
5123
+ "resources",
5124
+ "processes",
5125
+ "handoffs",
5126
+ "gates",
5127
+ "incident_acks",
5128
+ "sessions"
5129
+ ]) {
5130
+ this.ensureColumn(t, "actor_kind", "TEXT");
5131
+ this.ensureColumn(t, "actor_id", "TEXT");
5132
+ }
5133
+ const user = osUser();
5134
+ const fill = (table, ownerCol, sessionCol, key) => {
5135
+ const rows = db.query(`SELECT rowid AS rid, ${ownerCol ?? "NULL"} AS owner, ${sessionCol ?? "NULL"} AS sid FROM ${table} WHERE actor_kind IS NULL`).all();
5136
+ const upd = db.query(`UPDATE ${table} SET actor_kind = ?, actor_id = ? WHERE rowid = ?`);
5137
+ for (const r of rows) {
5138
+ const a = actorFrom(r.owner, r.sid, { user });
5139
+ upd.run(a.kind, a.id, r.rid);
5140
+ }
5141
+ return `${key}:${rows.length}`;
5142
+ };
5143
+ fill("claims", "owner", null, "claims");
5144
+ fill("resources", "owner", "session_id", "resources");
5145
+ fill("processes", "owner", "session_id", "processes");
5146
+ fill("handoffs", "by", "session_id", "handoffs");
5147
+ fill("gates", "NULL", "session_id", "gates");
5148
+ fill("incident_acks", "'dashboard'", null, "acks");
5149
+ fill("sessions", "NULL", "id", "sessions");
5150
+ fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
5151
+ }
5152
+ ];
5153
+ for (let v = this.schemaVersion();v < steps.length; v++) {
5154
+ const step = steps[v];
5155
+ this.db.transaction(() => {
5156
+ step(this.db);
5157
+ this.setMeta("schema_version", String(v + 1));
5158
+ })();
5159
+ }
5160
+ }
5161
+ actorFor(owner, sessionId, runId) {
5162
+ return actorFrom(owner, sessionId, { user: osUser(), runId });
5163
+ }
4604
5164
  migrateProjectsJson(file) {
4605
5165
  if (!existsSync5(file))
4606
5166
  return;
@@ -4621,7 +5181,8 @@ class Store {
4621
5181
  this.topCache.set(cwd, { v, t: Date.now() });
4622
5182
  return v;
4623
5183
  }
4624
- rulesCache = new Map;
5184
+ policyCache = new Map;
5185
+ policySeen = new Set;
4625
5186
  preregisterSpawnedSession(id, projectId, cwd, task) {
4626
5187
  const now = new Date().toISOString();
4627
5188
  this.db.query(`INSERT INTO sessions (id, project_id, kind, cwd, started_at, last_seen_at, last, last_type, state, title)
@@ -4650,8 +5211,8 @@ class Store {
4650
5211
  createdAt: new Date().toISOString()
4651
5212
  };
4652
5213
  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);
5214
+ const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
5215
+ 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
5216
  this.remember(handoffDoc(projectId, Number(ins.lastInsertRowid), handoff, sessionId));
4656
5217
  this.append({
4657
5218
  ts: handoff.createdAt,
@@ -4701,8 +5262,8 @@ class Store {
4701
5262
  if (!h)
4702
5263
  return null;
4703
5264
  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);
5265
+ const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
5266
+ 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
5267
  this.remember(handoffDoc(held.projectId, Number(ins.lastInsertRowid), h, sessionId));
4707
5268
  this.touch();
4708
5269
  return h;
@@ -5054,6 +5615,8 @@ class Store {
5054
5615
  const logDir = join8(this.home, "logs", projectId);
5055
5616
  mkdirSync4(logDir, { recursive: true });
5056
5617
  const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
5618
+ if (def.builtin === "review")
5619
+ return this.runReviewGate(projectId, task, gate, def, { worktree: worktree2, cwd, key, log }, opts);
5057
5620
  writeFileSync2(log, `$ ${def.cmd}
5058
5621
  # cwd ${cwd} \xB7 ${new Date().toISOString()}
5059
5622
  `);
@@ -5133,6 +5696,134 @@ class Store {
5133
5696
  this.gateJobs.set(key, done);
5134
5697
  return { ok: true, pid: proc.pid, log, done };
5135
5698
  }
5699
+ runReviewGate(projectId, task, gate, def, where, opts) {
5700
+ const bin = findBin("claude");
5701
+ if (!bin)
5702
+ return { ok: false, reason: "claude CLI not found \u2014 the review gate needs Claude Code" };
5703
+ const p = this.project(projectId);
5704
+ if (!p)
5705
+ return { ok: false, reason: "unknown project" };
5706
+ const started = Date.now();
5707
+ const record = (input) => {
5708
+ const run2 = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
5709
+ return run2.ok ? run2.run : null;
5710
+ };
5711
+ const done = (async () => {
5712
+ let diffText = "";
5713
+ let stat = "";
5714
+ try {
5715
+ const diff = await worktreeDiff(p.root, where.worktree);
5716
+ stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
5717
+ `);
5718
+ diffText = await worktreePatch(where.worktree, diff.base);
5719
+ } catch (e) {
5720
+ return record(reviewGateInput(task, gate, {
5721
+ kind: "error",
5722
+ reason: `diff failed: ${e.message}`,
5723
+ durationMs: Date.now() - started
5724
+ }));
5725
+ }
5726
+ if (!diffText.trim())
5727
+ return record(reviewGateInput(task, gate, {
5728
+ kind: "verdict",
5729
+ durationMs: Date.now() - started,
5730
+ verdict: { verdict: "pass", summary: "nothing to review \u2014 empty diff", findings: [] }
5731
+ }));
5732
+ const taskRow = this.tasks(projectId)?.tasks.find((t) => t.id === task) ?? null;
5733
+ const w = this.findWorktree(projectId, where.worktree);
5734
+ const prompt = reviewPrompt({
5735
+ task,
5736
+ title: taskRow?.title ?? null,
5737
+ branch: w?.branch ?? null,
5738
+ stat,
5739
+ patch: diffText
5740
+ });
5741
+ writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
5742
+ # cwd ${where.cwd} \xB7 ${new Date().toISOString()}
5743
+ `);
5744
+ let proc;
5745
+ try {
5746
+ proc = Bun.spawn([bin, ...reviewArgs(prompt, { model: def.model })], {
5747
+ cwd: where.cwd,
5748
+ stdin: "ignore",
5749
+ stdout: "pipe",
5750
+ stderr: "pipe",
5751
+ env: {
5752
+ ...process.env,
5753
+ SWARM_WORKTREE: where.worktree,
5754
+ SWARM_TASK: task,
5755
+ SWARM_GATE: gate,
5756
+ CLAUDE_CODE_DISABLE_AUTOUPDATE: "1"
5757
+ }
5758
+ });
5759
+ } catch (e) {
5760
+ return record(reviewGateInput(task, gate, {
5761
+ kind: "error",
5762
+ reason: e.message,
5763
+ durationMs: Date.now() - started
5764
+ }));
5765
+ }
5766
+ const reg = this.registerProcess({
5767
+ pid: proc.pid,
5768
+ projectId,
5769
+ sessionId: opts.sessionId ?? null,
5770
+ kind: "gate",
5771
+ name: `gate:${task}:${gate}`,
5772
+ cwd: where.cwd,
5773
+ cmd: "claude -p (review)",
5774
+ owner: opts.owner ?? "daemon",
5775
+ log: where.log
5776
+ });
5777
+ let timedOut = false;
5778
+ const timer = setTimeout(() => {
5779
+ timedOut = true;
5780
+ try {
5781
+ proc.kill("SIGTERM");
5782
+ setTimeout(() => {
5783
+ try {
5784
+ proc.kill("SIGKILL");
5785
+ } catch {}
5786
+ }, 5000).unref();
5787
+ } catch {}
5788
+ }, def.timeout * 1000);
5789
+ const [out, err] = await Promise.all([
5790
+ new Response(proc.stdout).text(),
5791
+ new Response(proc.stderr).text()
5792
+ ]);
5793
+ const code = await proc.exited;
5794
+ clearTimeout(timer);
5795
+ try {
5796
+ writeFileSync2(where.log, `${readFileSync3(where.log, "utf8")}${out}
5797
+ ${err}
5798
+ # exit ${timedOut ? "timeout" : code} \xB7 ${((Date.now() - started) / 1000).toFixed(0)}s
5799
+ `);
5800
+ } catch {}
5801
+ if (reg.ok)
5802
+ this.processes(projectId);
5803
+ const durationMs = Date.now() - started;
5804
+ if (timedOut)
5805
+ return record(reviewGateInput(task, gate, {
5806
+ kind: "error",
5807
+ reason: `timed out after ${def.timeout}s`,
5808
+ durationMs,
5809
+ output: err
5810
+ }));
5811
+ const verdict = parseReviewVerdict(out);
5812
+ if (!verdict)
5813
+ return record(reviewGateInput(task, gate, {
5814
+ kind: "error",
5815
+ reason: code === 0 ? "no JSON verdict in the reply" : `claude exited ${code}`,
5816
+ durationMs,
5817
+ output: err || out
5818
+ }));
5819
+ return record(reviewGateInput(task, gate, { kind: "verdict", verdict, durationMs }));
5820
+ })().finally(() => {
5821
+ this.gateJobs.delete(where.key);
5822
+ this.touch();
5823
+ });
5824
+ this.gateJobs.set(where.key, done);
5825
+ return { ok: true, pid: 0, log: where.log, done };
5826
+ }
5136
5827
  async runGates(projectId, task, gates2, opts = {}) {
5137
5828
  const cfg = this.gateDefs(projectId);
5138
5829
  const names = gates2?.length ? gates2 : (cfg?.required ?? []).filter((g) => cfg?.defs[g]);
@@ -5202,8 +5893,8 @@ class Store {
5202
5893
  return v;
5203
5894
  const createdAt = new Date().toISOString();
5204
5895
  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);
5896
+ const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
5897
+ 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
5898
  const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
5208
5899
  this.remember(gateDoc(projectId, run2.id, run2, sessionId));
5209
5900
  this.append({
@@ -5289,13 +5980,67 @@ class Store {
5289
5980
  return { source, required, tasks: board, error };
5290
5981
  }
5291
5982
  rulesFor(repoRoot) {
5983
+ return this.policyFor(repoRoot).config.rules;
5984
+ }
5985
+ policyFor(repoRoot) {
5292
5986
  const key = repoRoot ?? "";
5293
- const hit = this.rulesCache.get(key);
5987
+ const hit = this.policyCache.get(key);
5294
5988
  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;
5989
+ return hit.loaded;
5990
+ const loaded = loadConfigDetailed({ repoRoot, home: this.home });
5991
+ this.policyCache.set(key, { at: Date.now(), loaded });
5992
+ this.writePolicyCache(loaded);
5993
+ return loaded;
5994
+ }
5995
+ writePolicyCache(loaded) {
5996
+ const file = join8(this.home, POLICY_CACHE_FILE);
5997
+ try {
5998
+ if (!hasLockedRules(loaded)) {
5999
+ if (existsSync5(file))
6000
+ unlinkSync(file);
6001
+ return;
6002
+ }
6003
+ const cache = buildPolicyCache(loaded, this.liveSessions(), this.heldWorktrees());
6004
+ writeFileSync2(file, JSON.stringify(cache), { mode: 384 });
6005
+ } catch (e) {
6006
+ console.error(`swarm: policy cache: ${e.message}`);
6007
+ }
6008
+ }
6009
+ guardDisabled(repoRoot) {
6010
+ return process.env.SWARM_GUARD === "off" && !hasLockedRules(this.policyFor(repoRoot));
6011
+ }
6012
+ claudeSettings() {
6013
+ const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
6014
+ try {
6015
+ return existsSync5(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
6016
+ } catch {
6017
+ return null;
6018
+ }
6019
+ }
6020
+ checkPolicy(cwd, sessionId) {
6021
+ const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
6022
+ const repoRoot = project?.root ?? null;
6023
+ const loaded = this.policyFor(repoRoot);
6024
+ const settings = this.claudeSettings();
6025
+ const findings = policyFindings({
6026
+ loaded,
6027
+ coverage: settings === null ? null : hookCoverage(settings),
6028
+ guardOff: process.env.SWARM_GUARD === "off",
6029
+ repoRoot
6030
+ });
6031
+ for (const f of findings) {
6032
+ if (this.policySeen.has(f.key))
6033
+ continue;
6034
+ this.policySeen.add(f.key);
6035
+ this.append({
6036
+ ts: new Date().toISOString(),
6037
+ type: "incident.opened",
6038
+ projectId: project?.id ?? "p_unknown",
6039
+ sessionId,
6040
+ payload: { rule: "policy", action: "tampered", command: f.subject, reason: f.reason }
6041
+ });
6042
+ }
6043
+ return findings;
5299
6044
  }
5300
6045
  evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
5301
6046
  if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync5(cwd)) {
@@ -5498,6 +6243,8 @@ class Store {
5498
6243
  name: r.name,
5499
6244
  discovered: Boolean(r.discovered),
5500
6245
  order: typeof r.sort_order === "number" ? r.sort_order : null,
6246
+ icon: r.icon ?? null,
6247
+ color: r.color ?? null,
5501
6248
  createdAt: r.created_at
5502
6249
  }));
5503
6250
  }
@@ -5512,6 +6259,8 @@ class Store {
5512
6259
  name: r.name,
5513
6260
  discovered: Boolean(r.discovered),
5514
6261
  order: typeof r.sort_order === "number" ? r.sort_order : null,
6262
+ icon: r.icon ?? null,
6263
+ color: r.color ?? null,
5515
6264
  createdAt: r.created_at
5516
6265
  };
5517
6266
  }
@@ -5549,6 +6298,8 @@ class Store {
5549
6298
  ...ident,
5550
6299
  discovered: !explicit,
5551
6300
  order: null,
6301
+ icon: null,
6302
+ color: null,
5552
6303
  createdAt: new Date().toISOString()
5553
6304
  };
5554
6305
  if (name)
@@ -5569,8 +6320,24 @@ class Store {
5569
6320
  return;
5570
6321
  if (patch.pinned !== undefined)
5571
6322
  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);
6323
+ if (patch.name?.trim())
6324
+ this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name.trim(), id);
6325
+ if (patch.icon !== undefined) {
6326
+ const icon = (patch.icon ?? "").trim();
6327
+ const isImage = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(icon);
6328
+ if (!isImage && [...icon].length > 4)
6329
+ return;
6330
+ if (isImage && icon.length > 24000)
6331
+ return;
6332
+ this.db.query("UPDATE projects SET icon = ? WHERE id = ?").run(icon || null, id);
6333
+ }
6334
+ if (patch.color !== undefined) {
6335
+ const color = (patch.color ?? "").trim();
6336
+ if (color && !/^c[1-7]$/.test(color))
6337
+ return;
6338
+ this.db.query("UPDATE projects SET color = ? WHERE id = ?").run(color || null, id);
6339
+ }
6340
+ this.touch();
5574
6341
  return this.project(id);
5575
6342
  }
5576
6343
  reorderProjects(ids) {
@@ -5589,10 +6356,29 @@ class Store {
5589
6356
  this.touch();
5590
6357
  return this.db.query("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
5591
6358
  }
5592
- append(e) {
6359
+ redactions() {
6360
+ const cfg = this.policyFor(null).config.privacy;
6361
+ const key = cfg.redact.join("\x00");
6362
+ if (this.redactCache?.key !== key)
6363
+ this.redactCache = { key, res: compileRedactions(cfg.redact) };
6364
+ return this.redactCache.res;
6365
+ }
6366
+ redactCache = null;
6367
+ append(e0) {
6368
+ const privacy = this.policyFor(null).config.privacy;
6369
+ let e = e0;
6370
+ if (!privacy.store_prompts && e.type === "prompt.submitted" && e.payload && typeof e.payload === "object") {
6371
+ const { prompt: _p, ...rest } = e.payload;
6372
+ e = { ...e, payload: { ...rest, prompt: "[not stored]" } };
6373
+ }
6374
+ const res = this.redactions();
6375
+ if (res.length)
6376
+ e = { ...e, payload: redactValue(e.payload, res), raw: redactValue(e.raw, res) };
5593
6377
  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) };
6378
+ const p = e.payload ?? {};
6379
+ const actor2 = e.actor ?? this.actorFor(typeof p.owner === "string" ? p.owner : typeof p.by === "string" ? p.by : null, e.sessionId);
6380
+ 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);
6381
+ const stored = { ...e, actor: actor2, seq: Number(r.lastInsertRowid) };
5596
6382
  if (stored.type === "incident.opened")
5597
6383
  this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
5598
6384
  this.projectSession(stored);
@@ -5602,9 +6388,34 @@ class Store {
5602
6388
  l(wire);
5603
6389
  return stored;
5604
6390
  }
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;
6391
+ audit(opts = {}) {
6392
+ const where = [`type IN (${AUDIT_TYPES_SQL})`];
6393
+ const args = [];
6394
+ if (opts.since) {
6395
+ where.push("ts >= ?");
6396
+ args.push(opts.since);
6397
+ }
6398
+ if (opts.projectId) {
6399
+ where.push("project_id = ?");
6400
+ args.push(opts.projectId);
6401
+ }
6402
+ if (opts.type && isAuditType(opts.type)) {
6403
+ where.push("type = ?");
6404
+ args.push(opts.type);
6405
+ }
6406
+ const limit = Math.min(Math.max(opts.limit ?? 1e4, 1), 1e5);
6407
+ 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);
6408
+ return rows.map((r) => auditRow(wireRowToEvent(r)));
6409
+ }
6410
+ prune(days2) {
6411
+ const cfg = this.policyFor(null).config;
6412
+ const chatter = days2 ?? cfg.events.retain_days;
6413
+ const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
6414
+ let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
6415
+ if (cfg.audit.retain_days > 0) {
6416
+ const acut = new Date(Date.now() - cfg.audit.retain_days * 86400000).toISOString();
6417
+ n += this.db.query(`DELETE FROM events WHERE ts < ? AND type IN (${AUDIT_TYPES_SQL}) AND type != 'incident.opened'`).run(acut).changes;
6418
+ }
5608
6419
  const old = new Date(Date.now() - 7 * 86400000).toISOString();
5609
6420
  this.db.query("UPDATE events SET raw = NULL WHERE ts < ? AND raw IS NOT NULL").run(old);
5610
6421
  if (n > 0)
@@ -5702,13 +6513,15 @@ class Store {
5702
6513
  }
5703
6514
  }
5704
6515
  persistTurns(sessionId, agentId, turns) {
6516
+ const privacy = this.policyFor(null).config.privacy;
6517
+ const res = this.redactions();
5705
6518
  const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
5706
6519
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5707
6520
  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
6521
  cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
5709
6522
  const tx = this.db.transaction((ts) => {
5710
6523
  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));
6524
+ up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
5712
6525
  }
5713
6526
  });
5714
6527
  if (turns.length)
@@ -5939,7 +6752,7 @@ class Store {
5939
6752
  const p = this.project(projectId);
5940
6753
  return join8(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
5941
6754
  }
5942
- claim(projectId, task, owner, baseRef = "HEAD") {
6755
+ claim(projectId, task, owner, baseRef = "HEAD", sessionId = null) {
5943
6756
  const p = this.project(projectId);
5944
6757
  if (!p)
5945
6758
  return { ok: false, error: "unknown project" };
@@ -5958,15 +6771,17 @@ class Store {
5958
6771
  this.invalidateWorktrees(projectId);
5959
6772
  const expiresAt = nextExpiry(now);
5960
6773
  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')
6774
+ this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
6775
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held', ?, ?)
5963
6776
  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);
6777
+ acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held',
6778
+ actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(projectId, task, owner, created, branch, acquiredAt, expiresAt, ...actorCols(this.actorFor(owner, sessionId)));
5965
6779
  this.append({
5966
6780
  ts: acquiredAt,
5967
6781
  type: "claim.acquired",
5968
6782
  projectId,
5969
- sessionId: null,
6783
+ sessionId,
6784
+ actor: this.actorFor(owner, sessionId),
5970
6785
  payload: { task, owner, worktree: created, branch, summary: `claim ${task} by ${owner}` }
5971
6786
  });
5972
6787
  const bootstrap = this.bootstrapWorktree(projectId, task, p.root, created);
@@ -6642,19 +7457,19 @@ class Store {
6642
7457
  WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
6643
7458
  return r.n;
6644
7459
  }
6645
- ackIncident(seq) {
7460
+ ackIncident(seq, by) {
6646
7461
  const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
6647
7462
  if (!row)
6648
7463
  return false;
6649
- this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at) VALUES (?, ?)").run(seq, new Date().toISOString());
7464
+ 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
7465
  this.touch();
6651
7466
  return true;
6652
7467
  }
6653
- ackAllIncidents(projectId) {
7468
+ ackAllIncidents(projectId, by) {
6654
7469
  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] : []);
7470
+ const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id)
7471
+ SELECT e.seq, ?, ?, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
7472
+ 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
7473
  this.touch();
6659
7474
  return Number(r.changes);
6660
7475
  }
@@ -6821,8 +7636,8 @@ class Store {
6821
7636
  endedAt: null
6822
7637
  };
6823
7638
  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);
7639
+ 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)
7640
+ 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
7641
  this.append({
6827
7642
  ts: p.startedAt,
6828
7643
  type: "process.started",
@@ -6886,11 +7701,12 @@ class Store {
6886
7701
  expiresAt,
6887
7702
  released: false
6888
7703
  };
6889
- this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released)
6890
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
7704
+ this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released, actor_kind, actor_id)
7705
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
6891
7706
  ON CONFLICT(name, project_id) DO UPDATE SET
6892
7707
  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);
7708
+ port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0,
7709
+ 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
7710
  this.append({
6895
7711
  ts: resource.acquiredAt,
6896
7712
  type: "resource.acquired",
@@ -6937,7 +7753,7 @@ class Store {
6937
7753
  }
6938
7754
  snapshot() {
6939
7755
  const worktrees = {};
6940
- const projects = this.projects();
7756
+ const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
6941
7757
  for (const p of projects)
6942
7758
  worktrees[p.id] = this.worktrees(p.id);
6943
7759
  return {
@@ -6955,7 +7771,7 @@ class Store {
6955
7771
  };
6956
7772
  }
6957
7773
  }
6958
- var WIRE_COLS = "seq, ts, type, project_id, session_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
7774
+ var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
6959
7775
  var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
6960
7776
  var TOOL_INPUT_MAX = 2048;
6961
7777
  var TOOL_RESPONSE_MAX = 4096;
@@ -7001,7 +7817,7 @@ function toWire(e) {
7001
7817
  }
7002
7818
  function wireRowToEvent(r) {
7003
7819
  const p = JSON.parse(r.payload ?? "null");
7004
- return {
7820
+ const e = {
7005
7821
  seq: r.seq,
7006
7822
  ts: r.ts,
7007
7823
  type: r.type,
@@ -7009,6 +7825,10 @@ function wireRowToEvent(r) {
7009
7825
  sessionId: r.session_id ?? null,
7010
7826
  payload: p
7011
7827
  };
7828
+ const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
7829
+ if (a)
7830
+ e.actor = a;
7831
+ return e;
7012
7832
  }
7013
7833
  function rowToEvent(r) {
7014
7834
  const e = {
@@ -7021,11 +7841,26 @@ function rowToEvent(r) {
7021
7841
  };
7022
7842
  if (r.raw)
7023
7843
  e.raw = JSON.parse(r.raw);
7844
+ const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
7845
+ if (a)
7846
+ e.actor = a;
7024
7847
  return e;
7025
7848
  }
7849
+ function osUser() {
7850
+ try {
7851
+ return userInfo().username || process.env.USER || "me";
7852
+ } catch {
7853
+ return process.env.USER || "me";
7854
+ }
7855
+ }
7856
+ var actorCols = (a) => [a.kind, a.id];
7857
+ function isScratchRoot(root) {
7858
+ const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
7859
+ return tmp.some((t) => root === t || root.startsWith(`${t}/`));
7860
+ }
7026
7861
 
7027
7862
  // packages/daemon/src/app.ts
7028
- var VERSION = "0.7.0";
7863
+ var VERSION = "0.8.0";
7029
7864
  var WEB_DIR = (() => {
7030
7865
  if (process.env.SWARM_WEB_DIR)
7031
7866
  return process.env.SWARM_WEB_DIR;
@@ -7043,6 +7878,10 @@ function wireJson(e) {
7043
7878
  }
7044
7879
  return s;
7045
7880
  }
7881
+ function hookRepoRoot(store, raw2) {
7882
+ const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
7883
+ return cwd && existsSync6(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
7884
+ }
7046
7885
  function createApp(store = new Store) {
7047
7886
  const app = new Hono2;
7048
7887
  const forge2 = new ForgeService(store);
@@ -7053,7 +7892,27 @@ function createApp(store = new Store) {
7053
7892
  for (const run2 of runner.list(projectId))
7054
7893
  runner.stop(run2.id);
7055
7894
  });
7056
- app.get("/v1/health", (c) => c.json({ ok: true, version: VERSION }));
7895
+ app.use("/v1/*", async (c, next) => {
7896
+ if (c.req.path === "/v1/health")
7897
+ return next();
7898
+ const token = readToken(store.home);
7899
+ const given = c.req.header("authorization")?.replace(/^Bearer\s+/i, "") ?? c.req.query("token");
7900
+ if (token && given && given === token)
7901
+ return next();
7902
+ if (given)
7903
+ return c.json({ error: "unauthorized: wrong daemon token" }, 401);
7904
+ const ip = c.env?.requestIP?.(c.req.raw)?.address;
7905
+ const loopback = !ip || ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
7906
+ if (loopback && store.policyFor(null).config.daemon.auth !== "required")
7907
+ return next();
7908
+ return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
7909
+ });
7910
+ app.get("/v1/health", (c) => c.json({
7911
+ ok: true,
7912
+ version: VERSION,
7913
+ schema: store.schemaVersion(),
7914
+ auth: store.policyFor(null).config.daemon.auth
7915
+ }));
7057
7916
  app.get("/v1/projects", (c) => c.json(store.snapshot().projects));
7058
7917
  app.post("/v1/projects", async (c) => {
7059
7918
  const { path, name } = await c.req.json();
@@ -7072,9 +7931,11 @@ function createApp(store = new Store) {
7072
7931
  return c.json(store.reorderProjects(ids));
7073
7932
  });
7074
7933
  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);
7934
+ const { pinned, name, icon, color } = await c.req.json().catch(() => ({}));
7935
+ if (!store.project(c.req.param("id")))
7936
+ return c.json({ error: "not found" }, 404);
7937
+ const p = store.updateProject(c.req.param("id"), { pinned, name, icon, color });
7938
+ return p ? c.json(p) : c.json({ error: "icon is at most 4 characters; color is c1\u2026c7" }, 400);
7078
7939
  });
7079
7940
  app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
7080
7941
  app.get("/v1/fs/ls", (c) => {
@@ -7112,6 +7973,31 @@ function createApp(store = new Store) {
7112
7973
  })
7113
7974
  });
7114
7975
  });
7976
+ app.get("/v1/policy", (c) => {
7977
+ const id = c.req.query("project");
7978
+ const p = id ? store.project(id) : null;
7979
+ if (id && !p)
7980
+ return c.json({ error: "unknown project" }, 404);
7981
+ const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
7982
+ return c.json({ ...policy2, provenance, overridden });
7983
+ });
7984
+ app.get("/v1/audit", (c) => {
7985
+ const since = sinceToIso(c.req.query("since"));
7986
+ if (c.req.query("since") && !since)
7987
+ return c.json({ error: "since: use 30d / 12h / 90m or an ISO date" }, 400);
7988
+ const pid = c.req.query("project") || null;
7989
+ if (pid && !store.project(pid))
7990
+ return c.json({ error: "unknown project" }, 404);
7991
+ const rows = store.audit({
7992
+ since,
7993
+ projectId: pid,
7994
+ type: c.req.query("type") || null,
7995
+ limit: Number(c.req.query("limit")) || undefined
7996
+ });
7997
+ const format = c.req.query("format") === "csv" ? "csv" : c.req.query("format") === "jsonl" ? "jsonl" : "json";
7998
+ const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
7999
+ return c.body(formatAudit(rows, format), 200, { "content-type": ct });
8000
+ });
7115
8001
  app.get("/v1/rules/dryrun", (c) => {
7116
8002
  const projectId = c.req.query("project");
7117
8003
  if (!projectId)
@@ -7125,11 +8011,11 @@ function createApp(store = new Store) {
7125
8011
  });
7126
8012
  app.post("/v1/incidents/ack", async (c) => {
7127
8013
  const body = await c.req.json().catch(() => ({}));
7128
- return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined) });
8014
+ return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined, body.by) });
7129
8015
  });
7130
8016
  app.post("/v1/incidents/:seq/ack", (c) => {
7131
8017
  const seq = Number(c.req.param("seq"));
7132
- if (!Number.isInteger(seq) || !store.ackIncident(seq))
8018
+ if (!Number.isInteger(seq) || !store.ackIncident(seq, c.req.query("by")))
7133
8019
  return c.json({ ok: false, error: "no such incident" }, 404);
7134
8020
  return c.json({ ok: true });
7135
8021
  });
@@ -7377,7 +8263,7 @@ function createApp(store = new Store) {
7377
8263
  const b = await c.req.json();
7378
8264
  if (!b.projectId || !b.task)
7379
8265
  return c.json({ error: "projectId and task required" }, 400);
7380
- const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef);
8266
+ const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef, b.sessionId ?? null);
7381
8267
  return c.json(r, r.ok ? 201 : 409);
7382
8268
  });
7383
8269
  app.post("/v1/claims/renew", async (c) => {
@@ -7524,6 +8410,7 @@ function createApp(store = new Store) {
7524
8410
  const raw2 = await c.req.json().catch(() => ({}));
7525
8411
  store.ingestHook(event, raw2);
7526
8412
  if (event === "SessionStart" && typeof raw2.cwd === "string") {
8413
+ store.checkPolicy(raw2.cwd, typeof raw2.session_id === "string" ? raw2.session_id : null);
7527
8414
  const ctx = store.sessionContext(raw2.cwd);
7528
8415
  if (ctx)
7529
8416
  return c.json({
@@ -7533,7 +8420,7 @@ function createApp(store = new Store) {
7533
8420
  }
7534
8421
  const sid = typeof raw2.session_id === "string" ? raw2.session_id : null;
7535
8422
  const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
7536
- if (event === "PreToolUse" && process.env.SWARM_GUARD !== "off") {
8423
+ if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw2))) {
7537
8424
  const guard = store.guardHook(raw2);
7538
8425
  if (guard) {
7539
8426
  return c.json({
@@ -7609,6 +8496,7 @@ function serve() {
7609
8496
  }
7610
8497
  var server = serve();
7611
8498
  var port = server.port ?? DEFAULT_PORT2;
8499
+ ensureToken();
7612
8500
  writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
7613
8501
  var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
7614
8502
  var backfillMs = backfillDays * 24 * 60 * 60000;