@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-mcp.js
CHANGED
|
@@ -19876,6 +19876,127 @@ ${h.text}`).join(`
|
|
|
19876
19876
|
return fail(`REFUSED: ${r.error}`);
|
|
19877
19877
|
return ok(`recorded ${gate} ${verdict} on ${task}`, r.run);
|
|
19878
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
|
+
});
|
|
19879
20000
|
server.registerTool("swarm_gates", {
|
|
19880
20001
|
title: "Gate status",
|
|
19881
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.",
|
|
@@ -19908,7 +20029,7 @@ ${body}`, g);
|
|
|
19908
20029
|
});
|
|
19909
20030
|
if (!r.ok)
|
|
19910
20031
|
return fail(`REFUSED: ${r.error}`);
|
|
19911
|
-
return ok(`claimed ${task} \u2014 work in ${r.worktree} (branch ${r.branch}). cd there before editing
|
|
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);
|
|
19912
20033
|
});
|
|
19913
20034
|
server.registerTool("swarm_renew", {
|
|
19914
20035
|
title: "Renew a claim",
|