@ra3orblade/swarm 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/swarm.js CHANGED
@@ -50,6 +50,22 @@ function readDaemonInfo() {
50
50
  return null;
51
51
  }
52
52
  }
53
+ var tokenFile = (home = swarmHome()) => join(home, "token");
54
+ function readToken(home = swarmHome()) {
55
+ try {
56
+ const t = readFileSync(tokenFile(home), "utf8").trim();
57
+ return /^[a-f0-9]{64}$/.test(t) ? t : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ var authedFetch = (input, init) => {
63
+ const h = new Headers(init?.headers);
64
+ const t = readToken();
65
+ if (t && !h.has("authorization"))
66
+ h.set("authorization", `Bearer ${t}`);
67
+ return fetch(input, { ...init, headers: h });
68
+ };
53
69
  function alive(pid) {
54
70
  try {
55
71
  process.kill(pid, 0);
@@ -108,7 +124,7 @@ class SwarmClient {
108
124
  f;
109
125
  constructor(opts = {}) {
110
126
  this.baseUrl = resolveBaseUrl(opts.baseUrl);
111
- this.f = opts.fetch ?? fetch;
127
+ this.f = opts.fetch ?? authedFetch;
112
128
  }
113
129
  async health() {
114
130
  const r = await this.f(`${this.baseUrl}/v1/health`);
@@ -127,10 +143,9 @@ class SwarmClient {
127
143
  }
128
144
  }
129
145
 
130
- // packages/cli/src/install.ts
131
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
132
- import { homedir as homedir2 } from "os";
133
- import { join as join2 } from "path";
146
+ // packages/core/src/actor.ts
147
+ var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
148
+ var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
134
149
  // packages/core/src/adapters/claude-code/hooks.ts
135
150
  var HOOK_EVENTS = [
136
151
  "SessionStart",
@@ -144,22 +159,327 @@ var HOOK_EVENTS = [
144
159
  "Notification",
145
160
  "PreCompact"
146
161
  ];
162
+ // packages/core/src/audit.ts
163
+ var AUDIT_TYPES = new Set([
164
+ "session.started",
165
+ "session.ended",
166
+ "tool.denied",
167
+ "claim.acquired",
168
+ "claim.renewed",
169
+ "claim.released",
170
+ "claim.expired",
171
+ "claim.orphaned",
172
+ "worktree.created",
173
+ "worktree.removed",
174
+ "worktree.bootstrapped",
175
+ "pr.opened",
176
+ "question.asked",
177
+ "question.answered",
178
+ "dispatch.queued",
179
+ "dispatch.started",
180
+ "dispatch.finished",
181
+ "resource.acquired",
182
+ "resource.released",
183
+ "resource.reaped",
184
+ "process.started",
185
+ "process.exited",
186
+ "gate.recorded",
187
+ "handoff.recorded",
188
+ "permission.requested",
189
+ "permission.resolved",
190
+ "incident.opened",
191
+ "incident.acked",
192
+ "run.result"
193
+ ]);
194
+ var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
195
+ var DEFAULT_PRIVACY = {
196
+ store_prompts: true,
197
+ store_reasoning: true,
198
+ redact: []
199
+ };
200
+ // packages/core/src/budget.ts
201
+ var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
202
+ // packages/core/src/config.ts
203
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
204
+ import { join as join2 } from "path";
205
+ var DEFAULT_GATE_TIMEOUT_S = 900;
206
+ var AUTO_MODES = ["session-end", "stop", "off"];
207
+ function parseGateDefs(gates) {
208
+ const out = {};
209
+ if (!isRecord(gates))
210
+ return out;
211
+ for (const [name, v] of Object.entries(gates)) {
212
+ if (!isRecord(v))
213
+ continue;
214
+ const builtin = v.builtin === "review" ? "review" : null;
215
+ const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
216
+ if (!cmd && !builtin)
217
+ continue;
218
+ if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
219
+ continue;
220
+ const t = Number(v.timeout);
221
+ out[name] = {
222
+ cmd: builtin ? "" : cmd,
223
+ timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
224
+ cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
225
+ builtin,
226
+ model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
227
+ };
228
+ }
229
+ return out;
230
+ }
231
+ var DEFAULT_CONFIG = {
232
+ daemon: { port: 7777, auth: "loopback-optional" },
233
+ tasks: { source: null, labels: [], team: null },
234
+ gates: { required: [], auto: "session-end", defs: {} },
235
+ budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
236
+ events: { retain_days: 30 },
237
+ audit: { retain_days: 0 },
238
+ privacy: DEFAULT_PRIVACY,
239
+ dispatch: {
240
+ max_parallel: 2,
241
+ permission_mode: null,
242
+ model: null,
243
+ max_turns: null,
244
+ require_pr: true,
245
+ profile: null
246
+ },
247
+ worktree: { setup: null, copy: [], open: null },
248
+ rules: {
249
+ shared_tree: "ask",
250
+ destructive_git: "ask",
251
+ pattern_kill: "ask",
252
+ protected_ports: "ask",
253
+ no_foreign_worktree: "ask",
254
+ claim_required_to_write: "off",
255
+ protected: { ports: [] }
256
+ }
257
+ };
258
+ var MODES = ["ask", "deny", "off"];
259
+ function isRecord(v) {
260
+ return typeof v === "object" && v !== null && !Array.isArray(v);
261
+ }
262
+ function merge(a, b) {
263
+ if (!isRecord(a) || !isRecord(b))
264
+ return b === undefined ? a : b;
265
+ const out = { ...a };
266
+ for (const [k, v] of Object.entries(b))
267
+ out[k] = merge(a[k], v);
268
+ return out;
269
+ }
270
+ function parseToml(text, source) {
271
+ try {
272
+ return Bun.TOML.parse(text) ?? {};
273
+ } catch (e) {
274
+ console.error(`swarm: ignoring invalid TOML in ${source}: ${e.message}`);
275
+ return {};
276
+ }
277
+ }
278
+ function isRepoRelative(f) {
279
+ if (typeof f !== "string")
280
+ return false;
281
+ const t = f.trim();
282
+ if (!t || t.startsWith("/") || t.startsWith("\\") || /^[a-zA-Z]:/.test(t))
283
+ return false;
284
+ return !t.split(/[/\\]/).some((seg) => seg === "..");
285
+ }
286
+ var days = (v, fallback) => {
287
+ const n = Number(v);
288
+ return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
289
+ };
290
+ function validate(c) {
291
+ const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
292
+ const port = Number(c.daemon?.port);
293
+ const source = c.tasks?.source;
294
+ const setup = c.worktree?.setup;
295
+ const opener = c.worktree?.open;
296
+ const rawGates = c.gates;
297
+ const d = c.dispatch ?? {};
298
+ const mp = Number(d.max_parallel);
299
+ const mt = Number(d.max_turns);
300
+ const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
301
+ const b = c.budget ?? {};
302
+ const usd = (v) => {
303
+ const n = Number(v);
304
+ return Number.isFinite(n) && n > 0 ? n : null;
305
+ };
306
+ const warnAt = Number(b.warn_at);
307
+ const auto = rawGates?.auto;
308
+ return {
309
+ ...c,
310
+ daemon: {
311
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
312
+ auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
313
+ },
314
+ tasks: {
315
+ source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
316
+ labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
317
+ team: typeof c.tasks?.team === "string" && c.tasks.team.trim() ? c.tasks.team.trim() : null
318
+ },
319
+ gates: {
320
+ required: Array.isArray(rawGates?.required) ? rawGates.required.filter((g) => typeof g === "string" && g.trim() !== "") : [],
321
+ auto: AUTO_MODES.includes(auto) ? auto : "session-end",
322
+ defs: parseGateDefs(rawGates)
323
+ },
324
+ budget: {
325
+ daily: usd(b.daily),
326
+ weekly: usd(b.weekly),
327
+ warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
328
+ on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
329
+ },
330
+ events: {
331
+ retain_days: days(c.events?.retain_days, 30)
332
+ },
333
+ audit: {
334
+ retain_days: days(c.audit?.retain_days, 0)
335
+ },
336
+ privacy: {
337
+ store_prompts: c.privacy?.store_prompts !== false,
338
+ store_reasoning: c.privacy?.store_reasoning !== false,
339
+ redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
340
+ },
341
+ dispatch: {
342
+ max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
343
+ permission_mode: str(d.permission_mode),
344
+ model: str(d.model),
345
+ max_turns: Number.isInteger(mt) && mt > 0 ? mt : null,
346
+ require_pr: d.require_pr === undefined ? true : d.require_pr === true,
347
+ profile: ["full", "no-edits", "read-only"].includes(String(d.profile)) ? String(d.profile) : null
348
+ },
349
+ worktree: {
350
+ setup: typeof setup === "string" && setup.trim() ? setup.trim() : null,
351
+ copy: Array.isArray(c.worktree?.copy) ? c.worktree.copy.filter((f) => isRepoRelative(f)) : [],
352
+ open: typeof opener === "string" && opener.trim() ? opener.trim() : null
353
+ },
354
+ rules: {
355
+ ...c.rules,
356
+ shared_tree: mode(c.rules?.shared_tree, "ask"),
357
+ destructive_git: mode(c.rules?.destructive_git, "ask"),
358
+ pattern_kill: mode(c.rules?.pattern_kill, "ask"),
359
+ protected_ports: mode(c.rules?.protected_ports, "ask"),
360
+ no_foreign_worktree: mode(c.rules?.no_foreign_worktree, "ask"),
361
+ claim_required_to_write: mode(c.rules?.claim_required_to_write, "off"),
362
+ protected: {
363
+ ports: Array.isArray(c.rules?.protected?.ports) ? c.rules.protected.ports.filter((p) => Number.isInteger(p) && p > 0 && p < 65536) : []
364
+ }
365
+ }
366
+ };
367
+ }
368
+ function leafPaths(v, prefix = "") {
369
+ if (!isRecord(v))
370
+ return prefix ? [prefix] : [];
371
+ const keys = Object.keys(v);
372
+ if (keys.length === 0)
373
+ return prefix ? [prefix] : [];
374
+ return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
375
+ }
376
+ function getPath(v, path) {
377
+ let cur = v;
378
+ for (const seg of path.split(".")) {
379
+ if (!isRecord(cur))
380
+ return;
381
+ cur = cur[seg];
382
+ }
383
+ return cur;
384
+ }
385
+ function setPath(obj, path, value) {
386
+ const segs = path.split(".");
387
+ let cur = obj;
388
+ for (const seg of segs.slice(0, -1)) {
389
+ if (!isRecord(cur[seg]))
390
+ cur[seg] = {};
391
+ cur = cur[seg];
392
+ }
393
+ cur[segs[segs.length - 1]] = value;
394
+ }
395
+ var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
396
+ function readLayer(path) {
397
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
398
+ }
399
+ function loadConfigDetailed(opts = {}) {
400
+ const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
401
+ const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
402
+ const policyRaw = readLayer(policyPath);
403
+ const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
404
+ const policy = { ...policyRaw ?? {} };
405
+ delete policy.locked;
406
+ const layers = [
407
+ ["policy", policyRaw ? policy : null],
408
+ ["global", readLayer(join2(home, "config.toml"))],
409
+ ["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
410
+ ];
411
+ const provenance = {};
412
+ for (const p of leafPaths(DEFAULT_CONFIG))
413
+ provenance[p] = "default";
414
+ const overridden = [];
415
+ let cfg = DEFAULT_CONFIG;
416
+ for (const [layer, raw] of layers) {
417
+ if (!raw)
418
+ continue;
419
+ for (const p of leafPaths(raw)) {
420
+ const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
421
+ if (lock)
422
+ overridden.push({ key: p, layer, attempted: getPath(raw, p) });
423
+ else
424
+ provenance[p] = layer;
425
+ }
426
+ cfg = merge(cfg, raw);
427
+ }
428
+ if (overridden.length) {
429
+ const out = structuredClone(cfg);
430
+ for (const { key } of overridden) {
431
+ const fromPolicy = getPath(policy, key);
432
+ setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
433
+ provenance[key] = fromPolicy === undefined ? "default" : "policy";
434
+ }
435
+ cfg = out;
436
+ }
437
+ return {
438
+ config: validate(cfg),
439
+ provenance,
440
+ overridden,
441
+ policy: { path: policyRaw ? policyPath : null, locked }
442
+ };
443
+ }
147
444
  // packages/core/src/rules.ts
148
445
  var LIVE_WINDOW_MS = 10 * 60000;
149
446
  var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
150
447
  // packages/core/src/ledger.ts
151
448
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
449
+ // packages/core/src/policy.ts
450
+ var HOOK_MARK = "swarm-hook";
451
+ var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
452
+ var MIN_HOOK_TIMEOUT_S = 5;
453
+ function hookCoverage(settings) {
454
+ const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
455
+ const missing = [];
456
+ const short = [];
457
+ for (const ev of HOOK_EVENTS) {
458
+ const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
459
+ const ours = groups.flatMap((g) => {
460
+ const list = g?.hooks;
461
+ return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
462
+ });
463
+ if (!ours.length)
464
+ missing.push(ev);
465
+ else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
466
+ short.push(ev);
467
+ }
468
+ return { missing, short, complete: !missing.length && !short.length };
469
+ }
152
470
  // packages/cli/src/install.ts
153
- var MARK = "swarm-hook";
154
- var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
155
- var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join2(homedir2(), ".claude", "settings.json");
156
- var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join2(homedir2(), ".claude.json");
471
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
472
+ import { homedir as homedir2 } from "os";
473
+ import { join as join3 } from "path";
474
+ var isOurs = hookIsOurs;
475
+ var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join3(homedir2(), ".claude", "settings.json");
476
+ var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join3(homedir2(), ".claude.json");
157
477
  function loadClaudeJson() {
158
478
  const p = claudeJsonPath();
159
- if (!existsSync3(p))
479
+ if (!existsSync4(p))
160
480
  return {};
161
481
  try {
162
- return JSON.parse(readFileSync2(p, "utf8"));
482
+ return JSON.parse(readFileSync3(p, "utf8"));
163
483
  } catch {
164
484
  return {};
165
485
  }
@@ -192,6 +512,95 @@ function mcpRegistered() {
192
512
  const c = loadClaudeJson();
193
513
  return Boolean(c.mcpServers?.swarm);
194
514
  }
515
+ var codexConfigPath = () => process.env.CODEX_CONFIG ?? join3(homedir2(), ".codex", "config.toml");
516
+ var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join3(homedir2(), ".gemini", "settings.json");
517
+ function codexBlock() {
518
+ const { command, args } = mcpServerConfig();
519
+ return `[mcp_servers.swarm]
520
+ command = ${JSON.stringify(command)}
521
+ args = ${JSON.stringify(args)}
522
+ `;
523
+ }
524
+ var CODEX_BLOCK_RE = /\[mcp_servers\.swarm\]\n(?:(?!\[)[^\n]*\n?)*/;
525
+ function registerCodex() {
526
+ const p = codexConfigPath();
527
+ if (!existsSync4(join3(p, "..")))
528
+ return false;
529
+ const cur = existsSync4(p) ? readFileSync3(p, "utf8") : "";
530
+ const next = CODEX_BLOCK_RE.test(cur) ? cur.replace(CODEX_BLOCK_RE, codexBlock()) : `${cur.trimEnd()}${cur.trim() ? `
531
+
532
+ ` : ""}${codexBlock()}`;
533
+ if (next !== cur)
534
+ writeFileSync2(p, next);
535
+ return true;
536
+ }
537
+ function unregisterCodex() {
538
+ const p = codexConfigPath();
539
+ if (!existsSync4(p))
540
+ return false;
541
+ const cur = readFileSync3(p, "utf8");
542
+ if (!CODEX_BLOCK_RE.test(cur))
543
+ return false;
544
+ writeFileSync2(p, cur.replace(CODEX_BLOCK_RE, "").replace(/\n{3,}/g, `
545
+
546
+ `).trimEnd().concat(`
547
+ `));
548
+ return true;
549
+ }
550
+ function registerGemini() {
551
+ const p = geminiSettingsPath();
552
+ if (!existsSync4(join3(p, "..")))
553
+ return false;
554
+ let c = {};
555
+ try {
556
+ c = existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
557
+ } catch {
558
+ return false;
559
+ }
560
+ const mcp = c.mcpServers ?? {};
561
+ mcp.swarm = mcpServerConfig();
562
+ c.mcpServers = mcp;
563
+ writeFileSync2(p, `${JSON.stringify(c, null, 2)}
564
+ `);
565
+ return true;
566
+ }
567
+ function unregisterGemini() {
568
+ const p = geminiSettingsPath();
569
+ if (!existsSync4(p))
570
+ return false;
571
+ try {
572
+ const c = JSON.parse(readFileSync3(p, "utf8"));
573
+ const mcp = c.mcpServers ?? {};
574
+ if (!mcp.swarm)
575
+ return false;
576
+ delete mcp.swarm;
577
+ if (Object.keys(mcp).length)
578
+ c.mcpServers = mcp;
579
+ else
580
+ delete c.mcpServers;
581
+ writeFileSync2(p, `${JSON.stringify(c, null, 2)}
582
+ `);
583
+ return true;
584
+ } catch {
585
+ return false;
586
+ }
587
+ }
588
+ function registerOtherAgents() {
589
+ const out = [];
590
+ if (registerCodex())
591
+ out.push("codex");
592
+ if (registerGemini())
593
+ out.push("gemini");
594
+ return out;
595
+ }
596
+ function unregisterOtherAgents() {
597
+ const out = [];
598
+ if (unregisterCodex())
599
+ out.push("codex");
600
+ if (unregisterGemini())
601
+ out.push("gemini");
602
+ return out;
603
+ }
195
604
  var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
196
605
  var shimPath = () => resolveBin("swarm-hook").at(-1);
197
606
  function mcpServerConfig() {
@@ -200,7 +609,7 @@ function mcpServerConfig() {
200
609
  }
201
610
  function load() {
202
611
  const p = settingsPath();
203
- return existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
612
+ return existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
204
613
  }
205
614
  function save(s) {
206
615
  writeFileSync2(settingsPath(), `${JSON.stringify(s, null, 2)}
@@ -230,6 +639,7 @@ function install() {
230
639
  }
231
640
  save(s);
232
641
  registerMcp();
642
+ registerOtherAgents();
233
643
  return added;
234
644
  }
235
645
  function uninstall() {
@@ -267,20 +677,37 @@ function uninstall() {
267
677
  save(s);
268
678
  if (unregisterMcp())
269
679
  removed++;
680
+ removed += unregisterOtherAgents().length;
270
681
  return removed;
271
682
  }
272
683
  function status() {
273
684
  const s = load();
274
685
  const hooks2 = s.hooks ?? {};
275
686
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
276
- return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath() };
687
+ const otherAgents = [];
688
+ const cp = codexConfigPath();
689
+ if (existsSync4(cp) && CODEX_BLOCK_RE.test(readFileSync3(cp, "utf8")))
690
+ otherAgents.push("codex");
691
+ const gp = geminiSettingsPath();
692
+ try {
693
+ if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
694
+ otherAgents.push("gemini");
695
+ } catch {}
696
+ return {
697
+ installed,
698
+ coverage: hookCoverage(s),
699
+ mcp: mcpRegistered(),
700
+ path: settingsPath(),
701
+ shim: shimPath(),
702
+ otherAgents
703
+ };
277
704
  }
278
705
 
279
706
  // packages/cli/src/procs.ts
280
707
  import { mkdirSync as mkdirSync2, openSync } from "fs";
281
- import { join as join3, resolve as resolve2 } from "path";
708
+ import { join as join4, resolve as resolve2 } from "path";
282
709
  async function call(path, init) {
283
- const r = await fetch(`${new SwarmClient().baseUrl}${path}`, init);
710
+ const r = await authedFetch(`${new SwarmClient().baseUrl}${path}`, init);
284
711
  return await r.json();
285
712
  }
286
713
  var post = (path, body) => call(path, {
@@ -308,9 +735,9 @@ async function start(opts) {
308
735
  port = a.port;
309
736
  }
310
737
  }
311
- const logDir = join3(swarmHome(), "logs", slug(pid));
738
+ const logDir = join4(swarmHome(), "logs", slug(pid));
312
739
  mkdirSync2(logDir, { recursive: true });
313
- const log = join3(logDir, `${slug(opts.name)}.log`);
740
+ const log = join4(logDir, `${slug(opts.name)}.log`);
314
741
  const fd = openSync(log, "a");
315
742
  const cmdline = opts.cmd.join(" ");
316
743
  const child = Bun.spawn(["sh", "-c", cmdline], {
@@ -375,6 +802,14 @@ function fmt(r) {
375
802
  }
376
803
 
377
804
  // packages/cli/src/bin.ts
805
+ function gitToplevel() {
806
+ const r = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
807
+ stdout: "pipe",
808
+ stderr: "ignore"
809
+ });
810
+ const out = r.exitCode === 0 ? r.stdout.toString().trim() : "";
811
+ return out || null;
812
+ }
378
813
  var [cmd = "help", ...rest] = process.argv.slice(2);
379
814
  var json = rest.includes("--json");
380
815
  var arg = () => rest.find((a) => !a.startsWith("--"));
@@ -391,12 +826,17 @@ var help = `swarm \u2014 control plane for AI-agent development
391
826
  tail [--project p] [--session id] follow the live event stream
392
827
 
393
828
  claim <task> [--owner n] claim a task in a fresh isolated git worktree (fail-closed)
829
+ gate run <task> [gate\u2026] execute the repo's [gates.<name>] cmd gates in the task's worktree and record them
830
+ wt [ls|create|open|diff|rm|gc] first-class worktrees: create task-less ones, open, diff, remove, collect stale
831
+ pr open <task|worktree> push the branch and open a PR/MR prefilled from the task, handoff, gates and files
832
+ questions [--all] questions agents are waiting on a human for (this repo); answer <id> <text\u2026>
833
+ dispatch --ready | <task\u2026> claim + spawn a run per task, [dispatch] max_parallel at a time; status | clear
394
834
  renew <task> extend the lease; release <task> [--force] release + remove worktree
395
835
  claims list claims; reap release abandoned claims (keeps ones holding work)
396
836
  tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
397
837
  gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
398
838
  gate ls [task] latest verdict per gate (and the run history for one task)
399
- run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--allowed-tools a,b] [--max-turns n]
839
+ run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--profile full|no-edits|read-only] [--allowed-tools a,b] [--max-turns n]
400
840
  claim the task and spawn claude -p in its worktree; the session shows in Fleet
401
841
  run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
402
842
  run resume <session-id> [--model m] [--permission-mode m] spawn a run that picks up where a dead session stopped (its handoff + tail)
@@ -411,13 +851,14 @@ var help = `swarm \u2014 control plane for AI-agent development
411
851
  stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
412
852
  search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
413
853
  rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
854
+ audit export [--since 30d|ISO] [-p] [--type claim.acquired] [--format jsonl|csv|json] [--limit n] the audit log (ledger changes + decisions, with actor) to stdout
414
855
 
415
856
  install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
416
857
 
417
858
  Env: SWARM_URL, SWARM_PORT (default 7777), SWARM_HOME (~/.swarm)`;
418
859
  async function api(path, init) {
419
860
  const base = new SwarmClient().baseUrl;
420
- const r = await fetch(`${base}${path}`, init);
861
+ const r = await authedFetch(`${base}${path}`, init);
421
862
  if (!r.ok)
422
863
  throw new Error(`${path}: ${r.status} ${await r.text()}`);
423
864
  return r.status === 204 ? null : r.json();
@@ -447,7 +888,7 @@ try {
447
888
  const base = await ensureDaemon();
448
889
  const evs = install();
449
890
  console.log(`\u2713 daemon running at ${base}`);
450
- console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})`);
891
+ console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})${status().otherAgents.length ? ` \xB7 MCP also for ${status().otherAgents.join(", ")}` : ""}`);
451
892
  console.log("\u2713 any Claude session you start now will appear in Swarm");
452
893
  Bun.spawn(["open", base]).unref?.();
453
894
  console.log(`
@@ -490,8 +931,27 @@ Open the dashboard: ${base}`);
490
931
  line(Boolean(bun), `bun ${bun ? `(${bun})` : ""}`, "install bun: https://bun.sh");
491
932
  line(Boolean(claude), "claude CLI on PATH", "install Claude Code: https://claude.com/claude-code");
492
933
  line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
934
+ if (running) {
935
+ const h = await authedFetch(`${resolveBaseUrl()}/v1/health`).then((r) => r.json()).catch(() => null);
936
+ if (h)
937
+ console.log(`\xB7 daemon v${h.version ?? "?"} \xB7 schema v${h.schema ?? 0}`);
938
+ }
493
939
  line(st.installed, "hooks installed", "run: swarm install");
940
+ if (st.installed && !st.coverage.complete) {
941
+ if (st.coverage.missing.length)
942
+ line(false, `hooks missing: ${st.coverage.missing.join(", ")}`, "run: swarm install");
943
+ if (st.coverage.short.length)
944
+ line(false, `hook timeout too short: ${st.coverage.short.join(", ")}`, "run: swarm install");
945
+ }
946
+ const pol = loadConfigDetailed({ repoRoot: gitToplevel() });
947
+ if (pol.policy.path) {
948
+ line(true, `policy ${pol.policy.path} (locked: ${pol.policy.locked.join(", ") || "nothing"})`, "");
949
+ for (const o of pol.overridden)
950
+ line(false, `${o.layer} config overrides locked ${o.key}`, `remove it \u2014 policy value stays in effect`);
951
+ }
494
952
  line(st.mcp, "MCP server registered", "run: swarm install");
953
+ if (st.otherAgents.length)
954
+ console.log(`\u2713 MCP server also registered for ${st.otherAgents.join(", ")} (swarm_* tools in those CLIs too)`);
495
955
  const forge2 = (bin, auth) => {
496
956
  const path = Bun.which(bin);
497
957
  if (!path)
@@ -536,7 +996,7 @@ url: ${resolveBaseUrl()}`);
536
996
  headers: { "content-type": "application/json" },
537
997
  body: JSON.stringify({ path: resolve3(".") })
538
998
  });
539
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims`, {
999
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims`, {
540
1000
  method: "POST",
541
1001
  headers: { "content-type": "application/json" },
542
1002
  body: JSON.stringify({ projectId: proj.id, task, owner })
@@ -545,7 +1005,8 @@ url: ${resolveBaseUrl()}`);
545
1005
  console.log(JSON.stringify(r));
546
1006
  else if (r.ok)
547
1007
  console.log(`claimed ${task} \u2192 ${r.worktree}
548
- cd ${r.worktree}`);
1008
+ cd ${r.worktree}${r.bootstrap ? `
1009
+ bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
549
1010
  else {
550
1011
  console.error(`REFUSED: ${r.error}`);
551
1012
  process.exit(1);
@@ -563,7 +1024,7 @@ url: ${resolveBaseUrl()}`);
563
1024
  headers: { "content-type": "application/json" },
564
1025
  body: JSON.stringify({ path: resolve3(".") })
565
1026
  });
566
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
1027
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
567
1028
  method: "POST",
568
1029
  headers: { "content-type": "application/json" },
569
1030
  body: JSON.stringify({ projectId: proj.id, task, force: rest.includes("--force") })
@@ -578,6 +1039,317 @@ url: ${resolveBaseUrl()}`);
578
1039
  }
579
1040
  break;
580
1041
  }
1042
+ case "wt": {
1043
+ await ensureDaemon({ quiet: true });
1044
+ const sub = arg();
1045
+ const proj = await api("/v1/projects", {
1046
+ method: "POST",
1047
+ headers: { "content-type": "application/json" },
1048
+ body: JSON.stringify({ path: resolve3(".") })
1049
+ });
1050
+ const post2 = (path, body) => authedFetch(`${new SwarmClient().baseUrl}${path}`, {
1051
+ method: "POST",
1052
+ headers: { "content-type": "application/json" },
1053
+ body: JSON.stringify({ projectId: proj.id, ...body })
1054
+ }).then((x) => x.json());
1055
+ const refuse = (r) => {
1056
+ if (json)
1057
+ console.log(JSON.stringify(r));
1058
+ else if (!r.ok) {
1059
+ console.error(`REFUSED: ${r.error}`);
1060
+ process.exit(1);
1061
+ }
1062
+ return !r.ok;
1063
+ };
1064
+ const flag = (n) => {
1065
+ const i = rest.indexOf(n);
1066
+ return i >= 0 ? rest[i + 1] : undefined;
1067
+ };
1068
+ const VALUE_FLAGS = ["--base", "--branch"];
1069
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
1070
+ const target = positional[1];
1071
+ switch (sub) {
1072
+ case "create": {
1073
+ if (!target)
1074
+ throw new Error("usage: swarm wt create <name> [--base ref] [--branch name]");
1075
+ const r = await post2("/v1/worktrees", {
1076
+ name: target,
1077
+ baseRef: flag("--base"),
1078
+ branch: flag("--branch")
1079
+ });
1080
+ if (refuse(r) || json)
1081
+ break;
1082
+ console.log(`created ${r.name} \u2192 ${r.worktree} (branch ${r.branch})
1083
+ cd ${r.worktree}${r.bootstrap ? `
1084
+ bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
1085
+ break;
1086
+ }
1087
+ case "ls":
1088
+ case undefined: {
1089
+ const wts = await api(`/v1/worktrees?project=${proj.id}`);
1090
+ if (json)
1091
+ console.log(JSON.stringify(wts));
1092
+ else {
1093
+ for (const w of wts) {
1094
+ const st = w.main ? "main" : [
1095
+ w.dirty > 0 ? `${w.dirty} dirty` : "",
1096
+ w.ahead > 0 ? `${w.ahead} unpushed` : "",
1097
+ w.behind > 0 ? `${w.behind} behind` : "",
1098
+ w.merged ? "merged" : ""
1099
+ ].filter(Boolean).join(", ") || "clean";
1100
+ console.log(`${(w.branch ?? "(detached)").padEnd(32)} ${w.head} ${st.padEnd(24)} ${w.path}`);
1101
+ }
1102
+ if (!wts.length)
1103
+ console.log("no worktrees");
1104
+ }
1105
+ break;
1106
+ }
1107
+ case "open": {
1108
+ if (!target)
1109
+ throw new Error("usage: swarm wt open <name|path>");
1110
+ const r = await post2("/v1/worktrees/open", { worktree: target });
1111
+ if (refuse(r) || json)
1112
+ break;
1113
+ console.log(`opened ${r.worktree}`);
1114
+ break;
1115
+ }
1116
+ case "rm": {
1117
+ if (!target)
1118
+ throw new Error("usage: swarm wt rm <name|path> [--force]");
1119
+ const r = await post2("/v1/worktrees/remove", {
1120
+ worktree: target,
1121
+ force: rest.includes("--force")
1122
+ });
1123
+ if (refuse(r) || json)
1124
+ break;
1125
+ console.log(`removed ${r.worktree}`);
1126
+ break;
1127
+ }
1128
+ case "diff": {
1129
+ if (!target)
1130
+ throw new Error("usage: swarm wt diff <name|path|task> [--file f] [--patch]");
1131
+ const q = new URLSearchParams({ project: proj.id, worktree: target });
1132
+ const file = flag("--file");
1133
+ if (file)
1134
+ q.set("file", file);
1135
+ if (rest.includes("--patch"))
1136
+ q.set("patch", "1");
1137
+ const d = await api(`/v1/worktrees/diff?${q}`);
1138
+ if (d.error) {
1139
+ console.error(`REFUSED: ${d.error}`);
1140
+ process.exit(1);
1141
+ }
1142
+ if (json)
1143
+ console.log(JSON.stringify(d));
1144
+ else if (d.patch !== undefined)
1145
+ console.log(d.patch);
1146
+ else {
1147
+ console.log(`vs ${d.baseRef ?? "HEAD"} \xB7 ${d.commits.length} commit${d.commits.length === 1 ? "" : "s"} \xB7 ${d.files.length} file${d.files.length === 1 ? "" : "s"}${d.dirty ? " \xB7 dirty" : ""}`);
1148
+ for (const c of d.commits)
1149
+ console.log(` ${c}`);
1150
+ for (const f of d.files)
1151
+ console.log(`${f.status} ${f.added >= 0 ? `+${f.added}`.padStart(6) : " bin"} ${f.deleted >= 0 ? `-${f.deleted}`.padStart(6) : " "} ${f.path}`);
1152
+ }
1153
+ break;
1154
+ }
1155
+ case "gc": {
1156
+ const r = await post2("/v1/worktrees/gc", { apply: rest.includes("--apply") });
1157
+ if (json)
1158
+ console.log(JSON.stringify(r));
1159
+ else if (!r.candidates.length)
1160
+ console.log("nothing to collect");
1161
+ else {
1162
+ for (const x of r.candidates)
1163
+ console.log(`${r.removed.includes(x.path) ? "removed " : x.removable ? "removable" : `blocked (${x.blocker})`} ${x.why.padEnd(15)} ${x.branch ?? "(detached)"} ${x.path}`);
1164
+ if (!rest.includes("--apply") && r.candidates.some((x) => x.removable))
1165
+ console.log("\nrun `swarm wt gc --apply` to remove the removable ones");
1166
+ }
1167
+ break;
1168
+ }
1169
+ default:
1170
+ throw new Error("usage: swarm wt [ls] | create <name> | open <ref> | rm <ref> [--force] | gc [--apply]");
1171
+ }
1172
+ break;
1173
+ }
1174
+ case "questions":
1175
+ case "answer": {
1176
+ await ensureDaemon({ quiet: true });
1177
+ if (cmd === "questions") {
1178
+ const q = new URLSearchParams;
1179
+ if (!rest.includes("--all"))
1180
+ q.set("open", "1");
1181
+ if (!rest.includes("--everywhere")) {
1182
+ const proj = await api("/v1/projects", {
1183
+ method: "POST",
1184
+ headers: { "content-type": "application/json" },
1185
+ body: JSON.stringify({ path: resolve3(".") })
1186
+ });
1187
+ q.set("project", proj.id);
1188
+ }
1189
+ const qs = await api(`/v1/questions?${q}`);
1190
+ if (json)
1191
+ console.log(JSON.stringify(qs));
1192
+ else if (!qs.length)
1193
+ console.log("no open questions");
1194
+ else
1195
+ for (const x of qs)
1196
+ console.log(`#${x.id} ${x.createdAt.slice(0, 16).replace("T", " ")} ${x.task ? `[${x.task}] ` : ""}${x.text}${x.options.length ? `
1197
+ options: ${x.options.join(" | ")}` : ""}${x.answer ? `
1198
+ answered by ${x.answeredBy}: ${x.answer}` : ""}`);
1199
+ break;
1200
+ }
1201
+ const [idRaw, ...words] = rest.filter((a) => !a.startsWith("--"));
1202
+ const id = Number(idRaw);
1203
+ const text = words.join(" ");
1204
+ if (!id || !text)
1205
+ throw new Error("usage: swarm answer <id> <text\u2026>");
1206
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
1207
+ method: "POST",
1208
+ headers: { "content-type": "application/json" },
1209
+ body: JSON.stringify({ text, by: process.env.USER ?? "cli" })
1210
+ }).then((x) => x.json());
1211
+ if (json)
1212
+ console.log(JSON.stringify(r));
1213
+ else if (r.ok)
1214
+ console.log(`answered #${id}`);
1215
+ else {
1216
+ console.error(`REFUSED: ${r.error}`);
1217
+ process.exit(1);
1218
+ }
1219
+ break;
1220
+ }
1221
+ case "dispatch": {
1222
+ await ensureDaemon({ quiet: true });
1223
+ const proj = await api("/v1/projects", {
1224
+ method: "POST",
1225
+ headers: { "content-type": "application/json" },
1226
+ body: JSON.stringify({ path: resolve3(".") })
1227
+ });
1228
+ const VALUE_FLAGS = [
1229
+ "--max",
1230
+ "--parallel",
1231
+ "--model",
1232
+ "--permission-mode",
1233
+ "--max-turns",
1234
+ "--profile"
1235
+ ];
1236
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
1237
+ const flag = (n) => {
1238
+ const i = rest.indexOf(n);
1239
+ return i >= 0 ? rest[i + 1] : undefined;
1240
+ };
1241
+ const num = (n) => {
1242
+ const v = flag(n);
1243
+ return v ? Number(v) : undefined;
1244
+ };
1245
+ const sub = positional[0];
1246
+ if (sub === "status" || !sub && !rest.includes("--ready")) {
1247
+ const d = await api(`/v1/dispatch?project=${proj.id}`);
1248
+ if (json)
1249
+ console.log(JSON.stringify(d));
1250
+ else if (!d.entries.length)
1251
+ console.log(`nothing dispatched (max_parallel ${d.config.max_parallel}); swarm dispatch --ready | <task\u2026>`);
1252
+ else
1253
+ for (const e of d.entries)
1254
+ console.log(`${(e.state === "finished" ? e.outcome ?? "?" : e.state).padEnd(13)} ${e.task.padEnd(10)} ${e.runId ? `run ${e.runId} ` : ""}${e.costUsd != null ? `$${e.costUsd.toFixed(2)} ` : ""}${e.detail ?? ""}`);
1255
+ break;
1256
+ }
1257
+ if (sub === "clear") {
1258
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1259
+ method: "DELETE",
1260
+ headers: { "content-type": "application/json" },
1261
+ body: JSON.stringify({ projectId: proj.id, task: positional[1] })
1262
+ }).then((x) => x.json());
1263
+ console.log(json ? JSON.stringify(r2) : `cleared ${r2.cleared}`);
1264
+ break;
1265
+ }
1266
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1267
+ method: "POST",
1268
+ headers: { "content-type": "application/json" },
1269
+ body: JSON.stringify({
1270
+ projectId: proj.id,
1271
+ ready: rest.includes("--ready"),
1272
+ tasks: positional,
1273
+ max: num("--max"),
1274
+ maxParallel: num("--parallel"),
1275
+ model: flag("--model"),
1276
+ permissionMode: flag("--permission-mode"),
1277
+ maxTurns: num("--max-turns"),
1278
+ profile: flag("--profile"),
1279
+ owner: process.env.USER ?? "cli"
1280
+ })
1281
+ }).then((x) => x.json());
1282
+ if (json)
1283
+ console.log(JSON.stringify(r));
1284
+ else if (!r.ok) {
1285
+ console.error(`REFUSED: ${r.error}`);
1286
+ process.exit(1);
1287
+ } else {
1288
+ for (const t of r.started)
1289
+ console.log(`started ${t}`);
1290
+ for (const t of r.queued)
1291
+ console.log(`queued ${t}`);
1292
+ for (const x of r.rejected)
1293
+ console.log(`rejected ${x.id} \u2014 ${x.reason}`);
1294
+ if (!r.started.length && !r.queued.length)
1295
+ console.log("nothing to dispatch");
1296
+ else
1297
+ console.log(`
1298
+ watch: swarm dispatch status \xB7 swarm run ls \xB7 the Board`);
1299
+ }
1300
+ break;
1301
+ }
1302
+ case "pr": {
1303
+ await ensureDaemon({ quiet: true });
1304
+ const sub = arg();
1305
+ const VALUE_FLAGS = ["--title", "--body"];
1306
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
1307
+ const flag = (n) => {
1308
+ const i = rest.indexOf(n);
1309
+ return i >= 0 ? rest[i + 1] : undefined;
1310
+ };
1311
+ const target = positional[1];
1312
+ if (sub !== "open" || !target)
1313
+ throw new Error("usage: swarm pr open <task|worktree> [--title t] [--body b] [--draft] [--dry-run]");
1314
+ const proj = await api("/v1/projects", {
1315
+ method: "POST",
1316
+ headers: { "content-type": "application/json" },
1317
+ body: JSON.stringify({ path: resolve3(".") })
1318
+ });
1319
+ if (rest.includes("--dry-run")) {
1320
+ const d = await api(`/v1/prs/draft?project=${proj.id}&worktree=${encodeURIComponent(target)}`);
1321
+ if (json)
1322
+ console.log(JSON.stringify(d));
1323
+ else if (!d.ok) {
1324
+ console.error(`REFUSED: ${d.error}`);
1325
+ process.exit(1);
1326
+ } else
1327
+ console.log(`${flag("--title") ?? d.title}
1328
+
1329
+ ${flag("--body") ?? d.body}`);
1330
+ break;
1331
+ }
1332
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
1333
+ method: "POST",
1334
+ headers: { "content-type": "application/json" },
1335
+ body: JSON.stringify({
1336
+ projectId: proj.id,
1337
+ worktree: target,
1338
+ title: flag("--title"),
1339
+ body: flag("--body"),
1340
+ draft: rest.includes("--draft")
1341
+ })
1342
+ }).then((x) => x.json());
1343
+ if (json)
1344
+ console.log(JSON.stringify(r));
1345
+ else if (r.ok)
1346
+ console.log(`opened ${r.url}`);
1347
+ else {
1348
+ console.error(`REFUSED: ${r.error}`);
1349
+ process.exit(1);
1350
+ }
1351
+ break;
1352
+ }
581
1353
  case "reap": {
582
1354
  await ensureDaemon({ quiet: true });
583
1355
  const r = await api("/v1/claims/reap", { method: "POST" });
@@ -618,7 +1390,8 @@ url: ${resolveBaseUrl()}`);
618
1390
  "--permission-mode",
619
1391
  "--allowed-tools",
620
1392
  "--max-turns",
621
- "--owner"
1393
+ "--owner",
1394
+ "--profile"
622
1395
  ]);
623
1396
  const positionals = [];
624
1397
  for (let i = 0;i < rest.length; i++) {
@@ -648,11 +1421,11 @@ url: ${resolveBaseUrl()}`);
648
1421
  const target = positionals[1];
649
1422
  if (!target)
650
1423
  throw new Error(`usage: swarm run ${sub} <task|id>${sub === "send" ? ' "text"' : ""}`);
651
- const r2 = sub === "send" ? await fetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
1424
+ const r2 = sub === "send" ? await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
652
1425
  method: "POST",
653
1426
  headers: { "content-type": "application/json" },
654
1427
  body: JSON.stringify({ text: positionals.slice(2).join(" ") })
655
- }) : await fetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
1428
+ }) : await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
656
1429
  const j = await r2.json();
657
1430
  if (json)
658
1431
  console.log(JSON.stringify(j));
@@ -673,8 +1446,8 @@ url: ${resolveBaseUrl()}`);
673
1446
  if (!prompt && pf)
674
1447
  prompt = await Bun.file(resolve3(pf)).text();
675
1448
  if (!task || !prompt)
676
- throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--allowed-tools a,b] [--max-turns n]');
677
- const r = await fetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
1449
+ throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--profile p] [--allowed-tools a,b] [--max-turns n]');
1450
+ const r = await authedFetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
678
1451
  method: "POST",
679
1452
  headers: { "content-type": "application/json" },
680
1453
  body: JSON.stringify({
@@ -685,7 +1458,8 @@ url: ${resolveBaseUrl()}`);
685
1458
  model: flag("--model"),
686
1459
  permissionMode: flag("--permission-mode"),
687
1460
  allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
688
- maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
1461
+ maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined,
1462
+ profile: flag("--profile")
689
1463
  })
690
1464
  }).then((x) => x.json());
691
1465
  if (json)
@@ -714,7 +1488,7 @@ url: ${resolveBaseUrl()}`);
714
1488
  body: JSON.stringify({ path: resolve3(".") })
715
1489
  });
716
1490
  if (cmd === "resume") {
717
- const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
1491
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
718
1492
  const j = await r2.json();
719
1493
  if (json)
720
1494
  console.log(JSON.stringify(j.handoff));
@@ -726,7 +1500,7 @@ url: ${resolveBaseUrl()}`);
726
1500
  const i = rest.indexOf(n);
727
1501
  return i >= 0 ? rest[i + 1] : undefined;
728
1502
  };
729
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
1503
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
730
1504
  method: "POST",
731
1505
  headers: { "content-type": "application/json" },
732
1506
  body: JSON.stringify({
@@ -775,7 +1549,7 @@ url: ${resolveBaseUrl()}`);
775
1549
  const [, task, gate, verdict] = positionals;
776
1550
  if (!task || !gate || !verdict)
777
1551
  throw new Error('usage: swarm gate record <task> <gate> pass|fail --rubric "what was checked" [--evidence "\u2026"]');
778
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates`, {
1552
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates`, {
779
1553
  method: "POST",
780
1554
  headers: { "content-type": "application/json" },
781
1555
  body: JSON.stringify({
@@ -798,6 +1572,34 @@ url: ${resolveBaseUrl()}`);
798
1572
  }
799
1573
  break;
800
1574
  }
1575
+ if (sub === "run") {
1576
+ const [, task, ...gates2] = positionals;
1577
+ if (!task)
1578
+ throw new Error("usage: swarm gate run <task> [gate\u2026] (default: the required gates that have a cmd)");
1579
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
1580
+ method: "POST",
1581
+ headers: { "content-type": "application/json" },
1582
+ body: JSON.stringify({
1583
+ projectId: proj.id,
1584
+ task,
1585
+ gates: gates2,
1586
+ sessionId: process.env.CLAUDE_SESSION_ID ?? null
1587
+ })
1588
+ }).then((x) => x.json());
1589
+ if (json)
1590
+ console.log(JSON.stringify(r));
1591
+ else {
1592
+ for (const x of r.runs ?? [])
1593
+ console.log(`${x.verdict === "pass" ? "\u2713" : "\u2717"} ${x.gate.padEnd(12)} ${x.rubric}`);
1594
+ for (const x of r.skipped ?? [])
1595
+ console.log(`\u2013 ${x.gate.padEnd(12)} skipped: ${x.reason}`);
1596
+ if (r.error && !r.started?.length)
1597
+ console.error(`REFUSED: ${r.error}`);
1598
+ }
1599
+ if (!r.ok)
1600
+ process.exit(1);
1601
+ break;
1602
+ }
801
1603
  if (sub === "ls") {
802
1604
  const task = positionals[1];
803
1605
  const q = new URLSearchParams({ project: proj.id });
@@ -858,6 +1660,36 @@ url: ${resolveBaseUrl()}`);
858
1660
  session ${h.sessionId}` : ""}`);
859
1661
  break;
860
1662
  }
1663
+ case "audit": {
1664
+ if (rest[0] !== "export")
1665
+ throw new Error("usage: swarm audit export [--since 30d] [-p] [--type t] [--format jsonl|csv|json] [--limit n]");
1666
+ await ensureDaemon({ quiet: true });
1667
+ const q = new URLSearchParams;
1668
+ const val = (n) => {
1669
+ const i = rest.indexOf(n);
1670
+ return i >= 0 ? rest[i + 1] : undefined;
1671
+ };
1672
+ if (val("--since"))
1673
+ q.set("since", val("--since"));
1674
+ if (val("--type"))
1675
+ q.set("type", val("--type"));
1676
+ if (val("--limit"))
1677
+ q.set("limit", val("--limit"));
1678
+ q.set("format", val("--format") ?? "jsonl");
1679
+ if (rest.includes("-p")) {
1680
+ const proj = await api("/v1/projects", {
1681
+ method: "POST",
1682
+ headers: { "content-type": "application/json" },
1683
+ body: JSON.stringify({ path: resolve3(".") })
1684
+ });
1685
+ q.set("project", proj.id);
1686
+ }
1687
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/audit?${q}`);
1688
+ if (!r.ok)
1689
+ throw new Error((await r.json()).error ?? `audit: ${r.status}`);
1690
+ process.stdout.write(await r.text());
1691
+ break;
1692
+ }
861
1693
  case "rules": {
862
1694
  if (rest[0] !== "dryrun")
863
1695
  throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
@@ -1088,7 +1920,7 @@ would have fired (newest last):`);
1088
1920
  headers: { "content-type": "application/json" },
1089
1921
  body: JSON.stringify({ path: resolve3(".") })
1090
1922
  });
1091
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources`, {
1923
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources`, {
1092
1924
  method: "POST",
1093
1925
  headers: { "content-type": "application/json" },
1094
1926
  body: JSON.stringify({
@@ -1124,7 +1956,7 @@ would have fired (newest last):`);
1124
1956
  });
1125
1957
  if (rest.includes("--force"))
1126
1958
  q.set("force", "1");
1127
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
1959
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
1128
1960
  if (json)
1129
1961
  console.log(JSON.stringify(r));
1130
1962
  else if (r.ok)
@@ -1142,7 +1974,7 @@ would have fired (newest last):`);
1142
1974
  const base = new SwarmClient().baseUrl;
1143
1975
  const pIdx = rest.indexOf("--session");
1144
1976
  const wantSession = pIdx >= 0 ? rest[pIdx + 1] : undefined;
1145
- const res = await fetch(`${base}/v1/events?since=0`);
1977
+ const res = await authedFetch(`${base}/v1/events?since=0`);
1146
1978
  const reader = res.body?.getReader();
1147
1979
  if (!reader)
1148
1980
  throw new Error("no stream");
@@ -1174,7 +2006,8 @@ would have fired (newest last):`);
1174
2006
  }
1175
2007
  case "ui": {
1176
2008
  const base = await ensureDaemon();
1177
- Bun.spawn(["open", base]).unref?.();
2009
+ const tok = readToken();
2010
+ Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
1178
2011
  console.log(base);
1179
2012
  break;
1180
2013
  }