@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 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();
@@ -19853,6 +19876,127 @@ function buildServer() {
19853
19876
  return fail(`REFUSED: ${r.error}`);
19854
19877
  return ok(`recorded ${gate} ${verdict} on ${task}`, r.run);
19855
19878
  });
19879
+ server.registerTool("swarm_gate_run", {
19880
+ title: "Run the repo's executable gates",
19881
+ description: "Execute the gates this repo defines as commands (.swarm.toml [gates.<name>] cmd, e.g. tests, lint, typecheck) inside the task's held worktree and record the verdicts \u2014 exit 0 is a pass. Runs the required ones by default. Prefer this over recording a verdict yourself whenever a gate has a command: the record then says exactly what ran.",
19882
+ inputSchema: {
19883
+ task: exports_external.string().describe("task id whose worktree to run in (must be held)"),
19884
+ gates: exports_external.array(exports_external.string()).optional().describe("gate names; default = required gates that have a cmd")
19885
+ }
19886
+ }, async ({ task, gates }) => {
19887
+ const pid = await projectId();
19888
+ const r = await api2("/v1/gates/run", {
19889
+ method: "POST",
19890
+ headers: { "content-type": "application/json" },
19891
+ body: JSON.stringify({ projectId: pid, task, gates, sessionId: SESSION })
19892
+ });
19893
+ if (!r.started?.length)
19894
+ return fail(`REFUSED: ${r.error ?? r.skipped?.[0]?.reason ?? "nothing ran"}`);
19895
+ const lines = r.runs.map((x) => `${x.verdict === "pass" ? "\u2713" : "\u2717"} ${x.gate}: ${x.rubric}${x.verdict === "fail" && x.evidence ? `
19896
+ ${x.evidence.split(`
19897
+ `).slice(-15).join(`
19898
+ `)}` : ""}`);
19899
+ for (const x of r.skipped)
19900
+ lines.push(`\u2013 ${x.gate}: skipped (${x.reason})`);
19901
+ return ok(`${r.ok ? "all gates passed" : "gate failure"} on ${task}
19902
+ ${lines.join(`
19903
+ `)}`, r);
19904
+ });
19905
+ server.registerTool("swarm_context", {
19906
+ title: "Refresh your Swarm context",
19907
+ description: "What Swarm told you at session start, current as of now: what you hold (task, worktree, lease left), the latest handoff, gate status, held resources, the repo's rule modes \u2014 plus any answers to your questions and questions still open. Call it after a long stretch of work, when resuming, or when you are not sure what you hold.",
19908
+ inputSchema: {}
19909
+ }, async () => {
19910
+ const r = await api2(`/v1/context?cwd=${encodeURIComponent(process.cwd())}&session=${encodeURIComponent(SESSION ?? "")}`);
19911
+ return ok(r.text ?? "[swarm] nothing to report: no claim here, no open questions, default rules.", r);
19912
+ });
19913
+ server.registerTool("swarm_ask", {
19914
+ title: "Ask the human",
19915
+ description: "Park a question only a person can answer (a product decision, credentials, which of two designs). It shows on the dashboard with a notification; the answer comes back to you as context on a later tool call \u2014 or call swarm_inbox to check. Ask once, then either keep working on what doesn't depend on it or stop and say you're waiting. Don't use it for things you can find out yourself.",
19916
+ inputSchema: {
19917
+ question: exports_external.string().describe("the question, with enough context to answer it cold"),
19918
+ options: exports_external.array(exports_external.string()).optional().describe("up to 8 suggested answers")
19919
+ }
19920
+ }, async ({ question, options }) => {
19921
+ const pid = await projectId();
19922
+ const r = await api2("/v1/questions", {
19923
+ method: "POST",
19924
+ headers: { "content-type": "application/json" },
19925
+ body: JSON.stringify({
19926
+ projectId: pid,
19927
+ sessionId: SESSION,
19928
+ text: question,
19929
+ options,
19930
+ askedBy: "agent",
19931
+ cwd: process.cwd()
19932
+ })
19933
+ });
19934
+ if (!r.ok)
19935
+ return fail(`REFUSED: ${r.error}`);
19936
+ return ok(`asked as question #${r.question?.id}. A human will see it on the dashboard; the answer arrives as [swarm] context on a later tool call, or via swarm_inbox. Continue with what doesn't depend on it, or stop and say you're waiting.`, r.question);
19937
+ });
19938
+ server.registerTool("swarm_inbox", {
19939
+ title: "Answers waiting for you",
19940
+ description: "Answers a human has given to your swarm_ask questions that you haven't received yet.",
19941
+ inputSchema: {}
19942
+ }, async () => {
19943
+ const qs = await api2(`/v1/inbox?session=${encodeURIComponent(SESSION ?? "")}`);
19944
+ if (!qs.length)
19945
+ return ok("no new answers", []);
19946
+ return ok(qs.map((q) => `#${q.id} "${q.text}" \u2192 ${q.answeredBy ?? "a human"}: ${q.answer}`).join(`
19947
+ `), qs);
19948
+ });
19949
+ server.registerTool("swarm_dispatch", {
19950
+ title: "Dispatch tasks to spawned agents",
19951
+ description: "Hand ready tasks to autonomous runs: each gets its own claim + worktree and a `claude -p` run with a prompt built from the task (work in the worktree, run gates, hand off, open the PR). At most [dispatch] max_parallel run at once per repo; the rest queue. When a run ends Swarm derives the outcome from the ledger (gates satisfied, PR open) \u2014 never from the agent's word \u2014 and opens an incident if it fell short. Use from a lead session to fan work out; check progress with swarm_status or the Board.",
19952
+ inputSchema: {
19953
+ tasks: exports_external.array(exports_external.string()).optional().describe("task ids; omit with ready=true to take every ready task"),
19954
+ ready: exports_external.boolean().optional(),
19955
+ max: exports_external.number().int().positive().optional().describe("cap on tasks accepted this call")
19956
+ }
19957
+ }, async ({ tasks, ready, max }) => {
19958
+ const pid = await projectId();
19959
+ const r = await api2("/v1/dispatch", {
19960
+ method: "POST",
19961
+ headers: { "content-type": "application/json" },
19962
+ body: JSON.stringify({
19963
+ projectId: pid,
19964
+ tasks,
19965
+ ready: ready ?? !tasks?.length,
19966
+ max,
19967
+ owner: "mcp"
19968
+ })
19969
+ });
19970
+ if (!r.ok)
19971
+ return fail(`REFUSED: ${r.error}`);
19972
+ const lines = [
19973
+ ...r.started.map((t) => `started ${t}`),
19974
+ ...r.queued.map((t) => `queued ${t}`),
19975
+ ...r.rejected.map((x) => `rejected ${x.id} \u2014 ${x.reason}`)
19976
+ ];
19977
+ return ok(lines.join(`
19978
+ `) || "nothing to dispatch", r);
19979
+ });
19980
+ server.registerTool("swarm_pr_open", {
19981
+ title: "Open a pull request for a task",
19982
+ description: "Push the task's worktree branch and open a PR (GitHub via gh) or MR (GitLab via glab) for it, with the title and body drafted from the task, the latest handoff, the required gates' verdicts and the changed files \u2014 pass title/body to override. Refuses uncommitted changes: commit first. Use it as the last step of a task, after swarm_handoff and the gates.",
19983
+ inputSchema: {
19984
+ task: exports_external.string().describe("task id (held) or worktree name/path"),
19985
+ title: exports_external.string().optional(),
19986
+ body: exports_external.string().optional().describe("markdown; default is drafted from the handoff"),
19987
+ draft: exports_external.boolean().optional().describe("open as a draft PR")
19988
+ }
19989
+ }, async ({ task, title, body, draft }) => {
19990
+ const pid = await projectId();
19991
+ const r = await api2("/v1/prs/open", {
19992
+ method: "POST",
19993
+ headers: { "content-type": "application/json" },
19994
+ body: JSON.stringify({ projectId: pid, worktree: task, title, body, draft })
19995
+ });
19996
+ if (!r.ok)
19997
+ return fail(`REFUSED: ${r.error}`);
19998
+ return ok(`opened ${r.url}`, r);
19999
+ });
19856
20000
  server.registerTool("swarm_gates", {
19857
20001
  title: "Gate status",
19858
20002
  description: "The gates this repo requires (.swarm.toml [gates] required) and, for a task, the latest verdict per gate with run history. A task is done only when every required gate's latest run is a pass.",
@@ -19885,7 +20029,7 @@ ${body}`, g);
19885
20029
  });
19886
20030
  if (!r.ok)
19887
20031
  return fail(`REFUSED: ${r.error}`);
19888
- return ok(`claimed ${task} \u2014 work in ${r.worktree} (branch ${r.branch}). cd there before editing.`, r);
20032
+ return ok(`claimed ${task} \u2014 work in ${r.worktree} (branch ${r.branch}). cd there before editing.${r.bootstrap ? ` Worktree setup is running in the background (log: ${r.bootstrap}); wait for it before installing or running tests.` : ""}`, r);
19889
20033
  });
19890
20034
  server.registerTool("swarm_renew", {
19891
20035
  title: "Renew a claim",