@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-mcp.js +146 -2
- package/dist/swarm.js +551 -14
- package/dist/swarmd.js +2780 -438
- package/package.json +1 -1
- package/web/app.js +541 -24
- package/web/icons.js +2 -2
- package/web/index.html +39 -0
- package/web/release-notes.js +2 -0
- package/web/table.js +1 -1
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
|
-
|
|
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
|
|
663
|
-
|
|
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
|