@ra3orblade/swarm 0.8.0 → 0.9.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-hook.js +3 -1
- package/dist/swarm-mcp.js +35 -6
- package/dist/swarm.js +184 -9
- package/dist/swarmd.js +954 -75
- package/package.json +1 -1
- package/web/app.js +150 -11
- package/web/index.html +43 -0
- package/web/release-notes.js +1 -1
- package/web/viz.js +20 -3
package/dist/swarm-hook.js
CHANGED
|
@@ -77,7 +77,9 @@ var AUDIT_TYPES = new Set([
|
|
|
77
77
|
"permission.resolved",
|
|
78
78
|
"incident.opened",
|
|
79
79
|
"incident.acked",
|
|
80
|
-
"run.result"
|
|
80
|
+
"run.result",
|
|
81
|
+
"workflow.started",
|
|
82
|
+
"workflow.finished"
|
|
81
83
|
]);
|
|
82
84
|
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
83
85
|
// packages/core/src/budget.ts
|
package/dist/swarm-mcp.js
CHANGED
|
@@ -19952,15 +19952,44 @@ ${lines.join(`
|
|
|
19952
19952
|
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);
|
|
19953
19953
|
});
|
|
19954
19954
|
server.registerTool("swarm_inbox", {
|
|
19955
|
-
title: "
|
|
19956
|
-
description: "Answers
|
|
19955
|
+
title: "What's waiting for you",
|
|
19956
|
+
description: "Answers to your swarm_ask questions and messages other agents or the human sent you (swarm_send) that you haven't received yet.",
|
|
19957
19957
|
inputSchema: {}
|
|
19958
19958
|
}, async () => {
|
|
19959
19959
|
const qs = await api2(`/v1/inbox?session=${encodeURIComponent(SESSION ?? "")}`);
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19960
|
+
const ms = await api2(`/v1/messages/inbox?session=${encodeURIComponent(SESSION ?? "")}`);
|
|
19961
|
+
if (!qs.length && !ms.length)
|
|
19962
|
+
return ok("nothing new", []);
|
|
19963
|
+
const lines = [
|
|
19964
|
+
...qs.map((q) => `answer #${q.id} "${q.text}" \u2192 ${q.answeredBy ?? "a human"}: ${q.answer}`),
|
|
19965
|
+
...ms.map((m) => `message #${m.id} from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`)
|
|
19966
|
+
];
|
|
19967
|
+
return ok(lines.join(`
|
|
19968
|
+
`), { answers: qs, messages: ms });
|
|
19969
|
+
});
|
|
19970
|
+
server.registerTool("swarm_send", {
|
|
19971
|
+
title: "Message another agent (or the human)",
|
|
19972
|
+
description: `Send a short message to another session (session id), to whoever holds a task (task id), or to "lead" \u2014 the human's interactive session in this project. Delivery is on their next tool call (or immediately to a spawned run); replies come back via swarm_inbox.`,
|
|
19973
|
+
inputSchema: {
|
|
19974
|
+
to: exports_external.string().describe('session id, task id, or "lead"'),
|
|
19975
|
+
text: exports_external.string().max(4000)
|
|
19976
|
+
}
|
|
19977
|
+
}, async ({ to, text }) => {
|
|
19978
|
+
const pid = await projectId();
|
|
19979
|
+
const r = await api2("/v1/messages", {
|
|
19980
|
+
method: "POST",
|
|
19981
|
+
headers: { "content-type": "application/json" },
|
|
19982
|
+
body: JSON.stringify({
|
|
19983
|
+
projectId: pid,
|
|
19984
|
+
to,
|
|
19985
|
+
text,
|
|
19986
|
+
sessionId: SESSION,
|
|
19987
|
+
from: OWNER === "agent" && SESSION ? `agent ${SESSION.slice(0, 8)}` : OWNER
|
|
19988
|
+
})
|
|
19989
|
+
});
|
|
19990
|
+
if (!r.ok)
|
|
19991
|
+
return fail(r.error ?? "send failed");
|
|
19992
|
+
return ok(`sent #${r.message?.id}${r.message?.sessionId ? "" : " (queued until the target appears)"}`, r.message);
|
|
19964
19993
|
});
|
|
19965
19994
|
server.registerTool("swarm_dispatch", {
|
|
19966
19995
|
title: "Dispatch tasks to spawned agents",
|
package/dist/swarm.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/cli/src/bin.ts
|
|
5
|
-
import { resolve as resolve3 } from "path";
|
|
5
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
6
6
|
|
|
7
7
|
// packages/client/src/daemon.ts
|
|
8
8
|
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -189,7 +189,9 @@ var AUDIT_TYPES = new Set([
|
|
|
189
189
|
"permission.resolved",
|
|
190
190
|
"incident.opened",
|
|
191
191
|
"incident.acked",
|
|
192
|
-
"run.result"
|
|
192
|
+
"run.result",
|
|
193
|
+
"workflow.started",
|
|
194
|
+
"workflow.finished"
|
|
193
195
|
]);
|
|
194
196
|
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
195
197
|
var DEFAULT_PRIVACY = {
|
|
@@ -202,14 +204,65 @@ var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookE
|
|
|
202
204
|
// packages/core/src/config.ts
|
|
203
205
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
204
206
|
import { join as join2 } from "path";
|
|
207
|
+
|
|
208
|
+
// packages/core/src/workflows.ts
|
|
209
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
210
|
+
function isRecord(v) {
|
|
211
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
212
|
+
}
|
|
213
|
+
function parseWorkflows(raw) {
|
|
214
|
+
const out = {};
|
|
215
|
+
if (!Array.isArray(raw))
|
|
216
|
+
return out;
|
|
217
|
+
for (const w of raw) {
|
|
218
|
+
if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
|
|
219
|
+
continue;
|
|
220
|
+
if (!Array.isArray(w.steps) || !w.steps.length)
|
|
221
|
+
continue;
|
|
222
|
+
const prompts = isRecord(w.prompts) ? w.prompts : {};
|
|
223
|
+
const steps = [];
|
|
224
|
+
for (const s of w.steps) {
|
|
225
|
+
if (typeof s !== "string" || !s.trim()) {
|
|
226
|
+
steps.length = 0;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
const t = s.trim();
|
|
230
|
+
if (t === "pr")
|
|
231
|
+
steps.push({ kind: "pr" });
|
|
232
|
+
else if (t.startsWith("gate:")) {
|
|
233
|
+
const gate = t.slice(5);
|
|
234
|
+
if (!NAME_RE.test(gate)) {
|
|
235
|
+
steps.length = 0;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
steps.push({ kind: "gate", gate });
|
|
239
|
+
} else if (NAME_RE.test(t)) {
|
|
240
|
+
const p = prompts[t];
|
|
241
|
+
steps.push({
|
|
242
|
+
kind: "run",
|
|
243
|
+
name: t,
|
|
244
|
+
prompt: typeof p === "string" && p.trim() ? p.trim() : null
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
steps.length = 0;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (steps.length)
|
|
252
|
+
out[w.name] = { name: w.name, steps };
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// packages/core/src/config.ts
|
|
205
258
|
var DEFAULT_GATE_TIMEOUT_S = 900;
|
|
206
259
|
var AUTO_MODES = ["session-end", "stop", "off"];
|
|
207
260
|
function parseGateDefs(gates) {
|
|
208
261
|
const out = {};
|
|
209
|
-
if (!
|
|
262
|
+
if (!isRecord2(gates))
|
|
210
263
|
return out;
|
|
211
264
|
for (const [name, v] of Object.entries(gates)) {
|
|
212
|
-
if (!
|
|
265
|
+
if (!isRecord2(v))
|
|
213
266
|
continue;
|
|
214
267
|
const builtin = v.builtin === "review" ? "review" : null;
|
|
215
268
|
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
@@ -232,6 +285,7 @@ var DEFAULT_CONFIG = {
|
|
|
232
285
|
daemon: { port: 7777, auth: "loopback-optional" },
|
|
233
286
|
tasks: { source: null, labels: [], team: null },
|
|
234
287
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
288
|
+
workflows: {},
|
|
235
289
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
236
290
|
events: { retain_days: 30 },
|
|
237
291
|
audit: { retain_days: 0 },
|
|
@@ -256,11 +310,11 @@ var DEFAULT_CONFIG = {
|
|
|
256
310
|
}
|
|
257
311
|
};
|
|
258
312
|
var MODES = ["ask", "deny", "off"];
|
|
259
|
-
function
|
|
313
|
+
function isRecord2(v) {
|
|
260
314
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
261
315
|
}
|
|
262
316
|
function merge(a, b) {
|
|
263
|
-
if (!
|
|
317
|
+
if (!isRecord2(a) || !isRecord2(b))
|
|
264
318
|
return b === undefined ? a : b;
|
|
265
319
|
const out = { ...a };
|
|
266
320
|
for (const [k, v] of Object.entries(b))
|
|
@@ -327,6 +381,7 @@ function validate(c) {
|
|
|
327
381
|
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
328
382
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
329
383
|
},
|
|
384
|
+
workflows: parseWorkflows(c.workflows),
|
|
330
385
|
events: {
|
|
331
386
|
retain_days: days(c.events?.retain_days, 30)
|
|
332
387
|
},
|
|
@@ -366,7 +421,7 @@ function validate(c) {
|
|
|
366
421
|
};
|
|
367
422
|
}
|
|
368
423
|
function leafPaths(v, prefix = "") {
|
|
369
|
-
if (!
|
|
424
|
+
if (!isRecord2(v))
|
|
370
425
|
return prefix ? [prefix] : [];
|
|
371
426
|
const keys = Object.keys(v);
|
|
372
427
|
if (keys.length === 0)
|
|
@@ -376,7 +431,7 @@ function leafPaths(v, prefix = "") {
|
|
|
376
431
|
function getPath(v, path) {
|
|
377
432
|
let cur = v;
|
|
378
433
|
for (const seg of path.split(".")) {
|
|
379
|
-
if (!
|
|
434
|
+
if (!isRecord2(cur))
|
|
380
435
|
return;
|
|
381
436
|
cur = cur[seg];
|
|
382
437
|
}
|
|
@@ -386,7 +441,7 @@ function setPath(obj, path, value) {
|
|
|
386
441
|
const segs = path.split(".");
|
|
387
442
|
let cur = obj;
|
|
388
443
|
for (const seg of segs.slice(0, -1)) {
|
|
389
|
-
if (!
|
|
444
|
+
if (!isRecord2(cur[seg]))
|
|
390
445
|
cur[seg] = {};
|
|
391
446
|
cur = cur[seg];
|
|
392
447
|
}
|
|
@@ -851,6 +906,10 @@ var help = `swarm \u2014 control plane for AI-agent development
|
|
|
851
906
|
stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
|
|
852
907
|
search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
|
|
853
908
|
rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
|
|
909
|
+
workflow <name> <task> | workflow ls | workflow stop <task> run a [[workflows]] sequence on a task (M7.8)
|
|
910
|
+
msg send <to> <text\u2026> [-p] message a session id, a task's holder, or "lead" (M7.6)
|
|
911
|
+
msg ls [-p] [--json] recent messages
|
|
912
|
+
demo open a seeded demo dashboard (own home + port; your real data is untouched)
|
|
854
913
|
audit export [--since 30d|ISO] [-p] [--type claim.acquired] [--format jsonl|csv|json] [--limit n] the audit log (ledger changes + decisions, with actor) to stdout
|
|
855
914
|
|
|
856
915
|
install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
|
|
@@ -1660,6 +1719,95 @@ ${flag("--body") ?? d.body}`);
|
|
|
1660
1719
|
session ${h.sessionId}` : ""}`);
|
|
1661
1720
|
break;
|
|
1662
1721
|
}
|
|
1722
|
+
case "workflow": {
|
|
1723
|
+
await ensureDaemon({ quiet: true });
|
|
1724
|
+
const proj = await api("/v1/projects", {
|
|
1725
|
+
method: "POST",
|
|
1726
|
+
headers: { "content-type": "application/json" },
|
|
1727
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1728
|
+
});
|
|
1729
|
+
if (rest[0] === "ls" || !rest[0]) {
|
|
1730
|
+
const w = await api(`/v1/workflows?project=${proj.id}`);
|
|
1731
|
+
const names = Object.keys(w.defs);
|
|
1732
|
+
console.log(names.length ? `declared: ${names.join(", ")}` : "no [[workflows]] declared in .swarm.toml");
|
|
1733
|
+
for (const r2 of w.runs.slice(0, 20))
|
|
1734
|
+
console.log(`#${r2.id} ${r2.task} \xB7 ${r2.workflow} \xB7 ${r2.state === "running" ? `${r2.stepLabel} (${r2.step + 1}/${r2.steps.length})` : r2.state}${r2.detail ? ` \u2014 ${r2.detail.slice(0, 80)}` : ""}`);
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
if (rest[0] === "stop") {
|
|
1738
|
+
const task2 = rest[1];
|
|
1739
|
+
if (!task2)
|
|
1740
|
+
throw new Error("usage: swarm workflow stop <task>");
|
|
1741
|
+
const r2 = await api("/v1/workflows/stop", {
|
|
1742
|
+
method: "POST",
|
|
1743
|
+
headers: { "content-type": "application/json" },
|
|
1744
|
+
body: JSON.stringify({ projectId: proj.id, task: task2 })
|
|
1745
|
+
});
|
|
1746
|
+
if (!r2.ok)
|
|
1747
|
+
throw new Error(r2.error ?? "stop failed");
|
|
1748
|
+
console.log(`stopped the workflow on ${task2}`);
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
const [name, task] = rest;
|
|
1752
|
+
if (!name || !task)
|
|
1753
|
+
throw new Error("usage: swarm workflow <name> <task>");
|
|
1754
|
+
const r = await api("/v1/workflows", {
|
|
1755
|
+
method: "POST",
|
|
1756
|
+
headers: { "content-type": "application/json" },
|
|
1757
|
+
body: JSON.stringify({
|
|
1758
|
+
projectId: proj.id,
|
|
1759
|
+
task,
|
|
1760
|
+
workflow: name,
|
|
1761
|
+
owner: process.env.USER ?? "cli"
|
|
1762
|
+
})
|
|
1763
|
+
});
|
|
1764
|
+
if (!r.ok)
|
|
1765
|
+
throw new Error(r.error ?? "workflow failed to start");
|
|
1766
|
+
console.log(`workflow ${name} started on ${task} (#${r.id}) \u2014 watch the Board`);
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
case "msg": {
|
|
1770
|
+
await ensureDaemon({ quiet: true });
|
|
1771
|
+
const proj = await api("/v1/projects", {
|
|
1772
|
+
method: "POST",
|
|
1773
|
+
headers: { "content-type": "application/json" },
|
|
1774
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1775
|
+
});
|
|
1776
|
+
if (rest[0] === "send") {
|
|
1777
|
+
const [to, ...words] = rest.slice(1).filter((a) => a !== "-p");
|
|
1778
|
+
if (!to || !words.length)
|
|
1779
|
+
throw new Error('usage: swarm msg send <session|task|"lead"> <text\u2026>');
|
|
1780
|
+
const r = await api("/v1/messages", {
|
|
1781
|
+
method: "POST",
|
|
1782
|
+
headers: { "content-type": "application/json" },
|
|
1783
|
+
body: JSON.stringify({
|
|
1784
|
+
projectId: proj.id,
|
|
1785
|
+
to,
|
|
1786
|
+
text: words.join(" "),
|
|
1787
|
+
from: process.env.USER ?? "me"
|
|
1788
|
+
})
|
|
1789
|
+
});
|
|
1790
|
+
if (!r.ok)
|
|
1791
|
+
throw new Error(r.error ?? "send failed");
|
|
1792
|
+
console.log(`sent #${r.message?.id}${r.message?.sessionId ? "" : " (queued until the target appears)"}`);
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
if (rest[0] === "ls" || !rest[0]) {
|
|
1796
|
+
const ms = await api(`/v1/messages?project=${proj.id}&limit=50`);
|
|
1797
|
+
if (rest.includes("--json")) {
|
|
1798
|
+
console.log(JSON.stringify(ms, null, 2));
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
if (!ms.length) {
|
|
1802
|
+
console.log("no messages");
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
for (const m of ms)
|
|
1806
|
+
console.log(`#${m.id} ${m.deliveredAt ? "\u2713" : "\xB7"} ${m.from ?? "?"} \u2192 ${m.task ?? m.toKind}: ${m.text.slice(0, 100)}`);
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
throw new Error("usage: swarm msg send <to> <text\u2026> | swarm msg ls");
|
|
1810
|
+
}
|
|
1663
1811
|
case "audit": {
|
|
1664
1812
|
if (rest[0] !== "export")
|
|
1665
1813
|
throw new Error("usage: swarm audit export [--since 30d] [-p] [--type t] [--format jsonl|csv|json] [--limit n]");
|
|
@@ -2004,6 +2152,33 @@ would have fired (newest last):`);
|
|
|
2004
2152
|
}
|
|
2005
2153
|
break;
|
|
2006
2154
|
}
|
|
2155
|
+
case "demo": {
|
|
2156
|
+
const home = join5(swarmHome(), "demo");
|
|
2157
|
+
const port = "7799";
|
|
2158
|
+
const [cmd2, ...args] = daemonCommand();
|
|
2159
|
+
if (!cmd2)
|
|
2160
|
+
throw new Error("could not resolve the daemon command");
|
|
2161
|
+
const env = { ...process.env, SWARM_HOME: home, SWARM_PORT: port, SWARM_DEMO: "1" };
|
|
2162
|
+
const up = await authedFetch(`http://127.0.0.1:${port}/v1/health`).then((r) => r.ok).catch(() => false);
|
|
2163
|
+
if (!up) {
|
|
2164
|
+
Bun.spawn([cmd2, ...args], {
|
|
2165
|
+
stdin: "ignore",
|
|
2166
|
+
stdout: "ignore",
|
|
2167
|
+
stderr: "ignore",
|
|
2168
|
+
env
|
|
2169
|
+
}).unref();
|
|
2170
|
+
for (let i = 0;i < 40; i++) {
|
|
2171
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2172
|
+
if (await authedFetch(`http://127.0.0.1:${port}/v1/health`).then((r) => r.ok).catch(() => false))
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const tok = readToken(home);
|
|
2177
|
+
const url = `http://127.0.0.1:${port}/${tok ? `?token=${tok}` : ""}`;
|
|
2178
|
+
Bun.spawn(["open", url]).unref?.();
|
|
2179
|
+
console.log(`demo dashboard: http://127.0.0.1:${port} (home ${home} \u2014 delete it to reset)`);
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2007
2182
|
case "ui": {
|
|
2008
2183
|
const base = await ensureDaemon();
|
|
2009
2184
|
const tok = readToken();
|