@ra3orblade/swarm 0.13.2 → 0.14.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
@@ -15,7 +15,8 @@ var SRC = {
15
15
  swarm: "cli",
16
16
  swarmd: "daemon",
17
17
  "swarm-hook": "hook",
18
- "swarm-mcp": "mcp"
18
+ "swarm-mcp": "mcp",
19
+ "swarm-teamd": "team"
19
20
  };
20
21
  function resolveBin(name, from = import.meta.url) {
21
22
  const here = dirname(fileURLToPath(from));
@@ -360,9 +361,14 @@ function parseTranscriptChunk(chunk) {
360
361
  }
361
362
 
362
363
  // packages/core/src/adapters/codex/rollout.ts
363
- function parseCodexRollout(chunk) {
364
- const out = { turns: [], sessionId: null, cwd: null, model: null, title: null };
365
- let n = 0;
364
+ function parseCodexRollout(chunk, sessionIdHint) {
365
+ const out = {
366
+ turns: [],
367
+ sessionId: sessionIdHint ?? null,
368
+ cwd: null,
369
+ model: null,
370
+ title: null
371
+ };
366
372
  let text = "";
367
373
  let tools = [];
368
374
  let lastTs = "";
@@ -400,9 +406,10 @@ function parseCodexRollout(chunk) {
400
406
  if (!u)
401
407
  continue;
402
408
  const cacheRead = u.cached_input_tokens ?? 0;
409
+ const ts = d.timestamp ?? (lastTs || new Date(0).toISOString());
403
410
  const turn = {
404
- id: `${out.sessionId ?? "codex"}-t${n}`,
405
- ts: d.timestamp ?? (lastTs || new Date(0).toISOString()),
411
+ id: `${out.sessionId ?? "codex"}-${ts}-${u.input_tokens ?? 0}-${cacheRead}-${u.output_tokens ?? 0}-${u.reasoning_output_tokens ?? 0}`,
412
+ ts,
406
413
  model: out.model ?? "gpt-5",
407
414
  usage: {
408
415
  input: Math.max(0, (u.input_tokens ?? 0) - cacheRead),
@@ -418,7 +425,6 @@ function parseCodexRollout(chunk) {
418
425
  sidechain: false
419
426
  };
420
427
  out.turns.push(turn);
421
- n++;
422
428
  text = "";
423
429
  tools = [];
424
430
  }
@@ -443,8 +449,14 @@ function partText(content) {
443
449
  }
444
450
  return out.slice(0, 400);
445
451
  }
446
- function parseGeminiChat(chunk) {
447
- const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
452
+ function parseGeminiChat(chunk, sessionIdHint) {
453
+ const out = {
454
+ turns: [],
455
+ sessionId: sessionIdHint ?? null,
456
+ model: null,
457
+ cwd: null,
458
+ title: null
459
+ };
448
460
  let subagent = false;
449
461
  for (const raw of chunk.split(`
450
462
  `)) {
@@ -634,7 +646,8 @@ var HOOK_EVENTS = [
634
646
  "Stop",
635
647
  "SessionEnd",
636
648
  "Notification",
637
- "PreCompact"
649
+ "PreCompact",
650
+ "PermissionRequest"
638
651
  ];
639
652
  var MAP = {
640
653
  SessionStart: "session.started",
@@ -646,8 +659,20 @@ var MAP = {
646
659
  Stop: "agent.text",
647
660
  SessionEnd: "session.ended",
648
661
  Notification: "session.notification",
649
- PreCompact: "agent.text"
662
+ PreCompact: "agent.text",
663
+ PermissionRequest: "permission.requested"
650
664
  };
665
+ function permissionReason(raw) {
666
+ const sug = raw.permission_suggestions;
667
+ if (Array.isArray(sug)) {
668
+ for (const x of sug) {
669
+ const r = x?.reasoning;
670
+ if (typeof r === "string" && r.trim())
671
+ return r.trim();
672
+ }
673
+ }
674
+ return `Claude Code is asking before it runs this (${typeof raw.permission_mode === "string" ? raw.permission_mode : "default"} mode)`;
675
+ }
651
676
  function summarizeToolInput(tool, input) {
652
677
  const i = input ?? {};
653
678
  const s = (v) => typeof v === "string" ? v : JSON.stringify(v ?? "");
@@ -711,10 +736,20 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
711
736
  case "PreCompact":
712
737
  summary = "context compaction";
713
738
  break;
739
+ case "PermissionRequest":
740
+ summary = `permission: ${tool ?? "?"} ${summarizeToolInput(tool, raw.tool_input)}`.trim();
741
+ break;
714
742
  default:
715
743
  summary = event;
716
744
  }
717
745
  const payload = { hook: event, cwd: raw.cwd ?? null, summary };
746
+ if (event === "PermissionRequest") {
747
+ if (typeof raw.tool_use_id === "string")
748
+ payload.requestId = raw.tool_use_id;
749
+ payload.display = summarizeToolInput(tool, raw.tool_input);
750
+ payload.reason = permissionReason(raw);
751
+ payload.source = "interactive";
752
+ }
718
753
  if (tool)
719
754
  payload.tool = tool;
720
755
  if (raw.tool_input !== undefined)
@@ -848,6 +883,8 @@ var AUDIT_TYPES = new Set([
848
883
  "process.started",
849
884
  "process.exited",
850
885
  "gate.recorded",
886
+ "gate.blocked",
887
+ "codify.applied",
851
888
  "handoff.recorded",
852
889
  "permission.requested",
853
890
  "permission.resolved",
@@ -1027,6 +1064,31 @@ function runProfile(name) {
1027
1064
  return null;
1028
1065
  return RUN_PROFILES[name] ?? null;
1029
1066
  }
1067
+ // packages/core/src/collision-context.ts
1068
+ var DEFAULT_COLLISION_WINDOW_MIN = 15;
1069
+ function ago(ms) {
1070
+ const m = Math.floor(ms / 60000);
1071
+ if (m < 1)
1072
+ return "moments ago";
1073
+ if (m === 1)
1074
+ return "a minute ago";
1075
+ return `${m} minutes ago`;
1076
+ }
1077
+ function collisionWarning(path, sessionId, edits, live, now, windowMs) {
1078
+ const others = edits.filter((e) => e.sessionId !== sessionId && now - e.at <= windowMs && live.has(e.sessionId)).sort((a, b) => b.at - a.at).map((e) => ({ ...live.get(e.sessionId), agoMs: now - e.at }));
1079
+ if (!others.length)
1080
+ return null;
1081
+ const who = others.map((o) => {
1082
+ const name = o.title ? `"${o.title}"` : `session ${o.sessionId.slice(0, 8)}`;
1083
+ const where = [o.task ? `task ${o.task}` : null, o.branch ? `branch ${o.branch}` : null].filter(Boolean).join(", ");
1084
+ return `${name}${where ? ` (${where})` : ""} ${ago(o.agoMs)}`;
1085
+ }).join("; ");
1086
+ return {
1087
+ path,
1088
+ others,
1089
+ text: `[swarm] heads-up: ${path} was also edited by ${who} \u2014 still live. You may be changing the same thing twice or undoing theirs. Look at what changed there (git diff / git log -p on that file) before you go further, or leave that file to them.`
1090
+ };
1091
+ }
1030
1092
  // packages/core/src/config.ts
1031
1093
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1032
1094
  import { join as join2 } from "path";
@@ -1125,11 +1187,21 @@ function parseGateDefs(gates) {
1125
1187
  var DEFAULT_CONFIG = {
1126
1188
  daemon: { port: 7777, auth: "loopback-optional" },
1127
1189
  tasks: { source: null, labels: [], team: null },
1128
- gates: { required: [], auto: "session-end", defs: {} },
1190
+ gates: {
1191
+ required: [],
1192
+ auto: "session-end",
1193
+ on_stop: "record",
1194
+ max_blocks: 3,
1195
+ stop_timeout: 300,
1196
+ defs: {}
1197
+ },
1129
1198
  workflows: {},
1130
- budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
1199
+ budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn", window_warn_at: 0.8 },
1131
1200
  models: { allow: [] },
1132
1201
  notify: { webhook: null },
1202
+ messages: { wake: true },
1203
+ codify: { target: "both" },
1204
+ broker: { interactive_wait: 30 },
1133
1205
  team: { url: null, forward: ["ledger", "cost"], interval: 5 },
1134
1206
  events: { retain_days: 30 },
1135
1207
  audit: { retain_days: 0 },
@@ -1150,10 +1222,45 @@ var DEFAULT_CONFIG = {
1150
1222
  protected_ports: "ask",
1151
1223
  no_foreign_worktree: "ask",
1152
1224
  claim_required_to_write: "off",
1225
+ no_verify: "off",
1226
+ dry_run_first: "off",
1227
+ custom: [],
1228
+ collision_context: true,
1229
+ collision_window: 15,
1153
1230
  protected: { ports: [] }
1154
1231
  }
1155
1232
  };
1156
1233
  var MODES = ["ask", "deny", "off"];
1234
+ var REWRITE_MODES = ["rewrite", "ask", "deny", "off"];
1235
+ function parseCustomRules(raw) {
1236
+ if (!Array.isArray(raw))
1237
+ return [];
1238
+ const out = [];
1239
+ const seen = new Set;
1240
+ for (const v of raw) {
1241
+ if (!isRecord2(v))
1242
+ continue;
1243
+ const name = typeof v.name === "string" ? v.name.trim() : "";
1244
+ const match = typeof v.match === "string" ? v.match : "";
1245
+ if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name) || !match || seen.has(name))
1246
+ continue;
1247
+ try {
1248
+ new RegExp(match);
1249
+ } catch {
1250
+ continue;
1251
+ }
1252
+ const action = REWRITE_MODES.includes(v.action) ? v.action : "ask";
1253
+ seen.add(name);
1254
+ out.push({
1255
+ name,
1256
+ match,
1257
+ action,
1258
+ ...typeof v.replace === "string" ? { replace: v.replace } : {},
1259
+ ...typeof v.reason === "string" && v.reason.trim() ? { reason: v.reason.trim() } : {}
1260
+ });
1261
+ }
1262
+ return out;
1263
+ }
1157
1264
  function isRecord2(v) {
1158
1265
  return typeof v === "object" && v !== null && !Array.isArray(v);
1159
1266
  }
@@ -1187,6 +1294,7 @@ var days = (v, fallback) => {
1187
1294
  };
1188
1295
  function validate(c) {
1189
1296
  const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
1297
+ const rewriteMode = (v, fallback) => REWRITE_MODES.includes(v) ? v : fallback;
1190
1298
  const port = Number(c.daemon?.port);
1191
1299
  const source = c.tasks?.source;
1192
1300
  const setup = c.worktree?.setup;
@@ -1217,13 +1325,28 @@ function validate(c) {
1217
1325
  gates: {
1218
1326
  required: Array.isArray(rawGates?.required) ? rawGates.required.filter((g) => typeof g === "string" && g.trim() !== "") : [],
1219
1327
  auto: AUTO_MODES.includes(auto) ? auto : "session-end",
1328
+ on_stop: rawGates?.on_stop === "block" ? "block" : "record",
1329
+ max_blocks: (() => {
1330
+ const n = Number(rawGates?.max_blocks);
1331
+ return Number.isInteger(n) && n >= 0 && n <= 20 ? n : 3;
1332
+ })(),
1333
+ stop_timeout: (() => {
1334
+ const n = Number(rawGates?.stop_timeout);
1335
+ return Number.isFinite(n) && n >= 5 && n <= 1800 ? n : 300;
1336
+ })(),
1220
1337
  defs: parseGateDefs(rawGates)
1221
1338
  },
1222
1339
  budget: {
1223
1340
  daily: usd(b.daily),
1224
1341
  weekly: usd(b.weekly),
1225
1342
  warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
1226
- on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
1343
+ on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn",
1344
+ window_warn_at: (() => {
1345
+ if (b.window_warn_at === false || b.window_warn_at === null || b.window_warn_at === 0)
1346
+ return null;
1347
+ const w = Number(b.window_warn_at);
1348
+ return Number.isFinite(w) && w > 0 && w < 1 ? w : 0.8;
1349
+ })()
1227
1350
  },
1228
1351
  workflows: parseWorkflows(c.workflows),
1229
1352
  notify: {
@@ -1232,6 +1355,23 @@ function validate(c) {
1232
1355
  return typeof w === "string" && /^https?:\/\//.test(w.trim()) ? w.trim() : null;
1233
1356
  })()
1234
1357
  },
1358
+ messages: {
1359
+ wake: c.messages?.wake !== false
1360
+ },
1361
+ codify: {
1362
+ target: (() => {
1363
+ const t = c.codify?.target;
1364
+ return t === "claude-md" || t === "swarm-toml" ? t : "both";
1365
+ })()
1366
+ },
1367
+ broker: {
1368
+ interactive_wait: (() => {
1369
+ const w = Number(c.broker?.interactive_wait);
1370
+ if (!Number.isFinite(w) || w < 0)
1371
+ return 30;
1372
+ return Math.min(Math.round(w), 120);
1373
+ })()
1374
+ },
1235
1375
  models: {
1236
1376
  allow: Array.isArray(c.models?.allow) ? c.models.allow.filter((m) => typeof m === "string" && m.trim() !== "") : []
1237
1377
  },
@@ -1278,6 +1418,14 @@ function validate(c) {
1278
1418
  protected_ports: mode(c.rules?.protected_ports, "ask"),
1279
1419
  no_foreign_worktree: mode(c.rules?.no_foreign_worktree, "ask"),
1280
1420
  claim_required_to_write: mode(c.rules?.claim_required_to_write, "off"),
1421
+ no_verify: rewriteMode(c.rules?.no_verify, "off"),
1422
+ dry_run_first: rewriteMode(c.rules?.dry_run_first, "off"),
1423
+ custom: parseCustomRules(c.rules?.custom),
1424
+ collision_context: c.rules?.collision_context !== false,
1425
+ collision_window: (() => {
1426
+ const n = Number(c.rules?.collision_window);
1427
+ return Number.isFinite(n) && n >= 1 && n <= 240 ? Math.round(n) : 15;
1428
+ })(),
1281
1429
  protected: {
1282
1430
  ports: Array.isArray(c.rules?.protected?.ports) ? c.rules.protected.ports.filter((p) => Number.isInteger(p) && p > 0 && p < 65536) : []
1283
1431
  }
@@ -1719,14 +1867,120 @@ var DEFAULT_MODES = {
1719
1867
  protected_ports: "ask",
1720
1868
  no_foreign_worktree: "ask",
1721
1869
  claim_required_to_write: "off",
1870
+ no_verify: "off",
1871
+ dry_run_first: "off",
1872
+ custom: [],
1722
1873
  protected: { ports: [] }
1723
1874
  };
1724
- function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES) {
1875
+ function isSingleCommand(cmd) {
1876
+ return !/[&|;<>`\n]/.test(cmd) && !cmd.includes("$(");
1877
+ }
1878
+ function stripFlagsOutsideQuotes(cmd, flags) {
1879
+ let out = "";
1880
+ let quote = null;
1881
+ let i = 0;
1882
+ while (i < cmd.length) {
1883
+ const ch = cmd[i];
1884
+ if (quote) {
1885
+ out += ch;
1886
+ if (ch === quote && cmd[i - 1] !== "\\")
1887
+ quote = null;
1888
+ i++;
1889
+ continue;
1890
+ }
1891
+ if (ch === '"' || ch === "'") {
1892
+ quote = ch;
1893
+ out += ch;
1894
+ i++;
1895
+ continue;
1896
+ }
1897
+ const rest = cmd.slice(i);
1898
+ const hit = flags.find((f) => rest.startsWith(f) && /^(\s|$)/.test(rest.slice(f.length)));
1899
+ if (hit) {
1900
+ out = out.replace(/[ \t]+$/, "");
1901
+ i += hit.length;
1902
+ continue;
1903
+ }
1904
+ out += ch;
1905
+ i++;
1906
+ }
1907
+ return out;
1908
+ }
1909
+ function rewriteNoVerify(cmd) {
1910
+ if (!isSingleCommand(cmd) || !/^\s*git\s/.test(cmd))
1911
+ return null;
1912
+ if (!/--no-(verify|gpg-sign)\b/.test(cmd))
1913
+ return null;
1914
+ const out = stripFlagsOutsideQuotes(cmd, ["--no-verify", "--no-gpg-sign"]);
1915
+ return out === cmd ? null : out;
1916
+ }
1917
+ function rewriteDryRun(cmd) {
1918
+ if (!isSingleCommand(cmd))
1919
+ return null;
1920
+ const key = cmd.replace(/\s+/g, " ").trim();
1921
+ if (/^\s*terraform\s+apply\b/.test(cmd)) {
1922
+ const tail = cmd.replace(/^\s*terraform\s+apply\b/, "").trim();
1923
+ if (tail.length > 0 && tail.split(/\s+/).some((a) => !a.startsWith("-")))
1924
+ return null;
1925
+ return {
1926
+ key,
1927
+ command: cmd.replace(/^(\s*)terraform\s+apply\b/, "$1terraform plan").replace(/\s+-auto-approve\b/g, "").replace(/ {2,}/g, " ").trimEnd()
1928
+ };
1929
+ }
1930
+ if (/^\s*kubectl\s+delete\b/.test(cmd) && !/--dry-run\b/.test(cmd))
1931
+ return { key, command: `${cmd.trimEnd()} --dry-run=client` };
1932
+ if (/^\s*helm\s+(uninstall|delete)\b/.test(cmd) && !/--dry-run\b/.test(cmd))
1933
+ return { key, command: `${cmd.trimEnd()} --dry-run` };
1934
+ return null;
1935
+ }
1936
+ var customRe = new WeakMap;
1937
+ function compiled(rule) {
1938
+ const hit = customRe.get(rule);
1939
+ if (hit !== undefined)
1940
+ return hit;
1941
+ let re = null;
1942
+ try {
1943
+ re = new RegExp(rule.match, rule.action === "rewrite" ? "g" : "");
1944
+ } catch {
1945
+ re = null;
1946
+ }
1947
+ customRe.set(rule, re);
1948
+ return re;
1949
+ }
1950
+ function applyCustomRule(rule, cmd) {
1951
+ const re = compiled(rule);
1952
+ if (!re)
1953
+ return null;
1954
+ if (rule.action === "off" || !re.test(cmd))
1955
+ return null;
1956
+ const id = `custom:${rule.name}`;
1957
+ const reason = rule.reason ?? `matches custom rule "${rule.name}" (${rule.match})`;
1958
+ if (rule.action !== "rewrite")
1959
+ return { action: rule.action, rule: id, reason };
1960
+ if (!isSingleCommand(cmd))
1961
+ return {
1962
+ action: "ask",
1963
+ rule: id,
1964
+ reason: `${reason} \u2014 not rewritten: the command chains or redirects`
1965
+ };
1966
+ re.lastIndex = 0;
1967
+ const out = cmd.replace(re, rule.replace ?? "").trim();
1968
+ return out === cmd ? null : { action: "rewrite", rule: id, reason, command: out };
1969
+ }
1970
+ function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES, ctx = {}) {
1725
1971
  const other = () => otherLiveInSameTree(current, sessions, now);
1726
1972
  const hit = (rule, reason) => {
1727
1973
  const mode = modes[rule];
1728
1974
  return mode === "off" ? { action: "allow" } : { action: mode, rule, reason };
1729
1975
  };
1976
+ const fix = (rule, reason, command, key) => {
1977
+ const mode = modes[rule] ?? "off";
1978
+ if (mode === "off")
1979
+ return { action: "allow" };
1980
+ if (mode === "rewrite")
1981
+ return { action: "rewrite", rule, reason, command, ...key ? { key } : {} };
1982
+ return { action: mode, rule, reason };
1983
+ };
1730
1984
  if (modes.protected_ports !== "off" && modes.protected.ports.length) {
1731
1985
  const target = killedPorts(cmd).filter((p) => modes.protected.ports.includes(p));
1732
1986
  if (target.length) {
@@ -1756,6 +2010,23 @@ function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES) {
1756
2010
  return d;
1757
2011
  }
1758
2012
  }
2013
+ for (const rule of modes.custom ?? []) {
2014
+ const d = applyCustomRule(rule, cmd);
2015
+ if (d)
2016
+ return d;
2017
+ }
2018
+ const nv = rewriteNoVerify(cmd);
2019
+ if (nv) {
2020
+ const d = fix("no_verify", "Hooks and signing exist for a reason: `--no-verify` / `--no-gpg-sign` was dropped. If a hook is wrong, fix the hook.", nv);
2021
+ if (d.action !== "allow")
2022
+ return d;
2023
+ }
2024
+ const dr = rewriteDryRun(cmd);
2025
+ if (dr && !ctx.rewritesDone?.has(dr.key)) {
2026
+ const d = fix("dry_run_first", `The first \`${dr.key}\` in a session runs as a dry run so you can read what it would change; run the real one next.`, dr.command, dr.key);
2027
+ if (d.action !== "allow")
2028
+ return d;
2029
+ }
1759
2030
  return { action: "allow" };
1760
2031
  }
1761
2032
  function norm(p) {
@@ -1818,7 +2089,9 @@ var RULE_IDS = [
1818
2089
  "destructive_git",
1819
2090
  "protected_ports",
1820
2091
  "no_foreign_worktree",
1821
- "claim_required_to_write"
2092
+ "claim_required_to_write",
2093
+ "no_verify",
2094
+ "dry_run_first"
1822
2095
  ];
1823
2096
  function normalizeDisplay(s) {
1824
2097
  return s.replace(/\s+/g, " ").trim().slice(0, 160);
@@ -1828,7 +2101,7 @@ function dryRunRules(calls, modes, ctx) {
1828
2101
  const minRepeat = ctx.minRepeat ?? 3;
1829
2102
  const maxHits = ctx.maxHits ?? 200;
1830
2103
  const live = new Map;
1831
- const byRule = Object.fromEntries(RULE_IDS.map((r) => [r, { ask: 0, deny: 0 }]));
2104
+ const byRule = Object.fromEntries(RULE_IDS.map((r) => [r, { ask: 0, deny: 0, rewrite: 0 }]));
1832
2105
  const hits = [];
1833
2106
  const groups = new Map;
1834
2107
  let evaluated = 0;
@@ -1861,7 +2134,9 @@ function dryRunRules(calls, modes, ctx) {
1861
2134
  continue;
1862
2135
  if (d.action === "allow")
1863
2136
  continue;
1864
- byRule[d.rule][d.action]++;
2137
+ const tally = byRule[d.rule] ?? { ask: 0, deny: 0, rewrite: 0 };
2138
+ tally[d.action]++;
2139
+ byRule[d.rule] = tally;
1865
2140
  const norm = normalizeDisplay(display);
1866
2141
  if (hits.length < maxHits)
1867
2142
  hits.push({
@@ -2596,6 +2871,18 @@ function portsIn(cmd) {
2596
2871
  var RECURRING = 3;
2597
2872
  function suggestFromIncident(inc) {
2598
2873
  const n = inc.count ?? 1;
2874
+ if (inc.rule.startsWith("custom:")) {
2875
+ const name = inc.rule.slice("custom:".length);
2876
+ const escaped = inc.command.slice(0, 160).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/"/g, "\\\"");
2877
+ return {
2878
+ title: n >= RECURRING ? `Deny "${name}" outright (recurring)` : `Tighten custom rule "${name}"`,
2879
+ toml: `[[rules.custom]]
2880
+ name = "${name}"
2881
+ match = "${escaped}"
2882
+ action = "${n >= RECURRING ? "deny" : "ask"}"`,
2883
+ lesson: `Custom rule "${name}" fired on \`${inc.command.slice(0, 120)}\` \u2014 don't run that here; see .swarm.toml [[rules.custom]] for why.`
2884
+ };
2885
+ }
2599
2886
  switch (inc.rule) {
2600
2887
  case "protected_ports": {
2601
2888
  const ports = portsIn(inc.command);
@@ -2644,6 +2931,20 @@ no_foreign_worktree = "deny"`,
2644
2931
  claim_required_to_write = "deny"`,
2645
2932
  lesson: "Claim a task (`swarm claim`) and work in the worktree it creates before editing this repo."
2646
2933
  };
2934
+ case "no_verify":
2935
+ return {
2936
+ title: "Always drop --no-verify",
2937
+ toml: `[rules]
2938
+ no_verify = "rewrite"`,
2939
+ lesson: "Never pass `--no-verify` / `--no-gpg-sign` to git \u2014 the hooks are the repo's checks. Fix the hook if it is wrong."
2940
+ };
2941
+ case "dry_run_first":
2942
+ return {
2943
+ title: "Dry-run infrastructure changes first",
2944
+ toml: `[rules]
2945
+ dry_run_first = "rewrite"`,
2946
+ lesson: "Run `terraform plan` / `kubectl \u2026 --dry-run=client` / `helm \u2026 --dry-run` and read it before the real apply, delete or uninstall."
2947
+ };
2647
2948
  case "orphaned_claim":
2648
2949
  return {
2649
2950
  title: "A claim expired with unfinished work",
@@ -2669,6 +2970,143 @@ function incidentKey(inc) {
2669
2970
  return `protected_ports:${portsIn(inc.command).join(",")}`;
2670
2971
  return inc.rule;
2671
2972
  }
2973
+ // packages/core/src/lessons-apply.ts
2974
+ var LESSONS_HEADING = "## Lessons from Swarm";
2975
+ function nextHeading(text) {
2976
+ let fence = null;
2977
+ let at = 0;
2978
+ for (const line of text.split(`
2979
+ `)) {
2980
+ const open = line.match(/^\s*(```+|~~~+)/);
2981
+ if (fence) {
2982
+ if (open && line.trim().startsWith(fence))
2983
+ fence = null;
2984
+ } else if (open) {
2985
+ fence = open[1];
2986
+ } else if (at > 0 && /^#{1,6} /.test(line)) {
2987
+ return at - 1;
2988
+ }
2989
+ at += line.length + 1;
2990
+ }
2991
+ return -1;
2992
+ }
2993
+ function mergeLesson(existing, lesson) {
2994
+ const line = `- ${lesson.trim()}`;
2995
+ const text = existing ?? "";
2996
+ if (text.split(`
2997
+ `).some((l) => l.trim() === line))
2998
+ return text;
2999
+ const idx = text.indexOf(LESSONS_HEADING);
3000
+ if (idx < 0) {
3001
+ const base = text.trimEnd();
3002
+ return `${base ? `${base}
3003
+
3004
+ ` : `# CLAUDE.md
3005
+
3006
+ `}${LESSONS_HEADING}
3007
+
3008
+ ${line}
3009
+ `;
3010
+ }
3011
+ const after = idx + LESSONS_HEADING.length;
3012
+ const rest = text.slice(after);
3013
+ const next = nextHeading(rest);
3014
+ const cut = next < 0 ? text.length : after + next;
3015
+ const section = text.slice(after, cut).trimEnd();
3016
+ return `${text.slice(0, after)}${section}
3017
+ ${line}
3018
+ ${next < 0 ? "" : text.slice(cut)}`;
3019
+ }
3020
+ var SECTION_HEADER = /^\s*\[\[?[A-Za-z0-9_.-]+\]\]?\s*(#.*)?$/;
3021
+ function parseSnippet(snippet) {
3022
+ const out = [];
3023
+ for (const raw of snippet.split(`
3024
+ `)) {
3025
+ const line = raw.trimEnd();
3026
+ const h = SECTION_HEADER.test(line) ? line.match(/^\[\[?([^\]]+)\]\]?$/) : null;
3027
+ if (h) {
3028
+ out.push({ header: line.trim(), array: line.startsWith("[["), lines: [] });
3029
+ continue;
3030
+ }
3031
+ if (!line.trim())
3032
+ continue;
3033
+ const cur = out.at(-1);
3034
+ if (cur)
3035
+ cur.lines.push(line);
3036
+ }
3037
+ return out;
3038
+ }
3039
+ function sectionBounds(lines, header) {
3040
+ const start = lines.findIndex((l) => l.trim() === header);
3041
+ if (start < 0)
3042
+ return null;
3043
+ let end = lines.length;
3044
+ for (let i = start + 1;i < lines.length; i++) {
3045
+ if (SECTION_HEADER.test(lines[i])) {
3046
+ end = i;
3047
+ break;
3048
+ }
3049
+ }
3050
+ return { start, end };
3051
+ }
3052
+ function setKey(lines, start, end, key, value) {
3053
+ for (let i = start + 1;i < end; i++) {
3054
+ if (new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`).test(lines[i])) {
3055
+ lines[i] = `${key} = ${value}`;
3056
+ return end;
3057
+ }
3058
+ }
3059
+ let at = end;
3060
+ while (at > start + 1 && !lines[at - 1].trim())
3061
+ at--;
3062
+ lines.splice(at, 0, `${key} = ${value}`);
3063
+ return end + 1;
3064
+ }
3065
+ function unionPorts(existing, incoming) {
3066
+ const nums = (s) => [...s.replace(/#.*$/, "").matchAll(/\d+/g)].map((m) => Number(m[0])).filter((n) => Number.isInteger(n));
3067
+ const set = new Set([...nums(existing), ...nums(incoming)]);
3068
+ return `[${[...set].sort((a, b) => a - b).join(", ")}]`;
3069
+ }
3070
+ function mergeToml(existing, snippet) {
3071
+ const lines = (existing ?? "").replace(/\r\n/g, `
3072
+ `).split(`
3073
+ `);
3074
+ if (lines.length === 1 && lines[0] === "")
3075
+ lines.length = 0;
3076
+ for (const block of parseSnippet(snippet)) {
3077
+ if (block.array) {
3078
+ if (lines.length && lines.at(-1).trim() !== "")
3079
+ lines.push("");
3080
+ lines.push(block.header, ...block.lines);
3081
+ continue;
3082
+ }
3083
+ let bounds = sectionBounds(lines, block.header);
3084
+ if (!bounds) {
3085
+ if (lines.length && lines.at(-1).trim() !== "")
3086
+ lines.push("");
3087
+ lines.push(block.header);
3088
+ bounds = { start: lines.length - 1, end: lines.length };
3089
+ }
3090
+ let end = bounds.end;
3091
+ for (const l of block.lines) {
3092
+ const m = l.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$/);
3093
+ if (!m)
3094
+ continue;
3095
+ const [, key, value] = m;
3096
+ let v = value;
3097
+ if (block.header === "[rules.protected]" && key === "ports") {
3098
+ const cur = lines.slice(bounds.start + 1, end).find((x) => /^\s*ports\s*=/.test(x))?.split("=")[1];
3099
+ v = unionPorts(cur ?? "", value);
3100
+ }
3101
+ end = setKey(lines, bounds.start, end, key, v);
3102
+ }
3103
+ }
3104
+ const out = lines.join(`
3105
+ `);
3106
+ return out.endsWith(`
3107
+ `) ? out : `${out}
3108
+ `;
3109
+ }
2672
3110
  // packages/core/src/lineage.ts
2673
3111
  var LINEAGE_MAX_NODES = 40;
2674
3112
  var LINEAGE_FANOUT = 4;
@@ -3125,6 +3563,21 @@ function outcomeReport(sessions, prs, revertedShas) {
3125
3563
  function parseReverts(gitLog) {
3126
3564
  return new Set([...gitLog.matchAll(/This reverts commit ([0-9a-f]{7,40})/gi)].map((m) => m[1].toLowerCase()));
3127
3565
  }
3566
+ // packages/core/src/permissions.ts
3567
+ function permissionHookOutput(a, input) {
3568
+ if (!a.behavior)
3569
+ return {};
3570
+ return {
3571
+ hookSpecificOutput: {
3572
+ hookEventName: "PermissionRequest",
3573
+ decision: {
3574
+ behavior: a.behavior,
3575
+ message: a.message ?? `[swarm] ${a.behavior === "allow" ? "allowed" : "denied"} from the dashboard`,
3576
+ ...a.behavior === "allow" ? { updatedInput: input } : {}
3577
+ }
3578
+ }
3579
+ };
3580
+ }
3128
3581
  // packages/core/src/policy.ts
3129
3582
  import { createHash } from "crypto";
3130
3583
  var HOOK_MARK = "swarm-hook";
@@ -3473,6 +3926,143 @@ function formatOpenQuestions(qs) {
3473
3926
  return null;
3474
3927
  return `[swarm] waiting on a human for: ${open.map((q) => `#${q.id} "${q.text.slice(0, 120)}"`).join("; ")} \u2014 the answer arrives as context on a later tool call, or via swarm_inbox`;
3475
3928
  }
3929
+ // packages/core/src/quota.ts
3930
+ var QUOTA_WINDOWS = ["five_hour", "seven_day", "spend_limit"];
3931
+ var QUOTA_LABEL = {
3932
+ five_hour: "5-hour window",
3933
+ seven_day: "7-day window",
3934
+ spend_limit: "spend limit"
3935
+ };
3936
+ function quotaSamples(payload, at) {
3937
+ const out = [];
3938
+ const rl = payload.rate_limits;
3939
+ if (!rl || typeof rl !== "object")
3940
+ return out;
3941
+ for (const w of QUOTA_WINDOWS) {
3942
+ const v = rl[w];
3943
+ if (!v || typeof v.used_percentage !== "number" || !Number.isFinite(v.used_percentage))
3944
+ continue;
3945
+ out.push({
3946
+ window: w,
3947
+ usedPct: v.used_percentage,
3948
+ resetsAt: typeof v.resets_at === "number" && Number.isFinite(v.resets_at) ? v.resets_at : null,
3949
+ at
3950
+ });
3951
+ }
3952
+ return out;
3953
+ }
3954
+ var BURN_MIN_SPAN_MS = 10 * 60000;
3955
+ function quotaLevel(usedPct, warnAt) {
3956
+ if (usedPct >= 100)
3957
+ return "exceeded";
3958
+ if (warnAt != null && warnAt > 0 && usedPct >= warnAt * 100)
3959
+ return "warn";
3960
+ return "ok";
3961
+ }
3962
+ function quotaReport(samples, now, warnAt) {
3963
+ const windows = [];
3964
+ let newest = null;
3965
+ for (const w of QUOTA_WINDOWS) {
3966
+ const all = samples.filter((s) => s.window === w).sort((a, b) => a.at - b.at);
3967
+ const last = all.at(-1);
3968
+ if (!last)
3969
+ continue;
3970
+ newest = newest === null ? last.at : Math.max(newest, last.at);
3971
+ const period = all.filter((s) => s.resetsAt === last.resetsAt);
3972
+ const first = period.find((s) => last.at - s.at >= BURN_MIN_SPAN_MS);
3973
+ let burn = null;
3974
+ if (first) {
3975
+ burn = (last.usedPct - first.usedPct) / ((last.at - first.at) / 3600000);
3976
+ if (!Number.isFinite(burn))
3977
+ burn = null;
3978
+ }
3979
+ const hoursToLimit = burn != null && burn > 0 && last.usedPct < 100 ? (100 - last.usedPct) / burn : null;
3980
+ const hoursToReset = last.resetsAt != null ? Math.max(0, (last.resetsAt * 1000 - now) / 3600000) : null;
3981
+ windows.push({
3982
+ window: w,
3983
+ usedPct: last.usedPct,
3984
+ resetsAt: last.resetsAt,
3985
+ sampledAt: last.at,
3986
+ burnPctPerHour: burn,
3987
+ hoursToLimit,
3988
+ hoursToReset,
3989
+ limitBeforeReset: hoursToLimit != null && (hoursToReset == null || hoursToLimit < hoursToReset),
3990
+ level: quotaLevel(last.usedPct, warnAt)
3991
+ });
3992
+ }
3993
+ return { windows, sampledAt: newest };
3994
+ }
3995
+ function tightestWindow(report) {
3996
+ let best = null;
3997
+ for (const w of report.windows) {
3998
+ if (w.level === "exceeded")
3999
+ return w;
4000
+ if (!w.limitBeforeReset || w.hoursToLimit == null)
4001
+ continue;
4002
+ if (!best || (best.hoursToLimit ?? Number.POSITIVE_INFINITY) > w.hoursToLimit)
4003
+ best = w;
4004
+ }
4005
+ return best;
4006
+ }
4007
+ function hoursText(h) {
4008
+ const minutes = Math.round(h * 60);
4009
+ if (minutes < 1)
4010
+ return "<1m";
4011
+ if (minutes < 60)
4012
+ return `${minutes}m`;
4013
+ if (minutes < 24 * 60) {
4014
+ const m = minutes % 60;
4015
+ return m ? `${Math.floor(minutes / 60)}h ${m}m` : `${Math.floor(minutes / 60)}h`;
4016
+ }
4017
+ const hours = Math.round(h);
4018
+ const d = Math.floor(hours / 24);
4019
+ const rest = hours % 24;
4020
+ return rest ? `${d}d ${rest}h` : `${d}d`;
4021
+ }
4022
+ function quotaMessage(w) {
4023
+ const label = QUOTA_LABEL[w.window];
4024
+ const used = `${Math.round(w.usedPct)}% of the ${label}`;
4025
+ if (w.level === "exceeded")
4026
+ return `${used} used \u2014 the plan is out of budget until it resets${w.hoursToReset != null ? ` in ${hoursText(w.hoursToReset)}` : ""}`;
4027
+ const pace = w.hoursToLimit != null ? `, at the current pace the limit lands in ${hoursText(w.hoursToLimit)}${w.hoursToReset != null ? ` and the window resets in ${hoursText(w.hoursToReset)}` : ""}` : w.hoursToReset != null ? `, resets in ${hoursText(w.hoursToReset)}` : "";
4028
+ return `${used} used${pace}`;
4029
+ }
4030
+ // packages/core/src/repair.ts
4031
+ var REASON_TAIL_CHARS = 1200;
4032
+ function repairDecision(input) {
4033
+ if (input.onStop !== "block")
4034
+ return { kind: "allow", why: "record" };
4035
+ if (input.maxBlocks <= 0)
4036
+ return { kind: "allow", why: "disabled" };
4037
+ if (!input.runs.length)
4038
+ return { kind: "allow", why: "nothing-to-run" };
4039
+ const failed = input.runs.filter((r) => r.verdict !== "pass");
4040
+ if (!failed.length)
4041
+ return { kind: "allow", why: "passed" };
4042
+ if (input.blocksSoFar >= input.maxBlocks)
4043
+ return {
4044
+ kind: "exhausted",
4045
+ failed,
4046
+ reason: `${failed.map((r) => r.gate).join(", ")} still failing after ${input.maxBlocks} refusal${input.maxBlocks === 1 ? "" : "s"} \u2014 letting the session stop`
4047
+ };
4048
+ const attempt = input.blocksSoFar + 1;
4049
+ return { kind: "block", attempt, failed, reason: blockReason(failed, attempt, input.maxBlocks) };
4050
+ }
4051
+ function blockReason(failed, attempt, maxBlocks) {
4052
+ const parts = failed.map((r) => {
4053
+ const tail = (r.evidence ?? "").trim();
4054
+ const clipped = tail.length > REASON_TAIL_CHARS ? `\u2026${tail.slice(tail.length - REASON_TAIL_CHARS)}` : tail;
4055
+ return `gate "${r.gate}" failed (${r.rubric})${clipped ? `:
4056
+ ${clipped}` : ""}`;
4057
+ });
4058
+ const left = maxBlocks - attempt;
4059
+ return [
4060
+ `[swarm] not done yet \u2014 ${failed.length === 1 ? "a required gate is" : `${failed.length} required gates are`} failing in this worktree. Fix the cause, run it again, then finish.`,
4061
+ ...parts,
4062
+ left > 0 ? `(refusal ${attempt} of ${maxBlocks}; after ${left} more the stop goes through and an incident opens)` : `(refusal ${attempt} of ${maxBlocks}; the next stop goes through and an incident opens)`
4063
+ ].join(`
4064
+ `);
4065
+ }
3476
4066
  // packages/core/src/resourcegraph.ts
3477
4067
  var nodeId = (kind, name, projectId) => `${kind}:${projectId ?? ""}:${name}`;
3478
4068
  function resourceGraph(held, wanted = [], now = Date.now()) {
@@ -4134,6 +4724,145 @@ function clusterProjectKey(remoteUrl) {
4134
4724
  return null;
4135
4725
  return `${host}/${m[2]}`;
4136
4726
  }
4727
+ // packages/core/src/teamsetup.ts
4728
+ var DEFAULT_TEAM_SETUP = {
4729
+ name: null,
4730
+ host: "0.0.0.0",
4731
+ port: 7878,
4732
+ mode: "token",
4733
+ token: null,
4734
+ issuer: null,
4735
+ clientId: null,
4736
+ db: null
4737
+ };
4738
+ function mintTeamSecret() {
4739
+ return `swt_${crypto.randomUUID().replaceAll("-", "")}`;
4740
+ }
4741
+ var str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
4742
+ function parseTeamSetup(text) {
4743
+ const out = { ...DEFAULT_TEAM_SETUP };
4744
+ if (!text)
4745
+ return out;
4746
+ for (const raw of text.split(`
4747
+ `)) {
4748
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
4749
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
4750
+ if (!m)
4751
+ continue;
4752
+ const key = m[1];
4753
+ const value = m[2].trim().replace(/^["']|["']$/g, "");
4754
+ switch (key) {
4755
+ case "name":
4756
+ out.name = str(value);
4757
+ break;
4758
+ case "host":
4759
+ out.host = str(value) ?? out.host;
4760
+ break;
4761
+ case "port": {
4762
+ const n = Number(value);
4763
+ if (Number.isInteger(n) && n > 0 && n < 65536)
4764
+ out.port = n;
4765
+ break;
4766
+ }
4767
+ case "mode":
4768
+ if (value === "oidc" || value === "token" || value === "open")
4769
+ out.mode = value;
4770
+ break;
4771
+ case "token":
4772
+ out.token = str(value);
4773
+ break;
4774
+ case "issuer":
4775
+ out.issuer = str(value);
4776
+ break;
4777
+ case "client_id":
4778
+ out.clientId = str(value);
4779
+ break;
4780
+ case "db":
4781
+ out.db = str(value);
4782
+ break;
4783
+ default:
4784
+ break;
4785
+ }
4786
+ }
4787
+ return out;
4788
+ }
4789
+ function renderTeamSetup(s) {
4790
+ const lines = [
4791
+ "# swarm-teamd \u2014 written by `swarm-teamd setup` or the dashboard's Team panel.",
4792
+ "# Environment variables still win: SWARM_TEAM_PORT / _HOST / _TOKEN / _OIDC_ISSUER /",
4793
+ "# _OIDC_CLIENT_ID / _DB override anything here.",
4794
+ "",
4795
+ `name = ${JSON.stringify(s.name ?? "")}`,
4796
+ `host = ${JSON.stringify(s.host)}`,
4797
+ `port = ${s.port}`,
4798
+ `mode = ${JSON.stringify(s.mode)}`
4799
+ ];
4800
+ if (s.mode === "token")
4801
+ lines.push(`token = ${JSON.stringify(s.token ?? "")}`);
4802
+ if (s.mode === "oidc") {
4803
+ lines.push(`issuer = ${JSON.stringify(s.issuer ?? "")}`);
4804
+ lines.push(`client_id = ${JSON.stringify(s.clientId ?? "")}`);
4805
+ }
4806
+ if (s.db)
4807
+ lines.push(`db = ${JSON.stringify(s.db)}`);
4808
+ return `${lines.join(`
4809
+ `)}
4810
+ `;
4811
+ }
4812
+ function inviteLink(url, token) {
4813
+ const q = new URLSearchParams({ url: url.replace(/\/+$/, "") });
4814
+ if (token)
4815
+ q.set("token", token);
4816
+ return `swarm+team://join?${q}`;
4817
+ }
4818
+ function parseInvite(text) {
4819
+ const raw = text.trim();
4820
+ if (!raw)
4821
+ return null;
4822
+ const invite = raw.match(/^swarm\+team:\/\/join\?(.*)$/i);
4823
+ if (invite) {
4824
+ const q = new URLSearchParams(invite[1]);
4825
+ const url = q.get("url");
4826
+ return url && /^https?:\/\//i.test(url) ? { url: url.replace(/\/+$/, ""), token: q.get("token") || null } : null;
4827
+ }
4828
+ const hash = raw.match(/^(https?:\/\/[^\s#]+)#(.+)$/i);
4829
+ if (hash)
4830
+ return {
4831
+ url: hash[1].replace(/\/+$/, ""),
4832
+ token: hash[2].trim() || null
4833
+ };
4834
+ if (/^https?:\/\/\S+$/i.test(raw))
4835
+ return { url: raw.replace(/\/+$/, ""), token: null };
4836
+ if (/^[A-Za-z0-9][A-Za-z0-9.-]*(:\d{2,5})?$/.test(raw))
4837
+ return {
4838
+ url: `http://${raw.includes(":") ? raw : `${raw}:${DEFAULT_TEAM_SETUP.port}`}`,
4839
+ token: null
4840
+ };
4841
+ return null;
4842
+ }
4843
+ function hostedUrl(address, port) {
4844
+ return `http://${address}:${port}`;
4845
+ }
4846
+ function withTeamUrl(text, url) {
4847
+ const section = text.match(/(^|\n)\[team\]([\s\S]*?)(?=\n\[|$)/);
4848
+ if (!section) {
4849
+ if (!url)
4850
+ return text;
4851
+ return `${text}${text && !text.endsWith(`
4852
+ `) ? `
4853
+ ` : ""}
4854
+ [team]
4855
+ url = ${JSON.stringify(url)}
4856
+ `;
4857
+ }
4858
+ const body = section[2] ?? "";
4859
+ const line = url ? `url = ${JSON.stringify(url)}` : "";
4860
+ const next = /^[ \t]*url[ \t]*=/m.test(body) ? body.replace(/^[ \t]*url[ \t]*=.*$/m, line).replace(/\n{3,}/g, `
4861
+
4862
+ `) : url ? `
4863
+ ${line}${body}` : body;
4864
+ return text.replace(section[0], `${section[1]}[team]${next}`);
4865
+ }
4137
4866
  // packages/core/src/transitions.ts
4138
4867
  function transitionGraph(steps, { minWeight = 1 } = {}) {
4139
4868
  const calls = new Map;
@@ -4312,9 +5041,9 @@ function waitingReport(episodes) {
4312
5041
  };
4313
5042
  }
4314
5043
  // packages/daemon/src/app.ts
4315
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
5044
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
4316
5045
  import { homedir as homedir4 } from "os";
4317
- import { dirname as dirname4, join as join10 } from "path";
5046
+ import { dirname as dirname4, join as join12 } from "path";
4318
5047
  import { fileURLToPath as fileURLToPath2 } from "url";
4319
5048
 
4320
5049
  // node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
@@ -5934,738 +6663,853 @@ var streamSSE = (c, cb, onError) => {
5934
6663
  return c.newResponse(stream.responseReadable);
5935
6664
  };
5936
6665
 
5937
- // packages/daemon/src/dispatcher.ts
5938
- class Dispatcher {
5939
- store;
5940
- runner;
5941
- forge;
5942
- entries = new Map;
5943
- opts = new Map;
5944
- constructor(store, runner, forge) {
5945
- this.store = store;
5946
- this.runner = runner;
5947
- this.forge = forge;
5948
- runner.onEnd((run) => void this.onRunEnd(run));
6666
+ // packages/daemon/src/codify.ts
6667
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6668
+ import { join as join5 } from "path";
6669
+
6670
+ // packages/daemon/src/git.ts
6671
+ import { realpathSync } from "fs";
6672
+ import { join as join4 } from "path";
6673
+ function git(cwd, args) {
6674
+ try {
6675
+ const r = Bun.spawnSync(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" });
6676
+ return r.exitCode === 0 ? r.stdout.toString() : null;
6677
+ } catch {
6678
+ return null;
5949
6679
  }
5950
- project(projectId) {
5951
- let m = this.entries.get(projectId);
5952
- if (!m) {
5953
- m = new Map;
5954
- this.entries.set(projectId, m);
5955
- }
5956
- return m;
6680
+ }
6681
+ function gitCommonDir(cwd) {
6682
+ const out = git(cwd, ["rev-parse", "--git-common-dir"])?.trim();
6683
+ if (!out)
6684
+ return null;
6685
+ try {
6686
+ return realpathSync(out.startsWith("/") ? out : join4(cwd, out));
6687
+ } catch {
6688
+ return null;
5957
6689
  }
5958
- status(projectId) {
5959
- return [...this.entries.get(projectId)?.values() ?? []];
6690
+ }
6691
+ function gitToplevel(cwd) {
6692
+ const out = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
6693
+ if (!out)
6694
+ return null;
6695
+ try {
6696
+ return realpathSync(out);
6697
+ } catch {
6698
+ return null;
5960
6699
  }
5961
- async dispatch(projectId, wanted, o = {}) {
5962
- const board = this.store.tasks(projectId);
5963
- if (!board)
5964
- return {
5965
- ok: false,
5966
- error: "this repo has no task source ([tasks] source in .swarm.toml)"
5967
- };
5968
- if (board.error)
5969
- return { ok: false, error: `task source: ${board.error}` };
5970
- const cfg = this.store.config(projectId).dispatch;
5971
- const opts = {
5972
- owner: o.owner ?? "dispatch",
5973
- ...o,
5974
- maxParallel: o.maxParallel ?? cfg.max_parallel
5975
- };
5976
- this.opts.set(projectId, opts);
5977
- const m = this.project(projectId);
5978
- const running = [...m.values()].filter((e) => e.state === "running").length;
5979
- const plan = planDispatch(board.tasks, wanted, {
5980
- maxParallel: opts.maxParallel,
5981
- running,
5982
- max: o.max,
5983
- alreadyQueued: [...m.values()].filter((e) => e.state !== "finished").map((e) => e.task)
5984
- });
5985
- const now = new Date().toISOString();
5986
- for (const t of [...plan.start, ...plan.queued]) {
5987
- m.set(t.id, {
5988
- task: t.id,
5989
- title: t.title,
5990
- state: "queued",
5991
- runId: null,
5992
- sessionId: null,
5993
- queuedAt: now,
5994
- startedAt: null,
5995
- endedAt: null,
5996
- outcome: null,
5997
- detail: null,
5998
- costUsd: null
5999
- });
6000
- }
6001
- if (plan.start.length || plan.queued.length)
6002
- this.store.append({
6003
- ts: now,
6004
- type: "dispatch.queued",
6005
- projectId,
6006
- sessionId: null,
6007
- payload: {
6008
- tasks: [...plan.start, ...plan.queued].map((t) => t.id),
6009
- maxParallel: opts.maxParallel,
6010
- summary: `dispatch ${[...plan.start, ...plan.queued].map((t) => t.id).join(", ")}`
6011
- }
6012
- });
6013
- const started = [];
6014
- const failed = [];
6015
- for (const t of plan.start) {
6016
- const r = await this.startOne(projectId, t);
6017
- if (r.ok)
6018
- started.push(t.id);
6019
- else
6020
- failed.push({ id: t.id, reason: r.reason });
6021
- }
6022
- await this.fill(projectId);
6023
- return {
6024
- ok: true,
6025
- started,
6026
- queued: plan.queued.map((t) => t.id).filter((id) => m.get(id)?.state === "queued"),
6027
- rejected: [...plan.rejected, ...failed]
6028
- };
6029
- }
6030
- async startOne(projectId, t) {
6031
- const m = this.project(projectId);
6032
- const e = m.get(t.id);
6033
- const opts = this.opts.get(projectId) ?? { owner: "dispatch" };
6034
- const cfg = this.store.config(projectId);
6035
- const gates = cfg.gates;
6036
- const prompt = taskPrompt(t, {
6037
- requiredGates: gates.required,
6038
- executableGates: gates.required.filter((g) => gates.defs[g]),
6039
- openPr: cfg.dispatch.require_pr
6040
- });
6041
- const r = await this.runner.start({
6042
- projectId,
6043
- task: t.id,
6044
- prompt,
6045
- owner: opts.owner,
6046
- permissionMode: opts.permissionMode ?? cfg.dispatch.permission_mode ?? "acceptEdits",
6047
- model: opts.model ?? cfg.dispatch.model ?? undefined,
6048
- maxTurns: opts.maxTurns ?? cfg.dispatch.max_turns ?? undefined,
6049
- profile: opts.profile ?? cfg.dispatch.profile ?? undefined
6050
- });
6051
- if (!r.ok) {
6052
- if (e) {
6053
- e.state = "finished";
6054
- e.endedAt = new Date().toISOString();
6055
- e.outcome = "crashed";
6056
- e.detail = r.reason;
6057
- }
6058
- this.store.append({
6059
- ts: new Date().toISOString(),
6060
- type: "dispatch.finished",
6061
- projectId,
6062
- sessionId: null,
6063
- payload: {
6064
- task: t.id,
6065
- outcome: "crashed",
6066
- detail: r.reason,
6067
- summary: `dispatch ${t.id}: could not start \u2014 ${r.reason}`
6068
- }
6700
+ }
6701
+ function parseWorktreeList(out) {
6702
+ const wts = [];
6703
+ let cur = null;
6704
+ const flush = () => {
6705
+ if (cur?.path) {
6706
+ wts.push({
6707
+ path: cur.path,
6708
+ branch: cur.branch ?? null,
6709
+ head: (cur.head ?? "").slice(0, 7),
6710
+ main: wts.length === 0,
6711
+ dirty: -1,
6712
+ ahead: -1,
6713
+ behind: -1,
6714
+ merged: false
6069
6715
  });
6070
- return { ok: false, reason: r.reason };
6071
- }
6072
- if (e) {
6073
- e.state = "running";
6074
- e.runId = r.run.id;
6075
- e.sessionId = r.run.sessionId;
6076
- e.startedAt = r.run.startedAt;
6077
- }
6078
- this.store.append({
6079
- ts: r.run.startedAt,
6080
- type: "dispatch.started",
6081
- projectId,
6082
- sessionId: r.run.sessionId,
6083
- payload: {
6084
- task: t.id,
6085
- runId: r.run.id,
6086
- worktree: r.run.worktree,
6087
- by: this.opts.get(projectId)?.owner ?? null,
6088
- summary: `dispatch ${t.id} \u2192 run ${r.run.id}`
6089
- }
6090
- });
6091
- return { ok: true };
6092
- }
6093
- async fill(projectId) {
6094
- const m = this.project(projectId);
6095
- const cap = this.opts.get(projectId)?.maxParallel ?? this.store.config(projectId).dispatch.max_parallel;
6096
- for (const e of m.values()) {
6097
- const running = [...m.values()].filter((x) => x.state === "running").length;
6098
- if (running >= cap)
6099
- return;
6100
- if (e.state !== "queued")
6101
- continue;
6102
- await this.startOne(projectId, { id: e.task, title: e.title });
6103
6716
  }
6717
+ cur = null;
6718
+ };
6719
+ for (const line of out.split(`
6720
+ `)) {
6721
+ if (line.startsWith("worktree ")) {
6722
+ flush();
6723
+ cur = { path: line.slice(9) };
6724
+ } else if (line.startsWith("HEAD ") && cur)
6725
+ cur.head = line.slice(5);
6726
+ else if (line.startsWith("branch ") && cur)
6727
+ cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
6728
+ else if (line === "")
6729
+ flush();
6104
6730
  }
6105
- async onRunEnd(run) {
6106
- const m = this.entries.get(run.projectId);
6107
- const e = m?.get(run.task);
6108
- if (!e || e.runId !== run.id)
6109
- return;
6110
- const cfg = this.store.config(run.projectId);
6111
- const required = cfg.gates.required;
6112
- let runs = this.store.gateRuns(run.projectId, run.task);
6113
- const status = this.store.gateStatusFor(runs, required);
6114
- const missing = required.filter((g) => cfg.gates.defs[g] && status.find((s) => s.gate === g)?.verdict !== "pass");
6115
- if (missing.length && !run.stopped) {
6116
- await this.store.runGates(run.projectId, run.task, missing, {
6117
- sessionId: run.sessionId,
6118
- owner: "dispatch"
6119
- });
6120
- runs = this.store.gateRuns(run.projectId, run.task);
6121
- }
6122
- const satisfied = gatesSatisfied(runs, required);
6123
- await this.forge.refresh(0).catch(() => {});
6124
- const branch = `task/${run.task}`;
6125
- const pr = this.forge.prs().find((p) => p.projectId === run.projectId && p.branch === branch);
6126
- const outcome = dispatchOutcome({
6127
- exitCode: run.exitCode,
6128
- isError: run.result?.isError ?? false,
6129
- gatesSatisfied: satisfied,
6130
- prOpen: Boolean(pr),
6131
- requirePr: cfg.dispatch.require_pr,
6132
- stopped: run.stopped ?? false
6133
- });
6134
- const verdicts = this.store.gateStatusFor(runs, required).map((s) => `${s.gate} ${s.verdict ?? "\u2014"}`).join(", ");
6135
- const detail = [
6136
- `exit ${run.exitCode}${run.result?.isError ? " (error)" : ""}`,
6137
- required.length ? `gates: ${verdicts}` : null,
6138
- pr ? `PR ${pr.url}` : cfg.dispatch.require_pr ? "no PR" : null
6139
- ].filter(Boolean).join(" \xB7 ");
6140
- e.state = "finished";
6141
- e.endedAt = run.endedAt;
6142
- e.outcome = outcome;
6143
- e.detail = detail;
6144
- e.costUsd = run.result?.costUsd ?? null;
6145
- const ts = run.endedAt ?? new Date().toISOString();
6146
- this.store.append({
6147
- ts,
6148
- type: "dispatch.finished",
6149
- projectId: run.projectId,
6150
- sessionId: run.sessionId,
6151
- payload: {
6152
- task: run.task,
6153
- runId: run.id,
6154
- outcome,
6155
- detail,
6156
- costUsd: e.costUsd,
6157
- summary: `dispatch ${run.task}: ${outcome} \u2014 ${detail}`
6158
- }
6159
- });
6160
- if (outcome !== "done" && outcome !== "stopped")
6161
- this.store.append({
6162
- ts,
6163
- type: "incident.opened",
6164
- projectId: run.projectId,
6165
- sessionId: run.sessionId,
6166
- payload: {
6167
- rule: "dispatch_failed",
6168
- action: outcome,
6169
- command: run.task,
6170
- reason: `dispatched run on ${run.task} ended ${outcome}: ${detail}. The worktree and claim are kept; resume it from the session page or release it.`
6171
- }
6172
- });
6173
- this.store.touch();
6174
- await this.fill(run.projectId);
6731
+ flush();
6732
+ return wts;
6733
+ }
6734
+ function applyStatus(w, st, ah) {
6735
+ w.dirty = st === null ? -1 : st.split(`
6736
+ `).filter(Boolean).length;
6737
+ const a = ah?.trim();
6738
+ w.ahead = a === undefined || a === "" ? -1 : Number(a);
6739
+ }
6740
+ function applyDrift(w, behind, ancestor, firstParents, cherry = null) {
6741
+ const b = behind?.trim();
6742
+ w.behind = b === undefined || b === "" ? -1 : Number(b);
6743
+ const onLine = firstParents?.split(`
6744
+ `).some((sha) => sha.startsWith(w.head)) ?? true;
6745
+ w.merged = (ancestor || squashed(cherry)) && !onLine;
6746
+ }
6747
+ function squashed(cherry) {
6748
+ if (!cherry)
6749
+ return false;
6750
+ const lines = cherry.split(`
6751
+ `).filter((l) => l.trim());
6752
+ return lines.length > 0 && lines.every((l) => l.startsWith("-"));
6753
+ }
6754
+ var FIRST_PARENT_DEPTH = "5000";
6755
+ var baseOf = (wts) => wts[0]?.main ? wts[0].branch : null;
6756
+ async function gitAsync(cwd, args) {
6757
+ try {
6758
+ const p = Bun.spawn(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" });
6759
+ const [out, code] = await Promise.all([new Response(p.stdout).text(), p.exited]);
6760
+ return code === 0 ? out : null;
6761
+ } catch {
6762
+ return null;
6175
6763
  }
6176
- clear(projectId, task) {
6177
- const m = this.project(projectId);
6178
- let n = 0;
6179
- for (const [id, e] of m) {
6180
- if (task && id !== task)
6181
- continue;
6182
- if (e.state === "queued" || e.state === "finished" && !task) {
6183
- m.delete(id);
6184
- n++;
6185
- }
6186
- }
6187
- return n;
6764
+ }
6765
+ async function listWorktreesAsync(root) {
6766
+ const out = await gitAsync(root, ["worktree", "list", "--porcelain"]);
6767
+ if (!out)
6768
+ return [];
6769
+ const wts = parseWorktreeList(out);
6770
+ const base = baseOf(wts);
6771
+ const line = base ? await gitAsync(root, ["rev-list", "--first-parent", "-n", FIRST_PARENT_DEPTH, base]) : null;
6772
+ await Promise.all(wts.map(async (w) => {
6773
+ const drift = base && !w.main;
6774
+ const [st, ah, be, mg, ch] = await Promise.all([
6775
+ gitAsync(w.path, ["status", "--porcelain", "--untracked-files=no"]),
6776
+ gitAsync(w.path, ["rev-list", "--count", "@{upstream}..HEAD"]),
6777
+ drift ? gitAsync(w.path, ["rev-list", "--count", `HEAD..${base}`]) : null,
6778
+ drift ? gitAsync(w.path, ["merge-base", "--is-ancestor", "HEAD", base]) : null,
6779
+ drift ? gitAsync(w.path, ["cherry", base, "HEAD"]) : null
6780
+ ]);
6781
+ applyStatus(w, st, ah);
6782
+ if (drift)
6783
+ applyDrift(w, be, mg !== null, line, ch);
6784
+ }));
6785
+ return wts;
6786
+ }
6787
+ var branchCache = new Map;
6788
+ function currentBranch(cwd) {
6789
+ const hit = branchCache.get(cwd);
6790
+ const now = Date.now();
6791
+ if (hit && now - hit.t < 5000)
6792
+ return hit.v;
6793
+ const v = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ?? null;
6794
+ branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
6795
+ return branchCache.get(cwd)?.v ?? null;
6796
+ }
6797
+ var originCache = new Map;
6798
+ function originUrl(root) {
6799
+ const hit = originCache.get(root);
6800
+ const now = Date.now();
6801
+ if (hit && now - hit.t < 300000)
6802
+ return hit.v;
6803
+ const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
6804
+ originCache.set(root, { v, t: now });
6805
+ return v;
6806
+ }
6807
+ function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
6808
+ const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
6809
+ const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
6810
+ if (git(repoRoot, args) === null)
6811
+ return null;
6812
+ try {
6813
+ return realpathSync(path);
6814
+ } catch {
6815
+ return path;
6188
6816
  }
6189
6817
  }
6190
-
6191
- // packages/daemon/src/forge.ts
6192
- import { existsSync as existsSync4 } from "fs";
6193
- import { homedir as homedir2 } from "os";
6194
- import { join as join4 } from "path";
6195
- var EXTRA_BIN_DIRS = [
6196
- "/opt/homebrew/bin",
6197
- "/usr/local/bin",
6198
- "/home/linuxbrew/.linuxbrew/bin",
6199
- join4(homedir2(), ".local", "bin"),
6200
- join4(homedir2(), "bin")
6201
- ];
6202
- function findBin(name) {
6203
- if (!name)
6818
+ function commitPaths(cwd, paths, message) {
6819
+ if (!paths.length || git(cwd, ["add", "--", ...paths]) === null)
6204
6820
  return null;
6205
- const onPath = Bun.which(name, { PATH: process.env.PATH ?? "" });
6206
- if (onPath)
6207
- return onPath;
6208
- for (const d of EXTRA_BIN_DIRS) {
6209
- const p = join4(d, name);
6210
- if (existsSync4(p))
6211
- return p;
6821
+ if (git(cwd, ["diff", "--cached", "--quiet"]) !== null)
6822
+ return null;
6823
+ if (git(cwd, [
6824
+ "-c",
6825
+ "user.email=swarm@localhost",
6826
+ "-c",
6827
+ "user.name=Swarm",
6828
+ "commit",
6829
+ "-q",
6830
+ "-m",
6831
+ message
6832
+ ]) === null)
6833
+ return null;
6834
+ return git(cwd, ["rev-parse", "HEAD"])?.trim() ?? null;
6835
+ }
6836
+ function worktreeRemove(repoRoot, path, force) {
6837
+ const args = ["worktree", "remove", path];
6838
+ if (force)
6839
+ args.push("--force");
6840
+ return git(repoRoot, args) !== null;
6841
+ }
6842
+ function heldWork(path) {
6843
+ const status = git(path, ["status", "--porcelain"]);
6844
+ const dirty = status !== null && status.trim().length > 0;
6845
+ const count = (args) => {
6846
+ const out = git(path, ["rev-list", "--count", ...args])?.trim();
6847
+ return out !== undefined && out !== "" ? Number(out) : 0;
6848
+ };
6849
+ let unpushed;
6850
+ if (git(path, ["rev-parse", "--verify", "--quiet", "@{upstream}"]) !== null) {
6851
+ unpushed = count(["@{upstream}..HEAD"]) > 0;
6852
+ } else {
6853
+ const baselines = ["--remotes"];
6854
+ for (const b of ["main", "master"]) {
6855
+ if (git(path, ["rev-parse", "--verify", "--quiet", `refs/heads/${b}`]) !== null)
6856
+ baselines.push(b);
6857
+ }
6858
+ unpushed = baselines.length > 1 ? count(["HEAD", "--not", ...baselines]) > 0 : false;
6212
6859
  }
6213
- return null;
6860
+ return { dirty, unpushed };
6214
6861
  }
6215
- var GH_FIELDS = "number,title,headRefName,url,author,isDraft,mergeable,reviewDecision,statusCheckRollup,createdAt";
6216
-
6217
- class ForgeService {
6218
- store;
6219
- cache = new Map;
6220
- inflight = new Set;
6221
- constructor(store) {
6222
- this.store = store;
6862
+ async function worktreeDiff(root, path) {
6863
+ const wts = parseWorktreeList(await gitAsync(root, ["worktree", "list", "--porcelain"]) ?? "");
6864
+ const baseRef = wts[0]?.path === realpathOr(root) || wts[0]?.main ? wts[0]?.branch ?? null : null;
6865
+ const isMain = wts[0]?.path === path;
6866
+ const mb = baseRef && !isMain ? (await gitAsync(path, ["merge-base", baseRef, "HEAD"]))?.trim() : null;
6867
+ const from = mb || "HEAD";
6868
+ const [numstat, names, log, status] = await Promise.all([
6869
+ gitAsync(path, ["diff", "--numstat", from]),
6870
+ gitAsync(path, ["diff", "--name-status", from]),
6871
+ mb ? gitAsync(path, ["log", "--format=%s", `${mb}..HEAD`]) : Promise.resolve(""),
6872
+ gitAsync(path, ["status", "--porcelain"])
6873
+ ]);
6874
+ const files = parseNumstat(numstat ?? "", names ?? "");
6875
+ for (const line of (status ?? "").split(`
6876
+ `)) {
6877
+ if (line.startsWith("?? "))
6878
+ files.push({ path: line.slice(3), added: -1, deleted: -1, status: "?" });
6223
6879
  }
6224
- prs() {
6225
- this.refresh();
6226
- const all = [...this.cache.values()].flatMap((c) => c.prs);
6227
- return all.sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
6880
+ return {
6881
+ base: mb ?? null,
6882
+ baseRef,
6883
+ files,
6884
+ commits: (log ?? "").split(`
6885
+ `).filter(Boolean),
6886
+ dirty: (status ?? "").trim().length > 0
6887
+ };
6888
+ }
6889
+ async function worktreePatch(path, base, file) {
6890
+ const from = base ?? "HEAD";
6891
+ if (file) {
6892
+ const tracked = await gitAsync(path, ["ls-files", "--error-unmatch", "--", file]) !== null;
6893
+ if (!tracked) {
6894
+ const p = Bun.spawn(["git", "-C", path, "diff", "--no-index", "--", "/dev/null", file], {
6895
+ stdout: "pipe",
6896
+ stderr: "ignore"
6897
+ });
6898
+ const [out] = await Promise.all([new Response(p.stdout).text(), p.exited]);
6899
+ return out;
6900
+ }
6901
+ return await gitAsync(path, ["diff", from, "--", file]) ?? "";
6228
6902
  }
6229
- async refresh(maxAgeMs = 120000) {
6230
- const projects = this.store.liveProjects();
6231
- await Promise.all(projects.map(async (p) => {
6232
- const hit = this.cache.get(p.id);
6233
- if (hit && Date.now() - hit.at < maxAgeMs)
6234
- return;
6235
- if (this.inflight.has(p.id))
6236
- return;
6237
- this.inflight.add(p.id);
6238
- try {
6239
- const prs = await this.poll(p.id, p.root);
6240
- this.cache.set(p.id, { at: Date.now(), prs });
6241
- } catch {
6242
- this.cache.set(p.id, { at: Date.now(), prs: this.cache.get(p.id)?.prs ?? [] });
6243
- } finally {
6244
- this.inflight.delete(p.id);
6245
- }
6246
- }));
6903
+ return await gitAsync(path, ["diff", from]) ?? "";
6904
+ }
6905
+ function realpathOr(p) {
6906
+ try {
6907
+ return realpathSync(p);
6908
+ } catch {
6909
+ return p;
6247
6910
  }
6248
- outcomeCache = new Map;
6249
- outcomeInflight = new Map;
6250
- mergedCached(projectId, root) {
6251
- const hit = this.outcomeCache.get(projectId);
6252
- const fresh = !!hit && Date.now() - hit.at < 600000;
6253
- if (!fresh)
6254
- this.merged(projectId, root).catch(() => {});
6255
- return { merged: hit?.merged ?? [], reverted: hit?.reverted ?? [], fresh };
6911
+ }
6912
+
6913
+ // packages/daemon/src/codify.ts
6914
+ async function applyCodify(store, forge, projectId, seq, target) {
6915
+ const project = store.project(projectId);
6916
+ if (!project)
6917
+ return { ok: false, error: "unknown project" };
6918
+ const incident = store.incident(seq);
6919
+ if (!incident || incident.projectId !== projectId)
6920
+ return { ok: false, error: `no incident #${seq} in ${project.name}` };
6921
+ const s = incident.suggestion;
6922
+ if (!s)
6923
+ return { ok: false, error: "this incident carries no suggestion to apply" };
6924
+ const wantToml = target !== "claude-md" && Boolean(s.toml);
6925
+ const wantLesson = target !== "swarm-toml";
6926
+ if (!wantToml && !wantLesson)
6927
+ return { ok: false, error: "nothing to write for this target (the rule has no config form)" };
6928
+ const name = `codify-${seq}`;
6929
+ const branch = `swarm/${name}`;
6930
+ const created = store.createWorktree(projectId, name, "HEAD", branch);
6931
+ if (!created.ok)
6932
+ return { ok: false, error: created.error };
6933
+ const wt = created.worktree;
6934
+ const files = [];
6935
+ const read = (f) => existsSync4(join5(wt, f)) ? readFileSync3(join5(wt, f), "utf8") : null;
6936
+ if (wantLesson) {
6937
+ writeFileSync2(join5(wt, "CLAUDE.md"), mergeLesson(read("CLAUDE.md"), s.lesson));
6938
+ files.push("CLAUDE.md");
6256
6939
  }
6257
- async merged(projectId, root) {
6258
- const hit = this.outcomeCache.get(projectId);
6259
- if (hit && Date.now() - hit.at < 600000)
6260
- return hit;
6261
- const inflight = this.outcomeInflight.get(projectId);
6262
- if (inflight)
6263
- return inflight;
6264
- const run = this.fetchMerged(projectId, root).finally(() => this.outcomeInflight.delete(projectId));
6265
- this.outcomeInflight.set(projectId, run);
6266
- return run;
6940
+ if (wantToml && s.toml) {
6941
+ writeFileSync2(join5(wt, ".swarm.toml"), mergeToml(read(".swarm.toml"), s.toml));
6942
+ files.push(".swarm.toml");
6267
6943
  }
6268
- async fetchMerged(projectId, root) {
6269
- let merged = [];
6270
- const remote = await this.remote(root);
6271
- if (remote?.forge === "github") {
6272
- const out = await this.run([
6273
- "gh",
6274
- "pr",
6275
- "list",
6276
- "--state",
6277
- "merged",
6278
- "--limit",
6279
- "200",
6280
- "--json",
6281
- "number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
6282
- ], root);
6283
- if (out)
6284
- merged = JSON.parse(out).map((r) => ({
6285
- branch: String(r.headRefName ?? ""),
6286
- number: Number(r.number ?? 0),
6287
- title: String(r.title ?? ""),
6288
- url: String(r.url ?? ""),
6289
- createdAt: r.createdAt ?? null,
6290
- mergedAt: r.mergedAt ?? null,
6291
- mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
6292
- }));
6293
- } else if (remote?.forge === "gitlab") {
6294
- const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
6295
- if (out)
6296
- merged = JSON.parse(out).map((r) => ({
6297
- branch: String(r.source_branch ?? ""),
6298
- number: Number(r.iid ?? 0),
6299
- title: String(r.title ?? ""),
6300
- url: String(r.web_url ?? ""),
6301
- createdAt: r.created_at ?? null,
6302
- mergedAt: r.merged_at ?? null,
6303
- mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
6304
- }));
6944
+ const rule = String(incident.rule ?? "rule");
6945
+ const message = `${s.title}
6946
+
6947
+ Codified from Swarm incident #${seq} (${rule}):
6948
+ ${String(incident.command ?? "").slice(0, 200)}
6949
+
6950
+ ${s.lesson}`;
6951
+ const commit = commitPaths(wt, files, message);
6952
+ if (!commit) {
6953
+ await store.removeWorktree(projectId, wt, true);
6954
+ return { ok: false, error: "nothing changed \u2014 the lesson and the rule were already in place" };
6955
+ }
6956
+ store.append({
6957
+ ts: new Date().toISOString(),
6958
+ type: "codify.applied",
6959
+ projectId,
6960
+ sessionId: null,
6961
+ payload: {
6962
+ seq,
6963
+ rule,
6964
+ branch,
6965
+ files,
6966
+ commit,
6967
+ summary: `codified incident #${seq} onto ${branch}`
6305
6968
  }
6306
- const log = await this.run(["git", "log", "--grep", "This reverts commit", "--format=%B", "-n", "300"], root);
6307
- const reverted = log ? [...parseReverts(log)] : [];
6308
- const entry = { at: Date.now(), merged, reverted };
6309
- this.outcomeCache.set(projectId, entry);
6310
- return entry;
6969
+ });
6970
+ const pr = await forge.openPR(projectId, { path: wt, branch, dirty: 0, main: false }, {
6971
+ title: s.title,
6972
+ body: `Codified from Swarm incident #${seq} (\`${rule}\`).
6973
+
6974
+ \`\`\`
6975
+ ${String(incident.command ?? "").slice(0, 400)}
6976
+ \`\`\`
6977
+
6978
+ ${s.lesson}${s.toml ? `
6979
+
6980
+ \`\`\`toml
6981
+ ${s.toml}
6982
+ \`\`\`` : ""}`,
6983
+ isDraft: false
6984
+ });
6985
+ if (pr.ok) {
6986
+ store.recordPrOpened(projectId, name, wt, pr.url);
6987
+ await store.removeWorktree(projectId, wt, false);
6988
+ return {
6989
+ ok: true,
6990
+ branch,
6991
+ files,
6992
+ commit,
6993
+ pr: { url: pr.url, ...pr.number ? { number: pr.number } : {} }
6994
+ };
6311
6995
  }
6312
- remoteCache = new Map;
6313
- async remote(root) {
6314
- const hit = this.remoteCache.get(root);
6315
- if (hit && Date.now() - hit.at < (hit.v ? 600000 : 60000))
6316
- return hit.v;
6317
- const out = await this.run(["git", "remote", "get-url", "origin"], root);
6318
- const v = out ? parseRemote(out.trim()) : null;
6319
- this.remoteCache.set(root, { at: Date.now(), v });
6320
- return v;
6996
+ return {
6997
+ ok: true,
6998
+ branch,
6999
+ files,
7000
+ commit,
7001
+ worktree: wt,
7002
+ gitLine: `git -C ${JSON.stringify(wt)} push -u origin ${branch}`
7003
+ };
7004
+ }
7005
+
7006
+ // packages/daemon/src/dispatcher.ts
7007
+ class Dispatcher {
7008
+ store;
7009
+ runner;
7010
+ forge;
7011
+ entries = new Map;
7012
+ opts = new Map;
7013
+ constructor(store, runner, forge) {
7014
+ this.store = store;
7015
+ this.runner = runner;
7016
+ this.forge = forge;
7017
+ runner.onEnd((run) => void this.onRunEnd(run));
6321
7018
  }
6322
- async run(cmd, cwd, timeoutMs = 20000) {
6323
- const bin = findBin(cmd[0]);
6324
- if (!bin)
6325
- return null;
6326
- let proc;
6327
- try {
6328
- proc = Bun.spawn([bin, ...cmd.slice(1)], { cwd, stdout: "pipe", stderr: "ignore" });
6329
- } catch {
6330
- return null;
6331
- }
6332
- const killer = setTimeout(() => proc.kill(), timeoutMs);
6333
- try {
6334
- const out = await new Response(proc.stdout).text();
6335
- return await proc.exited === 0 ? out : null;
6336
- } catch {
6337
- return null;
6338
- } finally {
6339
- clearTimeout(killer);
7019
+ project(projectId) {
7020
+ let m = this.entries.get(projectId);
7021
+ if (!m) {
7022
+ m = new Map;
7023
+ this.entries.set(projectId, m);
6340
7024
  }
7025
+ return m;
6341
7026
  }
6342
- async poll(projectId, root) {
6343
- const remote = await this.remote(root);
6344
- if (!remote)
6345
- return [];
6346
- let prs = [];
6347
- if (remote.forge === "github") {
6348
- const out = await this.run(["gh", "pr", "list", "--json", GH_FIELDS], root);
6349
- if (out)
6350
- prs = normalizeGithub(JSON.parse(out), remote.repo);
6351
- } else {
6352
- const out = await this.run(["glab", "mr", "list", "--output", "json"], root);
6353
- if (out)
6354
- prs = normalizeGitlab(JSON.parse(out), remote.repo);
6355
- }
6356
- return prs.map((pr) => ({ ...pr, projectId, projectRoot: root }));
7027
+ status(projectId) {
7028
+ return [...this.entries.get(projectId)?.values() ?? []];
6357
7029
  }
6358
- async openPR(projectId, worktree, draft) {
6359
- const p = this.store.projects().find((x) => x.id === projectId);
6360
- if (!p)
6361
- return { ok: false, error: "unknown project" };
6362
- if (worktree.main)
6363
- return { ok: false, error: "that is the main checkout \u2014 open the PR from a task worktree" };
6364
- if (!worktree.branch)
6365
- return { ok: false, error: "detached HEAD \u2014 check out a branch first" };
6366
- if (worktree.dirty > 0)
7030
+ async dispatch(projectId, wanted, o = {}) {
7031
+ const board = this.store.tasks(projectId);
7032
+ if (!board)
6367
7033
  return {
6368
7034
  ok: false,
6369
- error: `${worktree.path} has uncommitted changes \u2014 commit them first (Swarm never commits for you)`
7035
+ error: "this repo has no task source ([tasks] source in .swarm.toml)"
6370
7036
  };
6371
- const remote = await this.remote(p.root);
6372
- if (!remote)
6373
- return { ok: false, error: "no GitHub/GitLab remote on origin" };
6374
- const cli = remote.forge === "github" ? "gh" : "glab";
6375
- const bin = findBin(cli);
6376
- if (!bin)
6377
- return { ok: false, error: `${cli} is not installed` };
6378
- const sh = async (cmd, cwd) => {
6379
- const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
6380
- const out = await new Response(proc.stdout).text() + await new Response(proc.stderr).text();
6381
- return { ok: await proc.exited === 0, out: out.trim() };
7037
+ if (board.error)
7038
+ return { ok: false, error: `task source: ${board.error}` };
7039
+ const cfg = this.store.config(projectId).dispatch;
7040
+ const opts = {
7041
+ owner: o.owner ?? "dispatch",
7042
+ ...o,
7043
+ maxParallel: o.maxParallel ?? cfg.max_parallel
7044
+ };
7045
+ this.opts.set(projectId, opts);
7046
+ const m = this.project(projectId);
7047
+ const running = [...m.values()].filter((e) => e.state === "running").length;
7048
+ const plan = planDispatch(board.tasks, wanted, {
7049
+ maxParallel: opts.maxParallel,
7050
+ running,
7051
+ max: o.max,
7052
+ alreadyQueued: [...m.values()].filter((e) => e.state !== "finished").map((e) => e.task)
7053
+ });
7054
+ const now = new Date().toISOString();
7055
+ for (const t of [...plan.start, ...plan.queued]) {
7056
+ m.set(t.id, {
7057
+ task: t.id,
7058
+ title: t.title,
7059
+ state: "queued",
7060
+ runId: null,
7061
+ sessionId: null,
7062
+ queuedAt: now,
7063
+ startedAt: null,
7064
+ endedAt: null,
7065
+ outcome: null,
7066
+ detail: null,
7067
+ costUsd: null
7068
+ });
7069
+ }
7070
+ if (plan.start.length || plan.queued.length)
7071
+ this.store.append({
7072
+ ts: now,
7073
+ type: "dispatch.queued",
7074
+ projectId,
7075
+ sessionId: null,
7076
+ payload: {
7077
+ tasks: [...plan.start, ...plan.queued].map((t) => t.id),
7078
+ maxParallel: opts.maxParallel,
7079
+ summary: `dispatch ${[...plan.start, ...plan.queued].map((t) => t.id).join(", ")}`
7080
+ }
7081
+ });
7082
+ const started = [];
7083
+ const failed = [];
7084
+ for (const t of plan.start) {
7085
+ const r = await this.startOne(projectId, t);
7086
+ if (r.ok)
7087
+ started.push(t.id);
7088
+ else
7089
+ failed.push({ id: t.id, reason: r.reason });
7090
+ }
7091
+ await this.fill(projectId);
7092
+ return {
7093
+ ok: true,
7094
+ started,
7095
+ queued: plan.queued.map((t) => t.id).filter((id) => m.get(id)?.state === "queued"),
7096
+ rejected: [...plan.rejected, ...failed]
6382
7097
  };
6383
- const push = await sh(["git", "push", "-u", "origin", worktree.branch], worktree.path);
6384
- if (!push.ok)
6385
- return { ok: false, error: `git push failed: ${push.out.slice(0, 400)}` };
6386
- const existing = this.prs().find((x) => x.projectId === projectId && x.branch === worktree.branch);
6387
- if (existing)
6388
- return { ok: true, url: existing.url, number: existing.number };
6389
- const cmd = remote.forge === "github" ? [
6390
- bin,
6391
- "pr",
6392
- "create",
6393
- "--head",
6394
- worktree.branch,
6395
- "--title",
6396
- draft.title,
6397
- "--body",
6398
- draft.body,
6399
- ...draft.isDraft ? ["--draft"] : []
6400
- ] : [
6401
- bin,
6402
- "mr",
6403
- "create",
6404
- "--source-branch",
6405
- worktree.branch,
6406
- "--title",
6407
- draft.title,
6408
- "--description",
6409
- draft.body,
6410
- "--yes",
6411
- ...draft.isDraft ? ["--draft"] : []
6412
- ];
6413
- const r = await sh(cmd, worktree.path);
6414
- if (!r.ok)
6415
- return { ok: false, error: `${cli} failed: ${r.out.slice(0, 400)}` };
6416
- const url = r.out.match(/https?:\/\/\S+/)?.[0] ?? r.out;
6417
- const num = Number(url.match(/\/(\d+)\s*$/)?.[1]);
6418
- this.cache.delete(projectId);
6419
- return { ok: true, url, number: Number.isFinite(num) ? num : null };
6420
- }
6421
- async merge(projectId, number) {
6422
- const p = this.store.projects().find((x) => x.id === projectId);
6423
- if (!p)
6424
- return { ok: false, output: "unknown project" };
6425
- const remote = await this.remote(p.root);
6426
- if (!remote)
6427
- return { ok: false, output: "no forge remote" };
6428
- const cmd = remote.forge === "github" ? ["gh", "pr", "merge", String(number), "--squash"] : ["glab", "mr", "merge", String(number), "--squash", "--yes"];
6429
- const bin = findBin(cmd[0]);
6430
- if (!bin)
6431
- return { ok: false, output: `${cmd[0] ?? "forge CLI"} is not installed` };
6432
- const proc = Bun.spawn([bin, ...cmd.slice(1)], { cwd: p.root, stdout: "pipe", stderr: "pipe" });
6433
- const out = await new Response(proc.stdout).text() + await new Response(proc.stderr).text();
6434
- const ok = await proc.exited === 0;
6435
- if (ok)
6436
- this.cache.delete(projectId);
6437
- return { ok, output: out.trim().slice(0, 800) };
6438
- }
6439
- }
6440
-
6441
- // packages/daemon/src/git.ts
6442
- import { realpathSync } from "fs";
6443
- import { join as join5 } from "path";
6444
- function git(cwd, args) {
6445
- try {
6446
- const r = Bun.spawnSync(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" });
6447
- return r.exitCode === 0 ? r.stdout.toString() : null;
6448
- } catch {
6449
- return null;
6450
7098
  }
6451
- }
6452
- function gitCommonDir(cwd) {
6453
- const out = git(cwd, ["rev-parse", "--git-common-dir"])?.trim();
6454
- if (!out)
6455
- return null;
6456
- try {
6457
- return realpathSync(out.startsWith("/") ? out : join5(cwd, out));
6458
- } catch {
6459
- return null;
7099
+ async startOne(projectId, t) {
7100
+ const m = this.project(projectId);
7101
+ const e = m.get(t.id);
7102
+ const opts = this.opts.get(projectId) ?? { owner: "dispatch" };
7103
+ const cfg = this.store.config(projectId);
7104
+ const gates = cfg.gates;
7105
+ const prompt = taskPrompt(t, {
7106
+ requiredGates: gates.required,
7107
+ executableGates: gates.required.filter((g) => gates.defs[g]),
7108
+ openPr: cfg.dispatch.require_pr
7109
+ });
7110
+ const r = await this.runner.start({
7111
+ projectId,
7112
+ task: t.id,
7113
+ prompt,
7114
+ owner: opts.owner,
7115
+ permissionMode: opts.permissionMode ?? cfg.dispatch.permission_mode ?? "acceptEdits",
7116
+ model: opts.model ?? cfg.dispatch.model ?? undefined,
7117
+ maxTurns: opts.maxTurns ?? cfg.dispatch.max_turns ?? undefined,
7118
+ profile: opts.profile ?? cfg.dispatch.profile ?? undefined
7119
+ });
7120
+ if (!r.ok) {
7121
+ if (e) {
7122
+ e.state = "finished";
7123
+ e.endedAt = new Date().toISOString();
7124
+ e.outcome = "crashed";
7125
+ e.detail = r.reason;
7126
+ }
7127
+ this.store.append({
7128
+ ts: new Date().toISOString(),
7129
+ type: "dispatch.finished",
7130
+ projectId,
7131
+ sessionId: null,
7132
+ payload: {
7133
+ task: t.id,
7134
+ outcome: "crashed",
7135
+ detail: r.reason,
7136
+ summary: `dispatch ${t.id}: could not start \u2014 ${r.reason}`
7137
+ }
7138
+ });
7139
+ return { ok: false, reason: r.reason };
7140
+ }
7141
+ if (e) {
7142
+ e.state = "running";
7143
+ e.runId = r.run.id;
7144
+ e.sessionId = r.run.sessionId;
7145
+ e.startedAt = r.run.startedAt;
7146
+ }
7147
+ this.store.append({
7148
+ ts: r.run.startedAt,
7149
+ type: "dispatch.started",
7150
+ projectId,
7151
+ sessionId: r.run.sessionId,
7152
+ payload: {
7153
+ task: t.id,
7154
+ runId: r.run.id,
7155
+ worktree: r.run.worktree,
7156
+ by: this.opts.get(projectId)?.owner ?? null,
7157
+ summary: `dispatch ${t.id} \u2192 run ${r.run.id}`
7158
+ }
7159
+ });
7160
+ return { ok: true };
6460
7161
  }
6461
- }
6462
- function gitToplevel(cwd) {
6463
- const out = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
6464
- if (!out)
6465
- return null;
6466
- try {
6467
- return realpathSync(out);
6468
- } catch {
6469
- return null;
7162
+ async fill(projectId) {
7163
+ const m = this.project(projectId);
7164
+ const cap = this.opts.get(projectId)?.maxParallel ?? this.store.config(projectId).dispatch.max_parallel;
7165
+ for (const e of m.values()) {
7166
+ const running = [...m.values()].filter((x) => x.state === "running").length;
7167
+ if (running >= cap)
7168
+ return;
7169
+ if (e.state !== "queued")
7170
+ continue;
7171
+ await this.startOne(projectId, { id: e.task, title: e.title });
7172
+ }
6470
7173
  }
6471
- }
6472
- function parseWorktreeList(out) {
6473
- const wts = [];
6474
- let cur = null;
6475
- const flush = () => {
6476
- if (cur?.path) {
6477
- wts.push({
6478
- path: cur.path,
6479
- branch: cur.branch ?? null,
6480
- head: (cur.head ?? "").slice(0, 7),
6481
- main: wts.length === 0,
6482
- dirty: -1,
6483
- ahead: -1,
6484
- behind: -1,
6485
- merged: false
7174
+ async onRunEnd(run) {
7175
+ const m = this.entries.get(run.projectId);
7176
+ const e = m?.get(run.task);
7177
+ if (!e || e.runId !== run.id)
7178
+ return;
7179
+ const cfg = this.store.config(run.projectId);
7180
+ const required = cfg.gates.required;
7181
+ let runs = this.store.gateRuns(run.projectId, run.task);
7182
+ const status = this.store.gateStatusFor(runs, required);
7183
+ const missing = required.filter((g) => cfg.gates.defs[g] && status.find((s) => s.gate === g)?.verdict !== "pass");
7184
+ if (missing.length && !run.stopped) {
7185
+ await this.store.runGates(run.projectId, run.task, missing, {
7186
+ sessionId: run.sessionId,
7187
+ owner: "dispatch"
6486
7188
  });
7189
+ runs = this.store.gateRuns(run.projectId, run.task);
6487
7190
  }
6488
- cur = null;
6489
- };
6490
- for (const line of out.split(`
6491
- `)) {
6492
- if (line.startsWith("worktree ")) {
6493
- flush();
6494
- cur = { path: line.slice(9) };
6495
- } else if (line.startsWith("HEAD ") && cur)
6496
- cur.head = line.slice(5);
6497
- else if (line.startsWith("branch ") && cur)
6498
- cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
6499
- else if (line === "")
6500
- flush();
7191
+ const satisfied = gatesSatisfied(runs, required);
7192
+ await this.forge.refresh(0).catch(() => {});
7193
+ const branch = `task/${run.task}`;
7194
+ const pr = this.forge.prs().find((p) => p.projectId === run.projectId && p.branch === branch);
7195
+ const outcome = dispatchOutcome({
7196
+ exitCode: run.exitCode,
7197
+ isError: run.result?.isError ?? false,
7198
+ gatesSatisfied: satisfied,
7199
+ prOpen: Boolean(pr),
7200
+ requirePr: cfg.dispatch.require_pr,
7201
+ stopped: run.stopped ?? false
7202
+ });
7203
+ const verdicts = this.store.gateStatusFor(runs, required).map((s) => `${s.gate} ${s.verdict ?? "\u2014"}`).join(", ");
7204
+ const detail = [
7205
+ `exit ${run.exitCode}${run.result?.isError ? " (error)" : ""}`,
7206
+ required.length ? `gates: ${verdicts}` : null,
7207
+ pr ? `PR ${pr.url}` : cfg.dispatch.require_pr ? "no PR" : null
7208
+ ].filter(Boolean).join(" \xB7 ");
7209
+ e.state = "finished";
7210
+ e.endedAt = run.endedAt;
7211
+ e.outcome = outcome;
7212
+ e.detail = detail;
7213
+ e.costUsd = run.result?.costUsd ?? null;
7214
+ const ts = run.endedAt ?? new Date().toISOString();
7215
+ this.store.append({
7216
+ ts,
7217
+ type: "dispatch.finished",
7218
+ projectId: run.projectId,
7219
+ sessionId: run.sessionId,
7220
+ payload: {
7221
+ task: run.task,
7222
+ runId: run.id,
7223
+ outcome,
7224
+ detail,
7225
+ costUsd: e.costUsd,
7226
+ summary: `dispatch ${run.task}: ${outcome} \u2014 ${detail}`
7227
+ }
7228
+ });
7229
+ if (outcome !== "done" && outcome !== "stopped")
7230
+ this.store.append({
7231
+ ts,
7232
+ type: "incident.opened",
7233
+ projectId: run.projectId,
7234
+ sessionId: run.sessionId,
7235
+ payload: {
7236
+ rule: "dispatch_failed",
7237
+ action: outcome,
7238
+ command: run.task,
7239
+ reason: `dispatched run on ${run.task} ended ${outcome}: ${detail}. The worktree and claim are kept; resume it from the session page or release it.`
7240
+ }
7241
+ });
7242
+ this.store.touch();
7243
+ await this.fill(run.projectId);
7244
+ }
7245
+ clear(projectId, task) {
7246
+ const m = this.project(projectId);
7247
+ let n = 0;
7248
+ for (const [id, e] of m) {
7249
+ if (task && id !== task)
7250
+ continue;
7251
+ if (e.state === "queued" || e.state === "finished" && !task) {
7252
+ m.delete(id);
7253
+ n++;
7254
+ }
7255
+ }
7256
+ return n;
6501
7257
  }
6502
- flush();
6503
- return wts;
6504
- }
6505
- function applyStatus(w, st, ah) {
6506
- w.dirty = st === null ? -1 : st.split(`
6507
- `).filter(Boolean).length;
6508
- const a = ah?.trim();
6509
- w.ahead = a === undefined || a === "" ? -1 : Number(a);
6510
- }
6511
- function applyDrift(w, behind, ancestor, firstParents, cherry = null) {
6512
- const b = behind?.trim();
6513
- w.behind = b === undefined || b === "" ? -1 : Number(b);
6514
- const onLine = firstParents?.split(`
6515
- `).some((sha) => sha.startsWith(w.head)) ?? true;
6516
- w.merged = (ancestor || squashed(cherry)) && !onLine;
6517
- }
6518
- function squashed(cherry) {
6519
- if (!cherry)
6520
- return false;
6521
- const lines = cherry.split(`
6522
- `).filter((l) => l.trim());
6523
- return lines.length > 0 && lines.every((l) => l.startsWith("-"));
6524
7258
  }
6525
- var FIRST_PARENT_DEPTH = "5000";
6526
- var baseOf = (wts) => wts[0]?.main ? wts[0].branch : null;
6527
- async function gitAsync(cwd, args) {
6528
- try {
6529
- const p = Bun.spawn(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" });
6530
- const [out, code] = await Promise.all([new Response(p.stdout).text(), p.exited]);
6531
- return code === 0 ? out : null;
6532
- } catch {
7259
+
7260
+ // packages/daemon/src/forge.ts
7261
+ import { existsSync as existsSync5 } from "fs";
7262
+ import { homedir as homedir2 } from "os";
7263
+ import { join as join6 } from "path";
7264
+ var EXTRA_BIN_DIRS = [
7265
+ "/opt/homebrew/bin",
7266
+ "/usr/local/bin",
7267
+ "/home/linuxbrew/.linuxbrew/bin",
7268
+ join6(homedir2(), ".local", "bin"),
7269
+ join6(homedir2(), "bin")
7270
+ ];
7271
+ function findBin(name) {
7272
+ if (!name)
6533
7273
  return null;
7274
+ const onPath = Bun.which(name, { PATH: process.env.PATH ?? "" });
7275
+ if (onPath)
7276
+ return onPath;
7277
+ for (const d of EXTRA_BIN_DIRS) {
7278
+ const p = join6(d, name);
7279
+ if (existsSync5(p))
7280
+ return p;
6534
7281
  }
7282
+ return null;
6535
7283
  }
6536
- async function listWorktreesAsync(root) {
6537
- const out = await gitAsync(root, ["worktree", "list", "--porcelain"]);
6538
- if (!out)
6539
- return [];
6540
- const wts = parseWorktreeList(out);
6541
- const base = baseOf(wts);
6542
- const line = base ? await gitAsync(root, ["rev-list", "--first-parent", "-n", FIRST_PARENT_DEPTH, base]) : null;
6543
- await Promise.all(wts.map(async (w) => {
6544
- const drift = base && !w.main;
6545
- const [st, ah, be, mg, ch] = await Promise.all([
6546
- gitAsync(w.path, ["status", "--porcelain", "--untracked-files=no"]),
6547
- gitAsync(w.path, ["rev-list", "--count", "@{upstream}..HEAD"]),
6548
- drift ? gitAsync(w.path, ["rev-list", "--count", `HEAD..${base}`]) : null,
6549
- drift ? gitAsync(w.path, ["merge-base", "--is-ancestor", "HEAD", base]) : null,
6550
- drift ? gitAsync(w.path, ["cherry", base, "HEAD"]) : null
6551
- ]);
6552
- applyStatus(w, st, ah);
6553
- if (drift)
6554
- applyDrift(w, be, mg !== null, line, ch);
6555
- }));
6556
- return wts;
6557
- }
6558
- var branchCache = new Map;
6559
- function currentBranch(cwd) {
6560
- const hit = branchCache.get(cwd);
6561
- const now = Date.now();
6562
- if (hit && now - hit.t < 5000)
6563
- return hit.v;
6564
- const v = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ?? null;
6565
- branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
6566
- return branchCache.get(cwd)?.v ?? null;
6567
- }
6568
- var originCache = new Map;
6569
- function originUrl(root) {
6570
- const hit = originCache.get(root);
6571
- const now = Date.now();
6572
- if (hit && now - hit.t < 300000)
6573
- return hit.v;
6574
- const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
6575
- originCache.set(root, { v, t: now });
6576
- return v;
6577
- }
6578
- function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
6579
- const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
6580
- const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
6581
- if (git(repoRoot, args) === null)
6582
- return null;
6583
- try {
6584
- return realpathSync(path);
6585
- } catch {
6586
- return path;
7284
+ var GH_FIELDS = "number,title,headRefName,url,author,isDraft,mergeable,reviewDecision,statusCheckRollup,createdAt";
7285
+
7286
+ class ForgeService {
7287
+ store;
7288
+ cache = new Map;
7289
+ inflight = new Set;
7290
+ constructor(store) {
7291
+ this.store = store;
6587
7292
  }
6588
- }
6589
- function worktreeRemove(repoRoot, path, force) {
6590
- const args = ["worktree", "remove", path];
6591
- if (force)
6592
- args.push("--force");
6593
- return git(repoRoot, args) !== null;
6594
- }
6595
- function heldWork(path) {
6596
- const status = git(path, ["status", "--porcelain"]);
6597
- const dirty = status !== null && status.trim().length > 0;
6598
- const count = (args) => {
6599
- const out = git(path, ["rev-list", "--count", ...args])?.trim();
6600
- return out !== undefined && out !== "" ? Number(out) : 0;
6601
- };
6602
- let unpushed;
6603
- if (git(path, ["rev-parse", "--verify", "--quiet", "@{upstream}"]) !== null) {
6604
- unpushed = count(["@{upstream}..HEAD"]) > 0;
6605
- } else {
6606
- const baselines = ["--remotes"];
6607
- for (const b of ["main", "master"]) {
6608
- if (git(path, ["rev-parse", "--verify", "--quiet", `refs/heads/${b}`]) !== null)
6609
- baselines.push(b);
7293
+ prs() {
7294
+ this.refresh();
7295
+ const all = [...this.cache.values()].flatMap((c) => c.prs);
7296
+ return all.sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
7297
+ }
7298
+ async refresh(maxAgeMs = 120000) {
7299
+ const projects = this.store.liveProjects();
7300
+ await Promise.all(projects.map(async (p) => {
7301
+ const hit = this.cache.get(p.id);
7302
+ if (hit && Date.now() - hit.at < maxAgeMs)
7303
+ return;
7304
+ if (this.inflight.has(p.id))
7305
+ return;
7306
+ this.inflight.add(p.id);
7307
+ try {
7308
+ const prs = await this.poll(p.id, p.root);
7309
+ this.cache.set(p.id, { at: Date.now(), prs });
7310
+ } catch {
7311
+ this.cache.set(p.id, { at: Date.now(), prs: this.cache.get(p.id)?.prs ?? [] });
7312
+ } finally {
7313
+ this.inflight.delete(p.id);
7314
+ }
7315
+ }));
7316
+ }
7317
+ outcomeCache = new Map;
7318
+ outcomeInflight = new Map;
7319
+ mergedCached(projectId, root) {
7320
+ const hit = this.outcomeCache.get(projectId);
7321
+ const fresh = !!hit && Date.now() - hit.at < 600000;
7322
+ if (!fresh)
7323
+ this.merged(projectId, root).catch(() => {});
7324
+ return { merged: hit?.merged ?? [], reverted: hit?.reverted ?? [], fresh };
7325
+ }
7326
+ async merged(projectId, root) {
7327
+ const hit = this.outcomeCache.get(projectId);
7328
+ if (hit && Date.now() - hit.at < 600000)
7329
+ return hit;
7330
+ const inflight = this.outcomeInflight.get(projectId);
7331
+ if (inflight)
7332
+ return inflight;
7333
+ const run = this.fetchMerged(projectId, root).finally(() => this.outcomeInflight.delete(projectId));
7334
+ this.outcomeInflight.set(projectId, run);
7335
+ return run;
7336
+ }
7337
+ async fetchMerged(projectId, root) {
7338
+ let merged = [];
7339
+ const remote = await this.remote(root);
7340
+ if (remote?.forge === "github") {
7341
+ const out = await this.run([
7342
+ "gh",
7343
+ "pr",
7344
+ "list",
7345
+ "--state",
7346
+ "merged",
7347
+ "--limit",
7348
+ "200",
7349
+ "--json",
7350
+ "number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
7351
+ ], root);
7352
+ if (out)
7353
+ merged = JSON.parse(out).map((r) => ({
7354
+ branch: String(r.headRefName ?? ""),
7355
+ number: Number(r.number ?? 0),
7356
+ title: String(r.title ?? ""),
7357
+ url: String(r.url ?? ""),
7358
+ createdAt: r.createdAt ?? null,
7359
+ mergedAt: r.mergedAt ?? null,
7360
+ mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
7361
+ }));
7362
+ } else if (remote?.forge === "gitlab") {
7363
+ const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
7364
+ if (out)
7365
+ merged = JSON.parse(out).map((r) => ({
7366
+ branch: String(r.source_branch ?? ""),
7367
+ number: Number(r.iid ?? 0),
7368
+ title: String(r.title ?? ""),
7369
+ url: String(r.web_url ?? ""),
7370
+ createdAt: r.created_at ?? null,
7371
+ mergedAt: r.merged_at ?? null,
7372
+ mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
7373
+ }));
7374
+ }
7375
+ const log = await this.run(["git", "log", "--grep", "This reverts commit", "--format=%B", "-n", "300"], root);
7376
+ const reverted = log ? [...parseReverts(log)] : [];
7377
+ const entry = { at: Date.now(), merged, reverted };
7378
+ this.outcomeCache.set(projectId, entry);
7379
+ return entry;
7380
+ }
7381
+ remoteCache = new Map;
7382
+ async remote(root) {
7383
+ const hit = this.remoteCache.get(root);
7384
+ if (hit && Date.now() - hit.at < (hit.v ? 600000 : 60000))
7385
+ return hit.v;
7386
+ const out = await this.run(["git", "remote", "get-url", "origin"], root);
7387
+ const v = out ? parseRemote(out.trim()) : null;
7388
+ this.remoteCache.set(root, { at: Date.now(), v });
7389
+ return v;
7390
+ }
7391
+ async run(cmd, cwd, timeoutMs = 20000) {
7392
+ const bin = findBin(cmd[0]);
7393
+ if (!bin)
7394
+ return null;
7395
+ let proc;
7396
+ try {
7397
+ proc = Bun.spawn([bin, ...cmd.slice(1)], { cwd, stdout: "pipe", stderr: "ignore" });
7398
+ } catch {
7399
+ return null;
7400
+ }
7401
+ const killer = setTimeout(() => proc.kill(), timeoutMs);
7402
+ try {
7403
+ const out = await new Response(proc.stdout).text();
7404
+ return await proc.exited === 0 ? out : null;
7405
+ } catch {
7406
+ return null;
7407
+ } finally {
7408
+ clearTimeout(killer);
6610
7409
  }
6611
- unpushed = baselines.length > 1 ? count(["HEAD", "--not", ...baselines]) > 0 : false;
6612
- }
6613
- return { dirty, unpushed };
6614
- }
6615
- async function worktreeDiff(root, path) {
6616
- const wts = parseWorktreeList(await gitAsync(root, ["worktree", "list", "--porcelain"]) ?? "");
6617
- const baseRef = wts[0]?.path === realpathOr(root) || wts[0]?.main ? wts[0]?.branch ?? null : null;
6618
- const isMain = wts[0]?.path === path;
6619
- const mb = baseRef && !isMain ? (await gitAsync(path, ["merge-base", baseRef, "HEAD"]))?.trim() : null;
6620
- const from = mb || "HEAD";
6621
- const [numstat, names, log, status] = await Promise.all([
6622
- gitAsync(path, ["diff", "--numstat", from]),
6623
- gitAsync(path, ["diff", "--name-status", from]),
6624
- mb ? gitAsync(path, ["log", "--format=%s", `${mb}..HEAD`]) : Promise.resolve(""),
6625
- gitAsync(path, ["status", "--porcelain"])
6626
- ]);
6627
- const files = parseNumstat(numstat ?? "", names ?? "");
6628
- for (const line of (status ?? "").split(`
6629
- `)) {
6630
- if (line.startsWith("?? "))
6631
- files.push({ path: line.slice(3), added: -1, deleted: -1, status: "?" });
6632
7410
  }
6633
- return {
6634
- base: mb ?? null,
6635
- baseRef,
6636
- files,
6637
- commits: (log ?? "").split(`
6638
- `).filter(Boolean),
6639
- dirty: (status ?? "").trim().length > 0
6640
- };
6641
- }
6642
- async function worktreePatch(path, base, file) {
6643
- const from = base ?? "HEAD";
6644
- if (file) {
6645
- const tracked = await gitAsync(path, ["ls-files", "--error-unmatch", "--", file]) !== null;
6646
- if (!tracked) {
6647
- const p = Bun.spawn(["git", "-C", path, "diff", "--no-index", "--", "/dev/null", file], {
6648
- stdout: "pipe",
6649
- stderr: "ignore"
6650
- });
6651
- const [out] = await Promise.all([new Response(p.stdout).text(), p.exited]);
6652
- return out;
7411
+ async poll(projectId, root) {
7412
+ const remote = await this.remote(root);
7413
+ if (!remote)
7414
+ return [];
7415
+ let prs = [];
7416
+ if (remote.forge === "github") {
7417
+ const out = await this.run(["gh", "pr", "list", "--json", GH_FIELDS], root);
7418
+ if (out)
7419
+ prs = normalizeGithub(JSON.parse(out), remote.repo);
7420
+ } else {
7421
+ const out = await this.run(["glab", "mr", "list", "--output", "json"], root);
7422
+ if (out)
7423
+ prs = normalizeGitlab(JSON.parse(out), remote.repo);
6653
7424
  }
6654
- return await gitAsync(path, ["diff", from, "--", file]) ?? "";
7425
+ return prs.map((pr) => ({ ...pr, projectId, projectRoot: root }));
6655
7426
  }
6656
- return await gitAsync(path, ["diff", from]) ?? "";
6657
- }
6658
- function realpathOr(p) {
6659
- try {
6660
- return realpathSync(p);
6661
- } catch {
6662
- return p;
7427
+ async openPR(projectId, worktree, draft) {
7428
+ const p = this.store.projects().find((x) => x.id === projectId);
7429
+ if (!p)
7430
+ return { ok: false, error: "unknown project" };
7431
+ if (worktree.main)
7432
+ return { ok: false, error: "that is the main checkout \u2014 open the PR from a task worktree" };
7433
+ if (!worktree.branch)
7434
+ return { ok: false, error: "detached HEAD \u2014 check out a branch first" };
7435
+ if (worktree.dirty > 0)
7436
+ return {
7437
+ ok: false,
7438
+ error: `${worktree.path} has uncommitted changes \u2014 commit them first (Swarm never commits for you)`
7439
+ };
7440
+ const remote = await this.remote(p.root);
7441
+ if (!remote)
7442
+ return { ok: false, error: "no GitHub/GitLab remote on origin" };
7443
+ const cli = remote.forge === "github" ? "gh" : "glab";
7444
+ const bin = findBin(cli);
7445
+ if (!bin)
7446
+ return { ok: false, error: `${cli} is not installed` };
7447
+ const sh = async (cmd, cwd) => {
7448
+ const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
7449
+ const out = await new Response(proc.stdout).text() + await new Response(proc.stderr).text();
7450
+ return { ok: await proc.exited === 0, out: out.trim() };
7451
+ };
7452
+ const push = await sh(["git", "push", "-u", "origin", worktree.branch], worktree.path);
7453
+ if (!push.ok)
7454
+ return { ok: false, error: `git push failed: ${push.out.slice(0, 400)}` };
7455
+ const existing = this.prs().find((x) => x.projectId === projectId && x.branch === worktree.branch);
7456
+ if (existing)
7457
+ return { ok: true, url: existing.url, number: existing.number };
7458
+ const cmd = remote.forge === "github" ? [
7459
+ bin,
7460
+ "pr",
7461
+ "create",
7462
+ "--head",
7463
+ worktree.branch,
7464
+ "--title",
7465
+ draft.title,
7466
+ "--body",
7467
+ draft.body,
7468
+ ...draft.isDraft ? ["--draft"] : []
7469
+ ] : [
7470
+ bin,
7471
+ "mr",
7472
+ "create",
7473
+ "--source-branch",
7474
+ worktree.branch,
7475
+ "--title",
7476
+ draft.title,
7477
+ "--description",
7478
+ draft.body,
7479
+ "--yes",
7480
+ ...draft.isDraft ? ["--draft"] : []
7481
+ ];
7482
+ const r = await sh(cmd, worktree.path);
7483
+ if (!r.ok)
7484
+ return { ok: false, error: `${cli} failed: ${r.out.slice(0, 400)}` };
7485
+ const url = r.out.match(/https?:\/\/\S+/)?.[0] ?? r.out;
7486
+ const num = Number(url.match(/\/(\d+)\s*$/)?.[1]);
7487
+ this.cache.delete(projectId);
7488
+ return { ok: true, url, number: Number.isFinite(num) ? num : null };
7489
+ }
7490
+ async merge(projectId, number) {
7491
+ const p = this.store.projects().find((x) => x.id === projectId);
7492
+ if (!p)
7493
+ return { ok: false, output: "unknown project" };
7494
+ const remote = await this.remote(p.root);
7495
+ if (!remote)
7496
+ return { ok: false, output: "no forge remote" };
7497
+ const cmd = remote.forge === "github" ? ["gh", "pr", "merge", String(number), "--squash"] : ["glab", "mr", "merge", String(number), "--squash", "--yes"];
7498
+ const bin = findBin(cmd[0]);
7499
+ if (!bin)
7500
+ return { ok: false, output: `${cmd[0] ?? "forge CLI"} is not installed` };
7501
+ const proc = Bun.spawn([bin, ...cmd.slice(1)], { cwd: p.root, stdout: "pipe", stderr: "pipe" });
7502
+ const out = await new Response(proc.stdout).text() + await new Response(proc.stderr).text();
7503
+ const ok = await proc.exited === 0;
7504
+ if (ok)
7505
+ this.cache.delete(projectId);
7506
+ return { ok, output: out.trim().slice(0, 800) };
6663
7507
  }
6664
7508
  }
6665
7509
 
6666
7510
  // packages/daemon/src/runner.ts
6667
7511
  import { appendFileSync, mkdirSync as mkdirSync2, openSync } from "fs";
6668
- import { join as join6 } from "path";
7512
+ import { join as join7 } from "path";
6669
7513
  var PERMISSION_MODES = [
6670
7514
  "acceptEdits",
6671
7515
  "auto",
@@ -6732,9 +7576,9 @@ class Runner {
6732
7576
  await this.store.awaitBootstrap(worktree);
6733
7577
  const sessionId = crypto.randomUUID();
6734
7578
  const id = sessionId.slice(0, 8);
6735
- const logDir = join6(this.home, "logs", project.id);
7579
+ const logDir = join7(this.home, "logs", project.id);
6736
7580
  mkdirSync2(logDir, { recursive: true });
6737
- const log = join6(logDir, `run-${input.task.replace(/[^a-zA-Z0-9_.-]+/g, "-")}-${id}.log`);
7581
+ const log = join7(logDir, `run-${input.task.replace(/[^a-zA-Z0-9_.-]+/g, "-")}-${id}.log`);
6738
7582
  const logFd = openSync(log, "a");
6739
7583
  const args = [
6740
7584
  bin,
@@ -6917,6 +7761,18 @@ class Runner {
6917
7761
  this.answerPermission(run.id, requestId, true);
6918
7762
  return;
6919
7763
  }
7764
+ if (decision.action === "rewrite") {
7765
+ const second = this.store.evaluateTool(tool, { command: decision.command }, run.sessionId, run.worktree, false).decision;
7766
+ if (second.action === "deny" || second.action === "ask") {
7767
+ this.answerPermission(run.id, requestId, false, `[swarm] ${second.reason}`);
7768
+ return;
7769
+ }
7770
+ this.answerPermission(run.id, requestId, true, undefined, {
7771
+ ...input,
7772
+ command: decision.command
7773
+ });
7774
+ return;
7775
+ }
6920
7776
  run.pending.push({
6921
7777
  requestId,
6922
7778
  tool,
@@ -6941,7 +7797,7 @@ class Runner {
6941
7797
  });
6942
7798
  this.store.touch();
6943
7799
  }
6944
- answerPermission(runId, requestId, allow, message) {
7800
+ answerPermission(runId, requestId, allow, message, updatedInput) {
6945
7801
  const entry = this.live.get(runId);
6946
7802
  if (!entry)
6947
7803
  return { ok: false, reason: "no live run" };
@@ -6949,7 +7805,7 @@ class Runner {
6949
7805
  if (!stdin || typeof stdin === "number")
6950
7806
  return { ok: false, reason: "stdin not available" };
6951
7807
  const pend = entry.run.pending.find((p) => p.requestId === requestId);
6952
- const response = allow ? { behavior: "allow", updatedInput: pend?.input ?? {} } : { behavior: "deny", message: message ?? "Denied from the Swarm dashboard" };
7808
+ const response = allow ? { behavior: "allow", updatedInput: updatedInput ?? pend?.input ?? {} } : { behavior: "deny", message: message ?? "Denied from the Swarm dashboard" };
6953
7809
  stdin.write(`${JSON.stringify({ type: "control_response", response: { subtype: "success", request_id: requestId, response } })}
6954
7810
  `);
6955
7811
  stdin.flush();
@@ -7013,33 +7869,33 @@ import { Database } from "bun:sqlite";
7013
7869
  import {
7014
7870
  closeSync,
7015
7871
  copyFileSync,
7016
- existsSync as existsSync6,
7872
+ existsSync as existsSync7,
7017
7873
  mkdirSync as mkdirSync4,
7018
7874
  openSync as openSync3,
7019
7875
  readdirSync,
7020
- readFileSync as readFileSync3,
7876
+ readFileSync as readFileSync4,
7021
7877
  readSync,
7022
7878
  realpathSync as realpathSync2,
7023
7879
  renameSync,
7024
7880
  rmSync as rmSync2,
7025
7881
  statSync,
7026
7882
  unlinkSync,
7027
- writeFileSync as writeFileSync2
7883
+ writeFileSync as writeFileSync3
7028
7884
  } from "fs";
7029
7885
  import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
7030
- import { basename, dirname as dirname3, join as join8 } from "path";
7886
+ import { basename, dirname as dirname3, join as join9 } from "path";
7031
7887
 
7032
7888
  // packages/daemon/src/bootstrap.ts
7033
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
7034
- import { dirname as dirname2, join as join7 } from "path";
7889
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
7890
+ import { dirname as dirname2, join as join8 } from "path";
7035
7891
  function runBootstrap(plan, opts) {
7036
- const logDir = join7(opts.home, "logs", opts.projectId);
7892
+ const logDir = join8(opts.home, "logs", opts.projectId);
7037
7893
  mkdirSync3(logDir, { recursive: true });
7038
- const log = join7(logDir, `bootstrap-${opts.task.replace(/[^a-zA-Z0-9_.-]+/g, "-")}.log`);
7894
+ const log = join8(logDir, `bootstrap-${opts.task.replace(/[^a-zA-Z0-9_.-]+/g, "-")}.log`);
7039
7895
  const copied = [];
7040
7896
  const skipped = [];
7041
7897
  for (const c of plan.copies) {
7042
- if (!existsSync5(c.from)) {
7898
+ if (!existsSync6(c.from)) {
7043
7899
  skipped.push(c.rel);
7044
7900
  continue;
7045
7901
  }
@@ -7209,6 +8065,15 @@ CREATE TABLE IF NOT EXISTS messages (
7209
8065
  answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
7210
8066
  );
7211
8067
  CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
8068
+ -- the statusline asks per session after every assistant message; messages_open cannot serve that
8069
+ CREATE INDEX IF NOT EXISTS messages_session ON messages(session_id, kind, answered_at);
8070
+ CREATE TABLE IF NOT EXISTS quota (
8071
+ id INTEGER PRIMARY KEY AUTOINCREMENT, at INTEGER, window TEXT, used_pct REAL, resets_at INTEGER,
8072
+ session_id TEXT, project_id TEXT
8073
+ );
8074
+ CREATE INDEX IF NOT EXISTS quota_window_at ON quota(window, at);
8075
+ -- the report reads a time range across every window; (window, at) cannot serve a bare at >= ?
8076
+ CREATE INDEX IF NOT EXISTS quota_at ON quota(at);
7212
8077
  CREATE TABLE IF NOT EXISTS workflow_runs (
7213
8078
  id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
7214
8079
  step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
@@ -7237,7 +8102,7 @@ class Store {
7237
8102
  constructor(home = swarmHome()) {
7238
8103
  mkdirSync4(home, { recursive: true });
7239
8104
  this.home = home;
7240
- this.db = new Database(join8(home, "swarm.db"));
8105
+ this.db = new Database(join9(home, "swarm.db"));
7241
8106
  this.loadPricing();
7242
8107
  this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
7243
8108
  this.db.exec(SCHEMA);
@@ -7250,7 +8115,7 @@ class Store {
7250
8115
  this.ensureColumn("messages", "to_kind", "TEXT");
7251
8116
  this.ensureColumn("messages", "from_session", "TEXT");
7252
8117
  this.migrate();
7253
- this.migrateProjectsJson(join8(home, "projects.json"));
8118
+ this.migrateProjectsJson(join9(home, "projects.json"));
7254
8119
  this.reconcileMovedProjects();
7255
8120
  this.slimExistingEvents();
7256
8121
  this.retypeNotificationIncidents();
@@ -7303,13 +8168,22 @@ class Store {
7303
8168
  }
7304
8169
  reconcileMovedProjects() {
7305
8170
  const all = this.projects();
8171
+ const byId = new Map(all.map((p) => [p.id, p]));
7306
8172
  for (const stale of all) {
7307
- if (existsSync6(stale.root))
8173
+ if (!byId.has(stale.id))
7308
8174
  continue;
7309
- const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
8175
+ if (existsSync7(stale.root)) {
8176
+ const current = projectIdentity({ root: stale.root, commonDir: gitCommonDir(stale.root) });
8177
+ const live = current.id !== stale.id ? byId.get(current.id) : undefined;
8178
+ if (live && this.mergeProject(stale.id, live.id))
8179
+ byId.delete(stale.id);
8180
+ continue;
8181
+ }
8182
+ const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync7(p.root));
7310
8183
  if (live.length !== 1)
7311
8184
  continue;
7312
- this.mergeProject(stale.id, live[0].id);
8185
+ if (this.mergeProject(stale.id, live[0].id))
8186
+ byId.delete(stale.id);
7313
8187
  }
7314
8188
  }
7315
8189
  mergeProject(from, into) {
@@ -7400,10 +8274,10 @@ class Store {
7400
8274
  return actorFrom(owner, sessionId, { user: osUser(), runId });
7401
8275
  }
7402
8276
  migrateProjectsJson(file) {
7403
- if (!existsSync6(file))
8277
+ if (!existsSync7(file))
7404
8278
  return;
7405
8279
  try {
7406
- const list = JSON.parse(readFileSync3(file, "utf8"));
8280
+ const list = JSON.parse(readFileSync4(file, "utf8"));
7407
8281
  const ins = this.db.query("INSERT OR IGNORE INTO projects (id, root, common_dir, name, discovered, created_at) VALUES (?, ?, ?, ?, ?, ?)");
7408
8282
  for (const p of list)
7409
8283
  ins.run(p.id, p.root, p.commonDir, p.name, p.discovered ? 1 : 0, p.createdAt);
@@ -7415,7 +8289,7 @@ class Store {
7415
8289
  const hit = this.topCache.get(cwd);
7416
8290
  if (hit && Date.now() - hit.t < 1e4)
7417
8291
  return hit.v;
7418
- const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
8292
+ const v = cwd && existsSync7(cwd) ? gitToplevel(cwd) : null;
7419
8293
  this.topCache.set(cwd, { v, t: Date.now() });
7420
8294
  return v;
7421
8295
  }
@@ -7621,7 +8495,7 @@ class Store {
7621
8495
  }));
7622
8496
  }
7623
8497
  sessionContext(cwd) {
7624
- if (!cwd || !existsSync6(cwd))
8498
+ if (!cwd || !existsSync7(cwd))
7625
8499
  return null;
7626
8500
  const toplevel = this.toplevel(cwd);
7627
8501
  const project = this.resolveProject(cwd);
@@ -7749,6 +8623,7 @@ class Store {
7749
8623
  payload: { id, task: q.task, answer: a, by, summary: `answer to #${id}: ${a.slice(0, 120)}` }
7750
8624
  });
7751
8625
  this.touch();
8626
+ this.wakeAll();
7752
8627
  return { ok: true, question: this.question(id) };
7753
8628
  }
7754
8629
  inbox(sessionId, opts = {}) {
@@ -7857,6 +8732,7 @@ class Store {
7857
8732
  summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
7858
8733
  }
7859
8734
  });
8735
+ this.wakeAll();
7860
8736
  return { ok: true, message };
7861
8737
  }
7862
8738
  message(id) {
@@ -7898,48 +8774,334 @@ class Store {
7898
8774
  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));
7899
8775
  return ms;
7900
8776
  }
7901
- markMessageDelivered(id, sessionId) {
7902
- 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);
8777
+ markMessageDelivered(id, sessionId) {
8778
+ 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);
8779
+ }
8780
+ leadSession(projectId) {
8781
+ 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);
8782
+ return r?.id ?? null;
8783
+ }
8784
+ sessionByPrefix(prefix) {
8785
+ const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
8786
+ return rows.length === 1 ? rows[0]?.id ?? null : null;
8787
+ }
8788
+ sessionForTask(projectId, task) {
8789
+ const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
8790
+ if (!claim?.worktree)
8791
+ return null;
8792
+ const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
8793
+ return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
8794
+ }
8795
+ questionContext(task, projectId) {
8796
+ if (!task)
8797
+ return null;
8798
+ const qs = this.db.query("SELECT * FROM messages WHERE kind = 'question' AND project_id = ? AND task = ? AND (answered_at IS NULL OR delivered_at IS NULL) ORDER BY id").all(projectId, task);
8799
+ const list = qs.map((r) => this.rowToQuestion(r));
8800
+ const parts = [formatAnswers(list), formatOpenQuestions(list)].filter(Boolean);
8801
+ if (list.some((q) => q.answer !== null))
8802
+ this.db.query("UPDATE messages SET delivered_at = ? WHERE kind = 'question' AND project_id = ? AND task = ? AND answered_at IS NOT NULL AND delivered_at IS NULL").run(new Date().toISOString(), projectId, task);
8803
+ return parts.length ? parts.join(`
8804
+ `) : null;
8805
+ }
8806
+ contextFor(cwd, sessionId) {
8807
+ const parts = [];
8808
+ const base = this.sessionContext(cwd);
8809
+ if (base)
8810
+ parts.push(base);
8811
+ const answers = this.answerContext(sessionId);
8812
+ if (answers)
8813
+ parts.push(answers);
8814
+ const open = formatOpenQuestions(this.questions({ sessionId: sessionId ?? undefined, open: true }));
8815
+ if (open && !base?.includes(open))
8816
+ parts.push(open);
8817
+ return { text: parts.length ? parts.join(`
8818
+ `) : null, parts };
8819
+ }
8820
+ dashboardSeenAt = 0;
8821
+ touchDashboard() {
8822
+ this.dashboardSeenAt = Date.now();
8823
+ }
8824
+ dashboardWatching(withinMs = 12000) {
8825
+ return Date.now() - this.dashboardSeenAt < withinMs;
8826
+ }
8827
+ interactive = new Map;
8828
+ interactiveSeq = 0;
8829
+ pendingPermissions() {
8830
+ return [...this.interactive.values()].map((x) => x.p);
8831
+ }
8832
+ askInteractive(raw, waitMs) {
8833
+ const sessionId = typeof raw.session_id === "string" ? raw.session_id : "";
8834
+ const cwd = typeof raw.cwd === "string" ? raw.cwd : "";
8835
+ const tool = typeof raw.tool_name === "string" ? raw.tool_name : "tool";
8836
+ const input = raw.tool_input ?? {};
8837
+ const id = typeof raw.tool_use_id === "string" && raw.tool_use_id ? raw.tool_use_id : `perm_${++this.interactiveSeq}`;
8838
+ const project = cwd && existsSync7(cwd) ? this.resolveProject(cwd) : null;
8839
+ const verdict = this.evaluateTool(tool, input, sessionId, cwd, false).decision;
8840
+ const rule = verdict.action === "ask" || verdict.action === "deny" ? verdict.rule : null;
8841
+ const now = Date.now();
8842
+ const p = {
8843
+ id,
8844
+ sessionId,
8845
+ projectId: project?.id ?? null,
8846
+ tool,
8847
+ display: summarizeToolInput(tool, input),
8848
+ input,
8849
+ reason: verdict.action === "ask" || verdict.action === "deny" ? verdict.reason : permissionReason(raw),
8850
+ rule,
8851
+ askedAt: new Date(now).toISOString(),
8852
+ terminalAt: new Date(now + waitMs).toISOString()
8853
+ };
8854
+ return new Promise((resolve) => {
8855
+ const done = (a) => {
8856
+ const cur = this.interactive.get(id);
8857
+ if (!cur || cur.resolve !== done)
8858
+ return;
8859
+ clearTimeout(cur.timer);
8860
+ this.interactive.delete(id);
8861
+ this.append({
8862
+ ts: new Date().toISOString(),
8863
+ type: "permission.resolved",
8864
+ projectId: p.projectId ?? "p_unknown",
8865
+ sessionId: sessionId || null,
8866
+ payload: {
8867
+ requestId: id,
8868
+ tool,
8869
+ display: p.display,
8870
+ decision: a?.behavior ?? "terminal",
8871
+ by: a?.by ?? "terminal",
8872
+ source: "interactive",
8873
+ summary: `${tool} ${a?.behavior ?? "handed to the terminal"} (${a?.by ?? "no answer in time"})`
8874
+ }
8875
+ });
8876
+ this.touch();
8877
+ resolve(a);
8878
+ };
8879
+ const timer = setTimeout(() => done(null), waitMs);
8880
+ this.interactive.set(id, { p, resolve: done, timer });
8881
+ this.touch();
8882
+ });
8883
+ }
8884
+ answerInteractive(id, a) {
8885
+ const cur = this.interactive.get(id);
8886
+ if (!cur)
8887
+ return {
8888
+ ok: false,
8889
+ reason: "no such pending permission (answered, or the terminal took over)"
8890
+ };
8891
+ cur.resolve(a);
8892
+ return { ok: true };
8893
+ }
8894
+ recentEdits = new Map;
8895
+ collisionWarned = new Map;
8896
+ static EDIT_CAP = 5000;
8897
+ lastEditPrune = 0;
8898
+ noteEdit(sessionId, path, at = Date.now()) {
8899
+ const m = this.recentEdits.get(path) ?? new Map;
8900
+ m.set(sessionId, at);
8901
+ this.recentEdits.delete(path);
8902
+ this.recentEdits.set(path, m);
8903
+ if (this.recentEdits.size > Store.EDIT_CAP && at - this.lastEditPrune > 60000)
8904
+ this.pruneEdits(at);
8905
+ }
8906
+ pruneEdits(now) {
8907
+ this.lastEditPrune = now;
8908
+ const keep = 4 * 60 * 60000;
8909
+ for (const [path, m] of this.recentEdits) {
8910
+ for (const [sid, at] of m)
8911
+ if (now - at > keep)
8912
+ m.delete(sid);
8913
+ if (!m.size)
8914
+ this.recentEdits.delete(path);
8915
+ }
8916
+ for (const [k, at] of this.collisionWarned)
8917
+ if (now - at > keep)
8918
+ this.collisionWarned.delete(k);
8919
+ for (const path of this.recentEdits.keys()) {
8920
+ if (this.recentEdits.size <= Store.EDIT_CAP)
8921
+ break;
8922
+ this.recentEdits.delete(path);
8923
+ }
8924
+ }
8925
+ collisionContext(sessionId, cwd, path) {
8926
+ const modes = this.rulesFor(this.toplevel(cwd));
8927
+ if (modes.collision_context === false)
8928
+ return null;
8929
+ const windowMs = (modes.collision_window ?? DEFAULT_COLLISION_WINDOW_MIN) * 60000;
8930
+ const now = Date.now();
8931
+ const edits = [...this.recentEdits.get(path) ?? []].map(([sid, at]) => ({
8932
+ sessionId: sid,
8933
+ at
8934
+ }));
8935
+ if (edits.length < 2)
8936
+ return null;
8937
+ const cutoff = new Date(now - LIVE_WINDOW_MS - 1e4).toISOString();
8938
+ const rows = this.db.query("SELECT id, cwd, branch, title FROM sessions WHERE state != 'ended' AND last_seen_at > ? AND id != ?").all(cutoff, sessionId);
8939
+ const held = this.heldClaimsWithWorktree();
8940
+ const live = new Map(rows.map((r) => [
8941
+ r.id,
8942
+ {
8943
+ sessionId: r.id,
8944
+ task: r.cwd ? held.find((c) => isInside(r.cwd, c.worktree))?.task ?? null : null,
8945
+ branch: r.branch,
8946
+ title: r.title
8947
+ }
8948
+ ]));
8949
+ const w = collisionWarning(path, sessionId, edits, live, now, windowMs);
8950
+ if (!w)
8951
+ return null;
8952
+ const fresh = w.others.filter((o) => {
8953
+ const k = `${sessionId}|${o.sessionId}|${path}`;
8954
+ const at = this.collisionWarned.get(k);
8955
+ if (at && now - at < windowMs)
8956
+ return false;
8957
+ this.collisionWarned.set(k, now);
8958
+ return true;
8959
+ });
8960
+ if (!fresh.length)
8961
+ return null;
8962
+ const project = cwd && existsSync7(cwd) ? this.resolveProject(cwd) : null;
8963
+ this.append({
8964
+ ts: new Date(now).toISOString(),
8965
+ type: "collision.warned",
8966
+ projectId: project?.id ?? "p_unknown",
8967
+ sessionId,
8968
+ payload: {
8969
+ path,
8970
+ others: fresh.map((o) => ({ sessionId: o.sessionId, task: o.task, branch: o.branch })),
8971
+ summary: `also edited by ${fresh.map((o) => o.title ?? o.sessionId.slice(0, 8)).join(", ")}: ${path}`
8972
+ }
8973
+ });
8974
+ return w.text;
8975
+ }
8976
+ wakers = new Map;
8977
+ waitForWake(sessionId, maxMs) {
8978
+ this.wakers.get(sessionId)?.resolve({ wake: false, reason: "replaced" });
8979
+ if (!this.policyFor(null).config.messages.wake)
8980
+ return Promise.resolve({ wake: false, reason: "off" });
8981
+ const already = this.answerContext(sessionId);
8982
+ if (already)
8983
+ return Promise.resolve({ wake: true, text: already });
8984
+ return new Promise((resolve) => {
8985
+ const done = (r) => {
8986
+ const cur = this.wakers.get(sessionId);
8987
+ if (cur?.resolve !== done)
8988
+ return;
8989
+ clearTimeout(cur.timer);
8990
+ this.wakers.delete(sessionId);
8991
+ resolve(r);
8992
+ };
8993
+ const timer = setTimeout(() => done({ wake: false, reason: "timeout" }), maxMs);
8994
+ this.wakers.set(sessionId, { resolve: done, timer });
8995
+ });
8996
+ }
8997
+ wakeAll() {
8998
+ for (const [sid, w] of [...this.wakers]) {
8999
+ if (!this.hasPending(sid))
9000
+ continue;
9001
+ const text = this.answerContext(sid);
9002
+ if (text) {
9003
+ w.resolve({ wake: true, text });
9004
+ this.append({
9005
+ ts: new Date().toISOString(),
9006
+ type: "message.delivered",
9007
+ projectId: this.sessionProject(sid) ?? "p_unknown",
9008
+ sessionId: sid,
9009
+ payload: { by: "wake", summary: "woke the session with its inbox" }
9010
+ });
9011
+ }
9012
+ }
9013
+ }
9014
+ hasPending(sessionId) {
9015
+ return this.inbox(sessionId, { peek: true }).length > 0 || this.messageInbox(sessionId, { peek: true }).length > 0;
7903
9016
  }
7904
- leadSession(projectId) {
7905
- 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);
7906
- return r?.id ?? null;
9017
+ cancelWake(sessionId, reason) {
9018
+ this.wakers.get(sessionId)?.resolve({ wake: false, reason });
7907
9019
  }
7908
- sessionByPrefix(prefix) {
7909
- const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
7910
- return rows.length === 1 ? rows[0]?.id ?? null : null;
9020
+ sessionProject(sessionId) {
9021
+ const r = this.db.query("SELECT project_id FROM sessions WHERE id = ?").get(sessionId);
9022
+ return r?.project_id ?? null;
7911
9023
  }
7912
- sessionForTask(projectId, task) {
7913
- const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
7914
- if (!claim?.worktree)
7915
- return null;
7916
- const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
7917
- return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
9024
+ statuslines = new Map;
9025
+ statuslineFor(payload) {
9026
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : null;
9027
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : payload.workspace?.current_dir ?? "";
9028
+ if (sessionId)
9029
+ this.statuslines.set(sessionId, { at: Date.now(), payload });
9030
+ const held = cwd ? this.heldClaimsWithWorktree().find((c) => isInside(cwd, c.worktree)) : null;
9031
+ const project = held ? this.project(held.projectId) : cwd && existsSync7(cwd) ? this.resolveProject(cwd) : null;
9032
+ const budget = project ? this.budgetFor(project.id) : null;
9033
+ this.recordQuota(payload, sessionId, project?.id ?? null);
9034
+ const tight = tightestWindow(this.quota());
9035
+ return {
9036
+ quota: tight ? { window: tight.window, hoursToLimit: tight.hoursToLimit } : null,
9037
+ task: held ? {
9038
+ id: held.task,
9039
+ leftMin: Math.max(0, Math.round((new Date(held.expiresAt).getTime() - Date.now()) / 60000))
9040
+ } : null,
9041
+ budget: budget?.status.limit != null ? { level: budget.status.level, pct: budget.status.pct } : null,
9042
+ incidents: project ? this.openIncidents(project.id) : 0,
9043
+ waitingOn: sessionId ? this.questions({ sessionId, open: true }).length : 0,
9044
+ inbox: sessionId ? this.inbox(sessionId, { peek: true }).length : 0
9045
+ };
7918
9046
  }
7919
- questionContext(task, projectId) {
7920
- if (!task)
7921
- return null;
7922
- const qs = this.db.query("SELECT * FROM messages WHERE kind = 'question' AND project_id = ? AND task = ? AND (answered_at IS NULL OR delivered_at IS NULL) ORDER BY id").all(projectId, task);
7923
- const list = qs.map((r) => this.rowToQuestion(r));
7924
- const parts = [formatAnswers(list), formatOpenQuestions(list)].filter(Boolean);
7925
- if (list.some((q) => q.answer !== null))
7926
- this.db.query("UPDATE messages SET delivered_at = ? WHERE kind = 'question' AND project_id = ? AND task = ? AND answered_at IS NOT NULL AND delivered_at IS NULL").run(new Date().toISOString(), projectId, task);
7927
- return parts.length ? parts.join(`
7928
- `) : null;
9047
+ recordQuota(payload, sessionId, projectId) {
9048
+ const now = Date.now();
9049
+ for (const q of quotaSamples(payload, now)) {
9050
+ const last = this.db.query("SELECT at, used_pct, resets_at FROM quota WHERE window = ? ORDER BY at DESC LIMIT 1").get(q.window);
9051
+ const moved = !last || Math.round(last.used_pct * 10) !== Math.round(q.usedPct * 10);
9052
+ const reset = !last || last.resets_at !== q.resetsAt;
9053
+ if (last && !reset && !moved && now - last.at < BURN_MIN_SPAN_MS)
9054
+ continue;
9055
+ this.db.query("INSERT INTO quota (at, window, used_pct, resets_at, session_id, project_id) VALUES (?, ?, ?, ?, ?, ?)").run(now, q.window, q.usedPct, q.resetsAt, sessionId, projectId);
9056
+ this.quotaDirty = true;
9057
+ this.quotaMemo = null;
9058
+ }
7929
9059
  }
7930
- contextFor(cwd, sessionId) {
7931
- const parts = [];
7932
- const base = this.sessionContext(cwd);
7933
- if (base)
7934
- parts.push(base);
7935
- const answers = this.answerContext(sessionId);
7936
- if (answers)
7937
- parts.push(answers);
7938
- const open = formatOpenQuestions(this.questions({ sessionId: sessionId ?? undefined, open: true }));
7939
- if (open && !base?.includes(open))
7940
- parts.push(open);
7941
- return { text: parts.length ? parts.join(`
7942
- `) : null, parts };
9060
+ quotaDirty = true;
9061
+ quotaMemo = null;
9062
+ quota() {
9063
+ const now = Date.now();
9064
+ if (!this.quotaDirty && this.quotaMemo && now - this.quotaMemo.at < 30000)
9065
+ return this.quotaMemo.report;
9066
+ const rows = this.db.query("SELECT at, window, used_pct, resets_at FROM quota WHERE at >= ? ORDER BY at").all(now - 8 * 86400000);
9067
+ const report = quotaReport(rows.map((r) => ({ window: r.window, usedPct: r.used_pct, resetsAt: r.resets_at, at: r.at })), now, this.policyFor(null).config.budget.window_warn_at);
9068
+ this.quotaMemo = { at: now, report };
9069
+ this.quotaDirty = false;
9070
+ return report;
9071
+ }
9072
+ quotaNotified = new Map;
9073
+ checkQuota() {
9074
+ const out = [];
9075
+ for (const w of this.quota().windows) {
9076
+ if (w.level === "ok")
9077
+ continue;
9078
+ out.push(w);
9079
+ const key = `${w.resetsAt ?? "none"}:${w.level}`;
9080
+ if (this.quotaNotified.get(w.window) === key)
9081
+ continue;
9082
+ this.quotaNotified.set(w.window, key);
9083
+ const src = this.db.query("SELECT project_id FROM quota WHERE window = ? ORDER BY at DESC LIMIT 1").get(w.window);
9084
+ const projectId = src?.project_id ?? this.projects()[0]?.id ?? null;
9085
+ if (!projectId)
9086
+ continue;
9087
+ this.append({
9088
+ ts: new Date().toISOString(),
9089
+ type: "incident.opened",
9090
+ projectId,
9091
+ sessionId: null,
9092
+ payload: {
9093
+ rule: "budget",
9094
+ action: "warn",
9095
+ command: `${QUOTA_LABEL[w.window]} (plan quota)`,
9096
+ reason: w.level === "exceeded" ? `${quotaMessage(w)}. Sessions on this plan will be rate-limited; API-key sessions are unaffected.` : `${quotaMessage(w)} \u2014 approaching the plan limit ([budget] window_warn_at)`
9097
+ }
9098
+ });
9099
+ this.touch();
9100
+ }
9101
+ return out;
9102
+ }
9103
+ lastStatusline(sessionId) {
9104
+ return this.statuslines.get(sessionId) ?? null;
7943
9105
  }
7944
9106
  rowToGate(r) {
7945
9107
  return {
@@ -7992,24 +9154,24 @@ class Store {
7992
9154
  };
7993
9155
  const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
7994
9156
  const worktree = claim?.worktree;
7995
- if (!worktree || !existsSync6(worktree))
9157
+ if (!worktree || !existsSync7(worktree))
7996
9158
  return {
7997
9159
  ok: false,
7998
9160
  reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
7999
9161
  };
8000
- const cwd = def.cwd ? join8(worktree, def.cwd) : worktree;
8001
- if (!existsSync6(cwd))
9162
+ const cwd = def.cwd ? join9(worktree, def.cwd) : worktree;
9163
+ if (!existsSync7(cwd))
8002
9164
  return { ok: false, reason: `gate cwd ${cwd} does not exist` };
8003
9165
  const key = `${projectId}:${task}:${gate}`;
8004
9166
  if (this.gateJobs.has(key))
8005
9167
  return { ok: false, reason: `${gate} is already running on ${task}` };
8006
9168
  const slug = (x) => x.replace(/[^a-zA-Z0-9_.-]+/g, "-");
8007
- const logDir = join8(this.home, "logs", projectId);
9169
+ const logDir = join9(this.home, "logs", projectId);
8008
9170
  mkdirSync4(logDir, { recursive: true });
8009
- const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
9171
+ const log = join9(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
8010
9172
  if (def.builtin === "review")
8011
9173
  return this.runReviewGate(projectId, task, gate, def, { worktree, cwd, key, log }, opts);
8012
- writeFileSync2(log, `$ ${def.cmd}
9174
+ writeFileSync3(log, `$ ${def.cmd}
8013
9175
  # cwd ${cwd} \xB7 ${new Date().toISOString()}
8014
9176
  `);
8015
9177
  const fd = openSync3(log, "a");
@@ -8069,7 +9231,7 @@ class Store {
8069
9231
  closeSync(fd);
8070
9232
  let output = "";
8071
9233
  try {
8072
- output = readFileSync3(log, "utf8");
9234
+ output = readFileSync4(log, "utf8");
8073
9235
  } catch {}
8074
9236
  const input = executedGateInput(task, gate, def.cmd, {
8075
9237
  exitCode: timedOut ? null : code,
@@ -8130,7 +9292,7 @@ class Store {
8130
9292
  stat,
8131
9293
  patch: diffText
8132
9294
  });
8133
- writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
9295
+ writeFileSync3(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
8134
9296
  # cwd ${where.cwd} \xB7 ${new Date().toISOString()}
8135
9297
  `);
8136
9298
  let proc;
@@ -8185,7 +9347,7 @@ class Store {
8185
9347
  const code = await proc.exited;
8186
9348
  clearTimeout(timer);
8187
9349
  try {
8188
- writeFileSync2(where.log, `${readFileSync3(where.log, "utf8")}${out}
9350
+ writeFileSync3(where.log, `${readFileSync4(where.log, "utf8")}${out}
8189
9351
  ${err}
8190
9352
  # exit ${timedOut ? "timeout" : code} \xB7 ${((Date.now() - started) / 1000).toFixed(0)}s
8191
9353
  `);
@@ -8258,6 +9420,8 @@ ${err}
8258
9420
  return;
8259
9421
  if (cfg.auto === "session-end" && event !== "SessionEnd")
8260
9422
  return;
9423
+ if (event === "Stop" && cfg.on_stop === "block")
9424
+ return;
8261
9425
  if (!cfg.required.some((g) => cfg.defs[g]))
8262
9426
  return;
8263
9427
  const key = `${held.projectId}:${held.task}`;
@@ -8265,13 +9429,119 @@ ${err}
8265
9429
  if (event === "Stop" && now - (this.autoGateAt.get(key) ?? 0) < 120000)
8266
9430
  return;
8267
9431
  this.autoGateAt.set(key, now);
8268
- this.runGates(held.projectId, held.task, undefined, { sessionId, owner: "auto" }).then((r) => {
8269
- if (!r.runs.length)
8270
- return;
8271
- const line = r.runs.map((x) => `${x.gate} ${x.verdict === "pass" ? "\u2713" : "\u2717"} (${x.rubric})`).join("; ");
8272
- this.db.query("UPDATE handoffs SET verify = ? WHERE project_id = ? AND task = ? AND session_id = ? AND by LIKE 'auto%'").run(`auto-gates: ${line}`, held.projectId, held.task, sessionId);
8273
- this.touch();
9432
+ this.runGates(held.projectId, held.task, undefined, { sessionId, owner: "auto" }).then((r) => this.writeAutoVerify(held.projectId, held.task, sessionId, r.runs));
9433
+ }
9434
+ writeAutoVerify(projectId, task, sessionId, runs) {
9435
+ if (!runs.length)
9436
+ return;
9437
+ const line = runs.map((x) => `${x.gate} ${x.verdict === "pass" ? "\u2713" : "\u2717"} (${x.rubric})`).join("; ");
9438
+ this.db.query("UPDATE handoffs SET verify = ? WHERE project_id = ? AND task = ? AND session_id = ? AND by LIKE 'auto%'").run(`auto-gates: ${line}`, projectId, task, sessionId);
9439
+ this.touch();
9440
+ }
9441
+ stopBlocks(sessionId) {
9442
+ const r = this.db.query("SELECT COUNT(*) AS n FROM events WHERE type = 'gate.blocked' AND session_id = ?").get(sessionId);
9443
+ return r.n;
9444
+ }
9445
+ stopExhausted = new Set;
9446
+ repairArmed(cwd) {
9447
+ if (!cwd || !existsSync7(cwd))
9448
+ return false;
9449
+ const held = this.heldClaimsWithWorktree().find((c) => isInside(cwd, c.worktree));
9450
+ if (!held)
9451
+ return false;
9452
+ const cfg = this.gateDefs(held.projectId);
9453
+ return cfg?.on_stop === "block" && cfg.max_blocks > 0 && cfg.required.some((g) => cfg.defs[g]);
9454
+ }
9455
+ async stopDecision(sessionId, cwd) {
9456
+ if (!cwd || !existsSync7(cwd))
9457
+ return null;
9458
+ const held = this.heldClaimsWithWorktree().find((c) => isInside(cwd, c.worktree));
9459
+ if (!held)
9460
+ return null;
9461
+ const cfg = this.gateDefs(held.projectId);
9462
+ if (cfg?.on_stop !== "block" || cfg.max_blocks <= 0)
9463
+ return null;
9464
+ const executable = cfg.required.filter((g) => cfg.defs[g]);
9465
+ if (!executable.length)
9466
+ return null;
9467
+ const blocksSoFar = this.stopBlocks(sessionId);
9468
+ if (blocksSoFar >= cfg.max_blocks) {
9469
+ if (!this.stopExhausted.has(sessionId)) {
9470
+ this.stopExhausted.add(sessionId);
9471
+ const failed = this.gateStatusFor(this.gateRuns(held.projectId, held.task), executable).filter((g) => g.verdict !== "pass").map((g) => g.gate);
9472
+ this.append({
9473
+ ts: new Date().toISOString(),
9474
+ type: "incident.opened",
9475
+ projectId: held.projectId,
9476
+ sessionId,
9477
+ payload: {
9478
+ rule: "gate_failed",
9479
+ action: "warn",
9480
+ command: `stop on ${held.task}`,
9481
+ reason: `${failed.join(", ") || "a required gate"} still failing after ${cfg.max_blocks} refusal${cfg.max_blocks === 1 ? "" : "s"} \u2014 letting the session stop. The runs are on the Board.`,
9482
+ task: held.task,
9483
+ gates: failed
9484
+ }
9485
+ });
9486
+ }
9487
+ return null;
9488
+ }
9489
+ const batch = this.runGates(held.projectId, held.task, undefined, {
9490
+ sessionId,
9491
+ owner: "stop"
9492
+ });
9493
+ let timer;
9494
+ const timeout = new Promise((resolve) => {
9495
+ timer = setTimeout(() => resolve(null), cfg.stop_timeout * 1000);
9496
+ });
9497
+ const r = await Promise.race([batch, timeout]).finally(() => clearTimeout(timer));
9498
+ if (!r)
9499
+ return null;
9500
+ this.writeAutoVerify(held.projectId, held.task, sessionId, r.runs);
9501
+ const d = repairDecision({
9502
+ onStop: cfg.on_stop,
9503
+ blocksSoFar,
9504
+ maxBlocks: cfg.max_blocks,
9505
+ runs: r.runs,
9506
+ unexecutable: cfg.required.filter((g) => !cfg.defs[g])
9507
+ });
9508
+ if (d.kind === "allow")
9509
+ return null;
9510
+ const failed = d.failed.map((x) => x.gate);
9511
+ if (d.kind === "exhausted") {
9512
+ if (!this.stopExhausted.has(sessionId)) {
9513
+ this.stopExhausted.add(sessionId);
9514
+ this.append({
9515
+ ts: new Date().toISOString(),
9516
+ type: "incident.opened",
9517
+ projectId: held.projectId,
9518
+ sessionId,
9519
+ payload: {
9520
+ rule: "gate_failed",
9521
+ action: "warn",
9522
+ command: `stop on ${held.task}`,
9523
+ reason: `${d.reason}. The session stopped with ${failed.join(", ")} failing; the runs are on the Board.`,
9524
+ task: held.task,
9525
+ gates: failed
9526
+ }
9527
+ });
9528
+ }
9529
+ return null;
9530
+ }
9531
+ this.append({
9532
+ ts: new Date().toISOString(),
9533
+ type: "gate.blocked",
9534
+ projectId: held.projectId,
9535
+ sessionId,
9536
+ payload: {
9537
+ task: held.task,
9538
+ gates: failed,
9539
+ attempt: d.attempt,
9540
+ maxBlocks: cfg.max_blocks,
9541
+ summary: `stop refused (${d.attempt}/${cfg.max_blocks}): ${failed.join(", ")} failing on ${held.task}`
9542
+ }
8274
9543
  });
9544
+ return { decision: "block", reason: d.reason };
8275
9545
  }
8276
9546
  requiredGates(projectId) {
8277
9547
  const p = this.project(projectId);
@@ -8340,8 +9610,8 @@ ${err}
8340
9610
  error = e.error;
8341
9611
  loading = e.at === 0 && e.error === null;
8342
9612
  } else {
8343
- const path = join8(p.root, source);
8344
- if (!existsSync6(path))
9613
+ const path = join9(p.root, source);
9614
+ if (!existsSync7(path))
8345
9615
  return {
8346
9616
  source,
8347
9617
  required: this.requiredGates(projectId),
@@ -8352,7 +9622,7 @@ ${err}
8352
9622
  const mtime = statSync(path).mtimeMs;
8353
9623
  let md = this.taskCache.get(projectId);
8354
9624
  if (!md || md.path !== path || md.mtime !== mtime) {
8355
- md = { path, mtime, tasks: parseMarkdownTasks(readFileSync3(path, "utf8")) };
9625
+ md = { path, mtime, tasks: parseMarkdownTasks(readFileSync4(path, "utf8")) };
8356
9626
  this.taskCache.set(projectId, md);
8357
9627
  }
8358
9628
  hit = md;
@@ -8382,6 +9652,9 @@ ${err}
8382
9652
  rulesFor(repoRoot) {
8383
9653
  return this.policyFor(repoRoot).config.rules;
8384
9654
  }
9655
+ invalidateConfig() {
9656
+ this.policyCache.clear();
9657
+ }
8385
9658
  policyFor(repoRoot) {
8386
9659
  const key = repoRoot ?? "";
8387
9660
  const hit = this.policyCache.get(key);
@@ -8431,15 +9704,15 @@ ${err}
8431
9704
  });
8432
9705
  }
8433
9706
  writePolicyCache(loaded) {
8434
- const file = join8(this.home, POLICY_CACHE_FILE);
9707
+ const file = join9(this.home, POLICY_CACHE_FILE);
8435
9708
  try {
8436
9709
  if (!hasLockedRules(loaded)) {
8437
- if (existsSync6(file))
9710
+ if (existsSync7(file))
8438
9711
  unlinkSync(file);
8439
9712
  return;
8440
9713
  }
8441
9714
  const cache = buildPolicyCache(loaded, this.liveSessions(), this.heldWorktrees());
8442
- writeFileSync2(file, JSON.stringify(cache), { mode: 384 });
9715
+ writeFileSync3(file, JSON.stringify(cache), { mode: 384 });
8443
9716
  } catch (e) {
8444
9717
  console.error(`swarm: policy cache: ${e.message}`);
8445
9718
  }
@@ -8448,15 +9721,15 @@ ${err}
8448
9721
  return process.env.SWARM_GUARD === "off" && !hasLockedRules(this.policyFor(repoRoot));
8449
9722
  }
8450
9723
  claudeSettings() {
8451
- const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
9724
+ const p = process.env.CLAUDE_SETTINGS ?? join9(homedir3(), ".claude", "settings.json");
8452
9725
  try {
8453
- return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
9726
+ return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
8454
9727
  } catch {
8455
9728
  return null;
8456
9729
  }
8457
9730
  }
8458
9731
  checkPolicy(cwd, sessionId) {
8459
- const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
9732
+ const project = existsSync7(cwd) ? this.resolveProject(cwd) : null;
8460
9733
  const repoRoot = project?.root ?? null;
8461
9734
  const loaded = this.policyFor(repoRoot);
8462
9735
  const settings = this.claudeSettings();
@@ -8481,7 +9754,7 @@ ${err}
8481
9754
  return findings;
8482
9755
  }
8483
9756
  evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
8484
- if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
9757
+ if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync7(cwd)) {
8485
9758
  const project = this.resolveProject(cwd);
8486
9759
  const b = this.budgetFor(project.id);
8487
9760
  if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
@@ -8530,7 +9803,7 @@ ${err}
8530
9803
  const d = guardBash(cmd, current, this.liveSessions(), Date.now(), {
8531
9804
  ...modes,
8532
9805
  protected: { ports: [...new Set([...modes.protected.ports, ...this.heldPorts()])] }
8533
- });
9806
+ }, this.rewriteCtx(sessionId));
8534
9807
  if (d.action !== "allow" && recordIncident)
8535
9808
  this.openIncident(d, cwd, sessionId, cmd);
8536
9809
  return { decision: d, display: cmd };
@@ -8578,22 +9851,37 @@ ${err}
8578
9851
  const d = guardBash(cmd, current, sessions, Date.now(), {
8579
9852
  ...modes,
8580
9853
  protected: { ports: [...new Set([...modes.protected.ports, ...this.heldPorts()])] }
8581
- });
9854
+ }, this.rewriteCtx(id));
8582
9855
  if (d.action === "allow")
8583
9856
  return null;
8584
9857
  return this.openIncident(d, cwd, id, cmd);
8585
9858
  }
8586
9859
  openIncident(d, cwd, sessionId, command) {
8587
- const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
9860
+ const project = cwd && existsSync7(cwd) ? this.resolveProject(cwd) : null;
9861
+ if (d.action === "rewrite" && d.key && sessionId) {
9862
+ const done = this.rewritesDone.get(sessionId) ?? new Set;
9863
+ done.add(d.key);
9864
+ this.rewritesDone.set(sessionId, done);
9865
+ }
8588
9866
  this.append({
8589
9867
  ts: new Date().toISOString(),
8590
9868
  type: "incident.opened",
8591
9869
  projectId: project?.id ?? "p_unknown",
8592
9870
  sessionId: sessionId || null,
8593
- payload: { rule: d.rule, action: d.action, command: command.slice(0, 400), reason: d.reason }
9871
+ payload: {
9872
+ rule: d.rule,
9873
+ action: d.action,
9874
+ command: command.slice(0, 400),
9875
+ reason: d.reason,
9876
+ ...d.action === "rewrite" ? { rewritten: d.command.slice(0, 400) } : {}
9877
+ }
8594
9878
  });
8595
9879
  return d;
8596
9880
  }
9881
+ rewritesDone = new Map;
9882
+ rewriteCtx(sessionId) {
9883
+ return { rewritesDone: this.rewritesDone.get(sessionId) ?? new Set };
9884
+ }
8597
9885
  heldWorktreesCache = null;
8598
9886
  dryRun(projectId, overrides = {}, limit = 5000) {
8599
9887
  const project = this.project(projectId);
@@ -8635,7 +9923,7 @@ ${err}
8635
9923
  }
8636
9924
  }
8637
9925
  const report = dryRunRules(calls, modes, {
8638
- toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
9926
+ toplevel: (cwd) => cwd && existsSync7(cwd) ? this.toplevel(cwd) : null,
8639
9927
  claims: this.heldWorktrees()
8640
9928
  });
8641
9929
  return { ...report, modes };
@@ -8650,11 +9938,11 @@ ${err}
8650
9938
  loadPricing() {
8651
9939
  this.prices = { ...PRICES };
8652
9940
  for (const f of ["pricing.litellm.json", "pricing.json"]) {
8653
- const p = join8(this.home, f);
8654
- if (!existsSync6(p))
9941
+ const p = join9(this.home, f);
9942
+ if (!existsSync7(p))
8655
9943
  continue;
8656
9944
  try {
8657
- const j = JSON.parse(readFileSync3(p, "utf8"));
9945
+ const j = JSON.parse(readFileSync4(p, "utf8"));
8658
9946
  const table = f === "pricing.json" ? j : fromLiteLLM(j);
8659
9947
  Object.assign(this.prices, table);
8660
9948
  } catch {}
@@ -8666,7 +9954,7 @@ ${err}
8666
9954
  throw new Error(`pricing fetch ${r.status}`);
8667
9955
  const j = await r.json();
8668
9956
  const slim = Object.fromEntries(Object.entries(j).filter(([k, v]) => typeof v.input_cost_per_token === "number" && !k.includes("/")));
8669
- writeFileSync2(join8(this.home, "pricing.litellm.json"), JSON.stringify(slim, null, 1));
9957
+ writeFileSync3(join9(this.home, "pricing.litellm.json"), JSON.stringify(slim, null, 1));
8670
9958
  this.loadPricing();
8671
9959
  this.reprice();
8672
9960
  }
@@ -8921,6 +10209,7 @@ ${p.reason ?? ""}`.trim(),
8921
10209
  const cfg = this.policyFor(null).config;
8922
10210
  const chatter = days ?? cfg.events.retain_days;
8923
10211
  const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
10212
+ this.db.query("DELETE FROM quota WHERE at < ?").run(Date.now() - 14 * 86400000);
8924
10213
  let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
8925
10214
  if (cfg.audit.retain_days > 0) {
8926
10215
  const acut = new Date(Date.now() - cfg.audit.retain_days * 86400000).toISOString();
@@ -8936,10 +10225,25 @@ ${p.reason ?? ""}`.trim(),
8936
10225
  if (typeof raw.cwd === "string")
8937
10226
  this.autoRenewFor(typeof raw.session_id === "string" ? raw.session_id : null, raw.cwd);
8938
10227
  const cwd = typeof raw.cwd === "string" ? raw.cwd : process.cwd();
8939
- const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
10228
+ if (typeof raw.session_id === "string") {
10229
+ if (event === "UserPromptSubmit" || event === "PreToolUse")
10230
+ this.cancelWake(raw.session_id, "active");
10231
+ else if (event === "SessionEnd") {
10232
+ this.cancelWake(raw.session_id, "ended");
10233
+ this.statuslines.delete(raw.session_id);
10234
+ this.rewritesDone.delete(raw.session_id);
10235
+ this.stopExhausted.delete(raw.session_id);
10236
+ }
10237
+ }
10238
+ if (event === "PreToolUse" && typeof raw.session_id === "string") {
10239
+ const fp = raw.tool_input?.file_path;
10240
+ if (WRITE_TOOLS.has(String(raw.tool_name)) && typeof fp === "string")
10241
+ this.noteEdit(raw.session_id, absolutePath(fp, cwd));
10242
+ }
10243
+ const project = existsSync7(cwd) ? this.resolveProject(cwd) : null;
8940
10244
  const e = this.append(normalizeHook(event, raw, project?.id ?? "p_unknown"));
8941
10245
  if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
8942
- if (existsSync6(cwd)) {
10246
+ if (existsSync7(cwd)) {
8943
10247
  this.autoHandoff(e.sessionId, cwd);
8944
10248
  this.autoGate(event, e.sessionId, cwd);
8945
10249
  }
@@ -8975,6 +10279,7 @@ ${p.reason ?? ""}`.trim(),
8975
10279
  "dispatch.started",
8976
10280
  "dispatch.finished",
8977
10281
  "gate.recorded",
10282
+ "gate.blocked",
8978
10283
  "handoff.recorded",
8979
10284
  "incident.opened",
8980
10285
  "incident.acked",
@@ -8985,7 +10290,7 @@ ${p.reason ?? ""}`.trim(),
8985
10290
  return;
8986
10291
  const p = e.payload;
8987
10292
  const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
8988
- const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
10293
+ const branch = p.cwd && existsSync7(p.cwd) ? currentBranch(p.cwd) : null;
8989
10294
  if (!row) {
8990
10295
  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);
8991
10296
  }
@@ -9057,12 +10362,12 @@ ${p.reason ?? ""}`.trim(),
9057
10362
  }
9058
10363
  tailSession(sessionId) {
9059
10364
  const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
9060
- if (!s?.transcript_path || !existsSync6(s.transcript_path))
10365
+ if (!s?.transcript_path || !existsSync7(s.transcript_path))
9061
10366
  return 0;
9062
10367
  let n = this.tailFile(s.transcript_path, sessionId, null);
9063
- const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
10368
+ const subDir = join9(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
9064
10369
  for (const f of this.subagentFiles(subDir)) {
9065
- n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
10370
+ n += this.tailFile(join9(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
9066
10371
  }
9067
10372
  return n;
9068
10373
  }
@@ -9094,7 +10399,7 @@ ${p.reason ?? ""}`.trim(),
9094
10399
  return n;
9095
10400
  }
9096
10401
  codexRoot() {
9097
- return process.env.SWARM_CODEX_DIR ?? join8(homedir3(), ".codex", "sessions");
10402
+ return process.env.SWARM_CODEX_DIR ?? join9(homedir3(), ".codex", "sessions");
9098
10403
  }
9099
10404
  codexRolloutFiles(sinceMs) {
9100
10405
  const root = this.codexRoot();
@@ -9109,18 +10414,18 @@ ${p.reason ?? ""}`.trim(),
9109
10414
  for (const y of ls(root)) {
9110
10415
  if (!/^\d{4}$/.test(y))
9111
10416
  continue;
9112
- for (const m of ls(join8(root, y))) {
10417
+ for (const m of ls(join9(root, y))) {
9113
10418
  if (!/^\d\d$/.test(m))
9114
10419
  continue;
9115
- for (const day of ls(join8(root, y, m))) {
10420
+ for (const day of ls(join9(root, y, m))) {
9116
10421
  if (!/^\d\d$/.test(day))
9117
10422
  continue;
9118
10423
  if (Date.parse(`${y}-${m}-${day}T23:59:59Z`) < sinceMs)
9119
10424
  continue;
9120
- const dir = join8(root, y, m, day);
10425
+ const dir = join9(root, y, m, day);
9121
10426
  for (const f of ls(dir)) {
9122
10427
  if (f.startsWith("rollout-") && f.endsWith(".jsonl"))
9123
- out.push(join8(dir, f));
10428
+ out.push(join9(dir, f));
9124
10429
  }
9125
10430
  }
9126
10431
  }
@@ -9128,7 +10433,7 @@ ${p.reason ?? ""}`.trim(),
9128
10433
  return out;
9129
10434
  }
9130
10435
  tailCodex(windowMs = 3 * 24 * 60 * 60000) {
9131
- if (!existsSync6(this.codexRoot()))
10436
+ if (!existsSync7(this.codexRoot()))
9132
10437
  return 0;
9133
10438
  let n = 0;
9134
10439
  for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
@@ -9137,12 +10442,12 @@ ${p.reason ?? ""}`.trim(),
9137
10442
  return n;
9138
10443
  }
9139
10444
  grokRoot() {
9140
- return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
10445
+ return process.env.SWARM_GROK_DIR ?? join9(homedir3(), ".grok", "sessions");
9141
10446
  }
9142
10447
  grokSummary = new Map;
9143
10448
  tailGemini(windowMs = 3 * 24 * 60 * 60000) {
9144
- const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
9145
- if (!existsSync6(root))
10449
+ const root = process.env.SWARM_GEMINI_ROOT ?? join9(homedir3(), ".gemini", "tmp");
10450
+ if (!existsSync7(root))
9146
10451
  return 0;
9147
10452
  const since = Date.now() - windowMs;
9148
10453
  const ls = (p) => {
@@ -9155,13 +10460,13 @@ ${p.reason ?? ""}`.trim(),
9155
10460
  let n = 0;
9156
10461
  const ingestDir = (dir) => {
9157
10462
  for (const f of ls(dir)) {
9158
- const path = join8(dir, f);
10463
+ const path = join9(dir, f);
9159
10464
  if (!f.endsWith(".jsonl")) {
9160
10465
  try {
9161
10466
  if (statSync(path).isDirectory()) {
9162
10467
  for (const g of ls(path))
9163
10468
  if (g.endsWith(".jsonl"))
9164
- ingestFile(join8(path, g));
10469
+ ingestFile(join9(path, g));
9165
10470
  }
9166
10471
  } catch {}
9167
10472
  continue;
@@ -9179,15 +10484,15 @@ ${p.reason ?? ""}`.trim(),
9179
10484
  n += this.ingestLog(path, "gemini", parseGeminiChat);
9180
10485
  };
9181
10486
  for (const hash of ls(root)) {
9182
- const chats = join8(root, hash, "chats");
9183
- if (existsSync6(chats))
10487
+ const chats = join9(root, hash, "chats");
10488
+ if (existsSync7(chats))
9184
10489
  ingestDir(chats);
9185
10490
  }
9186
10491
  return n;
9187
10492
  }
9188
10493
  tailGrok(windowMs = 3 * 24 * 60 * 60000) {
9189
10494
  const root = this.grokRoot();
9190
- if (!existsSync6(root))
10495
+ if (!existsSync7(root))
9191
10496
  return 0;
9192
10497
  const since = Date.now() - windowMs;
9193
10498
  const ls = (p) => {
@@ -9207,10 +10512,10 @@ ${p.reason ?? ""}`.trim(),
9207
10512
  } catch {
9208
10513
  cwd = enc;
9209
10514
  }
9210
- const cwdDir = join8(root, enc);
10515
+ const cwdDir = join9(root, enc);
9211
10516
  for (const sid of ls(cwdDir)) {
9212
- const path = join8(cwdDir, sid, "updates.jsonl");
9213
- if (!existsSync6(path))
10517
+ const path = join9(cwdDir, sid, "updates.jsonl");
10518
+ if (!existsSync7(path))
9214
10519
  continue;
9215
10520
  try {
9216
10521
  if (statSync(path).mtimeMs < since)
@@ -9218,7 +10523,7 @@ ${p.reason ?? ""}`.trim(),
9218
10523
  } catch {
9219
10524
  continue;
9220
10525
  }
9221
- const sumPath = join8(cwdDir, sid, "summary.json");
10526
+ const sumPath = join9(cwdDir, sid, "summary.json");
9222
10527
  let title;
9223
10528
  let fresh = false;
9224
10529
  try {
@@ -9227,7 +10532,7 @@ ${p.reason ?? ""}`.trim(),
9227
10532
  if (hit && hit.mtime === m)
9228
10533
  title = hit.title;
9229
10534
  else {
9230
- const sum = JSON.parse(readFileSync3(sumPath, "utf8"));
10535
+ const sum = JSON.parse(readFileSync4(sumPath, "utf8"));
9231
10536
  title = sum.session_summary;
9232
10537
  this.grokSummary.set(sumPath, { mtime: m, title });
9233
10538
  fresh = true;
@@ -9302,7 +10607,7 @@ ${p.reason ?? ""}`.trim(),
9302
10607
  const roots = this.db.query("SELECT DISTINCT root FROM projects WHERE root IS NOT NULL AND root != ''").all();
9303
10608
  let n = 0;
9304
10609
  for (const { root } of roots) {
9305
- const path = join8(root, ".aider.chat.history.md");
10610
+ const path = join9(root, ".aider.chat.history.md");
9306
10611
  let mtime;
9307
10612
  try {
9308
10613
  mtime = statSync(path).mtimeMs;
@@ -9336,7 +10641,7 @@ ${p.reason ?? ""}`.trim(),
9336
10641
  }
9337
10642
  ocDbs = new Map;
9338
10643
  tailOpencode(windowMs = 3 * 24 * 60 * 60000) {
9339
- const dir = process.env.SWARM_OPENCODE_DIR ?? join8(process.env.XDG_DATA_HOME ?? join8(homedir3(), ".local", "share"), "opencode");
10644
+ const dir = process.env.SWARM_OPENCODE_DIR ?? join9(process.env.XDG_DATA_HOME ?? join9(homedir3(), ".local", "share"), "opencode");
9340
10645
  let files;
9341
10646
  try {
9342
10647
  files = readdirSync(dir).filter((f) => /^opencode[^/]*\.db$/.test(f));
@@ -9345,7 +10650,7 @@ ${p.reason ?? ""}`.trim(),
9345
10650
  }
9346
10651
  let n = 0;
9347
10652
  for (const f of files) {
9348
- const path = join8(dir, f);
10653
+ const path = join9(dir, f);
9349
10654
  let db = this.ocDbs.get(path);
9350
10655
  if (!db) {
9351
10656
  try {
@@ -9396,12 +10701,12 @@ ${p.reason ?? ""}`.trim(),
9396
10701
  return n;
9397
10702
  }
9398
10703
  ingestLog(path, agent, parse, cwdHint, titleHint) {
9399
- const off = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path) ?? { offset: 0 };
10704
+ const off = this.db.query("SELECT offset, session_id FROM tails WHERE path = ?").get(path) ?? { offset: 0, session_id: null };
9400
10705
  const r = this.readFrom(path, off.offset);
9401
10706
  if (!r)
9402
10707
  return 0;
9403
- const d = parse(r.chunk);
9404
- const sid = d.sessionId;
10708
+ const d = parse(r.chunk, off.session_id);
10709
+ const sid = d.sessionId ?? off.session_id;
9405
10710
  if (!sid)
9406
10711
  return 0;
9407
10712
  const mtime = (() => {
@@ -9423,9 +10728,9 @@ ${p.reason ?? ""}`.trim(),
9423
10728
  ensureAgentSession(sid, agent, cwd, mtime) {
9424
10729
  if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
9425
10730
  return;
9426
- const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
10731
+ const project = cwd && existsSync7(cwd) ? this.resolveProject(cwd) : null;
9427
10732
  const ts = new Date(mtime).toISOString();
9428
- 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);
10733
+ 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 && existsSync7(cwd) ? currentBranch(cwd) : null, ts, ts);
9429
10734
  }
9430
10735
  claimRows(projectId) {
9431
10736
  return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
@@ -9460,7 +10765,7 @@ ${p.reason ?? ""}`.trim(),
9460
10765
  worktreePath(projectId, task) {
9461
10766
  const slug = (x) => x.replace(/[^a-zA-Z0-9._-]+/g, "-").toLowerCase();
9462
10767
  const p = this.project(projectId);
9463
- return join8(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
10768
+ return join9(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
9464
10769
  }
9465
10770
  claim(projectId, task, owner, baseRef = "HEAD", sessionId = null) {
9466
10771
  const p = this.project(projectId);
@@ -9488,7 +10793,7 @@ ${p.reason ?? ""}`.trim(),
9488
10793
  }
9489
10794
  const branch = `task/${task}`;
9490
10795
  const worktree = this.worktreePath(projectId, task);
9491
- if (existsSync6(worktree))
10796
+ if (existsSync7(worktree))
9492
10797
  return { ok: false, error: `${worktree} already exists; release ${task} first` };
9493
10798
  mkdirSync4(dirname3(worktree), { recursive: true });
9494
10799
  const created = worktreeAdd(p.root, worktree, branch, baseRef);
@@ -9677,7 +10982,7 @@ ${p.reason ?? ""}`.trim(),
9677
10982
  }
9678
10983
  static worktreeIdleMs(path) {
9679
10984
  let newest = 0;
9680
- for (const f of [path, join8(path, ".git")]) {
10985
+ for (const f of [path, join9(path, ".git")]) {
9681
10986
  try {
9682
10987
  newest = Math.max(newest, statSync(f).mtimeMs);
9683
10988
  } catch {}
@@ -10279,7 +11584,7 @@ ${p.reason ?? ""}`.trim(),
10279
11584
  for (const c of this.claimRows(p.id)) {
10280
11585
  if (c.state !== "held" || isActive(c, now))
10281
11586
  continue;
10282
- const exists = c.worktree ? existsSync6(c.worktree) : false;
11587
+ const exists = c.worktree ? existsSync7(c.worktree) : false;
10283
11588
  const work = exists ? heldWork(c.worktree) : null;
10284
11589
  if (reapAction(c, now, exists, work) !== "keep-orphaned")
10285
11590
  continue;
@@ -10335,7 +11640,7 @@ ${p.reason ?? ""}`.trim(),
10335
11640
  if (!row)
10336
11641
  return { ok: false, error: `no claim on ${task}` };
10337
11642
  const worktree = row.worktree ?? "";
10338
- if (worktree && existsSync6(worktree)) {
11643
+ if (worktree && existsSync7(worktree)) {
10339
11644
  const work = heldWork(worktree);
10340
11645
  const can = canRelease(work, force);
10341
11646
  if (!can.ok)
@@ -10370,7 +11675,7 @@ ${p.reason ?? ""}`.trim(),
10370
11675
  continue;
10371
11676
  if (isActive({ ...c, state: "held" }, now))
10372
11677
  continue;
10373
- const exists = c.worktree ? existsSync6(c.worktree) : false;
11678
+ const exists = c.worktree ? existsSync7(c.worktree) : false;
10374
11679
  const work = exists ? heldWork(c.worktree) : null;
10375
11680
  const action = reapAction({ ...c, state: "held" }, now, exists, work);
10376
11681
  if (action === "not-expired")
@@ -10469,7 +11774,7 @@ ${p.reason ?? ""}`.trim(),
10469
11774
  if (!slug || slug === "." || slug === "..")
10470
11775
  return { ok: false, error: "bad worktree name" };
10471
11776
  const path = this.worktreePath(projectId, slug);
10472
- if (existsSync6(path))
11777
+ if (existsSync7(path))
10473
11778
  return { ok: false, error: `${path} already exists` };
10474
11779
  mkdirSync4(dirname3(path), { recursive: true });
10475
11780
  const br = branch?.trim() || `wt/${slug}`;
@@ -10771,8 +12076,8 @@ ${p.reason ?? ""}`.trim(),
10771
12076
  backupTo(destDir) {
10772
12077
  mkdirSync4(destDir, { recursive: true });
10773
12078
  const files = [];
10774
- const dbDest = join8(destDir, "swarm.db");
10775
- if (existsSync6(dbDest))
12079
+ const dbDest = join9(destDir, "swarm.db");
12080
+ if (existsSync7(dbDest))
10776
12081
  unlinkSync(dbDest);
10777
12082
  this.db.exec(`VACUUM INTO '${dbDest.replaceAll("'", "''")}'`);
10778
12083
  files.push("swarm.db");
@@ -10785,10 +12090,10 @@ ${p.reason ?? ""}`.trim(),
10785
12090
  "pricing.litellm.json",
10786
12091
  "team-token"
10787
12092
  ]) {
10788
- const src = join8(this.home, f);
10789
- if (!existsSync6(src))
12093
+ const src = join9(this.home, f);
12094
+ if (!existsSync7(src))
10790
12095
  continue;
10791
- copyFileSync(src, join8(destDir, f));
12096
+ copyFileSync(src, join9(destDir, f));
10792
12097
  files.push(f);
10793
12098
  }
10794
12099
  return { dest: destDir, files };
@@ -10979,6 +12284,38 @@ ${p.reason ?? ""}`.trim(),
10979
12284
  return { ...i, count: counts.get(key) ?? 1, suggestion };
10980
12285
  });
10981
12286
  }
12287
+ incident(seq) {
12288
+ const r = this.db.query(`SELECT e.seq, e.ts, e.project_id, e.session_id, e.payload, a.acked_at FROM events e
12289
+ LEFT JOIN incident_acks a ON a.seq = e.seq WHERE e.seq = ? AND e.type = 'incident.opened'`).get(seq);
12290
+ if (!r)
12291
+ return null;
12292
+ const payload = JSON.parse(r.payload || "{}");
12293
+ const base = {
12294
+ seq: r.seq,
12295
+ ts: r.ts,
12296
+ projectId: r.project_id,
12297
+ sessionId: r.session_id,
12298
+ acked: r.acked_at,
12299
+ ...payload
12300
+ };
12301
+ if (typeof payload.rule !== "string")
12302
+ return base;
12303
+ const rule = payload.rule;
12304
+ const command = typeof payload.command === "string" ? payload.command : "";
12305
+ const count = this.db.query(`SELECT COUNT(*) AS n FROM events WHERE type = 'incident.opened' AND project_id = ?
12306
+ AND json_extract(payload,'$.rule') = ? AND json_extract(payload,'$.command') = ?`).get(r.project_id, rule, command).n;
12307
+ return {
12308
+ ...base,
12309
+ count: Math.max(1, count),
12310
+ suggestion: suggestFromIncident({
12311
+ rule,
12312
+ action: typeof payload.action === "string" ? payload.action : "",
12313
+ command,
12314
+ reason: typeof payload.reason === "string" ? payload.reason : "",
12315
+ count: Math.max(1, count)
12316
+ })
12317
+ };
12318
+ }
10982
12319
  openIncidents(projectId) {
10983
12320
  const r = this.db.query(`SELECT COUNT(*) AS n FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
10984
12321
  WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
@@ -11341,12 +12678,14 @@ ${p.reason ?? ""}`.trim(),
11341
12678
  sessions: this.sessions(),
11342
12679
  spend: this.memoised("spend", 30000, () => this.spend()),
11343
12680
  spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
12681
+ quota: this.quota(),
11344
12682
  claims: this.claims(),
11345
12683
  processes: this.memoised("processes", 5000, () => this.processes()),
11346
12684
  incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
11347
12685
  openIncidents: this.memoised("openIncidents", 30000, () => this.openIncidents()),
11348
12686
  openIncidentsByProject: this.memoised("openIncidentsByProject", 30000, () => this.openIncidentsByProject()),
11349
12687
  questions: this.questions({ open: true, limit: 50 }),
12688
+ permissions: this.pendingPermissions(),
11350
12689
  resources: this.resources(),
11351
12690
  seq: this.seq()
11352
12691
  };
@@ -11487,8 +12826,8 @@ function localDayIso(offsetDays) {
11487
12826
  }
11488
12827
 
11489
12828
  // packages/daemon/src/team.ts
11490
- import { writeFileSync as writeFileSync3 } from "fs";
11491
- import { join as join9 } from "path";
12829
+ import { writeFileSync as writeFileSync4 } from "fs";
12830
+ import { join as join10 } from "path";
11492
12831
  var SPEND_EVERY_MS = 60000;
11493
12832
  var MAX_BACKOFF_MS = 300000;
11494
12833
  var POLICY_EVERY_MS = 300000;
@@ -11656,12 +12995,12 @@ class TeamForwarder {
11656
12995
  this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
11657
12996
  return;
11658
12997
  }
11659
- const file = join9(this.store.home, "policy.toml");
12998
+ const file = join10(this.store.home, "policy.toml");
11660
12999
  const prev = this.store.metaValue("team_policy_sig");
11661
13000
  if (prev === policy.signature)
11662
13001
  return;
11663
- writeFileSync3(file, policy.toml, { mode: 384 });
11664
- writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
13002
+ writeFileSync4(file, policy.toml, { mode: 384 });
13003
+ writeFileSync4(join10(this.store.home, "policy.sig.json"), JSON.stringify({
11665
13004
  signature: policy.signature,
11666
13005
  publicKey: pinned,
11667
13006
  fetchedAt: new Date(now).toISOString(),
@@ -11672,6 +13011,237 @@ class TeamForwarder {
11672
13011
  }
11673
13012
  }
11674
13013
 
13014
+ // packages/daemon/src/teamctl.ts
13015
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
13016
+ import { networkInterfaces } from "os";
13017
+ import { join as join11 } from "path";
13018
+ var HOSTED_PID = "team_hosted_pid";
13019
+ function alive(pid) {
13020
+ if (!Number.isInteger(pid) || pid <= 1)
13021
+ return false;
13022
+ try {
13023
+ process.kill(pid, 0);
13024
+ return true;
13025
+ } catch {
13026
+ return false;
13027
+ }
13028
+ }
13029
+ function hostedPid(store) {
13030
+ const pid = Number(store.metaValue(HOSTED_PID) ?? 0);
13031
+ if (!alive(pid)) {
13032
+ if (pid)
13033
+ store.setMetaValue(HOSTED_PID, "");
13034
+ return null;
13035
+ }
13036
+ return pid;
13037
+ }
13038
+ async function hostedPidChecked(store) {
13039
+ const pid = hostedPid(store);
13040
+ if (!pid)
13041
+ return null;
13042
+ if (await healthy(hostedUrl("127.0.0.1", readSetup(store.home).port)))
13043
+ return pid;
13044
+ store.setMetaValue(HOSTED_PID, "");
13045
+ return null;
13046
+ }
13047
+ function lanAddress() {
13048
+ for (const list of Object.values(networkInterfaces())) {
13049
+ for (const n of list ?? []) {
13050
+ if (n.family === "IPv4" && !n.internal)
13051
+ return n.address;
13052
+ }
13053
+ }
13054
+ return null;
13055
+ }
13056
+ function setupPath(home) {
13057
+ return join11(home, "team.toml");
13058
+ }
13059
+ function readSetup(home) {
13060
+ const p = setupPath(home);
13061
+ return parseTeamSetup(existsSync8(p) ? readFileSync5(p, "utf8") : null);
13062
+ }
13063
+ function writeSetup(home, setup) {
13064
+ mkdirSync5(home, { recursive: true });
13065
+ writeFileSync5(setupPath(home), renderTeamSetup(setup), { mode: 384 });
13066
+ }
13067
+ function setTeamUrl(store, url) {
13068
+ const path = join11(store.home, "config.toml");
13069
+ const text = existsSync8(path) ? readFileSync5(path, "utf8") : "";
13070
+ mkdirSync5(store.home, { recursive: true });
13071
+ writeFileSync5(path, withTeamUrl(text, url));
13072
+ store.invalidateConfig();
13073
+ }
13074
+ async function healthy(url, ms = 500) {
13075
+ try {
13076
+ const r = await fetch(`${url}/t1/health`, { signal: AbortSignal.timeout(ms) });
13077
+ return r.ok;
13078
+ } catch {
13079
+ return false;
13080
+ }
13081
+ }
13082
+ async function registerMachine(store, url, token) {
13083
+ let mode = "open";
13084
+ let policyPublicKey;
13085
+ try {
13086
+ const cfg = await (await fetch(`${url}/t1/auth/config`, { signal: AbortSignal.timeout(4000) })).json();
13087
+ mode = cfg.mode ?? "open";
13088
+ policyPublicKey = cfg.policyPublicKey;
13089
+ } catch {
13090
+ return { ok: false, error: `no team daemon answering at ${url}` };
13091
+ }
13092
+ if (mode === "oidc" && !token)
13093
+ return {
13094
+ ok: false,
13095
+ error: "this team uses your identity provider \u2014 run `swarm login` in a terminal once"
13096
+ };
13097
+ const machine = store.machineIdentity();
13098
+ let reg = {};
13099
+ try {
13100
+ const res = await fetch(`${url}/t1/machines/register`, {
13101
+ method: "POST",
13102
+ headers: {
13103
+ "content-type": "application/json",
13104
+ ...token ? { authorization: `Bearer ${token}` } : {}
13105
+ },
13106
+ body: JSON.stringify({ id: machine.id, name: machine.name }),
13107
+ signal: AbortSignal.timeout(6000)
13108
+ });
13109
+ reg = await res.json().catch(() => ({}));
13110
+ if (!res.ok)
13111
+ return {
13112
+ ok: false,
13113
+ error: `registration refused (${res.status}): ${reg.error ?? "check the secret"}`
13114
+ };
13115
+ } catch {
13116
+ return { ok: false, error: `registration failed: ${url} did not answer` };
13117
+ }
13118
+ const machineToken = reg.token ?? (mode === "token" ? token : null);
13119
+ if (mode !== "open" && !machineToken)
13120
+ return { ok: false, error: `registration refused: ${reg.error ?? "check the secret"}` };
13121
+ if (machineToken)
13122
+ store.setMetaValue("team_machine_token", machineToken);
13123
+ if (policyPublicKey)
13124
+ store.setMetaValue("team_policy_pubkey", policyPublicKey);
13125
+ return { ok: true, mode };
13126
+ }
13127
+ async function hostTeam(store, input = {}) {
13128
+ const existing = await hostedPidChecked(store);
13129
+ if (existing)
13130
+ return { ok: false, error: `already hosting a team (pid ${existing})` };
13131
+ const cur = readSetup(store.home);
13132
+ const mode = input.mode ?? cur.mode;
13133
+ const setup = {
13134
+ ...DEFAULT_TEAM_SETUP,
13135
+ ...cur,
13136
+ mode,
13137
+ port: input.port ?? cur.port,
13138
+ name: input.name ?? cur.name,
13139
+ token: mode === "token" ? input.token ?? cur.token ?? mintTeamSecret() : null
13140
+ };
13141
+ if (mode === "oidc" && (!setup.issuer || !setup.clientId))
13142
+ return {
13143
+ ok: false,
13144
+ error: "an identity-provider team needs an issuer and a client id \u2014 run `swarm-teamd setup` once"
13145
+ };
13146
+ writeSetup(store.home, setup);
13147
+ const loopback = hostedUrl("127.0.0.1", setup.port);
13148
+ if (await healthy(loopback))
13149
+ return { ok: false, error: `something is already listening on port ${setup.port}` };
13150
+ const [cmd, ...args] = resolveBin("swarm-teamd");
13151
+ const logDir = join11(store.home, "logs");
13152
+ mkdirSync5(logDir, { recursive: true });
13153
+ const log = join11(logDir, "teamd.log");
13154
+ const proc = Bun.spawn([cmd, ...args], {
13155
+ cwd: store.home,
13156
+ env: { ...process.env, SWARM_HOME: store.home },
13157
+ stdout: Bun.file(log).writer(),
13158
+ stderr: Bun.file(log).writer(),
13159
+ stdin: "ignore"
13160
+ });
13161
+ proc.unref();
13162
+ const until = Date.now() + 15000;
13163
+ while (Date.now() < until) {
13164
+ if (await healthy(loopback))
13165
+ break;
13166
+ if (proc.exitCode !== null)
13167
+ return {
13168
+ ok: false,
13169
+ error: `swarm-teamd exited (${proc.exitCode}) \u2014 see ${log}`
13170
+ };
13171
+ await Bun.sleep(200);
13172
+ }
13173
+ if (!await healthy(loopback)) {
13174
+ proc.kill();
13175
+ return {
13176
+ ok: false,
13177
+ error: `swarm-teamd (${cmd} ${args.join(" ")}) did not come up on ${setup.port} \u2014 see ${log}. The team daemon is source-available and ships separately; run it from a clone, or put swarm-teamd on PATH.`
13178
+ };
13179
+ }
13180
+ store.setMetaValue(HOSTED_PID, String(proc.pid));
13181
+ setTeamUrl(store, loopback);
13182
+ const reg = await registerMachine(store, loopback, setup.token);
13183
+ if (!reg.ok) {
13184
+ setTeamUrl(store, null);
13185
+ store.setMetaValue(HOSTED_PID, "");
13186
+ try {
13187
+ process.kill(proc.pid, "SIGTERM");
13188
+ } catch {}
13189
+ return reg;
13190
+ }
13191
+ const address = lanAddress();
13192
+ return {
13193
+ ok: true,
13194
+ url: loopback,
13195
+ invite: inviteLink(hostedUrl(address ?? "127.0.0.1", setup.port), setup.token),
13196
+ setup,
13197
+ pid: proc.pid,
13198
+ address
13199
+ };
13200
+ }
13201
+ async function joinTeam(store, input) {
13202
+ const parsed = input.invite ? parseInvite(input.invite) : null;
13203
+ const url = (parsed?.url ?? input.url ?? "").replace(/\/+$/, "");
13204
+ if (!url)
13205
+ return { ok: false, error: "paste an invite link or the team daemon's URL" };
13206
+ const token = input.token ?? parsed?.token ?? null;
13207
+ const reg = await registerMachine(store, url, token);
13208
+ if (!reg.ok)
13209
+ return reg;
13210
+ setTeamUrl(store, url);
13211
+ return { ok: true, url, mode: reg.mode };
13212
+ }
13213
+ async function leaveTeam(store, opts = {}) {
13214
+ setTeamUrl(store, null);
13215
+ store.setMetaValue("team_machine_token", "");
13216
+ let stopped = false;
13217
+ const pid = hostedPid(store);
13218
+ if (opts.stopHosted && pid) {
13219
+ try {
13220
+ process.kill(pid, "SIGTERM");
13221
+ stopped = true;
13222
+ } catch {
13223
+ stopped = false;
13224
+ }
13225
+ store.setMetaValue(HOSTED_PID, "");
13226
+ }
13227
+ return { ok: true, stopped };
13228
+ }
13229
+ function hostingStatus(store) {
13230
+ const pid = hostedPid(store);
13231
+ if (!pid)
13232
+ return { hosting: false, pid: null, port: null, invite: null, mode: null, address: null };
13233
+ const setup = readSetup(store.home);
13234
+ const address = lanAddress();
13235
+ return {
13236
+ hosting: true,
13237
+ pid,
13238
+ port: setup.port,
13239
+ invite: inviteLink(hostedUrl(address ?? "127.0.0.1", setup.port), setup.token),
13240
+ mode: setup.mode,
13241
+ address
13242
+ };
13243
+ }
13244
+
11675
13245
  // packages/daemon/src/workflow.ts
11676
13246
  class WorkflowEngine {
11677
13247
  store;
@@ -11863,13 +13433,13 @@ class WorkflowEngine {
11863
13433
  }
11864
13434
 
11865
13435
  // packages/daemon/src/app.ts
11866
- var VERSION = "0.13.2";
13436
+ var VERSION = "0.14.0";
11867
13437
  var WEB_DIR = (() => {
11868
13438
  if (process.env.SWARM_WEB_DIR)
11869
13439
  return process.env.SWARM_WEB_DIR;
11870
13440
  const here = dirname4(fileURLToPath2(import.meta.url));
11871
- const dev = join10(here, "../../web/public");
11872
- return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
13441
+ const dev = join12(here, "../../web/public");
13442
+ return existsSync9(join12(dev, "index.html")) ? dev : join12(here, "../web");
11873
13443
  })();
11874
13444
  var REPLAY_TAIL = 200;
11875
13445
  var wireCache = new WeakMap;
@@ -11883,12 +13453,12 @@ function wireJson(e) {
11883
13453
  }
11884
13454
  function hookRepoRoot(store, raw) {
11885
13455
  const cwd = typeof raw.cwd === "string" ? raw.cwd : "";
11886
- return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
13456
+ return cwd && existsSync9(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
11887
13457
  }
11888
13458
  function claudeSettings() {
11889
13459
  try {
11890
- const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
11891
- return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
13460
+ const p = process.env.CLAUDE_SETTINGS ?? join12(homedir4(), ".claude", "settings.json");
13461
+ return existsSync9(p) ? JSON.parse(readFileSync6(p, "utf8")) : null;
11892
13462
  } catch {
11893
13463
  return null;
11894
13464
  }
@@ -11896,12 +13466,12 @@ function claudeSettings() {
11896
13466
  function diskVersion() {
11897
13467
  try {
11898
13468
  const entry = daemonCommand().at(-1);
11899
- if (!entry || !existsSync7(entry))
13469
+ if (!entry || !existsSync9(entry))
11900
13470
  return null;
11901
- for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
11902
- if (!existsSync7(f))
13471
+ for (const f of [entry, join12(dirname4(entry), "app.ts")]) {
13472
+ if (!existsSync9(f))
11903
13473
  continue;
11904
- const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
13474
+ const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync6(f, "utf8"));
11905
13475
  if (m?.[1])
11906
13476
  return m[1];
11907
13477
  }
@@ -11914,7 +13484,7 @@ function expandHome(p, home = homedir4()) {
11914
13484
  if (p === "~")
11915
13485
  return home;
11916
13486
  if (p.startsWith("~/"))
11917
- return join10(home, p.slice(2));
13487
+ return join12(home, p.slice(2));
11918
13488
  return p;
11919
13489
  }
11920
13490
  function createApp(store = new Store, hooks = {}) {
@@ -11979,7 +13549,7 @@ function createApp(store = new Store, hooks = {}) {
11979
13549
  app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
11980
13550
  app.get("/v1/fs/ls", (c) => {
11981
13551
  const q = expandHome(c.req.query("path") ?? "");
11982
- if (q && !existsSync7(q))
13552
+ if (q && !existsSync9(q))
11983
13553
  return c.json({ error: "no such folder", path: q }, 404);
11984
13554
  let dir;
11985
13555
  try {
@@ -11988,7 +13558,7 @@ function createApp(store = new Store, hooks = {}) {
11988
13558
  dir = homedir4();
11989
13559
  }
11990
13560
  try {
11991
- const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join10(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
13561
+ const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync9(join12(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
11992
13562
  const parent = dirname4(dir);
11993
13563
  return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
11994
13564
  } catch (e) {
@@ -11996,6 +13566,8 @@ function createApp(store = new Store, hooks = {}) {
11996
13566
  }
11997
13567
  });
11998
13568
  app.get("/v1/state", (c) => {
13569
+ if (c.req.query("watching") === "1")
13570
+ store.touchDashboard();
11999
13571
  const body = JSON.stringify(store.snapshot());
12000
13572
  const etag = `W/"${Bun.hash(body).toString(36)}"`;
12001
13573
  const head = { etag, "cache-control": "no-cache" };
@@ -12176,7 +13748,21 @@ function createApp(store = new Store, hooks = {}) {
12176
13748
  return c.json({ error: e.message }, 500);
12177
13749
  }
12178
13750
  });
12179
- app.get("/v1/team", (c) => c.json(team.status()));
13751
+ app.get("/v1/team", (c) => c.json({ ...team.status(), ...hostingStatus(store) }));
13752
+ app.post("/v1/team/host", async (c) => {
13753
+ const b = await c.req.json().catch(() => ({}));
13754
+ const r = await hostTeam(store, b);
13755
+ return c.json(r, r.ok ? 201 : 409);
13756
+ });
13757
+ app.post("/v1/team/join", async (c) => {
13758
+ const b = await c.req.json().catch(() => ({}));
13759
+ const r = await joinTeam(store, b);
13760
+ return c.json(r, r.ok ? 200 : 409);
13761
+ });
13762
+ app.post("/v1/team/leave", async (c) => {
13763
+ const b = await c.req.json().catch(() => ({}));
13764
+ return c.json(await leaveTeam(store, b));
13765
+ });
12180
13766
  app.post("/v1/team/credentials", async (c) => {
12181
13767
  const b = await c.req.json().catch(() => ({}));
12182
13768
  if (typeof b.token !== "string" || !b.token)
@@ -12223,7 +13809,7 @@ function createApp(store = new Store, hooks = {}) {
12223
13809
  return c.json({ ok: false, error: "project required" }, 400);
12224
13810
  const overrides = {};
12225
13811
  for (const [k, v] of Object.entries(c.req.query()))
12226
- if (RULE_IDS.includes(k) && ["ask", "deny", "off"].includes(v))
13812
+ if (RULE_IDS.includes(k) && ["ask", "deny", "off", "rewrite"].includes(v))
12227
13813
  overrides[k] = v;
12228
13814
  const limit = Math.min(20000, Math.max(100, Number(c.req.query("limit")) || 5000));
12229
13815
  return c.json(store.dryRun(projectId, overrides, limit));
@@ -12232,6 +13818,15 @@ function createApp(store = new Store, hooks = {}) {
12232
13818
  const body = await c.req.json().catch(() => ({}));
12233
13819
  return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined, body.by) });
12234
13820
  });
13821
+ app.post("/v1/incidents/:seq/apply", async (c) => {
13822
+ const b = await c.req.json().catch(() => ({}));
13823
+ const seq = Number(c.req.param("seq"));
13824
+ if (!b.projectId || !Number.isInteger(seq))
13825
+ return c.json({ ok: false, error: "projectId and a numeric seq required" }, 400);
13826
+ const target = b.target === "claude-md" || b.target === "swarm-toml" || b.target === "both" ? b.target : store.policyFor(null).config.codify.target;
13827
+ const r = await applyCodify(store, forge, b.projectId, seq, target);
13828
+ return c.json(r, r.ok ? 201 : 409);
13829
+ });
12235
13830
  app.post("/v1/incidents/:seq/ack", (c) => {
12236
13831
  const seq = Number(c.req.param("seq"));
12237
13832
  if (!Number.isInteger(seq) || !store.ackIncident(seq, c.req.query("by")))
@@ -12313,6 +13908,7 @@ function createApp(store = new Store, hooks = {}) {
12313
13908
  return c.json({ error: "project required" }, 400);
12314
13909
  return c.json(store.budgetFor(project) ?? { status: null, config: store.config(project).budget });
12315
13910
  });
13911
+ app.get("/v1/quota", (c) => c.json(store.quota()));
12316
13912
  app.get("/v1/context", (c) => {
12317
13913
  const cwd = c.req.query("cwd");
12318
13914
  if (!cwd)
@@ -12444,6 +14040,27 @@ function createApp(store = new Store, hooks = {}) {
12444
14040
  const r = runner.answerPermission(c.req.param("id"), c.req.param("reqId"), b.allow === true, b.message);
12445
14041
  return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
12446
14042
  });
14043
+ app.get("/v1/repair", (c) => {
14044
+ const cwd = c.req.query("cwd") ?? "";
14045
+ return c.json({ block: store.repairArmed(cwd) });
14046
+ });
14047
+ app.post("/v1/wake", async (c) => {
14048
+ const b = await c.req.json().catch(() => ({}));
14049
+ const sid = typeof b.session_id === "string" ? b.session_id : "";
14050
+ if (!sid)
14051
+ return c.json({ wake: false, reason: "no session" }, 400);
14052
+ return c.json(await store.waitForWake(sid, 10 * 60000));
14053
+ });
14054
+ app.get("/v1/permissions", (c) => c.json({ permissions: store.pendingPermissions() }));
14055
+ app.post("/v1/permissions/:id", async (c) => {
14056
+ const b = await c.req.json().catch(() => ({}));
14057
+ const r = store.answerInteractive(c.req.param("id"), {
14058
+ behavior: b.terminal ? null : b.allow === true ? "allow" : "deny",
14059
+ ...b.message ? { message: b.message } : {},
14060
+ by: b.terminal ? "terminal" : b.by ?? "dashboard"
14061
+ });
14062
+ return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
14063
+ });
12447
14064
  app.delete("/v1/runs/:id", async (c) => {
12448
14065
  const r = await runner.stop(c.req.param("id"));
12449
14066
  return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
@@ -12690,9 +14307,61 @@ function createApp(store = new Store, hooks = {}) {
12690
14307
  });
12691
14308
  }
12692
14309
  const sid = typeof raw.session_id === "string" ? raw.session_id : null;
14310
+ if (event === "PermissionRequest" && sid) {
14311
+ const waitS = store.policyFor(null).config.broker.interactive_wait;
14312
+ if (waitS > 0 && store.dashboardWatching()) {
14313
+ const a = await store.askInteractive(raw, waitS * 1000);
14314
+ return c.json(permissionHookOutput(a ?? { behavior: null, by: "terminal" }, raw.tool_input ?? {}));
14315
+ }
14316
+ store.append({
14317
+ ts: new Date().toISOString(),
14318
+ type: "permission.resolved",
14319
+ projectId: typeof raw.cwd === "string" && existsSync9(raw.cwd) ? store.resolveProject(raw.cwd).id : "p_unknown",
14320
+ sessionId: sid,
14321
+ payload: {
14322
+ requestId: raw.tool_use_id ?? null,
14323
+ decision: "terminal",
14324
+ by: "terminal",
14325
+ source: "interactive",
14326
+ summary: "handed to the terminal (no dashboard watching)"
14327
+ }
14328
+ });
14329
+ return c.json({});
14330
+ }
14331
+ if (event === "Stop" && sid && typeof raw.cwd === "string") {
14332
+ const d = await store.stopDecision(sid, raw.cwd);
14333
+ if (d)
14334
+ return c.json({
14335
+ decision: "block",
14336
+ reason: d.reason,
14337
+ hookSpecificOutput: { hookEventName: "Stop", decision: "block", reason: d.reason }
14338
+ });
14339
+ }
12693
14340
  const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
12694
14341
  if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw))) {
12695
14342
  const guard = store.guardHook(raw);
14343
+ if (guard?.action === "rewrite") {
14344
+ const input = raw.tool_input ?? {};
14345
+ const second = store.evaluateTool(typeof raw.tool_name === "string" ? raw.tool_name : "Bash", { command: guard.command }, sid ?? "", typeof raw.cwd === "string" ? raw.cwd : "", false).decision;
14346
+ if (second.action === "deny" || second.action === "ask")
14347
+ return c.json({
14348
+ hookSpecificOutput: {
14349
+ hookEventName: "PreToolUse",
14350
+ permissionDecision: second.action,
14351
+ permissionDecisionReason: `[swarm] ${second.reason}`
14352
+ }
14353
+ });
14354
+ const note = `[swarm] rewrote the command (${guard.rule}): ${guard.reason}
14355
+ ran: ${guard.command}`;
14356
+ return c.json({
14357
+ hookSpecificOutput: {
14358
+ hookEventName: "PreToolUse",
14359
+ updatedInput: { ...input, command: guard.command },
14360
+ additionalContext: answers ? `${note}
14361
+ ${answers}` : note
14362
+ }
14363
+ });
14364
+ }
12696
14365
  if (guard) {
12697
14366
  return c.json({
12698
14367
  hookSpecificOutput: {
@@ -12704,13 +14373,33 @@ function createApp(store = new Store, hooks = {}) {
12704
14373
  });
12705
14374
  }
12706
14375
  }
12707
- if (answers)
14376
+ let collision = null;
14377
+ if (event === "PostToolUse" && sid && typeof raw.cwd === "string") {
14378
+ const fp = raw.tool_input?.file_path;
14379
+ if (WRITE_TOOLS.has(String(raw.tool_name)) && typeof fp === "string")
14380
+ collision = store.collisionContext(sid, raw.cwd, absolutePath(fp, raw.cwd));
14381
+ }
14382
+ const context = [collision, answers].filter(Boolean).join(`
14383
+ `);
14384
+ if (context)
12708
14385
  return c.json({
12709
- additionalContext: answers,
12710
- hookSpecificOutput: { hookEventName: event, additionalContext: answers }
14386
+ additionalContext: context,
14387
+ hookSpecificOutput: { hookEventName: event, additionalContext: context }
12711
14388
  });
12712
14389
  return c.json({});
12713
14390
  });
14391
+ app.post("/v1/statusline", async (c) => {
14392
+ const raw = await c.req.json().catch(() => null);
14393
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
14394
+ return c.json({ ok: false, error: "expected the statusLine JSON object" }, 400);
14395
+ return c.json(store.statuslineFor(raw));
14396
+ });
14397
+ app.post("/v1/shutdown", (c) => {
14398
+ if (!hooks.shutdown)
14399
+ return c.json({ ok: false, error: "shutdown not available" }, 501);
14400
+ setTimeout(() => hooks.shutdown?.(), 50);
14401
+ return c.json({ ok: true, pid: process.pid });
14402
+ });
12714
14403
  app.post("/v1/events", async (c) => {
12715
14404
  const e = await c.req.json();
12716
14405
  return c.json(store.append(e), 201);
@@ -12737,7 +14426,7 @@ function createApp(store = new Store, hooks = {}) {
12737
14426
  });
12738
14427
  });
12739
14428
  });
12740
- app.get("/", (c) => c.html(readFileSync4(join10(WEB_DIR, "index.html"), "utf8")));
14429
+ app.get("/", (c) => c.html(readFileSync6(join12(WEB_DIR, "index.html"), "utf8")));
12741
14430
  app.get("/next", (c) => c.redirect("/", 308));
12742
14431
  const MIME = {
12743
14432
  js: "text/javascript",
@@ -12749,15 +14438,15 @@ function createApp(store = new Store, hooks = {}) {
12749
14438
  const BINARY = new Set(["ico", "png"]);
12750
14439
  app.get("/:file{[a-z0-9-]+\\.(js|css|svg|ico|png)}", (c) => {
12751
14440
  const f = c.req.param("file");
12752
- const p = join10(WEB_DIR, f);
12753
- if (!existsSync7(p))
14441
+ const p = join12(WEB_DIR, f);
14442
+ if (!existsSync9(p))
12754
14443
  return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
12755
14444
  const st = statSync2(p);
12756
14445
  const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
12757
14446
  if (c.req.header("if-none-match") === etag)
12758
14447
  return c.body(null, 304, { etag });
12759
14448
  const ext = f.split(".").pop() ?? "";
12760
- return c.body(BINARY.has(ext) ? readFileSync4(p) : readFileSync4(p, "utf8"), 200, {
14449
+ return c.body(BINARY.has(ext) ? readFileSync6(p) : readFileSync6(p, "utf8"), 200, {
12761
14450
  "content-type": MIME[ext] ?? "text/plain",
12762
14451
  "cache-control": "no-cache",
12763
14452
  etag
@@ -12892,6 +14581,7 @@ var restart = () => {
12892
14581
  setTimeout(() => process.exit(0), 100);
12893
14582
  };
12894
14583
  appHooks.restart = restart;
14584
+ appHooks.shutdown = () => void shutdown();
12895
14585
  server = serve();
12896
14586
  var port = server.port ?? DEFAULT_PORT2;
12897
14587
  ensureToken();
@@ -12924,6 +14614,8 @@ var tailer = setInterval(() => {
12924
14614
  store.sweepOrphans();
12925
14615
  if (tick % 6 === 0)
12926
14616
  store.checkBudgets();
14617
+ if (tick % 6 === 0)
14618
+ store.checkQuota();
12927
14619
  if (tick % 12 === 0)
12928
14620
  store.checkModels();
12929
14621
  if (tick % 2 === 0)
@@ -12952,4 +14644,15 @@ async function shutdown() {
12952
14644
  }
12953
14645
  process.on("SIGINT", shutdown);
12954
14646
  process.on("SIGTERM", shutdown);
14647
+ var parentPid = Number(process.env.SWARM_PARENT_PID);
14648
+ if (Number.isInteger(parentPid) && parentPid > 1) {
14649
+ setInterval(() => {
14650
+ try {
14651
+ process.kill(parentPid, 0);
14652
+ } catch {
14653
+ console.error(`swarmd: parent ${parentPid} is gone \u2014 shutting down`);
14654
+ shutdown();
14655
+ }
14656
+ }, 2000).unref();
14657
+ }
12955
14658
  process.on("exit", () => clearDaemonInfo());