@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-mcp.js +122 -1
- package/dist/swarm.js +458 -7
- package/dist/swarmd.js +1888 -304
- package/package.json +1 -1
- package/web/app.js +266 -23
- package/web/index.html +10 -0
- package/web/release-notes.js +1 -1
- package/web/table.js +1 -1
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
|
-
|
|
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 });
|