@ra3orblade/swarm 0.4.0 → 0.5.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
@@ -151,6 +151,45 @@ var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
151
151
  var MARK = "swarm-hook";
152
152
  var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
153
153
  var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join2(homedir2(), ".claude", "settings.json");
154
+ var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join2(homedir2(), ".claude.json");
155
+ function loadClaudeJson() {
156
+ const p = claudeJsonPath();
157
+ if (!existsSync3(p))
158
+ return {};
159
+ try {
160
+ return JSON.parse(readFileSync2(p, "utf8"));
161
+ } catch {
162
+ return {};
163
+ }
164
+ }
165
+ function saveClaudeJson(c) {
166
+ writeFileSync2(claudeJsonPath(), `${JSON.stringify(c, null, 2)}
167
+ `);
168
+ }
169
+ function registerMcp() {
170
+ const c = loadClaudeJson();
171
+ const mcp = c.mcpServers ?? {};
172
+ mcp.swarm = { type: "stdio", ...mcpServerConfig() };
173
+ c.mcpServers = mcp;
174
+ saveClaudeJson(c);
175
+ }
176
+ function unregisterMcp() {
177
+ const c = loadClaudeJson();
178
+ const mcp = c.mcpServers ?? {};
179
+ if (!mcp.swarm)
180
+ return false;
181
+ delete mcp.swarm;
182
+ if (Object.keys(mcp).length)
183
+ c.mcpServers = mcp;
184
+ else
185
+ delete c.mcpServers;
186
+ saveClaudeJson(c);
187
+ return true;
188
+ }
189
+ function mcpRegistered() {
190
+ const c = loadClaudeJson();
191
+ return Boolean(c.mcpServers?.swarm);
192
+ }
154
193
  var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
155
194
  var shimPath = () => resolveBin("swarm-hook").at(-1);
156
195
  function mcpServerConfig() {
@@ -179,10 +218,16 @@ function install() {
179
218
  added.push(ev);
180
219
  }
181
220
  s.hooks = hooks2;
182
- const mcp = s.mcpServers ?? {};
183
- mcp.swarm = { type: "stdio", ...mcpServerConfig() };
184
- s.mcpServers = mcp;
221
+ const stale = s.mcpServers ?? {};
222
+ if (stale.swarm) {
223
+ delete stale.swarm;
224
+ if (Object.keys(stale).length)
225
+ s.mcpServers = stale;
226
+ else
227
+ delete s.mcpServers;
228
+ }
185
229
  save(s);
230
+ registerMcp();
186
231
  return added;
187
232
  }
188
233
  function uninstall() {
@@ -211,23 +256,22 @@ function uninstall() {
211
256
  else
212
257
  delete s.hooks;
213
258
  const mcp = s.mcpServers ?? {};
214
- if (mcp.swarm) {
259
+ if (mcp.swarm)
215
260
  delete mcp.swarm;
216
- removed++;
217
- }
218
261
  if (Object.keys(mcp).length)
219
262
  s.mcpServers = mcp;
220
263
  else
221
264
  delete s.mcpServers;
222
265
  save(s);
266
+ if (unregisterMcp())
267
+ removed++;
223
268
  return removed;
224
269
  }
225
270
  function status() {
226
271
  const s = load();
227
272
  const hooks2 = s.hooks ?? {};
228
273
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
229
- const mcp = Boolean(s.mcpServers?.swarm);
230
- return { installed, mcp, path: settingsPath(), shim: shimPath() };
274
+ return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath() };
231
275
  }
232
276
 
233
277
  // packages/cli/src/procs.ts
@@ -348,6 +392,13 @@ var help = `swarm \u2014 control plane for AI-agent development
348
392
  renew <task> extend the lease; release <task> [--force] release + remove worktree
349
393
  claims list claims; reap release abandoned claims (keeps ones holding work)
350
394
  tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
395
+ gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
396
+ 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]
398
+ claim the task and spawn claude -p in its worktree; the session shows in Fleet
399
+ run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
400
+ handoff <task> --done "\u2026" --remaining "\u2026" [--files a,b] [--verify "\u2026"] leave notes for the next holder
401
+ resume <task> print the latest handoff (the next session gets it automatically on start)
351
402
  res ls | acquire <name> [--owner n] [--pid n] [--port n] | release <name> [--force]
352
403
  named singletons (ports, processes); fail-closed
353
404
  serve start [--name web] [--from-port 3400 | --port n] -- <cmd>
@@ -546,6 +597,226 @@ url: ${resolveBaseUrl()}`);
546
597
  console.log(`${c.state.padEnd(9)} ${c.task.padEnd(16)} ${(c.owner || "").padEnd(12)} ${c.worktree}`);
547
598
  break;
548
599
  }
600
+ case "run": {
601
+ await ensureDaemon({ quiet: true });
602
+ const proj = await api("/v1/projects", {
603
+ method: "POST",
604
+ headers: { "content-type": "application/json" },
605
+ body: JSON.stringify({ path: resolve3(".") })
606
+ });
607
+ const base = new SwarmClient().baseUrl;
608
+ const valueFlags = new Set([
609
+ "--task",
610
+ "--prompt",
611
+ "--prompt-file",
612
+ "--model",
613
+ "--permission-mode",
614
+ "--allowed-tools",
615
+ "--max-turns",
616
+ "--owner"
617
+ ]);
618
+ const positionals = [];
619
+ for (let i = 0;i < rest.length; i++) {
620
+ const a = rest[i];
621
+ if (valueFlags.has(a))
622
+ i++;
623
+ else if (!a.startsWith("--"))
624
+ positionals.push(a);
625
+ }
626
+ const flag = (n) => {
627
+ const i = rest.indexOf(n);
628
+ return i >= 0 ? rest[i + 1] : undefined;
629
+ };
630
+ const sub = positionals[0];
631
+ if (sub === "ls") {
632
+ const runs = await api(`/v1/runs?project=${proj.id}`);
633
+ if (json)
634
+ console.log(JSON.stringify(runs));
635
+ else if (!runs.length)
636
+ console.log("no live runs here");
637
+ else
638
+ for (const r2 of runs)
639
+ console.log(`${r2.id} ${r2.task.padEnd(12)} pid ${String(r2.pid).padEnd(7)} ${r2.owner.padEnd(10)} ${r2.result ? `$${r2.result.costUsd.toFixed(2)} \xB7 ${r2.result.turns} turns${r2.result.isError ? " \xB7 error" : ""}` : "starting\u2026"}`);
640
+ break;
641
+ }
642
+ if (sub === "send" || sub === "stop") {
643
+ const target = positionals[1];
644
+ if (!target)
645
+ throw new Error(`usage: swarm run ${sub} <task|id>${sub === "send" ? ' "text"' : ""}`);
646
+ const r2 = sub === "send" ? await fetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
647
+ method: "POST",
648
+ headers: { "content-type": "application/json" },
649
+ body: JSON.stringify({ text: positionals.slice(2).join(" ") })
650
+ }) : await fetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
651
+ const j = await r2.json();
652
+ if (json)
653
+ console.log(JSON.stringify(j));
654
+ else if (j.ok)
655
+ console.log(sub === "send" ? `sent to ${target}` : `stopped ${target}`);
656
+ else {
657
+ console.error(`REFUSED: ${j.error}`);
658
+ process.exit(1);
659
+ }
660
+ break;
661
+ }
662
+ const task = flag("--task") ?? sub;
663
+ let prompt = flag("--prompt");
664
+ const pf = flag("--prompt-file");
665
+ if (!prompt && pf)
666
+ prompt = await Bun.file(resolve3(pf)).text();
667
+ 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`, {
670
+ method: "POST",
671
+ headers: { "content-type": "application/json" },
672
+ body: JSON.stringify({
673
+ projectId: proj.id,
674
+ task,
675
+ prompt,
676
+ owner: flag("--owner") ?? process.env.USER ?? "me",
677
+ model: flag("--model"),
678
+ permissionMode: flag("--permission-mode"),
679
+ allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
680
+ maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
681
+ })
682
+ }).then((x) => x.json());
683
+ if (json)
684
+ console.log(JSON.stringify(r));
685
+ else if (r.ok && r.run)
686
+ console.log(`run ${r.run.id} on ${task} (pid ${r.run.pid})
687
+ worktree: ${r.run.worktree}
688
+ session: ${r.run.sessionId}
689
+ log: ${r.run.log}
690
+ steer: swarm run send ${task} "\u2026" stop: swarm run stop ${task} watch: swarm tail --session ${r.run.sessionId}`);
691
+ else {
692
+ console.error(`REFUSED: ${r.error}`);
693
+ process.exit(1);
694
+ }
695
+ break;
696
+ }
697
+ case "handoff":
698
+ case "resume": {
699
+ await ensureDaemon({ quiet: true });
700
+ const task = arg();
701
+ if (!task)
702
+ throw new Error(`usage: swarm ${cmd} <task> \u2026`);
703
+ const proj = await api("/v1/projects", {
704
+ method: "POST",
705
+ headers: { "content-type": "application/json" },
706
+ body: JSON.stringify({ path: resolve3(".") })
707
+ });
708
+ if (cmd === "resume") {
709
+ const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
710
+ const j = await r2.json();
711
+ if (json)
712
+ console.log(JSON.stringify(j.handoff));
713
+ else
714
+ console.log(j.text ?? `no handoff on ${task}`);
715
+ break;
716
+ }
717
+ const flag = (n) => {
718
+ const i = rest.indexOf(n);
719
+ return i >= 0 ? rest[i + 1] : undefined;
720
+ };
721
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
722
+ method: "POST",
723
+ headers: { "content-type": "application/json" },
724
+ body: JSON.stringify({
725
+ projectId: proj.id,
726
+ task,
727
+ done: flag("--done"),
728
+ remaining: flag("--remaining"),
729
+ files: (flag("--files") ?? "").split(",").map((f) => f.trim()).filter(Boolean),
730
+ verify: flag("--verify") ?? null,
731
+ by: flag("--by") ?? process.env.USER ?? null,
732
+ sessionId: process.env.CLAUDE_SESSION_ID ?? null
733
+ })
734
+ }).then((x) => x.json());
735
+ if (json)
736
+ console.log(JSON.stringify(r));
737
+ else if (r.ok)
738
+ console.log(`handoff recorded on ${task} \u2014 the next session in its worktree sees it on start`);
739
+ else {
740
+ console.error(`REFUSED: ${r.error}`);
741
+ process.exit(1);
742
+ }
743
+ break;
744
+ }
745
+ case "gate": {
746
+ await ensureDaemon({ quiet: true });
747
+ const proj = await api("/v1/projects", {
748
+ method: "POST",
749
+ headers: { "content-type": "application/json" },
750
+ body: JSON.stringify({ path: resolve3(".") })
751
+ });
752
+ const valueFlags = new Set(["--rubric", "--evidence"]);
753
+ const positionals = [];
754
+ for (let i = 0;i < rest.length; i++) {
755
+ const a = rest[i];
756
+ if (valueFlags.has(a))
757
+ i++;
758
+ else if (!a.startsWith("--"))
759
+ positionals.push(a);
760
+ }
761
+ const flag = (n) => {
762
+ const i = rest.indexOf(n);
763
+ return i >= 0 ? rest[i + 1] : undefined;
764
+ };
765
+ const sub = positionals[0] ?? "ls";
766
+ if (sub === "record") {
767
+ const [, task, gate, verdict] = positionals;
768
+ if (!task || !gate || !verdict)
769
+ throw new Error('usage: swarm gate record <task> <gate> pass|fail --rubric "what was checked" [--evidence "\u2026"]');
770
+ const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates`, {
771
+ method: "POST",
772
+ headers: { "content-type": "application/json" },
773
+ body: JSON.stringify({
774
+ projectId: proj.id,
775
+ task,
776
+ gate,
777
+ verdict,
778
+ rubric: flag("--rubric"),
779
+ evidence: flag("--evidence"),
780
+ sessionId: process.env.CLAUDE_SESSION_ID ?? null
781
+ })
782
+ }).then((x) => x.json());
783
+ if (json)
784
+ console.log(JSON.stringify(r));
785
+ else if (r.ok)
786
+ console.log(`recorded ${gate} ${verdict} on ${task}`);
787
+ else {
788
+ console.error(`REFUSED: ${r.error}`);
789
+ process.exit(1);
790
+ }
791
+ break;
792
+ }
793
+ if (sub === "ls") {
794
+ const task = positionals[1];
795
+ const q = new URLSearchParams({ project: proj.id });
796
+ if (task)
797
+ q.set("task", task);
798
+ const g = await api(`/v1/gates?${q}`);
799
+ if (json)
800
+ console.log(JSON.stringify(g));
801
+ else if (task) {
802
+ if (!g.status?.length)
803
+ console.log(`no gates on ${task}${g.required.length ? ` (required: ${g.required.join(", ")})` : ""}`);
804
+ for (const st of g.status ?? [])
805
+ console.log(`${(st.verdict ?? "\u2014").padEnd(5)} ${st.gate.padEnd(12)} ${st.runs} run${st.runs === 1 ? "" : "s"}, ${st.fails} fail${st.fails === 1 ? "" : "s"}`);
806
+ for (const r of g.runs)
807
+ console.log(` ${r.createdAt.slice(0, 16)} ${r.gate.padEnd(12)} ${r.verdict.padEnd(5)} ${r.rubric.slice(0, 70)}`);
808
+ } else {
809
+ if (g.required.length)
810
+ console.log(`required: ${g.required.join(", ")}`);
811
+ if (!g.runs.length)
812
+ console.log("no gate runs yet");
813
+ for (const r of g.runs.slice(0, 50))
814
+ console.log(`${r.createdAt.slice(0, 16)} ${r.task.padEnd(10)} ${r.gate.padEnd(12)} ${r.verdict.padEnd(5)} ${r.rubric.slice(0, 60)}`);
815
+ }
816
+ break;
817
+ }
818
+ throw new Error("usage: swarm gate record|ls");
819
+ }
549
820
  case "tasks": {
550
821
  await ensureDaemon({ quiet: true });
551
822
  const proj = await api("/v1/projects", {