@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.
@@ -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: "Answers waiting for you",
19956
- description: "Answers a human has given to your swarm_ask questions that you haven't received yet.",
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
- if (!qs.length)
19961
- return ok("no new answers", []);
19962
- return ok(qs.map((q) => `#${q.id} "${q.text}" \u2192 ${q.answeredBy ?? "a human"}: ${q.answer}`).join(`
19963
- `), qs);
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 (!isRecord(gates))
262
+ if (!isRecord2(gates))
210
263
  return out;
211
264
  for (const [name, v] of Object.entries(gates)) {
212
- if (!isRecord(v))
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 isRecord(v) {
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 (!isRecord(a) || !isRecord(b))
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 (!isRecord(v))
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 (!isRecord(cur))
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 (!isRecord(cur[seg]))
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();