@ra3orblade/swarm 0.6.0 → 0.8.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/README.md +63 -10
- package/dist/swarm-hook.js +281 -1
- package/dist/swarm-mcp.js +140 -3
- package/dist/swarm.js +869 -36
- package/dist/swarmd.js +2821 -349
- package/package.json +1 -1
- package/web/app.js +601 -97
- package/web/index.html +73 -3
- package/web/menus.js +1 -1
- package/web/release-notes.js +1 -1
- package/web/table.js +1 -1
package/dist/swarm.js
CHANGED
|
@@ -50,6 +50,22 @@ function readDaemonInfo() {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
var tokenFile = (home = swarmHome()) => join(home, "token");
|
|
54
|
+
function readToken(home = swarmHome()) {
|
|
55
|
+
try {
|
|
56
|
+
const t = readFileSync(tokenFile(home), "utf8").trim();
|
|
57
|
+
return /^[a-f0-9]{64}$/.test(t) ? t : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var authedFetch = (input, init) => {
|
|
63
|
+
const h = new Headers(init?.headers);
|
|
64
|
+
const t = readToken();
|
|
65
|
+
if (t && !h.has("authorization"))
|
|
66
|
+
h.set("authorization", `Bearer ${t}`);
|
|
67
|
+
return fetch(input, { ...init, headers: h });
|
|
68
|
+
};
|
|
53
69
|
function alive(pid) {
|
|
54
70
|
try {
|
|
55
71
|
process.kill(pid, 0);
|
|
@@ -108,7 +124,7 @@ class SwarmClient {
|
|
|
108
124
|
f;
|
|
109
125
|
constructor(opts = {}) {
|
|
110
126
|
this.baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
111
|
-
this.f = opts.fetch ??
|
|
127
|
+
this.f = opts.fetch ?? authedFetch;
|
|
112
128
|
}
|
|
113
129
|
async health() {
|
|
114
130
|
const r = await this.f(`${this.baseUrl}/v1/health`);
|
|
@@ -127,10 +143,9 @@ class SwarmClient {
|
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
// packages/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
import { join as join2 } from "path";
|
|
146
|
+
// packages/core/src/actor.ts
|
|
147
|
+
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
148
|
+
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
134
149
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
135
150
|
var HOOK_EVENTS = [
|
|
136
151
|
"SessionStart",
|
|
@@ -144,22 +159,327 @@ var HOOK_EVENTS = [
|
|
|
144
159
|
"Notification",
|
|
145
160
|
"PreCompact"
|
|
146
161
|
];
|
|
162
|
+
// packages/core/src/audit.ts
|
|
163
|
+
var AUDIT_TYPES = new Set([
|
|
164
|
+
"session.started",
|
|
165
|
+
"session.ended",
|
|
166
|
+
"tool.denied",
|
|
167
|
+
"claim.acquired",
|
|
168
|
+
"claim.renewed",
|
|
169
|
+
"claim.released",
|
|
170
|
+
"claim.expired",
|
|
171
|
+
"claim.orphaned",
|
|
172
|
+
"worktree.created",
|
|
173
|
+
"worktree.removed",
|
|
174
|
+
"worktree.bootstrapped",
|
|
175
|
+
"pr.opened",
|
|
176
|
+
"question.asked",
|
|
177
|
+
"question.answered",
|
|
178
|
+
"dispatch.queued",
|
|
179
|
+
"dispatch.started",
|
|
180
|
+
"dispatch.finished",
|
|
181
|
+
"resource.acquired",
|
|
182
|
+
"resource.released",
|
|
183
|
+
"resource.reaped",
|
|
184
|
+
"process.started",
|
|
185
|
+
"process.exited",
|
|
186
|
+
"gate.recorded",
|
|
187
|
+
"handoff.recorded",
|
|
188
|
+
"permission.requested",
|
|
189
|
+
"permission.resolved",
|
|
190
|
+
"incident.opened",
|
|
191
|
+
"incident.acked",
|
|
192
|
+
"run.result"
|
|
193
|
+
]);
|
|
194
|
+
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
195
|
+
var DEFAULT_PRIVACY = {
|
|
196
|
+
store_prompts: true,
|
|
197
|
+
store_reasoning: true,
|
|
198
|
+
redact: []
|
|
199
|
+
};
|
|
200
|
+
// packages/core/src/budget.ts
|
|
201
|
+
var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
202
|
+
// packages/core/src/config.ts
|
|
203
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
204
|
+
import { join as join2 } from "path";
|
|
205
|
+
var DEFAULT_GATE_TIMEOUT_S = 900;
|
|
206
|
+
var AUTO_MODES = ["session-end", "stop", "off"];
|
|
207
|
+
function parseGateDefs(gates) {
|
|
208
|
+
const out = {};
|
|
209
|
+
if (!isRecord(gates))
|
|
210
|
+
return out;
|
|
211
|
+
for (const [name, v] of Object.entries(gates)) {
|
|
212
|
+
if (!isRecord(v))
|
|
213
|
+
continue;
|
|
214
|
+
const builtin = v.builtin === "review" ? "review" : null;
|
|
215
|
+
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
216
|
+
if (!cmd && !builtin)
|
|
217
|
+
continue;
|
|
218
|
+
if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
|
|
219
|
+
continue;
|
|
220
|
+
const t = Number(v.timeout);
|
|
221
|
+
out[name] = {
|
|
222
|
+
cmd: builtin ? "" : cmd,
|
|
223
|
+
timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
|
|
224
|
+
cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
|
|
225
|
+
builtin,
|
|
226
|
+
model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
var DEFAULT_CONFIG = {
|
|
232
|
+
daemon: { port: 7777, auth: "loopback-optional" },
|
|
233
|
+
tasks: { source: null, labels: [], team: null },
|
|
234
|
+
gates: { required: [], auto: "session-end", defs: {} },
|
|
235
|
+
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
236
|
+
events: { retain_days: 30 },
|
|
237
|
+
audit: { retain_days: 0 },
|
|
238
|
+
privacy: DEFAULT_PRIVACY,
|
|
239
|
+
dispatch: {
|
|
240
|
+
max_parallel: 2,
|
|
241
|
+
permission_mode: null,
|
|
242
|
+
model: null,
|
|
243
|
+
max_turns: null,
|
|
244
|
+
require_pr: true,
|
|
245
|
+
profile: null
|
|
246
|
+
},
|
|
247
|
+
worktree: { setup: null, copy: [], open: null },
|
|
248
|
+
rules: {
|
|
249
|
+
shared_tree: "ask",
|
|
250
|
+
destructive_git: "ask",
|
|
251
|
+
pattern_kill: "ask",
|
|
252
|
+
protected_ports: "ask",
|
|
253
|
+
no_foreign_worktree: "ask",
|
|
254
|
+
claim_required_to_write: "off",
|
|
255
|
+
protected: { ports: [] }
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var MODES = ["ask", "deny", "off"];
|
|
259
|
+
function isRecord(v) {
|
|
260
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
261
|
+
}
|
|
262
|
+
function merge(a, b) {
|
|
263
|
+
if (!isRecord(a) || !isRecord(b))
|
|
264
|
+
return b === undefined ? a : b;
|
|
265
|
+
const out = { ...a };
|
|
266
|
+
for (const [k, v] of Object.entries(b))
|
|
267
|
+
out[k] = merge(a[k], v);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function parseToml(text, source) {
|
|
271
|
+
try {
|
|
272
|
+
return Bun.TOML.parse(text) ?? {};
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error(`swarm: ignoring invalid TOML in ${source}: ${e.message}`);
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function isRepoRelative(f) {
|
|
279
|
+
if (typeof f !== "string")
|
|
280
|
+
return false;
|
|
281
|
+
const t = f.trim();
|
|
282
|
+
if (!t || t.startsWith("/") || t.startsWith("\\") || /^[a-zA-Z]:/.test(t))
|
|
283
|
+
return false;
|
|
284
|
+
return !t.split(/[/\\]/).some((seg) => seg === "..");
|
|
285
|
+
}
|
|
286
|
+
var days = (v, fallback) => {
|
|
287
|
+
const n = Number(v);
|
|
288
|
+
return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
|
|
289
|
+
};
|
|
290
|
+
function validate(c) {
|
|
291
|
+
const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
|
|
292
|
+
const port = Number(c.daemon?.port);
|
|
293
|
+
const source = c.tasks?.source;
|
|
294
|
+
const setup = c.worktree?.setup;
|
|
295
|
+
const opener = c.worktree?.open;
|
|
296
|
+
const rawGates = c.gates;
|
|
297
|
+
const d = c.dispatch ?? {};
|
|
298
|
+
const mp = Number(d.max_parallel);
|
|
299
|
+
const mt = Number(d.max_turns);
|
|
300
|
+
const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
|
|
301
|
+
const b = c.budget ?? {};
|
|
302
|
+
const usd = (v) => {
|
|
303
|
+
const n = Number(v);
|
|
304
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
305
|
+
};
|
|
306
|
+
const warnAt = Number(b.warn_at);
|
|
307
|
+
const auto = rawGates?.auto;
|
|
308
|
+
return {
|
|
309
|
+
...c,
|
|
310
|
+
daemon: {
|
|
311
|
+
port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
|
|
312
|
+
auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
|
|
313
|
+
},
|
|
314
|
+
tasks: {
|
|
315
|
+
source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
|
|
316
|
+
labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
|
|
317
|
+
team: typeof c.tasks?.team === "string" && c.tasks.team.trim() ? c.tasks.team.trim() : null
|
|
318
|
+
},
|
|
319
|
+
gates: {
|
|
320
|
+
required: Array.isArray(rawGates?.required) ? rawGates.required.filter((g) => typeof g === "string" && g.trim() !== "") : [],
|
|
321
|
+
auto: AUTO_MODES.includes(auto) ? auto : "session-end",
|
|
322
|
+
defs: parseGateDefs(rawGates)
|
|
323
|
+
},
|
|
324
|
+
budget: {
|
|
325
|
+
daily: usd(b.daily),
|
|
326
|
+
weekly: usd(b.weekly),
|
|
327
|
+
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
328
|
+
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
329
|
+
},
|
|
330
|
+
events: {
|
|
331
|
+
retain_days: days(c.events?.retain_days, 30)
|
|
332
|
+
},
|
|
333
|
+
audit: {
|
|
334
|
+
retain_days: days(c.audit?.retain_days, 0)
|
|
335
|
+
},
|
|
336
|
+
privacy: {
|
|
337
|
+
store_prompts: c.privacy?.store_prompts !== false,
|
|
338
|
+
store_reasoning: c.privacy?.store_reasoning !== false,
|
|
339
|
+
redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
|
|
340
|
+
},
|
|
341
|
+
dispatch: {
|
|
342
|
+
max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
|
|
343
|
+
permission_mode: str(d.permission_mode),
|
|
344
|
+
model: str(d.model),
|
|
345
|
+
max_turns: Number.isInteger(mt) && mt > 0 ? mt : null,
|
|
346
|
+
require_pr: d.require_pr === undefined ? true : d.require_pr === true,
|
|
347
|
+
profile: ["full", "no-edits", "read-only"].includes(String(d.profile)) ? String(d.profile) : null
|
|
348
|
+
},
|
|
349
|
+
worktree: {
|
|
350
|
+
setup: typeof setup === "string" && setup.trim() ? setup.trim() : null,
|
|
351
|
+
copy: Array.isArray(c.worktree?.copy) ? c.worktree.copy.filter((f) => isRepoRelative(f)) : [],
|
|
352
|
+
open: typeof opener === "string" && opener.trim() ? opener.trim() : null
|
|
353
|
+
},
|
|
354
|
+
rules: {
|
|
355
|
+
...c.rules,
|
|
356
|
+
shared_tree: mode(c.rules?.shared_tree, "ask"),
|
|
357
|
+
destructive_git: mode(c.rules?.destructive_git, "ask"),
|
|
358
|
+
pattern_kill: mode(c.rules?.pattern_kill, "ask"),
|
|
359
|
+
protected_ports: mode(c.rules?.protected_ports, "ask"),
|
|
360
|
+
no_foreign_worktree: mode(c.rules?.no_foreign_worktree, "ask"),
|
|
361
|
+
claim_required_to_write: mode(c.rules?.claim_required_to_write, "off"),
|
|
362
|
+
protected: {
|
|
363
|
+
ports: Array.isArray(c.rules?.protected?.ports) ? c.rules.protected.ports.filter((p) => Number.isInteger(p) && p > 0 && p < 65536) : []
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function leafPaths(v, prefix = "") {
|
|
369
|
+
if (!isRecord(v))
|
|
370
|
+
return prefix ? [prefix] : [];
|
|
371
|
+
const keys = Object.keys(v);
|
|
372
|
+
if (keys.length === 0)
|
|
373
|
+
return prefix ? [prefix] : [];
|
|
374
|
+
return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
|
|
375
|
+
}
|
|
376
|
+
function getPath(v, path) {
|
|
377
|
+
let cur = v;
|
|
378
|
+
for (const seg of path.split(".")) {
|
|
379
|
+
if (!isRecord(cur))
|
|
380
|
+
return;
|
|
381
|
+
cur = cur[seg];
|
|
382
|
+
}
|
|
383
|
+
return cur;
|
|
384
|
+
}
|
|
385
|
+
function setPath(obj, path, value) {
|
|
386
|
+
const segs = path.split(".");
|
|
387
|
+
let cur = obj;
|
|
388
|
+
for (const seg of segs.slice(0, -1)) {
|
|
389
|
+
if (!isRecord(cur[seg]))
|
|
390
|
+
cur[seg] = {};
|
|
391
|
+
cur = cur[seg];
|
|
392
|
+
}
|
|
393
|
+
cur[segs[segs.length - 1]] = value;
|
|
394
|
+
}
|
|
395
|
+
var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
|
|
396
|
+
function readLayer(path) {
|
|
397
|
+
return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
|
|
398
|
+
}
|
|
399
|
+
function loadConfigDetailed(opts = {}) {
|
|
400
|
+
const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
|
|
401
|
+
const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
|
|
402
|
+
const policyRaw = readLayer(policyPath);
|
|
403
|
+
const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
|
|
404
|
+
const policy = { ...policyRaw ?? {} };
|
|
405
|
+
delete policy.locked;
|
|
406
|
+
const layers = [
|
|
407
|
+
["policy", policyRaw ? policy : null],
|
|
408
|
+
["global", readLayer(join2(home, "config.toml"))],
|
|
409
|
+
["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
|
|
410
|
+
];
|
|
411
|
+
const provenance = {};
|
|
412
|
+
for (const p of leafPaths(DEFAULT_CONFIG))
|
|
413
|
+
provenance[p] = "default";
|
|
414
|
+
const overridden = [];
|
|
415
|
+
let cfg = DEFAULT_CONFIG;
|
|
416
|
+
for (const [layer, raw] of layers) {
|
|
417
|
+
if (!raw)
|
|
418
|
+
continue;
|
|
419
|
+
for (const p of leafPaths(raw)) {
|
|
420
|
+
const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
|
|
421
|
+
if (lock)
|
|
422
|
+
overridden.push({ key: p, layer, attempted: getPath(raw, p) });
|
|
423
|
+
else
|
|
424
|
+
provenance[p] = layer;
|
|
425
|
+
}
|
|
426
|
+
cfg = merge(cfg, raw);
|
|
427
|
+
}
|
|
428
|
+
if (overridden.length) {
|
|
429
|
+
const out = structuredClone(cfg);
|
|
430
|
+
for (const { key } of overridden) {
|
|
431
|
+
const fromPolicy = getPath(policy, key);
|
|
432
|
+
setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
|
|
433
|
+
provenance[key] = fromPolicy === undefined ? "default" : "policy";
|
|
434
|
+
}
|
|
435
|
+
cfg = out;
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
config: validate(cfg),
|
|
439
|
+
provenance,
|
|
440
|
+
overridden,
|
|
441
|
+
policy: { path: policyRaw ? policyPath : null, locked }
|
|
442
|
+
};
|
|
443
|
+
}
|
|
147
444
|
// packages/core/src/rules.ts
|
|
148
445
|
var LIVE_WINDOW_MS = 10 * 60000;
|
|
149
446
|
var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
150
447
|
// packages/core/src/ledger.ts
|
|
151
448
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
449
|
+
// packages/core/src/policy.ts
|
|
450
|
+
var HOOK_MARK = "swarm-hook";
|
|
451
|
+
var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
|
|
452
|
+
var MIN_HOOK_TIMEOUT_S = 5;
|
|
453
|
+
function hookCoverage(settings) {
|
|
454
|
+
const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
|
|
455
|
+
const missing = [];
|
|
456
|
+
const short = [];
|
|
457
|
+
for (const ev of HOOK_EVENTS) {
|
|
458
|
+
const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
|
|
459
|
+
const ours = groups.flatMap((g) => {
|
|
460
|
+
const list = g?.hooks;
|
|
461
|
+
return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
|
|
462
|
+
});
|
|
463
|
+
if (!ours.length)
|
|
464
|
+
missing.push(ev);
|
|
465
|
+
else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
|
|
466
|
+
short.push(ev);
|
|
467
|
+
}
|
|
468
|
+
return { missing, short, complete: !missing.length && !short.length };
|
|
469
|
+
}
|
|
152
470
|
// packages/cli/src/install.ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
var
|
|
471
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
472
|
+
import { homedir as homedir2 } from "os";
|
|
473
|
+
import { join as join3 } from "path";
|
|
474
|
+
var isOurs = hookIsOurs;
|
|
475
|
+
var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join3(homedir2(), ".claude", "settings.json");
|
|
476
|
+
var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join3(homedir2(), ".claude.json");
|
|
157
477
|
function loadClaudeJson() {
|
|
158
478
|
const p = claudeJsonPath();
|
|
159
|
-
if (!
|
|
479
|
+
if (!existsSync4(p))
|
|
160
480
|
return {};
|
|
161
481
|
try {
|
|
162
|
-
return JSON.parse(
|
|
482
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
163
483
|
} catch {
|
|
164
484
|
return {};
|
|
165
485
|
}
|
|
@@ -192,6 +512,95 @@ function mcpRegistered() {
|
|
|
192
512
|
const c = loadClaudeJson();
|
|
193
513
|
return Boolean(c.mcpServers?.swarm);
|
|
194
514
|
}
|
|
515
|
+
var codexConfigPath = () => process.env.CODEX_CONFIG ?? join3(homedir2(), ".codex", "config.toml");
|
|
516
|
+
var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join3(homedir2(), ".gemini", "settings.json");
|
|
517
|
+
function codexBlock() {
|
|
518
|
+
const { command, args } = mcpServerConfig();
|
|
519
|
+
return `[mcp_servers.swarm]
|
|
520
|
+
command = ${JSON.stringify(command)}
|
|
521
|
+
args = ${JSON.stringify(args)}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
var CODEX_BLOCK_RE = /\[mcp_servers\.swarm\]\n(?:(?!\[)[^\n]*\n?)*/;
|
|
525
|
+
function registerCodex() {
|
|
526
|
+
const p = codexConfigPath();
|
|
527
|
+
if (!existsSync4(join3(p, "..")))
|
|
528
|
+
return false;
|
|
529
|
+
const cur = existsSync4(p) ? readFileSync3(p, "utf8") : "";
|
|
530
|
+
const next = CODEX_BLOCK_RE.test(cur) ? cur.replace(CODEX_BLOCK_RE, codexBlock()) : `${cur.trimEnd()}${cur.trim() ? `
|
|
531
|
+
|
|
532
|
+
` : ""}${codexBlock()}`;
|
|
533
|
+
if (next !== cur)
|
|
534
|
+
writeFileSync2(p, next);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
function unregisterCodex() {
|
|
538
|
+
const p = codexConfigPath();
|
|
539
|
+
if (!existsSync4(p))
|
|
540
|
+
return false;
|
|
541
|
+
const cur = readFileSync3(p, "utf8");
|
|
542
|
+
if (!CODEX_BLOCK_RE.test(cur))
|
|
543
|
+
return false;
|
|
544
|
+
writeFileSync2(p, cur.replace(CODEX_BLOCK_RE, "").replace(/\n{3,}/g, `
|
|
545
|
+
|
|
546
|
+
`).trimEnd().concat(`
|
|
547
|
+
`));
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
function registerGemini() {
|
|
551
|
+
const p = geminiSettingsPath();
|
|
552
|
+
if (!existsSync4(join3(p, "..")))
|
|
553
|
+
return false;
|
|
554
|
+
let c = {};
|
|
555
|
+
try {
|
|
556
|
+
c = existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
|
|
557
|
+
} catch {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const mcp = c.mcpServers ?? {};
|
|
561
|
+
mcp.swarm = mcpServerConfig();
|
|
562
|
+
c.mcpServers = mcp;
|
|
563
|
+
writeFileSync2(p, `${JSON.stringify(c, null, 2)}
|
|
564
|
+
`);
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
function unregisterGemini() {
|
|
568
|
+
const p = geminiSettingsPath();
|
|
569
|
+
if (!existsSync4(p))
|
|
570
|
+
return false;
|
|
571
|
+
try {
|
|
572
|
+
const c = JSON.parse(readFileSync3(p, "utf8"));
|
|
573
|
+
const mcp = c.mcpServers ?? {};
|
|
574
|
+
if (!mcp.swarm)
|
|
575
|
+
return false;
|
|
576
|
+
delete mcp.swarm;
|
|
577
|
+
if (Object.keys(mcp).length)
|
|
578
|
+
c.mcpServers = mcp;
|
|
579
|
+
else
|
|
580
|
+
delete c.mcpServers;
|
|
581
|
+
writeFileSync2(p, `${JSON.stringify(c, null, 2)}
|
|
582
|
+
`);
|
|
583
|
+
return true;
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function registerOtherAgents() {
|
|
589
|
+
const out = [];
|
|
590
|
+
if (registerCodex())
|
|
591
|
+
out.push("codex");
|
|
592
|
+
if (registerGemini())
|
|
593
|
+
out.push("gemini");
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
function unregisterOtherAgents() {
|
|
597
|
+
const out = [];
|
|
598
|
+
if (unregisterCodex())
|
|
599
|
+
out.push("codex");
|
|
600
|
+
if (unregisterGemini())
|
|
601
|
+
out.push("gemini");
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
195
604
|
var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
|
|
196
605
|
var shimPath = () => resolveBin("swarm-hook").at(-1);
|
|
197
606
|
function mcpServerConfig() {
|
|
@@ -200,7 +609,7 @@ function mcpServerConfig() {
|
|
|
200
609
|
}
|
|
201
610
|
function load() {
|
|
202
611
|
const p = settingsPath();
|
|
203
|
-
return
|
|
612
|
+
return existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
|
|
204
613
|
}
|
|
205
614
|
function save(s) {
|
|
206
615
|
writeFileSync2(settingsPath(), `${JSON.stringify(s, null, 2)}
|
|
@@ -230,6 +639,7 @@ function install() {
|
|
|
230
639
|
}
|
|
231
640
|
save(s);
|
|
232
641
|
registerMcp();
|
|
642
|
+
registerOtherAgents();
|
|
233
643
|
return added;
|
|
234
644
|
}
|
|
235
645
|
function uninstall() {
|
|
@@ -267,20 +677,37 @@ function uninstall() {
|
|
|
267
677
|
save(s);
|
|
268
678
|
if (unregisterMcp())
|
|
269
679
|
removed++;
|
|
680
|
+
removed += unregisterOtherAgents().length;
|
|
270
681
|
return removed;
|
|
271
682
|
}
|
|
272
683
|
function status() {
|
|
273
684
|
const s = load();
|
|
274
685
|
const hooks2 = s.hooks ?? {};
|
|
275
686
|
const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
|
|
276
|
-
|
|
687
|
+
const otherAgents = [];
|
|
688
|
+
const cp = codexConfigPath();
|
|
689
|
+
if (existsSync4(cp) && CODEX_BLOCK_RE.test(readFileSync3(cp, "utf8")))
|
|
690
|
+
otherAgents.push("codex");
|
|
691
|
+
const gp = geminiSettingsPath();
|
|
692
|
+
try {
|
|
693
|
+
if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
|
|
694
|
+
otherAgents.push("gemini");
|
|
695
|
+
} catch {}
|
|
696
|
+
return {
|
|
697
|
+
installed,
|
|
698
|
+
coverage: hookCoverage(s),
|
|
699
|
+
mcp: mcpRegistered(),
|
|
700
|
+
path: settingsPath(),
|
|
701
|
+
shim: shimPath(),
|
|
702
|
+
otherAgents
|
|
703
|
+
};
|
|
277
704
|
}
|
|
278
705
|
|
|
279
706
|
// packages/cli/src/procs.ts
|
|
280
707
|
import { mkdirSync as mkdirSync2, openSync } from "fs";
|
|
281
|
-
import { join as
|
|
708
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
282
709
|
async function call(path, init) {
|
|
283
|
-
const r = await
|
|
710
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}${path}`, init);
|
|
284
711
|
return await r.json();
|
|
285
712
|
}
|
|
286
713
|
var post = (path, body) => call(path, {
|
|
@@ -308,9 +735,9 @@ async function start(opts) {
|
|
|
308
735
|
port = a.port;
|
|
309
736
|
}
|
|
310
737
|
}
|
|
311
|
-
const logDir =
|
|
738
|
+
const logDir = join4(swarmHome(), "logs", slug(pid));
|
|
312
739
|
mkdirSync2(logDir, { recursive: true });
|
|
313
|
-
const log =
|
|
740
|
+
const log = join4(logDir, `${slug(opts.name)}.log`);
|
|
314
741
|
const fd = openSync(log, "a");
|
|
315
742
|
const cmdline = opts.cmd.join(" ");
|
|
316
743
|
const child = Bun.spawn(["sh", "-c", cmdline], {
|
|
@@ -375,6 +802,14 @@ function fmt(r) {
|
|
|
375
802
|
}
|
|
376
803
|
|
|
377
804
|
// packages/cli/src/bin.ts
|
|
805
|
+
function gitToplevel() {
|
|
806
|
+
const r = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
|
|
807
|
+
stdout: "pipe",
|
|
808
|
+
stderr: "ignore"
|
|
809
|
+
});
|
|
810
|
+
const out = r.exitCode === 0 ? r.stdout.toString().trim() : "";
|
|
811
|
+
return out || null;
|
|
812
|
+
}
|
|
378
813
|
var [cmd = "help", ...rest] = process.argv.slice(2);
|
|
379
814
|
var json = rest.includes("--json");
|
|
380
815
|
var arg = () => rest.find((a) => !a.startsWith("--"));
|
|
@@ -391,12 +826,17 @@ var help = `swarm \u2014 control plane for AI-agent development
|
|
|
391
826
|
tail [--project p] [--session id] follow the live event stream
|
|
392
827
|
|
|
393
828
|
claim <task> [--owner n] claim a task in a fresh isolated git worktree (fail-closed)
|
|
829
|
+
gate run <task> [gate\u2026] execute the repo's [gates.<name>] cmd gates in the task's worktree and record them
|
|
830
|
+
wt [ls|create|open|diff|rm|gc] first-class worktrees: create task-less ones, open, diff, remove, collect stale
|
|
831
|
+
pr open <task|worktree> push the branch and open a PR/MR prefilled from the task, handoff, gates and files
|
|
832
|
+
questions [--all] questions agents are waiting on a human for (this repo); answer <id> <text\u2026>
|
|
833
|
+
dispatch --ready | <task\u2026> claim + spawn a run per task, [dispatch] max_parallel at a time; status | clear
|
|
394
834
|
renew <task> extend the lease; release <task> [--force] release + remove worktree
|
|
395
835
|
claims list claims; reap release abandoned claims (keeps ones holding work)
|
|
396
836
|
tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
|
|
397
837
|
gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
|
|
398
838
|
gate ls [task] latest verdict per gate (and the run history for one task)
|
|
399
|
-
run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--allowed-tools a,b] [--max-turns n]
|
|
839
|
+
run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--profile full|no-edits|read-only] [--allowed-tools a,b] [--max-turns n]
|
|
400
840
|
claim the task and spawn claude -p in its worktree; the session shows in Fleet
|
|
401
841
|
run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
|
|
402
842
|
run resume <session-id> [--model m] [--permission-mode m] spawn a run that picks up where a dead session stopped (its handoff + tail)
|
|
@@ -411,13 +851,14 @@ var help = `swarm \u2014 control plane for AI-agent development
|
|
|
411
851
|
stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
|
|
412
852
|
search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
|
|
413
853
|
rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
|
|
854
|
+
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
|
|
414
855
|
|
|
415
856
|
install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
|
|
416
857
|
|
|
417
858
|
Env: SWARM_URL, SWARM_PORT (default 7777), SWARM_HOME (~/.swarm)`;
|
|
418
859
|
async function api(path, init) {
|
|
419
860
|
const base = new SwarmClient().baseUrl;
|
|
420
|
-
const r = await
|
|
861
|
+
const r = await authedFetch(`${base}${path}`, init);
|
|
421
862
|
if (!r.ok)
|
|
422
863
|
throw new Error(`${path}: ${r.status} ${await r.text()}`);
|
|
423
864
|
return r.status === 204 ? null : r.json();
|
|
@@ -447,7 +888,7 @@ try {
|
|
|
447
888
|
const base = await ensureDaemon();
|
|
448
889
|
const evs = install();
|
|
449
890
|
console.log(`\u2713 daemon running at ${base}`);
|
|
450
|
-
console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})`);
|
|
891
|
+
console.log(`\u2713 installed hooks for ${evs.length} events + MCP server (${status().path})${status().otherAgents.length ? ` \xB7 MCP also for ${status().otherAgents.join(", ")}` : ""}`);
|
|
451
892
|
console.log("\u2713 any Claude session you start now will appear in Swarm");
|
|
452
893
|
Bun.spawn(["open", base]).unref?.();
|
|
453
894
|
console.log(`
|
|
@@ -490,8 +931,27 @@ Open the dashboard: ${base}`);
|
|
|
490
931
|
line(Boolean(bun), `bun ${bun ? `(${bun})` : ""}`, "install bun: https://bun.sh");
|
|
491
932
|
line(Boolean(claude), "claude CLI on PATH", "install Claude Code: https://claude.com/claude-code");
|
|
492
933
|
line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
|
|
934
|
+
if (running) {
|
|
935
|
+
const h = await authedFetch(`${resolveBaseUrl()}/v1/health`).then((r) => r.json()).catch(() => null);
|
|
936
|
+
if (h)
|
|
937
|
+
console.log(`\xB7 daemon v${h.version ?? "?"} \xB7 schema v${h.schema ?? 0}`);
|
|
938
|
+
}
|
|
493
939
|
line(st.installed, "hooks installed", "run: swarm install");
|
|
940
|
+
if (st.installed && !st.coverage.complete) {
|
|
941
|
+
if (st.coverage.missing.length)
|
|
942
|
+
line(false, `hooks missing: ${st.coverage.missing.join(", ")}`, "run: swarm install");
|
|
943
|
+
if (st.coverage.short.length)
|
|
944
|
+
line(false, `hook timeout too short: ${st.coverage.short.join(", ")}`, "run: swarm install");
|
|
945
|
+
}
|
|
946
|
+
const pol = loadConfigDetailed({ repoRoot: gitToplevel() });
|
|
947
|
+
if (pol.policy.path) {
|
|
948
|
+
line(true, `policy ${pol.policy.path} (locked: ${pol.policy.locked.join(", ") || "nothing"})`, "");
|
|
949
|
+
for (const o of pol.overridden)
|
|
950
|
+
line(false, `${o.layer} config overrides locked ${o.key}`, `remove it \u2014 policy value stays in effect`);
|
|
951
|
+
}
|
|
494
952
|
line(st.mcp, "MCP server registered", "run: swarm install");
|
|
953
|
+
if (st.otherAgents.length)
|
|
954
|
+
console.log(`\u2713 MCP server also registered for ${st.otherAgents.join(", ")} (swarm_* tools in those CLIs too)`);
|
|
495
955
|
const forge2 = (bin, auth) => {
|
|
496
956
|
const path = Bun.which(bin);
|
|
497
957
|
if (!path)
|
|
@@ -536,7 +996,7 @@ url: ${resolveBaseUrl()}`);
|
|
|
536
996
|
headers: { "content-type": "application/json" },
|
|
537
997
|
body: JSON.stringify({ path: resolve3(".") })
|
|
538
998
|
});
|
|
539
|
-
const r = await
|
|
999
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims`, {
|
|
540
1000
|
method: "POST",
|
|
541
1001
|
headers: { "content-type": "application/json" },
|
|
542
1002
|
body: JSON.stringify({ projectId: proj.id, task, owner })
|
|
@@ -545,7 +1005,8 @@ url: ${resolveBaseUrl()}`);
|
|
|
545
1005
|
console.log(JSON.stringify(r));
|
|
546
1006
|
else if (r.ok)
|
|
547
1007
|
console.log(`claimed ${task} \u2192 ${r.worktree}
|
|
548
|
-
cd ${r.worktree}`
|
|
1008
|
+
cd ${r.worktree}${r.bootstrap ? `
|
|
1009
|
+
bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
|
|
549
1010
|
else {
|
|
550
1011
|
console.error(`REFUSED: ${r.error}`);
|
|
551
1012
|
process.exit(1);
|
|
@@ -563,7 +1024,7 @@ url: ${resolveBaseUrl()}`);
|
|
|
563
1024
|
headers: { "content-type": "application/json" },
|
|
564
1025
|
body: JSON.stringify({ path: resolve3(".") })
|
|
565
1026
|
});
|
|
566
|
-
const r = await
|
|
1027
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
|
|
567
1028
|
method: "POST",
|
|
568
1029
|
headers: { "content-type": "application/json" },
|
|
569
1030
|
body: JSON.stringify({ projectId: proj.id, task, force: rest.includes("--force") })
|
|
@@ -578,6 +1039,317 @@ url: ${resolveBaseUrl()}`);
|
|
|
578
1039
|
}
|
|
579
1040
|
break;
|
|
580
1041
|
}
|
|
1042
|
+
case "wt": {
|
|
1043
|
+
await ensureDaemon({ quiet: true });
|
|
1044
|
+
const sub = arg();
|
|
1045
|
+
const proj = await api("/v1/projects", {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
headers: { "content-type": "application/json" },
|
|
1048
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1049
|
+
});
|
|
1050
|
+
const post2 = (path, body) => authedFetch(`${new SwarmClient().baseUrl}${path}`, {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers: { "content-type": "application/json" },
|
|
1053
|
+
body: JSON.stringify({ projectId: proj.id, ...body })
|
|
1054
|
+
}).then((x) => x.json());
|
|
1055
|
+
const refuse = (r) => {
|
|
1056
|
+
if (json)
|
|
1057
|
+
console.log(JSON.stringify(r));
|
|
1058
|
+
else if (!r.ok) {
|
|
1059
|
+
console.error(`REFUSED: ${r.error}`);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
return !r.ok;
|
|
1063
|
+
};
|
|
1064
|
+
const flag = (n) => {
|
|
1065
|
+
const i = rest.indexOf(n);
|
|
1066
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
1067
|
+
};
|
|
1068
|
+
const VALUE_FLAGS = ["--base", "--branch"];
|
|
1069
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
|
|
1070
|
+
const target = positional[1];
|
|
1071
|
+
switch (sub) {
|
|
1072
|
+
case "create": {
|
|
1073
|
+
if (!target)
|
|
1074
|
+
throw new Error("usage: swarm wt create <name> [--base ref] [--branch name]");
|
|
1075
|
+
const r = await post2("/v1/worktrees", {
|
|
1076
|
+
name: target,
|
|
1077
|
+
baseRef: flag("--base"),
|
|
1078
|
+
branch: flag("--branch")
|
|
1079
|
+
});
|
|
1080
|
+
if (refuse(r) || json)
|
|
1081
|
+
break;
|
|
1082
|
+
console.log(`created ${r.name} \u2192 ${r.worktree} (branch ${r.branch})
|
|
1083
|
+
cd ${r.worktree}${r.bootstrap ? `
|
|
1084
|
+
bootstrapping in the background (setup log: ${r.bootstrap})` : ""}`);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
case "ls":
|
|
1088
|
+
case undefined: {
|
|
1089
|
+
const wts = await api(`/v1/worktrees?project=${proj.id}`);
|
|
1090
|
+
if (json)
|
|
1091
|
+
console.log(JSON.stringify(wts));
|
|
1092
|
+
else {
|
|
1093
|
+
for (const w of wts) {
|
|
1094
|
+
const st = w.main ? "main" : [
|
|
1095
|
+
w.dirty > 0 ? `${w.dirty} dirty` : "",
|
|
1096
|
+
w.ahead > 0 ? `${w.ahead} unpushed` : "",
|
|
1097
|
+
w.behind > 0 ? `${w.behind} behind` : "",
|
|
1098
|
+
w.merged ? "merged" : ""
|
|
1099
|
+
].filter(Boolean).join(", ") || "clean";
|
|
1100
|
+
console.log(`${(w.branch ?? "(detached)").padEnd(32)} ${w.head} ${st.padEnd(24)} ${w.path}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (!wts.length)
|
|
1103
|
+
console.log("no worktrees");
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "open": {
|
|
1108
|
+
if (!target)
|
|
1109
|
+
throw new Error("usage: swarm wt open <name|path>");
|
|
1110
|
+
const r = await post2("/v1/worktrees/open", { worktree: target });
|
|
1111
|
+
if (refuse(r) || json)
|
|
1112
|
+
break;
|
|
1113
|
+
console.log(`opened ${r.worktree}`);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case "rm": {
|
|
1117
|
+
if (!target)
|
|
1118
|
+
throw new Error("usage: swarm wt rm <name|path> [--force]");
|
|
1119
|
+
const r = await post2("/v1/worktrees/remove", {
|
|
1120
|
+
worktree: target,
|
|
1121
|
+
force: rest.includes("--force")
|
|
1122
|
+
});
|
|
1123
|
+
if (refuse(r) || json)
|
|
1124
|
+
break;
|
|
1125
|
+
console.log(`removed ${r.worktree}`);
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
case "diff": {
|
|
1129
|
+
if (!target)
|
|
1130
|
+
throw new Error("usage: swarm wt diff <name|path|task> [--file f] [--patch]");
|
|
1131
|
+
const q = new URLSearchParams({ project: proj.id, worktree: target });
|
|
1132
|
+
const file = flag("--file");
|
|
1133
|
+
if (file)
|
|
1134
|
+
q.set("file", file);
|
|
1135
|
+
if (rest.includes("--patch"))
|
|
1136
|
+
q.set("patch", "1");
|
|
1137
|
+
const d = await api(`/v1/worktrees/diff?${q}`);
|
|
1138
|
+
if (d.error) {
|
|
1139
|
+
console.error(`REFUSED: ${d.error}`);
|
|
1140
|
+
process.exit(1);
|
|
1141
|
+
}
|
|
1142
|
+
if (json)
|
|
1143
|
+
console.log(JSON.stringify(d));
|
|
1144
|
+
else if (d.patch !== undefined)
|
|
1145
|
+
console.log(d.patch);
|
|
1146
|
+
else {
|
|
1147
|
+
console.log(`vs ${d.baseRef ?? "HEAD"} \xB7 ${d.commits.length} commit${d.commits.length === 1 ? "" : "s"} \xB7 ${d.files.length} file${d.files.length === 1 ? "" : "s"}${d.dirty ? " \xB7 dirty" : ""}`);
|
|
1148
|
+
for (const c of d.commits)
|
|
1149
|
+
console.log(` ${c}`);
|
|
1150
|
+
for (const f of d.files)
|
|
1151
|
+
console.log(`${f.status} ${f.added >= 0 ? `+${f.added}`.padStart(6) : " bin"} ${f.deleted >= 0 ? `-${f.deleted}`.padStart(6) : " "} ${f.path}`);
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
case "gc": {
|
|
1156
|
+
const r = await post2("/v1/worktrees/gc", { apply: rest.includes("--apply") });
|
|
1157
|
+
if (json)
|
|
1158
|
+
console.log(JSON.stringify(r));
|
|
1159
|
+
else if (!r.candidates.length)
|
|
1160
|
+
console.log("nothing to collect");
|
|
1161
|
+
else {
|
|
1162
|
+
for (const x of r.candidates)
|
|
1163
|
+
console.log(`${r.removed.includes(x.path) ? "removed " : x.removable ? "removable" : `blocked (${x.blocker})`} ${x.why.padEnd(15)} ${x.branch ?? "(detached)"} ${x.path}`);
|
|
1164
|
+
if (!rest.includes("--apply") && r.candidates.some((x) => x.removable))
|
|
1165
|
+
console.log("\nrun `swarm wt gc --apply` to remove the removable ones");
|
|
1166
|
+
}
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
default:
|
|
1170
|
+
throw new Error("usage: swarm wt [ls] | create <name> | open <ref> | rm <ref> [--force] | gc [--apply]");
|
|
1171
|
+
}
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
case "questions":
|
|
1175
|
+
case "answer": {
|
|
1176
|
+
await ensureDaemon({ quiet: true });
|
|
1177
|
+
if (cmd === "questions") {
|
|
1178
|
+
const q = new URLSearchParams;
|
|
1179
|
+
if (!rest.includes("--all"))
|
|
1180
|
+
q.set("open", "1");
|
|
1181
|
+
if (!rest.includes("--everywhere")) {
|
|
1182
|
+
const proj = await api("/v1/projects", {
|
|
1183
|
+
method: "POST",
|
|
1184
|
+
headers: { "content-type": "application/json" },
|
|
1185
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1186
|
+
});
|
|
1187
|
+
q.set("project", proj.id);
|
|
1188
|
+
}
|
|
1189
|
+
const qs = await api(`/v1/questions?${q}`);
|
|
1190
|
+
if (json)
|
|
1191
|
+
console.log(JSON.stringify(qs));
|
|
1192
|
+
else if (!qs.length)
|
|
1193
|
+
console.log("no open questions");
|
|
1194
|
+
else
|
|
1195
|
+
for (const x of qs)
|
|
1196
|
+
console.log(`#${x.id} ${x.createdAt.slice(0, 16).replace("T", " ")} ${x.task ? `[${x.task}] ` : ""}${x.text}${x.options.length ? `
|
|
1197
|
+
options: ${x.options.join(" | ")}` : ""}${x.answer ? `
|
|
1198
|
+
answered by ${x.answeredBy}: ${x.answer}` : ""}`);
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const [idRaw, ...words] = rest.filter((a) => !a.startsWith("--"));
|
|
1202
|
+
const id = Number(idRaw);
|
|
1203
|
+
const text = words.join(" ");
|
|
1204
|
+
if (!id || !text)
|
|
1205
|
+
throw new Error("usage: swarm answer <id> <text\u2026>");
|
|
1206
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
|
|
1207
|
+
method: "POST",
|
|
1208
|
+
headers: { "content-type": "application/json" },
|
|
1209
|
+
body: JSON.stringify({ text, by: process.env.USER ?? "cli" })
|
|
1210
|
+
}).then((x) => x.json());
|
|
1211
|
+
if (json)
|
|
1212
|
+
console.log(JSON.stringify(r));
|
|
1213
|
+
else if (r.ok)
|
|
1214
|
+
console.log(`answered #${id}`);
|
|
1215
|
+
else {
|
|
1216
|
+
console.error(`REFUSED: ${r.error}`);
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
case "dispatch": {
|
|
1222
|
+
await ensureDaemon({ quiet: true });
|
|
1223
|
+
const proj = await api("/v1/projects", {
|
|
1224
|
+
method: "POST",
|
|
1225
|
+
headers: { "content-type": "application/json" },
|
|
1226
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1227
|
+
});
|
|
1228
|
+
const VALUE_FLAGS = [
|
|
1229
|
+
"--max",
|
|
1230
|
+
"--parallel",
|
|
1231
|
+
"--model",
|
|
1232
|
+
"--permission-mode",
|
|
1233
|
+
"--max-turns",
|
|
1234
|
+
"--profile"
|
|
1235
|
+
];
|
|
1236
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
|
|
1237
|
+
const flag = (n) => {
|
|
1238
|
+
const i = rest.indexOf(n);
|
|
1239
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
1240
|
+
};
|
|
1241
|
+
const num = (n) => {
|
|
1242
|
+
const v = flag(n);
|
|
1243
|
+
return v ? Number(v) : undefined;
|
|
1244
|
+
};
|
|
1245
|
+
const sub = positional[0];
|
|
1246
|
+
if (sub === "status" || !sub && !rest.includes("--ready")) {
|
|
1247
|
+
const d = await api(`/v1/dispatch?project=${proj.id}`);
|
|
1248
|
+
if (json)
|
|
1249
|
+
console.log(JSON.stringify(d));
|
|
1250
|
+
else if (!d.entries.length)
|
|
1251
|
+
console.log(`nothing dispatched (max_parallel ${d.config.max_parallel}); swarm dispatch --ready | <task\u2026>`);
|
|
1252
|
+
else
|
|
1253
|
+
for (const e of d.entries)
|
|
1254
|
+
console.log(`${(e.state === "finished" ? e.outcome ?? "?" : e.state).padEnd(13)} ${e.task.padEnd(10)} ${e.runId ? `run ${e.runId} ` : ""}${e.costUsd != null ? `$${e.costUsd.toFixed(2)} ` : ""}${e.detail ?? ""}`);
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
if (sub === "clear") {
|
|
1258
|
+
const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
|
|
1259
|
+
method: "DELETE",
|
|
1260
|
+
headers: { "content-type": "application/json" },
|
|
1261
|
+
body: JSON.stringify({ projectId: proj.id, task: positional[1] })
|
|
1262
|
+
}).then((x) => x.json());
|
|
1263
|
+
console.log(json ? JSON.stringify(r2) : `cleared ${r2.cleared}`);
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
|
|
1267
|
+
method: "POST",
|
|
1268
|
+
headers: { "content-type": "application/json" },
|
|
1269
|
+
body: JSON.stringify({
|
|
1270
|
+
projectId: proj.id,
|
|
1271
|
+
ready: rest.includes("--ready"),
|
|
1272
|
+
tasks: positional,
|
|
1273
|
+
max: num("--max"),
|
|
1274
|
+
maxParallel: num("--parallel"),
|
|
1275
|
+
model: flag("--model"),
|
|
1276
|
+
permissionMode: flag("--permission-mode"),
|
|
1277
|
+
maxTurns: num("--max-turns"),
|
|
1278
|
+
profile: flag("--profile"),
|
|
1279
|
+
owner: process.env.USER ?? "cli"
|
|
1280
|
+
})
|
|
1281
|
+
}).then((x) => x.json());
|
|
1282
|
+
if (json)
|
|
1283
|
+
console.log(JSON.stringify(r));
|
|
1284
|
+
else if (!r.ok) {
|
|
1285
|
+
console.error(`REFUSED: ${r.error}`);
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
} else {
|
|
1288
|
+
for (const t of r.started)
|
|
1289
|
+
console.log(`started ${t}`);
|
|
1290
|
+
for (const t of r.queued)
|
|
1291
|
+
console.log(`queued ${t}`);
|
|
1292
|
+
for (const x of r.rejected)
|
|
1293
|
+
console.log(`rejected ${x.id} \u2014 ${x.reason}`);
|
|
1294
|
+
if (!r.started.length && !r.queued.length)
|
|
1295
|
+
console.log("nothing to dispatch");
|
|
1296
|
+
else
|
|
1297
|
+
console.log(`
|
|
1298
|
+
watch: swarm dispatch status \xB7 swarm run ls \xB7 the Board`);
|
|
1299
|
+
}
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
case "pr": {
|
|
1303
|
+
await ensureDaemon({ quiet: true });
|
|
1304
|
+
const sub = arg();
|
|
1305
|
+
const VALUE_FLAGS = ["--title", "--body"];
|
|
1306
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && !VALUE_FLAGS.includes(rest[i - 1] ?? ""));
|
|
1307
|
+
const flag = (n) => {
|
|
1308
|
+
const i = rest.indexOf(n);
|
|
1309
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
1310
|
+
};
|
|
1311
|
+
const target = positional[1];
|
|
1312
|
+
if (sub !== "open" || !target)
|
|
1313
|
+
throw new Error("usage: swarm pr open <task|worktree> [--title t] [--body b] [--draft] [--dry-run]");
|
|
1314
|
+
const proj = await api("/v1/projects", {
|
|
1315
|
+
method: "POST",
|
|
1316
|
+
headers: { "content-type": "application/json" },
|
|
1317
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1318
|
+
});
|
|
1319
|
+
if (rest.includes("--dry-run")) {
|
|
1320
|
+
const d = await api(`/v1/prs/draft?project=${proj.id}&worktree=${encodeURIComponent(target)}`);
|
|
1321
|
+
if (json)
|
|
1322
|
+
console.log(JSON.stringify(d));
|
|
1323
|
+
else if (!d.ok) {
|
|
1324
|
+
console.error(`REFUSED: ${d.error}`);
|
|
1325
|
+
process.exit(1);
|
|
1326
|
+
} else
|
|
1327
|
+
console.log(`${flag("--title") ?? d.title}
|
|
1328
|
+
|
|
1329
|
+
${flag("--body") ?? d.body}`);
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
|
|
1333
|
+
method: "POST",
|
|
1334
|
+
headers: { "content-type": "application/json" },
|
|
1335
|
+
body: JSON.stringify({
|
|
1336
|
+
projectId: proj.id,
|
|
1337
|
+
worktree: target,
|
|
1338
|
+
title: flag("--title"),
|
|
1339
|
+
body: flag("--body"),
|
|
1340
|
+
draft: rest.includes("--draft")
|
|
1341
|
+
})
|
|
1342
|
+
}).then((x) => x.json());
|
|
1343
|
+
if (json)
|
|
1344
|
+
console.log(JSON.stringify(r));
|
|
1345
|
+
else if (r.ok)
|
|
1346
|
+
console.log(`opened ${r.url}`);
|
|
1347
|
+
else {
|
|
1348
|
+
console.error(`REFUSED: ${r.error}`);
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
581
1353
|
case "reap": {
|
|
582
1354
|
await ensureDaemon({ quiet: true });
|
|
583
1355
|
const r = await api("/v1/claims/reap", { method: "POST" });
|
|
@@ -618,7 +1390,8 @@ url: ${resolveBaseUrl()}`);
|
|
|
618
1390
|
"--permission-mode",
|
|
619
1391
|
"--allowed-tools",
|
|
620
1392
|
"--max-turns",
|
|
621
|
-
"--owner"
|
|
1393
|
+
"--owner",
|
|
1394
|
+
"--profile"
|
|
622
1395
|
]);
|
|
623
1396
|
const positionals = [];
|
|
624
1397
|
for (let i = 0;i < rest.length; i++) {
|
|
@@ -648,11 +1421,11 @@ url: ${resolveBaseUrl()}`);
|
|
|
648
1421
|
const target = positionals[1];
|
|
649
1422
|
if (!target)
|
|
650
1423
|
throw new Error(`usage: swarm run ${sub} <task|id>${sub === "send" ? ' "text"' : ""}`);
|
|
651
|
-
const r2 = sub === "send" ? await
|
|
1424
|
+
const r2 = sub === "send" ? await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
|
|
652
1425
|
method: "POST",
|
|
653
1426
|
headers: { "content-type": "application/json" },
|
|
654
1427
|
body: JSON.stringify({ text: positionals.slice(2).join(" ") })
|
|
655
|
-
}) : await
|
|
1428
|
+
}) : await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
|
|
656
1429
|
const j = await r2.json();
|
|
657
1430
|
if (json)
|
|
658
1431
|
console.log(JSON.stringify(j));
|
|
@@ -673,8 +1446,8 @@ url: ${resolveBaseUrl()}`);
|
|
|
673
1446
|
if (!prompt && pf)
|
|
674
1447
|
prompt = await Bun.file(resolve3(pf)).text();
|
|
675
1448
|
if (!task || !prompt)
|
|
676
|
-
throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--allowed-tools a,b] [--max-turns n]');
|
|
677
|
-
const r = await
|
|
1449
|
+
throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--profile p] [--allowed-tools a,b] [--max-turns n]');
|
|
1450
|
+
const r = await authedFetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
|
|
678
1451
|
method: "POST",
|
|
679
1452
|
headers: { "content-type": "application/json" },
|
|
680
1453
|
body: JSON.stringify({
|
|
@@ -685,7 +1458,8 @@ url: ${resolveBaseUrl()}`);
|
|
|
685
1458
|
model: flag("--model"),
|
|
686
1459
|
permissionMode: flag("--permission-mode"),
|
|
687
1460
|
allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
688
|
-
maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
|
|
1461
|
+
maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined,
|
|
1462
|
+
profile: flag("--profile")
|
|
689
1463
|
})
|
|
690
1464
|
}).then((x) => x.json());
|
|
691
1465
|
if (json)
|
|
@@ -714,7 +1488,7 @@ url: ${resolveBaseUrl()}`);
|
|
|
714
1488
|
body: JSON.stringify({ path: resolve3(".") })
|
|
715
1489
|
});
|
|
716
1490
|
if (cmd === "resume") {
|
|
717
|
-
const r2 = await
|
|
1491
|
+
const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
|
|
718
1492
|
const j = await r2.json();
|
|
719
1493
|
if (json)
|
|
720
1494
|
console.log(JSON.stringify(j.handoff));
|
|
@@ -726,7 +1500,7 @@ url: ${resolveBaseUrl()}`);
|
|
|
726
1500
|
const i = rest.indexOf(n);
|
|
727
1501
|
return i >= 0 ? rest[i + 1] : undefined;
|
|
728
1502
|
};
|
|
729
|
-
const r = await
|
|
1503
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
|
|
730
1504
|
method: "POST",
|
|
731
1505
|
headers: { "content-type": "application/json" },
|
|
732
1506
|
body: JSON.stringify({
|
|
@@ -775,7 +1549,7 @@ url: ${resolveBaseUrl()}`);
|
|
|
775
1549
|
const [, task, gate, verdict] = positionals;
|
|
776
1550
|
if (!task || !gate || !verdict)
|
|
777
1551
|
throw new Error('usage: swarm gate record <task> <gate> pass|fail --rubric "what was checked" [--evidence "\u2026"]');
|
|
778
|
-
const r = await
|
|
1552
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates`, {
|
|
779
1553
|
method: "POST",
|
|
780
1554
|
headers: { "content-type": "application/json" },
|
|
781
1555
|
body: JSON.stringify({
|
|
@@ -798,6 +1572,34 @@ url: ${resolveBaseUrl()}`);
|
|
|
798
1572
|
}
|
|
799
1573
|
break;
|
|
800
1574
|
}
|
|
1575
|
+
if (sub === "run") {
|
|
1576
|
+
const [, task, ...gates2] = positionals;
|
|
1577
|
+
if (!task)
|
|
1578
|
+
throw new Error("usage: swarm gate run <task> [gate\u2026] (default: the required gates that have a cmd)");
|
|
1579
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
headers: { "content-type": "application/json" },
|
|
1582
|
+
body: JSON.stringify({
|
|
1583
|
+
projectId: proj.id,
|
|
1584
|
+
task,
|
|
1585
|
+
gates: gates2,
|
|
1586
|
+
sessionId: process.env.CLAUDE_SESSION_ID ?? null
|
|
1587
|
+
})
|
|
1588
|
+
}).then((x) => x.json());
|
|
1589
|
+
if (json)
|
|
1590
|
+
console.log(JSON.stringify(r));
|
|
1591
|
+
else {
|
|
1592
|
+
for (const x of r.runs ?? [])
|
|
1593
|
+
console.log(`${x.verdict === "pass" ? "\u2713" : "\u2717"} ${x.gate.padEnd(12)} ${x.rubric}`);
|
|
1594
|
+
for (const x of r.skipped ?? [])
|
|
1595
|
+
console.log(`\u2013 ${x.gate.padEnd(12)} skipped: ${x.reason}`);
|
|
1596
|
+
if (r.error && !r.started?.length)
|
|
1597
|
+
console.error(`REFUSED: ${r.error}`);
|
|
1598
|
+
}
|
|
1599
|
+
if (!r.ok)
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
801
1603
|
if (sub === "ls") {
|
|
802
1604
|
const task = positionals[1];
|
|
803
1605
|
const q = new URLSearchParams({ project: proj.id });
|
|
@@ -858,6 +1660,36 @@ url: ${resolveBaseUrl()}`);
|
|
|
858
1660
|
session ${h.sessionId}` : ""}`);
|
|
859
1661
|
break;
|
|
860
1662
|
}
|
|
1663
|
+
case "audit": {
|
|
1664
|
+
if (rest[0] !== "export")
|
|
1665
|
+
throw new Error("usage: swarm audit export [--since 30d] [-p] [--type t] [--format jsonl|csv|json] [--limit n]");
|
|
1666
|
+
await ensureDaemon({ quiet: true });
|
|
1667
|
+
const q = new URLSearchParams;
|
|
1668
|
+
const val = (n) => {
|
|
1669
|
+
const i = rest.indexOf(n);
|
|
1670
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
1671
|
+
};
|
|
1672
|
+
if (val("--since"))
|
|
1673
|
+
q.set("since", val("--since"));
|
|
1674
|
+
if (val("--type"))
|
|
1675
|
+
q.set("type", val("--type"));
|
|
1676
|
+
if (val("--limit"))
|
|
1677
|
+
q.set("limit", val("--limit"));
|
|
1678
|
+
q.set("format", val("--format") ?? "jsonl");
|
|
1679
|
+
if (rest.includes("-p")) {
|
|
1680
|
+
const proj = await api("/v1/projects", {
|
|
1681
|
+
method: "POST",
|
|
1682
|
+
headers: { "content-type": "application/json" },
|
|
1683
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
1684
|
+
});
|
|
1685
|
+
q.set("project", proj.id);
|
|
1686
|
+
}
|
|
1687
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/audit?${q}`);
|
|
1688
|
+
if (!r.ok)
|
|
1689
|
+
throw new Error((await r.json()).error ?? `audit: ${r.status}`);
|
|
1690
|
+
process.stdout.write(await r.text());
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
861
1693
|
case "rules": {
|
|
862
1694
|
if (rest[0] !== "dryrun")
|
|
863
1695
|
throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
|
|
@@ -1088,7 +1920,7 @@ would have fired (newest last):`);
|
|
|
1088
1920
|
headers: { "content-type": "application/json" },
|
|
1089
1921
|
body: JSON.stringify({ path: resolve3(".") })
|
|
1090
1922
|
});
|
|
1091
|
-
const r = await
|
|
1923
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources`, {
|
|
1092
1924
|
method: "POST",
|
|
1093
1925
|
headers: { "content-type": "application/json" },
|
|
1094
1926
|
body: JSON.stringify({
|
|
@@ -1124,7 +1956,7 @@ would have fired (newest last):`);
|
|
|
1124
1956
|
});
|
|
1125
1957
|
if (rest.includes("--force"))
|
|
1126
1958
|
q.set("force", "1");
|
|
1127
|
-
const r = await
|
|
1959
|
+
const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
|
|
1128
1960
|
if (json)
|
|
1129
1961
|
console.log(JSON.stringify(r));
|
|
1130
1962
|
else if (r.ok)
|
|
@@ -1142,7 +1974,7 @@ would have fired (newest last):`);
|
|
|
1142
1974
|
const base = new SwarmClient().baseUrl;
|
|
1143
1975
|
const pIdx = rest.indexOf("--session");
|
|
1144
1976
|
const wantSession = pIdx >= 0 ? rest[pIdx + 1] : undefined;
|
|
1145
|
-
const res = await
|
|
1977
|
+
const res = await authedFetch(`${base}/v1/events?since=0`);
|
|
1146
1978
|
const reader = res.body?.getReader();
|
|
1147
1979
|
if (!reader)
|
|
1148
1980
|
throw new Error("no stream");
|
|
@@ -1174,7 +2006,8 @@ would have fired (newest last):`);
|
|
|
1174
2006
|
}
|
|
1175
2007
|
case "ui": {
|
|
1176
2008
|
const base = await ensureDaemon();
|
|
1177
|
-
|
|
2009
|
+
const tok = readToken();
|
|
2010
|
+
Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
|
|
1178
2011
|
console.log(base);
|
|
1179
2012
|
break;
|
|
1180
2013
|
}
|