@ra3orblade/swarm 0.5.0 → 0.7.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
@@ -144,9 +144,13 @@ var HOOK_EVENTS = [
144
144
  "Notification",
145
145
  "PreCompact"
146
146
  ];
147
+ // packages/core/src/budget.ts
148
+ var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
147
149
  // packages/core/src/rules.ts
148
150
  var LIVE_WINDOW_MS = 10 * 60000;
149
151
  var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
152
+ // packages/core/src/ledger.ts
153
+ var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
150
154
  // packages/cli/src/install.ts
151
155
  var MARK = "swarm-hook";
152
156
  var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
@@ -190,6 +194,95 @@ function mcpRegistered() {
190
194
  const c = loadClaudeJson();
191
195
  return Boolean(c.mcpServers?.swarm);
192
196
  }
197
+ var codexConfigPath = () => process.env.CODEX_CONFIG ?? join2(homedir2(), ".codex", "config.toml");
198
+ var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join2(homedir2(), ".gemini", "settings.json");
199
+ function codexBlock() {
200
+ const { command, args } = mcpServerConfig();
201
+ return `[mcp_servers.swarm]
202
+ command = ${JSON.stringify(command)}
203
+ args = ${JSON.stringify(args)}
204
+ `;
205
+ }
206
+ var CODEX_BLOCK_RE = /\[mcp_servers\.swarm\]\n(?:(?!\[)[^\n]*\n?)*/;
207
+ function registerCodex() {
208
+ const p = codexConfigPath();
209
+ if (!existsSync3(join2(p, "..")))
210
+ return false;
211
+ const cur = existsSync3(p) ? readFileSync2(p, "utf8") : "";
212
+ const next = CODEX_BLOCK_RE.test(cur) ? cur.replace(CODEX_BLOCK_RE, codexBlock()) : `${cur.trimEnd()}${cur.trim() ? `
213
+
214
+ ` : ""}${codexBlock()}`;
215
+ if (next !== cur)
216
+ writeFileSync2(p, next);
217
+ return true;
218
+ }
219
+ function unregisterCodex() {
220
+ const p = codexConfigPath();
221
+ if (!existsSync3(p))
222
+ return false;
223
+ const cur = readFileSync2(p, "utf8");
224
+ if (!CODEX_BLOCK_RE.test(cur))
225
+ return false;
226
+ writeFileSync2(p, cur.replace(CODEX_BLOCK_RE, "").replace(/\n{3,}/g, `
227
+
228
+ `).trimEnd().concat(`
229
+ `));
230
+ return true;
231
+ }
232
+ function registerGemini() {
233
+ const p = geminiSettingsPath();
234
+ if (!existsSync3(join2(p, "..")))
235
+ return false;
236
+ let c = {};
237
+ try {
238
+ c = existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
239
+ } catch {
240
+ return false;
241
+ }
242
+ const mcp = c.mcpServers ?? {};
243
+ mcp.swarm = mcpServerConfig();
244
+ c.mcpServers = mcp;
245
+ writeFileSync2(p, `${JSON.stringify(c, null, 2)}
246
+ `);
247
+ return true;
248
+ }
249
+ function unregisterGemini() {
250
+ const p = geminiSettingsPath();
251
+ if (!existsSync3(p))
252
+ return false;
253
+ try {
254
+ const c = JSON.parse(readFileSync2(p, "utf8"));
255
+ const mcp = c.mcpServers ?? {};
256
+ if (!mcp.swarm)
257
+ return false;
258
+ delete mcp.swarm;
259
+ if (Object.keys(mcp).length)
260
+ c.mcpServers = mcp;
261
+ else
262
+ delete c.mcpServers;
263
+ writeFileSync2(p, `${JSON.stringify(c, null, 2)}
264
+ `);
265
+ return true;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ function registerOtherAgents() {
271
+ const out = [];
272
+ if (registerCodex())
273
+ out.push("codex");
274
+ if (registerGemini())
275
+ out.push("gemini");
276
+ return out;
277
+ }
278
+ function unregisterOtherAgents() {
279
+ const out = [];
280
+ if (unregisterCodex())
281
+ out.push("codex");
282
+ if (unregisterGemini())
283
+ out.push("gemini");
284
+ return out;
285
+ }
193
286
  var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
194
287
  var shimPath = () => resolveBin("swarm-hook").at(-1);
195
288
  function mcpServerConfig() {
@@ -228,6 +321,7 @@ function install() {
228
321
  }
229
322
  save(s);
230
323
  registerMcp();
324
+ registerOtherAgents();
231
325
  return added;
232
326
  }
233
327
  function uninstall() {
@@ -265,13 +359,23 @@ function uninstall() {
265
359
  save(s);
266
360
  if (unregisterMcp())
267
361
  removed++;
362
+ removed += unregisterOtherAgents().length;
268
363
  return removed;
269
364
  }
270
365
  function status() {
271
366
  const s = load();
272
367
  const hooks2 = s.hooks ?? {};
273
368
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
274
- return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath() };
369
+ const otherAgents = [];
370
+ const cp = codexConfigPath();
371
+ if (existsSync3(cp) && CODEX_BLOCK_RE.test(readFileSync2(cp, "utf8")))
372
+ otherAgents.push("codex");
373
+ const gp = geminiSettingsPath();
374
+ try {
375
+ if (existsSync3(gp) && JSON.parse(readFileSync2(gp, "utf8")).mcpServers?.swarm)
376
+ otherAgents.push("gemini");
377
+ } catch {}
378
+ return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath(), otherAgents };
275
379
  }
276
380
 
277
381
  // packages/cli/src/procs.ts
@@ -389,14 +493,20 @@ var help = `swarm \u2014 control plane for AI-agent development
389
493
  tail [--project p] [--session id] follow the live event stream
390
494
 
391
495
  claim <task> [--owner n] claim a task in a fresh isolated git worktree (fail-closed)
496
+ gate run <task> [gate\u2026] execute the repo's [gates.<name>] cmd gates in the task's worktree and record them
497
+ wt [ls|create|open|diff|rm|gc] first-class worktrees: create task-less ones, open, diff, remove, collect stale
498
+ pr open <task|worktree> push the branch and open a PR/MR prefilled from the task, handoff, gates and files
499
+ questions [--all] questions agents are waiting on a human for (this repo); answer <id> <text\u2026>
500
+ dispatch --ready | <task\u2026> claim + spawn a run per task, [dispatch] max_parallel at a time; status | clear
392
501
  renew <task> extend the lease; release <task> [--force] release + remove worktree
393
502
  claims list claims; reap release abandoned claims (keeps ones holding work)
394
503
  tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
395
504
  gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
396
505
  gate ls [task] latest verdict per gate (and the run history for one task)
397
- run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--allowed-tools a,b] [--max-turns n]
506
+ 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]
398
507
  claim the task and spawn claude -p in its worktree; the session shows in Fleet
399
508
  run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
509
+ run resume <session-id> [--model m] [--permission-mode m] spawn a run that picks up where a dead session stopped (its handoff + tail)
400
510
  handoff <task> --done "\u2026" --remaining "\u2026" [--files a,b] [--verify "\u2026"] leave notes for the next holder
401
511
  resume <task> print the latest handoff (the next session gets it automatically on start)
402
512
  res ls | acquire <name> [--owner n] [--pid n] [--port n] | release <name> [--force]
@@ -406,6 +516,8 @@ var help = `swarm \u2014 control plane for AI-agent development
406
516
  serve ls | stop [name] list / stop servers this project started (by pid, never by pattern)
407
517
  proc start [--name n] -- <cmd> | ls | stop <name|pid> same, for workers without a port
408
518
  stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
519
+ search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
520
+ rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
409
521
 
410
522
  install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
411
523
 
@@ -442,7 +554,7 @@ try {
442
554
  const base = await ensureDaemon();
443
555
  const evs = install();
444
556
  console.log(`\u2713 daemon running at ${base}`);
445
- console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})`);
557
+ console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})${status().otherAgents.length ? ` \xB7 MCP also for ${status().otherAgents.join(", ")}` : ""}`);
446
558
  console.log("\u2713 any Claude session you start now will appear in Swarm");
447
559
  Bun.spawn(["open", base]).unref?.();
448
560
  console.log(`
@@ -487,6 +599,8 @@ Open the dashboard: ${base}`);
487
599
  line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
488
600
  line(st.installed, "hooks installed", "run: swarm install");
489
601
  line(st.mcp, "MCP server registered", "run: swarm install");
602
+ if (st.otherAgents.length)
603
+ console.log(`\u2713 MCP server also registered for ${st.otherAgents.join(", ")} (swarm_* tools in those CLIs too)`);
490
604
  const forge2 = (bin, auth) => {
491
605
  const path = Bun.which(bin);
492
606
  if (!path)
@@ -540,7 +654,8 @@ url: ${resolveBaseUrl()}`);
540
654
  console.log(JSON.stringify(r));
541
655
  else if (r.ok)
542
656
  console.log(`claimed ${task} \u2192 ${r.worktree}
543
- cd ${r.worktree}`);
657
+ cd ${r.worktree}${r.bootstrap ? `
658
+ bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
544
659
  else {
545
660
  console.error(`REFUSED: ${r.error}`);
546
661
  process.exit(1);
@@ -573,6 +688,317 @@ url: ${resolveBaseUrl()}`);
573
688
  }
574
689
  break;
575
690
  }
691
+ case "wt": {
692
+ await ensureDaemon({ quiet: true });
693
+ const sub = arg();
694
+ const proj = await api("/v1/projects", {
695
+ method: "POST",
696
+ headers: { "content-type": "application/json" },
697
+ body: JSON.stringify({ path: resolve3(".") })
698
+ });
699
+ const post2 = (path, body) => fetch(`${new SwarmClient().baseUrl}${path}`, {
700
+ method: "POST",
701
+ headers: { "content-type": "application/json" },
702
+ body: JSON.stringify({ projectId: proj.id, ...body })
703
+ }).then((x) => x.json());
704
+ const refuse = (r) => {
705
+ if (json)
706
+ console.log(JSON.stringify(r));
707
+ else if (!r.ok) {
708
+ console.error(`REFUSED: ${r.error}`);
709
+ process.exit(1);
710
+ }
711
+ return !r.ok;
712
+ };
713
+ const flag = (n) => {
714
+ const i = rest.indexOf(n);
715
+ return i >= 0 ? rest[i + 1] : undefined;
716
+ };
717
+ const VALUE_FLAGS = ["--base", "--branch"];
718
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
719
+ const target = positional[1];
720
+ switch (sub) {
721
+ case "create": {
722
+ if (!target)
723
+ throw new Error("usage: swarm wt create <name> [--base ref] [--branch name]");
724
+ const r = await post2("/v1/worktrees", {
725
+ name: target,
726
+ baseRef: flag("--base"),
727
+ branch: flag("--branch")
728
+ });
729
+ if (refuse(r) || json)
730
+ break;
731
+ console.log(`created ${r.name} \u2192 ${r.worktree} (branch ${r.branch})
732
+ cd ${r.worktree}${r.bootstrap ? `
733
+ bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
734
+ break;
735
+ }
736
+ case "ls":
737
+ case undefined: {
738
+ const wts = await api(`/v1/worktrees?project=${proj.id}`);
739
+ if (json)
740
+ console.log(JSON.stringify(wts));
741
+ else {
742
+ for (const w of wts) {
743
+ const st = w.main ? "main" : [
744
+ w.dirty > 0 ? `${w.dirty} dirty` : "",
745
+ w.ahead > 0 ? `${w.ahead} unpushed` : "",
746
+ w.behind > 0 ? `${w.behind} behind` : "",
747
+ w.merged ? "merged" : ""
748
+ ].filter(Boolean).join(", ") || "clean";
749
+ console.log(`${(w.branch ?? "(detached)").padEnd(32)} ${w.head} ${st.padEnd(24)} ${w.path}`);
750
+ }
751
+ if (!wts.length)
752
+ console.log("no worktrees");
753
+ }
754
+ break;
755
+ }
756
+ case "open": {
757
+ if (!target)
758
+ throw new Error("usage: swarm wt open <name|path>");
759
+ const r = await post2("/v1/worktrees/open", { worktree: target });
760
+ if (refuse(r) || json)
761
+ break;
762
+ console.log(`opened ${r.worktree}`);
763
+ break;
764
+ }
765
+ case "rm": {
766
+ if (!target)
767
+ throw new Error("usage: swarm wt rm <name|path> [--force]");
768
+ const r = await post2("/v1/worktrees/remove", {
769
+ worktree: target,
770
+ force: rest.includes("--force")
771
+ });
772
+ if (refuse(r) || json)
773
+ break;
774
+ console.log(`removed ${r.worktree}`);
775
+ break;
776
+ }
777
+ case "diff": {
778
+ if (!target)
779
+ throw new Error("usage: swarm wt diff <name|path|task> [--file f] [--patch]");
780
+ const q = new URLSearchParams({ project: proj.id, worktree: target });
781
+ const file = flag("--file");
782
+ if (file)
783
+ q.set("file", file);
784
+ if (rest.includes("--patch"))
785
+ q.set("patch", "1");
786
+ const d = await api(`/v1/worktrees/diff?${q}`);
787
+ if (d.error) {
788
+ console.error(`REFUSED: ${d.error}`);
789
+ process.exit(1);
790
+ }
791
+ if (json)
792
+ console.log(JSON.stringify(d));
793
+ else if (d.patch !== undefined)
794
+ console.log(d.patch);
795
+ else {
796
+ 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" : ""}`);
797
+ for (const c of d.commits)
798
+ console.log(` ${c}`);
799
+ for (const f of d.files)
800
+ console.log(`${f.status} ${f.added >= 0 ? `+${f.added}`.padStart(6) : " bin"} ${f.deleted >= 0 ? `-${f.deleted}`.padStart(6) : " "} ${f.path}`);
801
+ }
802
+ break;
803
+ }
804
+ case "gc": {
805
+ const r = await post2("/v1/worktrees/gc", { apply: rest.includes("--apply") });
806
+ if (json)
807
+ console.log(JSON.stringify(r));
808
+ else if (!r.candidates.length)
809
+ console.log("nothing to collect");
810
+ else {
811
+ for (const x of r.candidates)
812
+ console.log(`${r.removed.includes(x.path) ? "removed " : x.removable ? "removable" : `blocked (${x.blocker})`} ${x.why.padEnd(15)} ${x.branch ?? "(detached)"} ${x.path}`);
813
+ if (!rest.includes("--apply") && r.candidates.some((x) => x.removable))
814
+ console.log("\nrun `swarm wt gc --apply` to remove the removable ones");
815
+ }
816
+ break;
817
+ }
818
+ default:
819
+ throw new Error("usage: swarm wt [ls] | create <name> | open <ref> | rm <ref> [--force] | gc [--apply]");
820
+ }
821
+ break;
822
+ }
823
+ case "questions":
824
+ case "answer": {
825
+ await ensureDaemon({ quiet: true });
826
+ if (cmd === "questions") {
827
+ const q = new URLSearchParams;
828
+ if (!rest.includes("--all"))
829
+ q.set("open", "1");
830
+ if (!rest.includes("--everywhere")) {
831
+ const proj = await api("/v1/projects", {
832
+ method: "POST",
833
+ headers: { "content-type": "application/json" },
834
+ body: JSON.stringify({ path: resolve3(".") })
835
+ });
836
+ q.set("project", proj.id);
837
+ }
838
+ const qs = await api(`/v1/questions?${q}`);
839
+ if (json)
840
+ console.log(JSON.stringify(qs));
841
+ else if (!qs.length)
842
+ console.log("no open questions");
843
+ else
844
+ for (const x of qs)
845
+ console.log(`#${x.id} ${x.createdAt.slice(0, 16).replace("T", " ")} ${x.task ? `[${x.task}] ` : ""}${x.text}${x.options.length ? `
846
+ options: ${x.options.join(" | ")}` : ""}${x.answer ? `
847
+ answered by ${x.answeredBy}: ${x.answer}` : ""}`);
848
+ break;
849
+ }
850
+ const [idRaw, ...words] = rest.filter((a) => !a.startsWith("--"));
851
+ const id = Number(idRaw);
852
+ const text = words.join(" ");
853
+ if (!id || !text)
854
+ throw new Error("usage: swarm answer <id> <text\u2026>");
855
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
856
+ method: "POST",
857
+ headers: { "content-type": "application/json" },
858
+ body: JSON.stringify({ text, by: process.env.USER ?? "cli" })
859
+ }).then((x) => x.json());
860
+ if (json)
861
+ console.log(JSON.stringify(r));
862
+ else if (r.ok)
863
+ console.log(`answered #${id}`);
864
+ else {
865
+ console.error(`REFUSED: ${r.error}`);
866
+ process.exit(1);
867
+ }
868
+ break;
869
+ }
870
+ case "dispatch": {
871
+ await ensureDaemon({ quiet: true });
872
+ const proj = await api("/v1/projects", {
873
+ method: "POST",
874
+ headers: { "content-type": "application/json" },
875
+ body: JSON.stringify({ path: resolve3(".") })
876
+ });
877
+ const VALUE_FLAGS = [
878
+ "--max",
879
+ "--parallel",
880
+ "--model",
881
+ "--permission-mode",
882
+ "--max-turns",
883
+ "--profile"
884
+ ];
885
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
886
+ const flag = (n) => {
887
+ const i = rest.indexOf(n);
888
+ return i >= 0 ? rest[i + 1] : undefined;
889
+ };
890
+ const num = (n) => {
891
+ const v = flag(n);
892
+ return v ? Number(v) : undefined;
893
+ };
894
+ const sub = positional[0];
895
+ if (sub === "status" || !sub && !rest.includes("--ready")) {
896
+ const d = await api(`/v1/dispatch?project=${proj.id}`);
897
+ if (json)
898
+ console.log(JSON.stringify(d));
899
+ else if (!d.entries.length)
900
+ console.log(`nothing dispatched (max_parallel ${d.config.max_parallel}); swarm dispatch --ready | <task\u2026>`);
901
+ else
902
+ for (const e of d.entries)
903
+ 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 ?? ""}`);
904
+ break;
905
+ }
906
+ if (sub === "clear") {
907
+ const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
908
+ method: "DELETE",
909
+ headers: { "content-type": "application/json" },
910
+ body: JSON.stringify({ projectId: proj.id, task: positional[1] })
911
+ }).then((x) => x.json());
912
+ console.log(json ? JSON.stringify(r2) : `cleared ${r2.cleared}`);
913
+ break;
914
+ }
915
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
916
+ method: "POST",
917
+ headers: { "content-type": "application/json" },
918
+ body: JSON.stringify({
919
+ projectId: proj.id,
920
+ ready: rest.includes("--ready"),
921
+ tasks: positional,
922
+ max: num("--max"),
923
+ maxParallel: num("--parallel"),
924
+ model: flag("--model"),
925
+ permissionMode: flag("--permission-mode"),
926
+ maxTurns: num("--max-turns"),
927
+ profile: flag("--profile"),
928
+ owner: process.env.USER ?? "cli"
929
+ })
930
+ }).then((x) => x.json());
931
+ if (json)
932
+ console.log(JSON.stringify(r));
933
+ else if (!r.ok) {
934
+ console.error(`REFUSED: ${r.error}`);
935
+ process.exit(1);
936
+ } else {
937
+ for (const t of r.started)
938
+ console.log(`started ${t}`);
939
+ for (const t of r.queued)
940
+ console.log(`queued ${t}`);
941
+ for (const x of r.rejected)
942
+ console.log(`rejected ${x.id} \u2014 ${x.reason}`);
943
+ if (!r.started.length && !r.queued.length)
944
+ console.log("nothing to dispatch");
945
+ else
946
+ console.log(`
947
+ watch: swarm dispatch status \xB7 swarm run ls \xB7 the Board`);
948
+ }
949
+ break;
950
+ }
951
+ case "pr": {
952
+ await ensureDaemon({ quiet: true });
953
+ const sub = arg();
954
+ const VALUE_FLAGS = ["--title", "--body"];
955
+ const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
956
+ const flag = (n) => {
957
+ const i = rest.indexOf(n);
958
+ return i >= 0 ? rest[i + 1] : undefined;
959
+ };
960
+ const target = positional[1];
961
+ if (sub !== "open" || !target)
962
+ throw new Error("usage: swarm pr open <task|worktree> [--title t] [--body b] [--draft] [--dry-run]");
963
+ const proj = await api("/v1/projects", {
964
+ method: "POST",
965
+ headers: { "content-type": "application/json" },
966
+ body: JSON.stringify({ path: resolve3(".") })
967
+ });
968
+ if (rest.includes("--dry-run")) {
969
+ const d = await api(`/v1/prs/draft?project=${proj.id}&worktree=${encodeURIComponent(target)}`);
970
+ if (json)
971
+ console.log(JSON.stringify(d));
972
+ else if (!d.ok) {
973
+ console.error(`REFUSED: ${d.error}`);
974
+ process.exit(1);
975
+ } else
976
+ console.log(`${flag("--title") ?? d.title}
977
+
978
+ ${flag("--body") ?? d.body}`);
979
+ break;
980
+ }
981
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
982
+ method: "POST",
983
+ headers: { "content-type": "application/json" },
984
+ body: JSON.stringify({
985
+ projectId: proj.id,
986
+ worktree: target,
987
+ title: flag("--title"),
988
+ body: flag("--body"),
989
+ draft: rest.includes("--draft")
990
+ })
991
+ }).then((x) => x.json());
992
+ if (json)
993
+ console.log(JSON.stringify(r));
994
+ else if (r.ok)
995
+ console.log(`opened ${r.url}`);
996
+ else {
997
+ console.error(`REFUSED: ${r.error}`);
998
+ process.exit(1);
999
+ }
1000
+ break;
1001
+ }
576
1002
  case "reap": {
577
1003
  await ensureDaemon({ quiet: true });
578
1004
  const r = await api("/v1/claims/reap", { method: "POST" });
@@ -613,7 +1039,8 @@ url: ${resolveBaseUrl()}`);
613
1039
  "--permission-mode",
614
1040
  "--allowed-tools",
615
1041
  "--max-turns",
616
- "--owner"
1042
+ "--owner",
1043
+ "--profile"
617
1044
  ]);
618
1045
  const positionals = [];
619
1046
  for (let i = 0;i < rest.length; i++) {
@@ -659,35 +1086,39 @@ url: ${resolveBaseUrl()}`);
659
1086
  }
660
1087
  break;
661
1088
  }
662
- const task = flag("--task") ?? sub;
663
- let prompt = flag("--prompt");
1089
+ const resumeFrom = sub === "resume" ? positionals[1] : undefined;
1090
+ if (sub === "resume" && !resumeFrom)
1091
+ throw new Error("usage: swarm run resume <session-id>");
1092
+ const task = resumeFrom ? "(resumed)" : flag("--task") ?? sub;
1093
+ let prompt = resumeFrom ? "(from handoff)" : flag("--prompt");
664
1094
  const pf = flag("--prompt-file");
665
1095
  if (!prompt && pf)
666
1096
  prompt = await Bun.file(resolve3(pf)).text();
667
1097
  if (!task || !prompt)
668
- throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--allowed-tools a,b] [--max-turns n]');
669
- const r = await fetch(`${base}/v1/runs`, {
1098
+ 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]');
1099
+ const r = await fetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
670
1100
  method: "POST",
671
1101
  headers: { "content-type": "application/json" },
672
1102
  body: JSON.stringify({
673
1103
  projectId: proj.id,
674
1104
  task,
675
1105
  prompt,
676
- owner: flag("--owner") ?? process.env.USER ?? "me",
1106
+ owner: flag("--owner") ?? (resumeFrom ? undefined : process.env.USER ?? "me"),
677
1107
  model: flag("--model"),
678
1108
  permissionMode: flag("--permission-mode"),
679
1109
  allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
680
- maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
1110
+ maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined,
1111
+ profile: flag("--profile")
681
1112
  })
682
1113
  }).then((x) => x.json());
683
1114
  if (json)
684
1115
  console.log(JSON.stringify(r));
685
1116
  else if (r.ok && r.run)
686
- console.log(`run ${r.run.id} on ${task} (pid ${r.run.pid})
1117
+ console.log(`run ${r.run.id} on ${r.run.task} (pid ${r.run.pid})
687
1118
  worktree: ${r.run.worktree}
688
1119
  session: ${r.run.sessionId}
689
1120
  log: ${r.run.log}
690
- steer: swarm run send ${task} "\u2026" stop: swarm run stop ${task} watch: swarm tail --session ${r.run.sessionId}`);
1121
+ steer: swarm run send ${r.run.task} "\u2026" stop: swarm run stop ${r.run.task} watch: swarm tail --session ${r.run.sessionId}`);
691
1122
  else {
692
1123
  console.error(`REFUSED: ${r.error}`);
693
1124
  process.exit(1);
@@ -790,6 +1221,34 @@ url: ${resolveBaseUrl()}`);
790
1221
  }
791
1222
  break;
792
1223
  }
1224
+ if (sub === "run") {
1225
+ const [, task, ...gates2] = positionals;
1226
+ if (!task)
1227
+ throw new Error("usage: swarm gate run <task> [gate\u2026] (default: the required gates that have a cmd)");
1228
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
1229
+ method: "POST",
1230
+ headers: { "content-type": "application/json" },
1231
+ body: JSON.stringify({
1232
+ projectId: proj.id,
1233
+ task,
1234
+ gates: gates2,
1235
+ sessionId: process.env.CLAUDE_SESSION_ID ?? null
1236
+ })
1237
+ }).then((x) => x.json());
1238
+ if (json)
1239
+ console.log(JSON.stringify(r));
1240
+ else {
1241
+ for (const x of r.runs ?? [])
1242
+ console.log(`${x.verdict === "pass" ? "\u2713" : "\u2717"} ${x.gate.padEnd(12)} ${x.rubric}`);
1243
+ for (const x of r.skipped ?? [])
1244
+ console.log(`\u2013 ${x.gate.padEnd(12)} skipped: ${x.reason}`);
1245
+ if (r.error && !r.started?.length)
1246
+ console.error(`REFUSED: ${r.error}`);
1247
+ }
1248
+ if (!r.ok)
1249
+ process.exit(1);
1250
+ break;
1251
+ }
793
1252
  if (sub === "ls") {
794
1253
  const task = positionals[1];
795
1254
  const q = new URLSearchParams({ project: proj.id });
@@ -817,6 +1276,82 @@ url: ${resolveBaseUrl()}`);
817
1276
  }
818
1277
  throw new Error("usage: swarm gate record|ls");
819
1278
  }
1279
+ case "search": {
1280
+ await ensureDaemon({ quiet: true });
1281
+ const q = new URLSearchParams({ limit: "30" });
1282
+ const words = [];
1283
+ for (let i = 0;i < rest.length; i++) {
1284
+ const a = rest[i];
1285
+ if (a === "--kind")
1286
+ q.set("kind", rest[++i] ?? "");
1287
+ else if (a === "-p") {
1288
+ const proj = await api("/v1/projects", {
1289
+ method: "POST",
1290
+ headers: { "content-type": "application/json" },
1291
+ body: JSON.stringify({ path: resolve3(".") })
1292
+ });
1293
+ q.set("project", proj.id);
1294
+ } else if (!a.startsWith("--"))
1295
+ words.push(a);
1296
+ }
1297
+ if (!words.length)
1298
+ throw new Error("usage: swarm search <query\u2026> [-p] [--kind k]");
1299
+ q.set("q", words.join(" "));
1300
+ const r = await api(`/v1/memory?${q}`);
1301
+ if (json)
1302
+ console.log(JSON.stringify(r.hits));
1303
+ else if (!r.hits.length)
1304
+ console.log("nothing in memory matches");
1305
+ else
1306
+ for (const h of r.hits)
1307
+ console.log(`${h.kind.padEnd(8)} ${h.ts.slice(0, 16).replace("T", " ")} ${h.title}${h.task ? ` [${h.task}]` : ""}
1308
+ ${h.snippet.split("\x01").join("").split("\x02").join("").replace(/\s+/g, " ")}${h.sessionId ? `
1309
+ session ${h.sessionId}` : ""}`);
1310
+ break;
1311
+ }
1312
+ case "rules": {
1313
+ if (rest[0] !== "dryrun")
1314
+ throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
1315
+ await ensureDaemon({ quiet: true });
1316
+ const proj = await api("/v1/projects", {
1317
+ method: "POST",
1318
+ headers: { "content-type": "application/json" },
1319
+ body: JSON.stringify({ path: resolve3(".") })
1320
+ });
1321
+ const q = new URLSearchParams({ project: proj.id });
1322
+ const si = rest.indexOf("--set");
1323
+ if (si >= 0)
1324
+ for (const kv of (rest[si + 1] ?? "").split(",")) {
1325
+ const [k, v] = kv.split("=");
1326
+ if (k && v)
1327
+ q.set(k.trim(), v.trim());
1328
+ }
1329
+ const li = rest.indexOf("--limit");
1330
+ if (li >= 0)
1331
+ q.set("limit", rest[li + 1] ?? "");
1332
+ const r = await api(`/v1/rules/dryrun?${q}`);
1333
+ if (json) {
1334
+ console.log(JSON.stringify(r));
1335
+ break;
1336
+ }
1337
+ console.log(`dry-run over ${r.evaluated} of ${r.calls} recorded calls (nothing recorded)`);
1338
+ for (const [rule, n] of Object.entries(r.byRule))
1339
+ console.log(` ${rule.padEnd(26)} ${String(r.modes[rule]).padEnd(5)} ask ${String(n.ask).padStart(4)} deny ${String(n.deny).padStart(4)}`);
1340
+ if (r.flaky.length) {
1341
+ console.log(`
1342
+ flaky signals:`);
1343
+ for (const f of r.flaky)
1344
+ console.log(` ${f.display}
1345
+ ${f.suggestion}`);
1346
+ }
1347
+ if (r.hits.length) {
1348
+ console.log(`
1349
+ would have fired (newest last):`);
1350
+ for (const h of r.hits.slice(-20))
1351
+ console.log(` ${h.ts.slice(11, 19)} ${h.action.padEnd(4)} ${h.rule.padEnd(20)} ${h.display}${h.completed ? " (ran)" : ""}`);
1352
+ }
1353
+ break;
1354
+ }
820
1355
  case "tasks": {
821
1356
  await ensureDaemon({ quiet: true });
822
1357
  const proj = await api("/v1/projects", {
@@ -830,7 +1365,9 @@ url: ${resolveBaseUrl()}`);
830
1365
  if (json)
831
1366
  console.log(JSON.stringify(rows));
832
1367
  else if (!t.source)
833
- console.log('no task source \u2014 add `[tasks] source = "path/to/plan.md"` to .swarm.toml');
1368
+ console.log('no task source \u2014 add `[tasks] source = "path/to/plan.md"` (or "github" / "linear") to .swarm.toml');
1369
+ else if (t.error && !t.tasks.length)
1370
+ console.log(`${t.source}: ${t.error}`);
834
1371
  else if (!rows.length)
835
1372
  console.log(ready ? "nothing ready to claim" : `no tasks in ${t.source}`);
836
1373
  else