@ra3orblade/swarm 0.5.0 → 0.6.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 CHANGED
@@ -19765,6 +19765,29 @@ function buildServer() {
19765
19765
  resources
19766
19766
  });
19767
19767
  });
19768
+ server.registerTool("swarm_search", {
19769
+ title: "Search Swarm's memory",
19770
+ description: "Full-text search over what Swarm remembers about this repo: handoffs (what was done / what's left), incidents (commands the rules stopped, and why), gate runs (rubric + evidence) and what previous sessions last said. Not the codebase \u2014 grep that. Words are AND-ed, the last one is a prefix; quote a phrase; `kind:incident` / `task:M1.2` filter. Use it before redoing work someone may have done, or when a rule blocks you and you want to know how it was handled before.",
19771
+ inputSchema: {
19772
+ query: exports_external.string(),
19773
+ kind: exports_external.enum(["handoff", "incident", "gate", "session"]).optional(),
19774
+ all_projects: exports_external.boolean().optional().describe("search every project, not just this repo"),
19775
+ limit: exports_external.number().int().min(1).max(100).optional()
19776
+ }
19777
+ }, async ({ query, kind, all_projects, limit }) => {
19778
+ const q = new URLSearchParams({ q: query, limit: String(limit ?? 20) });
19779
+ if (!all_projects)
19780
+ q.set("project", await projectId());
19781
+ if (kind)
19782
+ q.set("kind", kind);
19783
+ const r = await api2(`/v1/memory?${q}`);
19784
+ if (!r.hits.length)
19785
+ return ok(`nothing in memory matches "${query}"`, { hits: [] });
19786
+ return ok(r.hits.map((h) => `[${h.kind}] ${h.title}${h.task ? ` (${h.task})` : ""} \u2014 ${h.ts.slice(0, 16)}
19787
+ ${h.text}`).join(`
19788
+
19789
+ `), r.hits);
19790
+ });
19768
19791
  server.registerTool("swarm_next_task", {
19769
19792
  title: "Next claimable task",
19770
19793
  description: "The first unclaimed task in this repo's task source whose dependencies are done \u2014 what to pick up next. Needs `[tasks] source` in .swarm.toml. Pass all=true to list every ready task.",
@@ -19815,7 +19838,7 @@ function buildServer() {
19815
19838
  });
19816
19839
  server.registerTool("swarm_resume", {
19817
19840
  title: "Read the latest handoff",
19818
- description: "The latest handoff on a task: done, remaining, files, verify. Read it before continuing someone else's work.",
19841
+ description: "The latest handoff on a task: done, remaining, files, verify. Read it before continuing someone else's work. Handoffs marked `auto:` were derived by Swarm from what the previous session did (files edited, last verify command) when it stopped without leaving one.",
19819
19842
  inputSchema: { task: exports_external.string() }
19820
19843
  }, async ({ task }) => {
19821
19844
  const pid = await projectId();
package/dist/swarm.js CHANGED
@@ -147,6 +147,8 @@ var HOOK_EVENTS = [
147
147
  // packages/core/src/rules.ts
148
148
  var LIVE_WINDOW_MS = 10 * 60000;
149
149
  var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
150
+ // packages/core/src/ledger.ts
151
+ var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
150
152
  // packages/cli/src/install.ts
151
153
  var MARK = "swarm-hook";
152
154
  var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
@@ -397,6 +399,7 @@ var help = `swarm \u2014 control plane for AI-agent development
397
399
  run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--allowed-tools a,b] [--max-turns n]
398
400
  claim the task and spawn claude -p in its worktree; the session shows in Fleet
399
401
  run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
402
+ run resume <session-id> [--model m] [--permission-mode m] spawn a run that picks up where a dead session stopped (its handoff + tail)
400
403
  handoff <task> --done "\u2026" --remaining "\u2026" [--files a,b] [--verify "\u2026"] leave notes for the next holder
401
404
  resume <task> print the latest handoff (the next session gets it automatically on start)
402
405
  res ls | acquire <name> [--owner n] [--pid n] [--port n] | release <name> [--force]
@@ -406,6 +409,8 @@ var help = `swarm \u2014 control plane for AI-agent development
406
409
  serve ls | stop [name] list / stop servers this project started (by pid, never by pattern)
407
410
  proc start [--name n] -- <cmd> | ls | stop <name|pid> same, for workers without a port
408
411
  stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
412
+ search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
413
+ rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
409
414
 
410
415
  install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
411
416
 
@@ -659,21 +664,24 @@ url: ${resolveBaseUrl()}`);
659
664
  }
660
665
  break;
661
666
  }
662
- const task = flag("--task") ?? sub;
663
- let prompt = flag("--prompt");
667
+ const resumeFrom = sub === "resume" ? positionals[1] : undefined;
668
+ if (sub === "resume" && !resumeFrom)
669
+ throw new Error("usage: swarm run resume <session-id>");
670
+ const task = resumeFrom ? "(resumed)" : flag("--task") ?? sub;
671
+ let prompt = resumeFrom ? "(from handoff)" : flag("--prompt");
664
672
  const pf = flag("--prompt-file");
665
673
  if (!prompt && pf)
666
674
  prompt = await Bun.file(resolve3(pf)).text();
667
675
  if (!task || !prompt)
668
676
  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`, {
677
+ const r = await fetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
670
678
  method: "POST",
671
679
  headers: { "content-type": "application/json" },
672
680
  body: JSON.stringify({
673
681
  projectId: proj.id,
674
682
  task,
675
683
  prompt,
676
- owner: flag("--owner") ?? process.env.USER ?? "me",
684
+ owner: flag("--owner") ?? (resumeFrom ? undefined : process.env.USER ?? "me"),
677
685
  model: flag("--model"),
678
686
  permissionMode: flag("--permission-mode"),
679
687
  allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
@@ -683,11 +691,11 @@ url: ${resolveBaseUrl()}`);
683
691
  if (json)
684
692
  console.log(JSON.stringify(r));
685
693
  else if (r.ok && r.run)
686
- console.log(`run ${r.run.id} on ${task} (pid ${r.run.pid})
694
+ console.log(`run ${r.run.id} on ${r.run.task} (pid ${r.run.pid})
687
695
  worktree: ${r.run.worktree}
688
696
  session: ${r.run.sessionId}
689
697
  log: ${r.run.log}
690
- steer: swarm run send ${task} "\u2026" stop: swarm run stop ${task} watch: swarm tail --session ${r.run.sessionId}`);
698
+ steer: swarm run send ${r.run.task} "\u2026" stop: swarm run stop ${r.run.task} watch: swarm tail --session ${r.run.sessionId}`);
691
699
  else {
692
700
  console.error(`REFUSED: ${r.error}`);
693
701
  process.exit(1);
@@ -817,6 +825,82 @@ url: ${resolveBaseUrl()}`);
817
825
  }
818
826
  throw new Error("usage: swarm gate record|ls");
819
827
  }
828
+ case "search": {
829
+ await ensureDaemon({ quiet: true });
830
+ const q = new URLSearchParams({ limit: "30" });
831
+ const words = [];
832
+ for (let i = 0;i < rest.length; i++) {
833
+ const a = rest[i];
834
+ if (a === "--kind")
835
+ q.set("kind", rest[++i] ?? "");
836
+ else if (a === "-p") {
837
+ const proj = await api("/v1/projects", {
838
+ method: "POST",
839
+ headers: { "content-type": "application/json" },
840
+ body: JSON.stringify({ path: resolve3(".") })
841
+ });
842
+ q.set("project", proj.id);
843
+ } else if (!a.startsWith("--"))
844
+ words.push(a);
845
+ }
846
+ if (!words.length)
847
+ throw new Error("usage: swarm search <query\u2026> [-p] [--kind k]");
848
+ q.set("q", words.join(" "));
849
+ const r = await api(`/v1/memory?${q}`);
850
+ if (json)
851
+ console.log(JSON.stringify(r.hits));
852
+ else if (!r.hits.length)
853
+ console.log("nothing in memory matches");
854
+ else
855
+ for (const h of r.hits)
856
+ console.log(`${h.kind.padEnd(8)} ${h.ts.slice(0, 16).replace("T", " ")} ${h.title}${h.task ? ` [${h.task}]` : ""}
857
+ ${h.snippet.split("\x01").join("").split("\x02").join("").replace(/\s+/g, " ")}${h.sessionId ? `
858
+ session ${h.sessionId}` : ""}`);
859
+ break;
860
+ }
861
+ case "rules": {
862
+ if (rest[0] !== "dryrun")
863
+ throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
864
+ await ensureDaemon({ quiet: true });
865
+ const proj = await api("/v1/projects", {
866
+ method: "POST",
867
+ headers: { "content-type": "application/json" },
868
+ body: JSON.stringify({ path: resolve3(".") })
869
+ });
870
+ const q = new URLSearchParams({ project: proj.id });
871
+ const si = rest.indexOf("--set");
872
+ if (si >= 0)
873
+ for (const kv of (rest[si + 1] ?? "").split(",")) {
874
+ const [k, v] = kv.split("=");
875
+ if (k && v)
876
+ q.set(k.trim(), v.trim());
877
+ }
878
+ const li = rest.indexOf("--limit");
879
+ if (li >= 0)
880
+ q.set("limit", rest[li + 1] ?? "");
881
+ const r = await api(`/v1/rules/dryrun?${q}`);
882
+ if (json) {
883
+ console.log(JSON.stringify(r));
884
+ break;
885
+ }
886
+ console.log(`dry-run over ${r.evaluated} of ${r.calls} recorded calls (nothing recorded)`);
887
+ for (const [rule, n] of Object.entries(r.byRule))
888
+ console.log(` ${rule.padEnd(26)} ${String(r.modes[rule]).padEnd(5)} ask ${String(n.ask).padStart(4)} deny ${String(n.deny).padStart(4)}`);
889
+ if (r.flaky.length) {
890
+ console.log(`
891
+ flaky signals:`);
892
+ for (const f of r.flaky)
893
+ console.log(` ${f.display}
894
+ ${f.suggestion}`);
895
+ }
896
+ if (r.hits.length) {
897
+ console.log(`
898
+ would have fired (newest last):`);
899
+ for (const h of r.hits.slice(-20))
900
+ console.log(` ${h.ts.slice(11, 19)} ${h.action.padEnd(4)} ${h.rule.padEnd(20)} ${h.display}${h.completed ? " (ran)" : ""}`);
901
+ }
902
+ break;
903
+ }
820
904
  case "tasks": {
821
905
  await ensureDaemon({ quiet: true });
822
906
  const proj = await api("/v1/projects", {
@@ -830,7 +914,9 @@ url: ${resolveBaseUrl()}`);
830
914
  if (json)
831
915
  console.log(JSON.stringify(rows));
832
916
  else if (!t.source)
833
- console.log('no task source \u2014 add `[tasks] source = "path/to/plan.md"` to .swarm.toml');
917
+ console.log('no task source \u2014 add `[tasks] source = "path/to/plan.md"` (or "github" / "linear") to .swarm.toml');
918
+ else if (t.error && !t.tasks.length)
919
+ console.log(`${t.source}: ${t.error}`);
834
920
  else if (!rows.length)
835
921
  console.log(ready ? "nothing ready to claim" : `no tasks in ${t.source}`);
836
922
  else