@ra3orblade/swarm 0.6.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,6 +144,8 @@ 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"]);
@@ -192,6 +194,95 @@ function mcpRegistered() {
192
194
  const c = loadClaudeJson();
193
195
  return Boolean(c.mcpServers?.swarm);
194
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
+ }
195
286
  var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
196
287
  var shimPath = () => resolveBin("swarm-hook").at(-1);
197
288
  function mcpServerConfig() {
@@ -230,6 +321,7 @@ function install() {
230
321
  }
231
322
  save(s);
232
323
  registerMcp();
324
+ registerOtherAgents();
233
325
  return added;
234
326
  }
235
327
  function uninstall() {
@@ -267,13 +359,23 @@ function uninstall() {
267
359
  save(s);
268
360
  if (unregisterMcp())
269
361
  removed++;
362
+ removed += unregisterOtherAgents().length;
270
363
  return removed;
271
364
  }
272
365
  function status() {
273
366
  const s = load();
274
367
  const hooks2 = s.hooks ?? {};
275
368
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
276
- 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 };
277
379
  }
278
380
 
279
381
  // packages/cli/src/procs.ts
@@ -391,12 +493,17 @@ var help = `swarm \u2014 control plane for AI-agent development
391
493
  tail [--project p] [--session id] follow the live event stream
392
494
 
393
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
394
501
  renew <task> extend the lease; release <task> [--force] release + remove worktree
395
502
  claims list claims; reap release abandoned claims (keeps ones holding work)
396
503
  tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
397
504
  gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
398
505
  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]
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]
400
507
  claim the task and spawn claude -p in its worktree; the session shows in Fleet
401
508
  run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
402
509
  run resume <session-id> [--model m] [--permission-mode m] spawn a run that picks up where a dead session stopped (its handoff + tail)
@@ -447,7 +554,7 @@ try {
447
554
  const base = await ensureDaemon();
448
555
  const evs = install();
449
556
  console.log(`\u2713 daemon running at ${base}`);
450
- 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(", ")}` : ""}`);
451
558
  console.log("\u2713 any Claude session you start now will appear in Swarm");
452
559
  Bun.spawn(["open", base]).unref?.();
453
560
  console.log(`
@@ -492,6 +599,8 @@ Open the dashboard: ${base}`);
492
599
  line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
493
600
  line(st.installed, "hooks installed", "run: swarm install");
494
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)`);
495
604
  const forge2 = (bin, auth) => {
496
605
  const path = Bun.which(bin);
497
606
  if (!path)
@@ -545,7 +654,8 @@ url: ${resolveBaseUrl()}`);
545
654
  console.log(JSON.stringify(r));
546
655
  else if (r.ok)
547
656
  console.log(`claimed ${task} \u2192 ${r.worktree}
548
- cd ${r.worktree}`);
657
+ cd ${r.worktree}${r.bootstrap ? `
658
+ bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
549
659
  else {
550
660
  console.error(`REFUSED: ${r.error}`);
551
661
  process.exit(1);
@@ -578,6 +688,317 @@ url: ${resolveBaseUrl()}`);
578
688
  }
579
689
  break;
580
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
+ }
581
1002
  case "reap": {
582
1003
  await ensureDaemon({ quiet: true });
583
1004
  const r = await api("/v1/claims/reap", { method: "POST" });
@@ -618,7 +1039,8 @@ url: ${resolveBaseUrl()}`);
618
1039
  "--permission-mode",
619
1040
  "--allowed-tools",
620
1041
  "--max-turns",
621
- "--owner"
1042
+ "--owner",
1043
+ "--profile"
622
1044
  ]);
623
1045
  const positionals = [];
624
1046
  for (let i = 0;i < rest.length; i++) {
@@ -673,7 +1095,7 @@ url: ${resolveBaseUrl()}`);
673
1095
  if (!prompt && pf)
674
1096
  prompt = await Bun.file(resolve3(pf)).text();
675
1097
  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]');
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]');
677
1099
  const r = await fetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
678
1100
  method: "POST",
679
1101
  headers: { "content-type": "application/json" },
@@ -685,7 +1107,8 @@ url: ${resolveBaseUrl()}`);
685
1107
  model: flag("--model"),
686
1108
  permissionMode: flag("--permission-mode"),
687
1109
  allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
688
- maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
1110
+ maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined,
1111
+ profile: flag("--profile")
689
1112
  })
690
1113
  }).then((x) => x.json());
691
1114
  if (json)
@@ -798,6 +1221,34 @@ url: ${resolveBaseUrl()}`);
798
1221
  }
799
1222
  break;
800
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
+ }
801
1252
  if (sub === "ls") {
802
1253
  const task = positionals[1];
803
1254
  const q = new URLSearchParams({ project: proj.id });