@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ra3orblade/swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Local-first control plane for AI-agent development: watch every Claude Code / Codex / Grok session on your machine, ledger tasks and worktrees, enforce rules as hook denials.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/web/app.js
CHANGED
|
@@ -155,7 +155,7 @@ async function refresh() {
|
|
|
155
155
|
const txt = await (await fetch("/v1/state")).text();
|
|
156
156
|
const same = txt === lastSnap;
|
|
157
157
|
if (!same) { lastSnap = txt; Object.assign(state, JSON.parse(txt)); }
|
|
158
|
-
if (!state.version) fetch("/v1/health").then((r) => r.json()).then((h) => { state.version = h.version; maybeWhatsNew(); }).catch(() => {});
|
|
158
|
+
if (!state.version) fetch("/v1/health").then((r) => r.json()).then((h) => { state.version = h.version; state.hooksInstalled = h.hooksInstalled !== false; maybeUpdateNudge(h); maybeWhatsNew(); }).catch(() => {});
|
|
159
159
|
let prsChanged = false;
|
|
160
160
|
if (state.view === "prs" && !state.session) {
|
|
161
161
|
const prs = await (await fetch("/v1/prs")).json().catch(() => state.prs ?? []);
|
|
@@ -175,6 +175,10 @@ async function refresh() {
|
|
|
175
175
|
state.attribution = null;
|
|
176
176
|
}
|
|
177
177
|
let runsChanged = false;
|
|
178
|
+
if (state.session) {
|
|
179
|
+
const ms = await fetch(`/v1/messages?session=${encodeURIComponent(state.session)}&limit=50`).then((r) => r.json()).catch(() => state.msgs ?? []);
|
|
180
|
+
if (JSON.stringify(ms) !== JSON.stringify(state.msgs)) { state.msgs = ms; state.dirty = true; }
|
|
181
|
+
}
|
|
178
182
|
const openSpawned = state.session && state.sessions.find((x) => x.id === state.session)?.kind === "spawned";
|
|
179
183
|
if (openSpawned || (state.view === "board" && !state.session) || (state.view === "fleet" && !state.session)) {
|
|
180
184
|
const runs = await fetch("/v1/runs").then((r) => r.json()).catch(() => state.runs ?? []);
|
|
@@ -183,13 +187,14 @@ async function refresh() {
|
|
|
183
187
|
}
|
|
184
188
|
let tasksChanged = false;
|
|
185
189
|
if (state.view === "board" && state.sel && !state.session) {
|
|
186
|
-
const [t, g, d] = await Promise.all([
|
|
190
|
+
const [t, g, d, wf] = await Promise.all([
|
|
187
191
|
fetch(`/v1/tasks?project=${encodeURIComponent(state.sel)}`).then((r) => r.json()).catch(() => state.tasks),
|
|
188
192
|
fetch(`/v1/gates?project=${encodeURIComponent(state.sel)}`).then((r) => r.json()).catch(() => state.gates),
|
|
189
193
|
fetch(`/v1/dispatch?project=${encodeURIComponent(state.sel)}`).then((r) => r.json()).catch(() => state.dispatch),
|
|
194
|
+
fetch(`/v1/workflows?project=${encodeURIComponent(state.sel)}`).then((r) => r.json()).catch(() => state.workflows),
|
|
190
195
|
]);
|
|
191
|
-
tasksChanged = JSON.stringify(t) !== JSON.stringify(state.tasks) || JSON.stringify(g) !== JSON.stringify(state.gates) || JSON.stringify(d) !== JSON.stringify(state.dispatch);
|
|
192
|
-
state.tasks = t; state.gates = g; state.dispatch = d;
|
|
196
|
+
tasksChanged = JSON.stringify(t) !== JSON.stringify(state.tasks) || JSON.stringify(g) !== JSON.stringify(state.gates) || JSON.stringify(d) !== JSON.stringify(state.dispatch) || JSON.stringify(wf) !== JSON.stringify(state.workflows);
|
|
197
|
+
state.tasks = t; state.gates = g; state.dispatch = d; state.workflows = wf;
|
|
193
198
|
}
|
|
194
199
|
let incChanged = false;
|
|
195
200
|
if (state.view === "incidents" && !state.session) {
|
|
@@ -256,6 +261,12 @@ function liveCounts() {
|
|
|
256
261
|
for (const s of state.sessions) if (isLive(s)) { m.set(s.projectId, (m.get(s.projectId) ?? 0) + 1); m.set("", (m.get("") ?? 0) + 1); }
|
|
257
262
|
return m;
|
|
258
263
|
}
|
|
264
|
+
// M5.7: 14-day spend sparkline per pinned project; hidden when the fortnight cost is ~zero.
|
|
265
|
+
function spendSpark(pid) {
|
|
266
|
+
const pts = state.spendSparks?.[pid];
|
|
267
|
+
if (!pts || pts.reduce((a, b) => a + b, 0) < 0.5) return "";
|
|
268
|
+
return `<span class="proj-spark" title="last 14 days · $${pts.reduce((a, b) => a + b, 0).toFixed(0)}">${viz.sparkline(pts, "var(--c1)")}</span>`;
|
|
269
|
+
}
|
|
259
270
|
function renderProjects() {
|
|
260
271
|
const lc = liveCounts();
|
|
261
272
|
const live = (pid) => lc.get(pid) ?? 0;
|
|
@@ -272,7 +283,7 @@ function renderProjects() {
|
|
|
272
283
|
const row = (p) => {
|
|
273
284
|
const act = `<span class="act more" data-menu="project" data-pid="${p.id}" title="Project actions">${ic("dots-three", 15)}</span>`;
|
|
274
285
|
return `<div class="proj ${state.sel === p.id ? "sel" : ""}" data-id="${p.id}" data-ctx="project" data-pid="${p.id}" title="${esc(p.root)}"${p.discovered ? "" : ' draggable="true"'}>
|
|
275
|
-
<span class="st ${live(p.id) ? "live" : ""}"></span>${projGlyph(p)}<span class="nm">${disamb(p)}${esc(p.name)}</span
|
|
286
|
+
<span class="st ${live(p.id) ? "live" : ""}"></span>${projGlyph(p)}<span class="nm">${disamb(p)}${esc(p.name)}</span>${spendSpark(p.id)}<small>${live(p.id) || ""}</small>${act}</div>`;
|
|
276
287
|
};
|
|
277
288
|
const liveAll = live("");
|
|
278
289
|
$("#projects").innerHTML =
|
|
@@ -317,6 +328,21 @@ projectsEl.addEventListener("dragend", () => {
|
|
|
317
328
|
fetch("/v1/projects/order", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ ids }) }).then(refresh);
|
|
318
329
|
});
|
|
319
330
|
|
|
331
|
+
// First run: no sessions have ever been seen. Say exactly what to do next, and whether hooks are in.
|
|
332
|
+
function onboarding() {
|
|
333
|
+
const hooksOk = state.hooksInstalled !== false;
|
|
334
|
+
const step = (n, done, html) => `<div class="ob-step ${done ? "done" : ""}"><span class="ob-n">${done ? "✓" : n}</span><div>${html}</div></div>`;
|
|
335
|
+
return `<div class="onboard">${PX.idle()}
|
|
336
|
+
<h3>Swarm is running and watching this machine.</h3>
|
|
337
|
+
<div class="ob-steps">
|
|
338
|
+
${step(1, hooksOk, `<b>Hook into Claude Code</b> — <code>swarm install</code> once${hooksOk ? "" : " <span class='badge warn'>not installed</span>"}. Codex and Grok are picked up automatically, nothing to configure.`)}
|
|
339
|
+
${step(2, false, `<b>Open any agent session</b> — run <code>claude</code> in any repository, in any terminal. No changes to the repo, the agent doesn't know Swarm is there.`)}
|
|
340
|
+
${step(3, false, `<b>Watch it appear here</b> — live status, branch, tokens and cost per session; Board, Timeline and Spend fill up as you work.`)}
|
|
341
|
+
</div>
|
|
342
|
+
<div class="dim">Something off? <code>swarm doctor</code> checks every piece and prints the fix.</div>
|
|
343
|
+
</div>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
320
346
|
// ---------- fleet
|
|
321
347
|
// Fleet data-grid columns (sortable/resizable/reorderable/filterable via table.js).
|
|
322
348
|
const FLEET_COLS = [
|
|
@@ -363,7 +389,7 @@ function renderFleet() {
|
|
|
363
389
|
: "";
|
|
364
390
|
$("#main").innerHTML = chips +
|
|
365
391
|
`<h2>Live <span>${live.length} sessions · ${usd(sumBy(live, (s) => s.costUsd))}</span></h2>` +
|
|
366
|
-
(live.length ? table(live, "fleet-live") : `<div class="empty">${PX.idle()}Nothing running
|
|
392
|
+
(live.length ? table(live, "fleet-live") : state.sessions.length ? `<div class="empty">${PX.idle()}Nothing running.</div>` : onboarding()) +
|
|
367
393
|
(rest.length ? `<h2 class="mt-sec">Earlier <span>${rest.length}</span></h2>${table(rest.slice(0, 30), "fleet-earlier")}` : "") +
|
|
368
394
|
"";
|
|
369
395
|
}
|
|
@@ -430,7 +456,7 @@ function renderBoardKpis() {
|
|
|
430
456
|
}
|
|
431
457
|
|
|
432
458
|
function renderBoard() {
|
|
433
|
-
const parts = [renderBoardKpis(), renderTasks(), renderDispatch(), renderGates(), renderProcesses(), renderResources(), renderClaims(), renderWorktrees(), renderIncidents()].filter(Boolean);
|
|
459
|
+
const parts = [renderBoardKpis(), renderTasks(), renderDispatch(), renderWorkflowRuns(), renderGates(), renderProcesses(), renderResources(), renderClaims(), renderWorktrees(), renderIncidents()].filter(Boolean);
|
|
434
460
|
$("#main").innerHTML = parts.length
|
|
435
461
|
? parts.join("").replace(/^(<div class="kpis[^>]*>[\s\S]*?<\/div><\/div>|)(<h2) class="mt-sec"/, "$1$2") // first section needs no top gap
|
|
436
462
|
: `<div class="empty">${PX.idle()}Nothing on the board.<br>Tasks, processes, claims, worktrees, and incidents appear here.</div>`;
|
|
@@ -627,7 +653,13 @@ function renderGates() {
|
|
|
627
653
|
{ key: "evidence", label: "evidence", width: 220, get: (r) => r.evidence ?? "", cell: (r) => (r.evidence ? `<span class="dim now" title="${esc(r.evidence)}">${esc(r.evidence)}</span>` : '<span class="dim">—</span>') },
|
|
628
654
|
{ key: "session", label: "session", width: 140, get: (r) => sess(r.sessionId)?.title ?? "", cell: (r) => (r.sessionId ? `<a href="#" data-s="${r.sessionId}">${esc(sess(r.sessionId)?.title ?? r.sessionId.slice(0, 8))}</a>` : '<span class="dim">—</span>') },
|
|
629
655
|
];
|
|
630
|
-
|
|
656
|
+
const history = (gate) => {
|
|
657
|
+
const rs = runs.filter((r) => r.gate === gate).slice(0, 12).reverse();
|
|
658
|
+
if (!rs.length) return "";
|
|
659
|
+
return `<span class="gh" title="${esc(gate)} — last ${rs.length} run${rs.length === 1 ? "" : "s"}, oldest first">${esc(gate)} ${rs.map((r) => `<i class="${r.verdict === "pass" ? "ok" : "bad"}" title="${esc(r.rubric)}"></i>`).join("")}</span>`;
|
|
660
|
+
};
|
|
661
|
+
const gateNames = [...new Set(runs.map((r) => r.gate))];
|
|
662
|
+
return `<h2 class="mt-sec">Recent gates <span>${runs.length} run${runs.length === 1 ? "" : "s"}${required.length ? ` · required: ${required.map(esc).join(", ")}` : ""} · latest run per gate decides</span>${gateNames.length ? `<span class="grow"></span><span class="gh-strip">${gateNames.map(history).join("")}</span>` : ""}</h2>` +
|
|
631
663
|
(runs.length
|
|
632
664
|
? dataTable({
|
|
633
665
|
id: "gates",
|
|
@@ -772,6 +804,37 @@ function renderWorktrees() {
|
|
|
772
804
|
}));
|
|
773
805
|
}
|
|
774
806
|
|
|
807
|
+
// ---------- workflows (M7.8)
|
|
808
|
+
function renderWorkflowRuns() {
|
|
809
|
+
const w = state.workflows;
|
|
810
|
+
if (!state.sel || !w?.runs?.length) return "";
|
|
811
|
+
const chip = (r, i) => {
|
|
812
|
+
const label = esc(r.steps[i]);
|
|
813
|
+
if (i < r.step || (r.state === "done" && i <= r.step)) return `<span class="wfs ok" title="${label}">✓ ${label}</span>`;
|
|
814
|
+
if (i === r.step) return r.state === "running" ? `<span class="wfs run" title="${label}">● ${label}</span>` : r.state === "failed" ? `<span class="wfs bad" title="${label}">✗ ${label}</span>` : `<span class="wfs" title="${label}">◦ ${label}</span>`;
|
|
815
|
+
return `<span class="wfs" title="${label}">○ ${label}</span>`;
|
|
816
|
+
};
|
|
817
|
+
const badge = (r) => r.state === "running" ? '<span class="badge acc">Running</span>' : r.state === "done" ? '<span class="badge ok">Done</span>' : r.state === "failed" ? '<span class="badge warn">Failed</span>' : '<span class="badge">Stopped</span>';
|
|
818
|
+
const cols = [
|
|
819
|
+
{ key: "task", label: "task", width: 90, get: (r) => r.task, cell: (r) => `<b>${esc(r.task)}</b>` },
|
|
820
|
+
{ key: "workflow", label: "workflow", width: 100, get: (r) => r.workflow, cell: (r) => `<span class="br">${esc(r.workflow)}</span>` },
|
|
821
|
+
{ key: "steps", label: "steps", flex: true, sortable: false, get: (r) => r.step, cell: (r) => `<span class="wf-steps">${r.steps.map((_, i) => chip(r, i)).join("")}</span>` },
|
|
822
|
+
{ key: "state", label: "state", width: 90, get: (r) => r.state, cell: badge },
|
|
823
|
+
{ key: "detail", label: "detail", width: 260, get: (r) => r.detail ?? "", cell: (r) => `<span class="dim now" title="${esc(r.detail ?? "")}">${esc(r.detail ?? "")}</span>` },
|
|
824
|
+
{ key: "when", label: "updated", width: 76, get: (r) => r.updatedAt, cell: (r) => `<span class="dim">${ago(r.updatedAt)}</span>` },
|
|
825
|
+
];
|
|
826
|
+
const running = w.runs.filter((r) => r.state === "running").length;
|
|
827
|
+
return `<h2 class="mt-sec">Workflows <span>${running ? `${running} running · ` : ""}${Object.keys(w.defs ?? {}).map(esc).join(", ") || "none declared"}</span></h2>` +
|
|
828
|
+
dataTable({
|
|
829
|
+
id: "workflows",
|
|
830
|
+
columns: cols,
|
|
831
|
+
rows: w.runs.slice(0, 20),
|
|
832
|
+
leading: { width: 24, cell: (r) => `<span class="s ${r.state === "running" ? "active" : r.state === "failed" ? "waiting" : "ended"}"></span>` },
|
|
833
|
+
trailing: { width: 60, cell: (r) => (r.state === "running" ? `<a href="#" data-wfstop="${esc(r.task)}">Stop</a>` : "") },
|
|
834
|
+
rerender: touch,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
775
838
|
// ---------- dispatch (M7.5)
|
|
776
839
|
function renderDispatch() {
|
|
777
840
|
const d = state.dispatch;
|
|
@@ -1059,7 +1122,22 @@ function renderStats() {
|
|
|
1059
1122
|
}
|
|
1060
1123
|
|
|
1061
1124
|
// ---------- timeline
|
|
1125
|
+
let tlDetail = { key: "", data: null, busy: false };
|
|
1126
|
+
async function loadTimelineDetail() {
|
|
1127
|
+
const hours = state.tlHours ?? 12;
|
|
1128
|
+
const key = `${hours}:${state.sel ?? ""}`;
|
|
1129
|
+
if (tlDetail.busy || (tlDetail.key === key && tlDetail.at && Date.now() - tlDetail.at < 15_000)) return;
|
|
1130
|
+
tlDetail.busy = true;
|
|
1131
|
+
try {
|
|
1132
|
+
const q = new URLSearchParams({ hours: String(hours) });
|
|
1133
|
+
if (state.sel) q.set("project", state.sel);
|
|
1134
|
+
const data = await (await fetch(`/v1/timeline?${q}`)).json();
|
|
1135
|
+
tlDetail = { key, at: Date.now(), data, busy: false };
|
|
1136
|
+
if (state.view === "timeline" && !state.session) touch();
|
|
1137
|
+
} finally { tlDetail.busy = false; }
|
|
1138
|
+
}
|
|
1062
1139
|
function renderTimeline() {
|
|
1140
|
+
loadTimelineDetail();
|
|
1063
1141
|
const now = Date.now();
|
|
1064
1142
|
const hours = state.tlHours ?? 12;
|
|
1065
1143
|
const from = now - hours * 3.6e6, to = now + 0.25 * 3.6e6;
|
|
@@ -1068,7 +1146,7 @@ function renderTimeline() {
|
|
|
1068
1146
|
const chip = (h) => `<a href="#" class="nav ${hours === h ? "on" : ""}" data-tl="${h}">${h}h</a>`;
|
|
1069
1147
|
$("#main").innerHTML =
|
|
1070
1148
|
`<h2>Timeline <span>${rows.length} sessions · last ${hours}h · ${usd(sumBy(rows, (s) => s.costUsd))}</span><span style="margin-left:auto;display:flex;gap:2px">${[3, 6, 12, 24, 72].map(chip).join("")}</span></h2>
|
|
1071
|
-
${rows.length ? viz.timeline(rows, { from, to, projName, now }) : `<div class="empty">${PX.clock()}No sessions in the last ${hours}h.</div>`}
|
|
1149
|
+
${rows.length ? viz.timeline(rows, { from, to, projName, now, detail: tlDetail.key === `${hours}:${state.sel ?? ""}` ? tlDetail.data : null }) : `<div class="empty">${PX.clock()}No sessions in the last ${hours}h.</div>`}
|
|
1072
1150
|
${agents.length ? `<div style="margin-top:10px">${viz.legend(agents)}</div>` : ""}`;
|
|
1073
1151
|
}
|
|
1074
1152
|
|
|
@@ -1185,6 +1263,18 @@ function replayGo(delta) {
|
|
|
1185
1263
|
}
|
|
1186
1264
|
|
|
1187
1265
|
// Spawned sessions get a stdin box while their run is live (M3.3); interactive ones are told where to type.
|
|
1266
|
+
// M7.6: the session's message thread (sent + received) and a compose box.
|
|
1267
|
+
function messageThread(s) {
|
|
1268
|
+
const ms = (state.msgs ?? []).filter((m) => m.sessionId === s.id || m.fromSession === s.id).slice().reverse();
|
|
1269
|
+
const row = (m) => {
|
|
1270
|
+
const out = m.fromSession === s.id;
|
|
1271
|
+
return `<div class="msg ${out ? "out" : "in"}" title="${esc(m.createdAt)}${m.deliveredAt ? "" : " · not delivered yet"}">
|
|
1272
|
+
<span class="msg-f">${out ? `→ ${esc(m.task ?? m.toKind)}` : esc(m.from ?? "?")}${m.deliveredAt ? "" : ' <i class="dim">·queued</i>'}</span>${esc(m.text)}</div>`;
|
|
1273
|
+
};
|
|
1274
|
+
return `<h4>messages</h4>${ms.length ? `<div class="msgs">${ms.map(row).join("")}</div>` : '<span class="dim">None yet.</span>'}
|
|
1275
|
+
<div class="msg-compose"><input id="msgText" placeholder="Message this agent… (delivered on its next tool call)" autocomplete="off"><button id="msgSend" data-sid="${s.id}" data-pid="${s.projectId}">${ic("arrow-right", 13)}</button></div>`;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1188
1278
|
// M7.7: questions this session is waiting on a human for
|
|
1189
1279
|
function questionCards(s) {
|
|
1190
1280
|
const qs = (state.questions ?? []).filter((q) => q.sessionId === s.id);
|
|
@@ -1251,6 +1341,7 @@ function renderSession() {
|
|
|
1251
1341
|
<h4>tokens</h4>${viz.compositionBar([{ label: "cache read", v: t.cacheRead }, { label: "cache write", v: t.cacheWrite }, { label: "input", v: t.input }, { label: "thinking", v: t.thinking }, { label: "output", v: t.output }])}
|
|
1252
1342
|
${state.turns.length > 1 ? `<h4>cost per turn</h4>${viz.turnStrip(state.turns, { height: 54 })}` : ""}
|
|
1253
1343
|
<h4>tools</h4>${tools.length ? viz.hbars(tools.slice(0, 8).map(([k, v]) => [k.replace(/^mcp__[a-z0-9-]+__/i, ""), v])) : '<span class="dim">None yet</span>'}
|
|
1344
|
+
${messageThread(s)}
|
|
1254
1345
|
${questionCards(s)}
|
|
1255
1346
|
${s.transcriptPath ? `<h4>transcript</h4><div class="dim mono" style="word-break:break-all">${ic("file-text", 12)} ${esc(short(s.transcriptPath))}</div>` : ""}`;
|
|
1256
1347
|
if (logEl && isAppend(rows)) {
|
|
@@ -1527,6 +1618,33 @@ function whatsNew(version) {
|
|
|
1527
1618
|
}
|
|
1528
1619
|
window.swarmWhatsNew = (v) => whatsNew(v);
|
|
1529
1620
|
// auto-open once per version, but never on the very first run (nothing to compare against)
|
|
1621
|
+
// M-launch: after an update the running daemon is the old build until it restarts. The daemon
|
|
1622
|
+
// reports the version on disk; when it differs, offer a one-click restart, then reload.
|
|
1623
|
+
let updateNudged = false;
|
|
1624
|
+
setInterval(() => { fetch("/v1/health").then((r) => r.json()).then(maybeUpdateNudge).catch(() => {}); }, 300_000);
|
|
1625
|
+
function maybeUpdateNudge(h) {
|
|
1626
|
+
if (!h?.disk || !h.version || h.disk === h.version || updateNudged) return;
|
|
1627
|
+
updateNudged = true;
|
|
1628
|
+
const el = document.createElement("div");
|
|
1629
|
+
el.className = "nudge";
|
|
1630
|
+
el.innerHTML = `${ic("arrows-clockwise", 18, "ic")}<div><b>Swarm ${esc(h.disk)} is installed</b>The daemon is still running ${esc(h.version)} — restart it to switch. Sessions and history are unaffected.
|
|
1631
|
+
<div class="row"><button class="pri" id="updRestart">${ic("arrows-clockwise", 13)} Restart daemon</button><button id="updLater">Later</button></div></div>`;
|
|
1632
|
+
document.body.appendChild(el);
|
|
1633
|
+
el.addEventListener("click", async (e) => {
|
|
1634
|
+
if (e.target.id === "updLater") return el.remove();
|
|
1635
|
+
if (e.target.id !== "updRestart") return;
|
|
1636
|
+
e.target.textContent = "restarting…";
|
|
1637
|
+
await fetch("/v1/daemon/restart", { method: "POST" }).catch(() => {});
|
|
1638
|
+
const t0 = Date.now();
|
|
1639
|
+
const wait = setInterval(async () => {
|
|
1640
|
+
try {
|
|
1641
|
+
const j = await (await fetch("/v1/health")).json();
|
|
1642
|
+
if (j.version === h.disk) { clearInterval(wait); location.reload(); }
|
|
1643
|
+
} catch {}
|
|
1644
|
+
if (Date.now() - t0 > 30_000) { clearInterval(wait); el.remove(); }
|
|
1645
|
+
}, 800);
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1530
1648
|
function maybeWhatsNew() {
|
|
1531
1649
|
if (!state.version || !window.RELEASE_NOTES) return;
|
|
1532
1650
|
let seen; try { seen = localStorage.getItem("swarm.seenVersion"); } catch {}
|
|
@@ -1581,6 +1699,9 @@ function openMenu(kind, anchor, d) {
|
|
|
1581
1699
|
if (!window.menus) { console.warn("menus.js not built — run: bun run build:web"); return; }
|
|
1582
1700
|
window.menus.open(anchor, spec);
|
|
1583
1701
|
}
|
|
1702
|
+
document.addEventListener("keydown", (e) => {
|
|
1703
|
+
if (e.key === "Enter" && e.target.id === "msgText") { e.preventDefault(); $("#msgSend")?.click(); }
|
|
1704
|
+
});
|
|
1584
1705
|
// Enter / Space on a focused card, tile or kebab opens its menu like a click.
|
|
1585
1706
|
document.addEventListener("keydown", (ev) => {
|
|
1586
1707
|
if (ev.key !== "Enter" && ev.key !== " ") return;
|
|
@@ -1598,7 +1719,7 @@ document.addEventListener("contextmenu", (ev) => {
|
|
|
1598
1719
|
|
|
1599
1720
|
// ---------- events
|
|
1600
1721
|
document.addEventListener("click", async (ev) => {
|
|
1601
|
-
const t = ev.target.closest("[data-menu],#settings,#feedback,[data-id],[data-s],#back,[data-view],.chip,[data-tl],[data-days],[data-sdays],[data-release],[data-forcerelease],[data-resrelease],[data-merge],[data-ack],[data-ackall],[data-inc],[data-task-filter],[data-claim],[data-procstop],[data-run],[data-runstop],[data-wtopen],[data-wtrm],[data-wtdiff],[data-wtpr],[data-dffile],#prGo,#sessDiff,#wtnew,#wtgc,[data-gaterun],[data-codify],[data-bmode],[data-emoji],#psAllEmoji,.swatch,#psSave,#dispatch,#dispatchGo,#dispatchClear");
|
|
1722
|
+
const t = ev.target.closest("[data-menu],#settings,#feedback,[data-id],[data-s],#back,[data-view],.chip,[data-tl],[data-days],[data-sdays],[data-release],[data-forcerelease],[data-resrelease],[data-merge],[data-ack],[data-ackall],[data-inc],[data-task-filter],[data-claim],[data-procstop],[data-run],[data-runstop],[data-wtopen],[data-wtrm],[data-wtdiff],[data-wtpr],[data-dffile],#prGo,#sessDiff,#wtnew,#wtgc,[data-gaterun],[data-codify],[data-wfstop],[data-bmode],[data-emoji],#psAllEmoji,.swatch,#psSave,#msgSend,#dispatch,#dispatchGo,#dispatchClear");
|
|
1602
1723
|
if (!t) return;
|
|
1603
1724
|
if (t.dataset.menu) { ev.preventDefault(); ev.stopPropagation(); return openMenu(t.dataset.menu, t, t.dataset); }
|
|
1604
1725
|
if (t.id === "settings") { ev.preventDefault(); return openMenu("settings", t, {}); }
|
|
@@ -1609,7 +1730,25 @@ document.addEventListener("click", async (ev) => {
|
|
|
1609
1730
|
if (t.dataset.emoji !== undefined) { $("#psIcon").value = t.dataset.emoji; $("#psImage").value = ""; setIconPreview(t.dataset.emoji); for (const e of $$(".emoji")) e.classList.toggle("on", e.dataset.emoji === t.dataset.emoji); return; }
|
|
1610
1731
|
if (t.id === "psAllEmoji") { const all = $("#psEmojiAll"); if (all.hidden) { all.innerHTML = buildEmojiGrid(); all.hidden = false; } else all.hidden = true; return; }
|
|
1611
1732
|
if (t.dataset.color !== undefined && t.classList.contains("swatch")) { for (const e of $$(".swatch")) e.classList.toggle("on", e === t); return; }
|
|
1733
|
+
if (t.id === "msgSend") {
|
|
1734
|
+
ev.preventDefault();
|
|
1735
|
+
const text = $("#msgText")?.value.trim();
|
|
1736
|
+
if (!text) return;
|
|
1737
|
+
const r = await post("/v1/messages", { projectId: t.dataset.pid, to: t.dataset.sid, text, from: "dashboard" });
|
|
1738
|
+
if (!r.ok) return alert(r.error);
|
|
1739
|
+
$("#msgText").value = "";
|
|
1740
|
+
state.msgs = null;
|
|
1741
|
+
return refresh();
|
|
1742
|
+
}
|
|
1612
1743
|
if (t.id === "psSave") { ev.preventDefault(); return saveProjectSettings(t.dataset.pid); }
|
|
1744
|
+
if (t.dataset.wfstop !== undefined) {
|
|
1745
|
+
ev.preventDefault();
|
|
1746
|
+
if (!confirm(`Stop the workflow on ${t.dataset.wfstop}? A live step's run is stopped too.`)) return;
|
|
1747
|
+
const r = await post("/v1/workflows/stop", { projectId: state.sel, task: t.dataset.wfstop });
|
|
1748
|
+
if (!r.ok) alert(r.error);
|
|
1749
|
+
state.workflows = null;
|
|
1750
|
+
return refresh();
|
|
1751
|
+
}
|
|
1613
1752
|
if (t.dataset.bmode) { ev.preventDefault(); const [k, v] = t.dataset.bmode.split(":"); localStorage.setItem(`swarm.board.${k}`, v); return touch(); }
|
|
1614
1753
|
if (t.dataset.run) { ev.preventDefault(); return openRunDrawer(t.dataset.run); }
|
|
1615
1754
|
if (t.dataset.runstop) {
|
|
@@ -2008,7 +2147,7 @@ function connect() {
|
|
|
2008
2147
|
if (fresh) notifyForEvent(ev);
|
|
2009
2148
|
pollSoon();
|
|
2010
2149
|
};
|
|
2011
|
-
for (const t of ["session.started", "session.ended", "prompt.submitted", "tool.requested", "tool.completed", "subagent.started", "subagent.stopped", "agent.text", "session.notification", "incident.opened", "claim.acquired", "claim.released", "resource.acquired", "resource.released", "resource.reaped", "process.started", "process.exited", "gate.recorded", "claim.orphaned", "claim.renewed", "worktree.bootstrapped", "worktree.created", "worktree.removed", "pr.opened", "question.asked", "question.answered", "dispatch.queued", "dispatch.started", "dispatch.finished", "permission.requested", "permission.resolved"]) es.addEventListener(t, onAny);
|
|
2150
|
+
for (const t of ["session.started", "session.ended", "prompt.submitted", "tool.requested", "tool.completed", "subagent.started", "subagent.stopped", "agent.text", "session.notification", "incident.opened", "claim.acquired", "claim.released", "resource.acquired", "resource.released", "resource.reaped", "process.started", "process.exited", "gate.recorded", "claim.orphaned", "claim.renewed", "worktree.bootstrapped", "worktree.created", "worktree.removed", "pr.opened", "question.asked", "question.answered", "message.sent", "dispatch.queued", "dispatch.started", "dispatch.finished", "workflow.started", "workflow.step", "workflow.finished", "permission.requested", "permission.resolved"]) es.addEventListener(t, onAny);
|
|
2012
2151
|
}
|
|
2013
2152
|
refresh().then(() => {
|
|
2014
2153
|
const sid = new URLSearchParams(location.search).get("session");
|
package/web/index.html
CHANGED
|
@@ -228,6 +228,12 @@
|
|
|
228
228
|
h2{font-size:var(--fs-md);margin:0 0 12px;color:var(--dim);font-weight:600;text-transform:uppercase;
|
|
229
229
|
letter-spacing:.07em;display:flex;gap:10px;align-items:baseline}
|
|
230
230
|
h2 span{font-weight:500;text-transform:none;letter-spacing:0;color:var(--faint);font-size:var(--fs-md)}
|
|
231
|
+
/* section-header actions (New worktree, Collect stale, Dispatch clear, timeline ranges):
|
|
232
|
+
small buttons, not shouting header text — normal case, centered icon, hover chip */
|
|
233
|
+
h2 .nav{display:inline-flex;align-items:center;gap:5px;font:500 var(--fs-md) var(--sans);text-transform:none;letter-spacing:0;color:var(--acc);text-decoration:none;padding:2px 8px;border-radius:var(--r-sm);transition:background var(--t-fast)}
|
|
234
|
+
h2 .nav:hover{background:var(--acc-soft)}
|
|
235
|
+
h2 .nav.on{background:var(--acc-soft)}
|
|
236
|
+
h2 .nav .ph{display:block}
|
|
231
237
|
h2 .back{color:var(--fg-2);text-transform:none;letter-spacing:0;font-weight:600;font-size:var(--fs-md);display:inline-flex;align-items:center;gap:5px;padding:5px 11px 5px 8px;border:1px solid var(--line);border-radius:var(--r-pill);background:var(--panel);transition:border-color var(--t-fast),color var(--t-fast)}
|
|
232
238
|
|
|
233
239
|
/* tables */
|
|
@@ -519,6 +525,43 @@
|
|
|
519
525
|
.seg a:hover{color:var(--fg-2)}
|
|
520
526
|
.seg a.on{background:var(--acc-soft);color:var(--acc)}
|
|
521
527
|
.chip b{font-weight:700}
|
|
528
|
+
/* workflow step chips (M7.8) */
|
|
529
|
+
.wf-steps{display:inline-flex;gap:6px;flex-wrap:wrap}
|
|
530
|
+
.wfs{font:500 var(--fs-sm) var(--sans);color:var(--dim);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-pill);padding:1px 8px;white-space:nowrap}
|
|
531
|
+
.wfs.ok{color:var(--ok);background:var(--ok-soft);border-color:transparent}
|
|
532
|
+
.wfs.run{color:var(--acc);background:var(--acc-soft);border-color:transparent}
|
|
533
|
+
.wfs.bad{color:var(--warn);background:var(--warn-soft);border-color:transparent}
|
|
534
|
+
/* M5.7: timeline ticks, idle-gap thin bars, claim lane; sidebar spend sparks; gate history */
|
|
535
|
+
.tl-track u{position:absolute;top:4px;bottom:4px;width:2px;border-radius:1px;opacity:.95}
|
|
536
|
+
.tl-track i.thin{opacity:.25}
|
|
537
|
+
.tl-row.claimlane .tl-name{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.06em}
|
|
538
|
+
.tl-track i.claim{top:7px;height:4px;border-radius:2px;background:var(--violet);opacity:.6}
|
|
539
|
+
.tl-track i.claim.orphaned{background:var(--warn);opacity:.9}
|
|
540
|
+
.tl-track i.claim.expired{background:var(--faint)}
|
|
541
|
+
.proj-spark{flex:none;width:44px;display:inline-flex;opacity:.7}
|
|
542
|
+
.proj-spark .spark{width:44px;height:14px}
|
|
543
|
+
.gh-strip{display:inline-flex;gap:12px;font-weight:400}
|
|
544
|
+
.gh{font-size:var(--fs-sm);color:var(--dim);display:inline-flex;align-items:center;gap:3px}
|
|
545
|
+
.gh i{width:7px;height:7px;border-radius:2px;display:inline-block}
|
|
546
|
+
.gh i.ok{background:var(--ok)} .gh i.bad{background:var(--warn)}
|
|
547
|
+
/* session message thread (M7.6) */
|
|
548
|
+
.msgs{display:flex;flex-direction:column;gap:6px;max-height:220px;overflow:auto;margin-bottom:8px}
|
|
549
|
+
.msg{background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:6px 9px;font-size:var(--fs-md);color:var(--fg-2);word-break:break-word}
|
|
550
|
+
.msg.out{background:var(--acc-soft);border-color:transparent}
|
|
551
|
+
.msg-f{display:block;font-size:var(--fs-2xs);color:var(--dim);margin-bottom:2px}
|
|
552
|
+
.msg-compose{display:flex;gap:6px}
|
|
553
|
+
.msg-compose input{flex:1;background:var(--panel-2);border:1px solid var(--line);color:var(--fg);border-radius:var(--r-sm);padding:6px 9px;font:400 var(--fs-md) var(--sans)}
|
|
554
|
+
.msg-compose input:focus{border-color:var(--acc);outline:none}
|
|
555
|
+
.msg-compose button{background:var(--acc-soft);border:1px solid transparent;color:var(--acc);border-radius:var(--r-sm);padding:6px 10px;cursor:pointer}
|
|
556
|
+
/* first-run onboarding card */
|
|
557
|
+
.onboard{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow);padding:28px 30px;display:flex;flex-direction:column;gap:16px;align-items:flex-start;max-width:640px;margin:40px auto}
|
|
558
|
+
.onboard h3{font-size:var(--fs-xl);color:var(--fg)}
|
|
559
|
+
.onboard .px{margin:0 auto}
|
|
560
|
+
.ob-steps{display:flex;flex-direction:column;gap:12px}
|
|
561
|
+
.ob-step{display:flex;gap:12px;align-items:flex-start;color:var(--fg-2);line-height:1.5}
|
|
562
|
+
.ob-step.done{color:var(--dim)}
|
|
563
|
+
.ob-n{flex:none;width:22px;height:22px;border-radius:50%;border:1px solid var(--line);display:inline-flex;align-items:center;justify-content:center;font:600 var(--fs-sm) var(--sans);color:var(--dim);margin-top:1px}
|
|
564
|
+
.ob-step.done .ob-n{background:var(--ok-soft);color:var(--ok);border-color:transparent}
|
|
522
565
|
/* star nudge (once a month, dismissable) */
|
|
523
566
|
.nudge{position:fixed;right:18px;bottom:18px;z-index:40;max-width:400px;background:var(--panel);border:1px solid var(--line);
|
|
524
567
|
border-radius:var(--r);box-shadow:var(--shadow-pop);padding:14px 16px;display:flex;gap:12px;align-items:flex-start;
|
package/web/release-notes.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by tools/build.ts from CHANGELOG.md
|
|
2
|
-
window.RELEASE_NOTES = {"0.8.0":{"date":"2026-08-23","html":"<p>The trust release: Swarm becomes something a team and a security reviewer can rely on, without giving up local-first. An org can pin the rules that matter and they hold even when the daemon is down; every record says who did it; the daemon has a credential; what is stored is exportable and redactable; a second agent can be the reviewer. And the dashboard stopped looking like a list of tables.</p>\n<h4>Added</h4>\n<ul><li><b>Org policy layer</b> — a third config file, <code>~/.swarm/policy.toml</code> (or <code>$SWARM_POLICY</code>), sits under global and repo config and may declare <code>locked = [\"rules.destructive_git\", \"rules.protected\", …]</code>: dotted keys or whole subtrees the layers below cannot change. A locked key keeps the policy's value, every attempt to override it is a <code>policy</code> incident, and <code>swarm doctor</code> shows which file set each value and who tried to change it. <code>GET /v1/policy</code> exposes provenance (M8.1).</li><li><b>Tamper detection</b> — on every session start the daemon checks that all ten hook entries are still in <code>~/.claude/settings.json</code> with a sane timeout, that no lower config layer fights a locked key, and that <code>SWARM_GUARD=off</code> isn't set while the policy locks rules (it is then ignored). Each finding opens a <code>policy</code> incident once; <code>swarm doctor</code> prints the same (M8.1b).</li><li><b>Fail-closed for locked rules</b> — while the policy locks any rule the daemon keeps <code>~/.swarm/policy.cache.json</code> (locked modes + a snapshot of live sessions and held worktrees, integrity-hashed). If the daemon is unreachable on a tool call, the hook shim enforces exactly those rules from the cache; everything else still fails open (M8.1c, OQ-3 resolved).</li><li><b>Who did it</b> — every ledger record and every event carries an <code>actor</code> (<code>human</code> / <code>agent</code> / <code>run</code> / <code>daemon</code> + id); existing rows were back-filled from the owner strings clients always sent. Schema changes now go through versioned migrations (<code>/v1/health</code> and <code>doctor</code> report the schema version) (M8.2a).</li><li><b>Daemon token</b> — the daemon creates <code>~/.swarm/token</code> on first start; the CLI, MCP server, hook shim and <code>swarm ui</code> send it. Local callers may still omit it by default; <code>[daemon] auth = \"required\"</code> makes every call carry it, a wrong token is always refused, and anything that isn't loopback always needs it (M8.2b).</li><li><b>Audit log + export</b> — the ledger-changing subset of events (claims, worktrees, PRs, questions, dispatch, resources, processes, gates, handoffs, permissions, incidents, run results, session start/end), with actor: <code>swarm audit export [--since 30d] [-p] [--type …] [--format jsonl|csv|json]</code> and <code>GET /v1/audit</code>. Retention is split — <code>[events] retain_days = 30</code> for chatter, <code>[audit] retain_days = 0</code> (forever) for audit rows (M8.2c).</li><li><b>Privacy on ingest</b> — <code>[privacy] store_prompts = false</code> keeps the event but not the prompt text, <code>store_reasoning = false</code> keeps token counts but not assistant text, <code>redact = [\"ACME-[0-9]+\"]</code> scrubs stored strings; API-key-looking tokens and <code>Bearer …</code> credentials are always redacted. Global-only keys an org can lock (M8.2c).</li><li><b>Review as a gate</b> — <code>[gates.review] builtin = \"review\"</code> (optional <code>model</code>, <code>timeout</code>) spawns a read-only <code>claude -p</code> over the worktree's diff with a fixed rubric. The verdict is derived from the findings (any blocker/major fails, whatever the reviewer claims), findings are the evidence, and a reviewer that times out or won't answer in JSON is a fail with that reason. Same registry, logs, incidents and triggers as executed gates (M7.9).</li><li><b>Project settings</b> — sidebar menu → <b>Settings…</b>: name, icon (any emoji — a quick row, a browse-all grid, or the OS picker — or an image file, downsized to a square), a color slot, pinned. The glyph replaces the folder icon everywhere the project appears.</li><li><b>Board that is a board</b> — a KPI strip (live / held / worktrees / ready / incidents), tasks as a kanban (Ready · In progress · Blocked · Done), and a worktree map of tiles grouped by project and colored by live / dirty / unpushed / merged. Tasks and Worktrees keep a Cards | Table toggle.</li><li><b>Row menus</b> — every row on the Board, PRs and Incidents has one menu (hover kebab, right-click, or Enter) carrying its actions — open, diff, PR, run, claim, gates, release, stop, ack, codify, merge, copy — instead of inline links; destructive ones last and confirmed. Menus are wider and labels never ellipsize.</li></ul><h4>Changed</h4>\n<ul><li><code>doctor</code> reports the daemon and schema version, per-event hook coverage, and the policy file with its locked keys.</li><li><code>/v1/health</code> reports <code>schema</code> and <code>auth</code>.</li></ul><h4>Fixed</h4>\n<ul><li>Scratch repositories under the OS temp dir (test fixtures, spawned-run clones) no longer appear as projects in the sidebar.</li><li>Fleet's <code>now</code> column had no room; ended sessions show their last assistant line instead of \"session ended\".</li><li>Incidents show the command's gist (the leading <code>cd … &&</code> stripped) and <code>(removed)</code> instead of a raw id for a deleted project; Spend's attribution tables lost their empty first column.</li><li>Emoji tiles and the icon preview no longer clip in the desktop app's WebKit view.</li></ul>"},"0.7.0":{"date":"2026-08-23","html":"<p>The orchestrate release: Swarm runs a task end to end on its own and you stay in control. New worktrees start warm, gates execute instead of being vouched for, <code>swarm dispatch</code> hands ready tasks to autonomous runs whose outcome is derived from the ledger, an agent that needs a human decision can ask for one, and a budget keeps the bill in bounds.</p>\n<h4>Added</h4>\n<ul><li><b>Budgets</b> — <code>[budget] daily = 25</code> / <code>weekly = 100</code> in <code>.swarm.toml</code> puts a spend ceiling on a repo, judged from the same transcript-priced numbers as the Spend view. At <code>warn_at</code> (80%) a <code>budget</code> incident opens; past 100% <code>on_exceed</code> decides: <code>\"warn\"</code>, <code>\"ask\"</code> (every Bash / Edit / Write in the repo asks first), or <code>\"stop\"</code> (spawned runs stopped, dispatch queue cleared). One incident per level per day; a budget tile on the Spend view.</li><li><b>Run profiles</b> — <code>swarm run --profile no-edits|read-only</code> (and the Run / Dispatch drawers, <code>[dispatch] profile</code>) narrow what a spawned agent may do: <code>no-edits</code> keeps the shell but not the file tools, <code>read-only</code> keeps only read and search.</li><li><b><code>swarm_context</code></b> — an agent can re-read what Swarm told it at session start, current as of now (holds, lease left, handoff, gates, resources, rule modes), plus answers to its questions. <code>GET /v1/context</code>. And <code>swarm install</code> now registers the same MCP server with <b>Codex CLI</b> and <b>Gemini CLI</b> when they're installed, so they get the <code>swarm_*</code> tools too (M7.10).</li><li><b>Ask the human</b> — an agent that hits a decision only a person can make calls <code>swarm_ask</code> (with optional suggested answers). The question shows on the session page under <b>waiting on you</b> with the options as buttons, the session gets an <b>Asking</b> badge on Fleet, and a desktop notification fires. Answer there, or <code>swarm answer <id> <text></code>; <code>swarm questions</code> lists what's open. The answer reaches the agent by itself — stdin for a spawned run, <code>[swarm]</code> context on the next tool call for an interactive session, <code>swarm_inbox</code> on demand — and a session starting later in the same task's worktree is told about open questions and answers that never arrived (M7.7).</li><li><b>Dispatch</b> — <code>swarm dispatch --ready</code> (or pick tasks; the <b>Dispatch</b> chip on the Board's Tasks; <code>swarm_dispatch</code> from a lead agent) hands ready tasks to autonomous runs: each gets its own claim and worktree and a <code>claude -p</code> run told to work there, run the gates, hand off and open the PR; <code>[dispatch] max_parallel</code> (default 2) run at once per repo, the rest queue. When a run ends Swarm derives the outcome from the ledger — executable gates re-run by the daemon, PR looked up on the forge — and reports <b>done</b>, <b>gates-failed</b>, <b>no-pr</b>, <b>crashed</b> or <b>stopped</b>; anything short of done opens a <code>dispatch_failed</code> incident and keeps the claim for you to resume or release. A dispatched run never edits the task list. <code>swarm dispatch status | clear</code>, <code>GET/POST/DELETE /v1/dispatch</code>, a <b>Dispatch</b> section on the Board (M7.5).</li><li><b>Diff and Open PR</b> — every worktree row on the Board (and a session page whose cwd is a worktree) gets <b>Diff</b>: the commits and files it carries beyond the main checkout's branch, uncommitted and untracked changes included, with a coloured unified diff per file. <b>PR</b> pushes the branch and opens a pull request (<code>gh</code>) or merge request (<code>glab</code>) prefilled from the task's title, the latest handoff, the required gates as a checklist and the file list — editable before it goes; refuses uncommitted changes, reuses an open PR for the branch. <code>swarm wt diff</code>, <code>swarm pr open [--dry-run]</code>, MCP <code>swarm_pr_open</code>; a <code>pr.opened</code> event on the Timeline (M7.3).</li><li><b>Gates that run themselves</b> — a gate with a command in <code>.swarm.toml</code> (<code>[gates.tests] cmd = \"bun test\"</code>, optional <code>timeout</code> / <code>cwd</code>) is executed rather than vouched for: <code>swarm gate run <task></code> (or <code>swarm_gate_run</code> from the agent, or <b>Gates</b> on a held task row on the Board) runs every required gate that has a command inside the task's worktree and records the verdict — exit 0 passes, the rubric is the command and how it ended, the evidence is the output tail, the log lives in <code>~/.swarm/logs</code>. Runs go through the process registry and are killed at <code>timeout</code>. When a session in a held worktree ends, the daemon runs them on its own and writes the verdicts into that session's auto-handoff (<code>[gates] auto = \"session-end\" | \"stop\" | \"off\"</code>) (M7.4).</li><li><b>Worktrees without a task</b> — <code>swarm wt create <name></code> makes a worktree for a spike or a review checkout (under <code>~/.swarm/worktrees/<project>/</code>, branch <code>wt/<name></code>, bootstrapped like a claim); <code>swarm wt</code> lists every worktree with <b>drift</b> against the main checkout's branch (<em>N behind</em>, <em>merged</em>); <code>swarm wt open</code> opens it with <code>[worktree] open = \"code {path}\"</code> or the file manager; <code>swarm wt rm</code> removes it with the same refusals as <code>release</code> (dirty, unpushed, never the main checkout, never a held claim); <code>swarm wt gc [--apply]</code> finds worktrees whose branch was merged or whose claim was released and the folder left behind. The Board's Worktrees section gets the drift column, <b>Open</b> / <b>Remove</b> per row, <b>New worktree</b> and <b>Collect stale</b> (M7.2).</li><li><b>Warm worktrees</b> — <code>.swarm.toml [worktree] copy = [\".env.local\"]</code> and <code>setup = \"bun install\"</code> bootstrap every new worktree: the files are copied from the main checkout as the claim is made and <code>setup</code> runs inside the worktree in the background (log in <code>~/.swarm/logs/<project>/bootstrap-<task>.log</code>, a <code>worktree.bootstrapped</code> event on the Timeline). <code>swarm run</code> waits for it before starting the agent; an interactive <code>swarm claim</code> prints the log path and returns at once. A failing setup opens a <code>bootstrap_failed</code> incident but never takes the claim away. Paths are repo-relative only (M7.1).</li></ul><h4>Fixed</h4>\n<ul><li>The Fleet agent badge no longer renders a stray \"…\" after the pill: badge-only cells clip instead of ellipsizing, and the column got a few more pixels.</li></ul>"},"0.6.0":{"date":"2026-08-23","html":"<p>The learn release: the data Swarm has been collecting starts paying back. Replay what an agent did, see what each task cost, turn an incident into a rule, resume a session that died, try a rule on history before switching it on, read your backlog from GitHub or Linear, and search everything Swarm remembers.</p>\n<h4>Added</h4>\n<ul><li><b>Session Replay</b> — a <b>Replay</b> button on any session steps through its tool calls one at a time, showing the full input and output of each (Prev/Next, a slider, ←/→ keys). See exactly what an agent did, in order (M4.1).</li><li><b>Cost by task</b> — the Spend view attributes cost and tokens to each task (matched to a claim by the session's worktree), plus a <b>Context budget</b> table ranking sessions by how much context they re-processed — a signal for agents re-reading the same material. <code>GET /v1/attribution</code> (M4.2).</li><li><b>Codify an incident</b> — the Incidents feed has a <b>Codify</b> action that turns an incident into a <code>.swarm.toml</code> rule snippet and a CLAUDE.md lesson, both copyable. A rule that keeps firing as <code>ask</code> suggests hardening to <code>deny</code> (M4.3).</li><li><b>Desktop notifications</b> — opt-in native notifications (settings menu) for a spawned run waiting on a permission, or a claim orphaned with unfinished work; clicking opens the Allow/Deny card or the Board. Quiet while you're looking at the dashboard (M4.7).</li><li><b>Auto-handoff, and resume where it died</b> — whenever a session working in a claimed worktree pauses or ends, Swarm derives a handoff from what it did: files edited, the last verification-looking command, the last request, the last thing it said. One <code>auto:</code> handoff per session, replaced on every pause, silenced by a handoff left on purpose. An ended session's page gets <b>Resume where it died</b>, which spawns a run on the task from that handoff plus the session's last actions; <code>swarm run resume <session-id></code>; <code>GET/POST /v1/sessions/:id/resume</code> (M4.4).</li><li><b>Rule dry-run</b> — <b>Dry-run rules</b> on the Incidents view replays a project's recorded tool calls through the rules under modes you pick: what would have been asked or denied, per rule, before you switch anything on. It also flags <b>flaky signals</b> — a rule that keeps firing on the same command that is then allowed through anyway. Nothing is recorded. <code>swarm rules dryrun [--set rule=mode,…]</code>; <code>GET /v1/rules/dryrun</code> (M4.6).</li><li><b>GitHub Issues and Linear as task sources</b> — <code>[tasks] source = \"github\"</code> reads the repo's issues through the logged-in <code>gh</code> (optional <code>labels</code> filter); <code>source = \"linear\"</code> reads Linear through its API with <code>LINEAR_API_KEY</code> from the daemon's environment (optional <code>team</code>). Both land in the Board's Tasks, <code>swarm tasks</code> and <code>swarm_next_task</code> like a markdown backlog: closed/completed is done, in-progress is active, <em>depends on #n</em> / <em>blocked by</em> become dependencies. Read-only; no credential stored (M4.8).</li><li><b>What's New in the app</b> — the dashboard shows the release notes for the running version: a <b>What's New</b> item in the settings menu, in the desktop app's <b>Swarm</b> menu, and in the tray. It also opens once on its own the first time you run a new version. Notes are parsed from <code>CHANGELOG.md</code> into <code>release-notes.js</code> at build time, so they work offline with no repo checkout.</li><li>The desktop <b>Check for Updates…</b> is in the system menu bar (Swarm menu), not only the tray.</li></ul>"},"0.5.0":{"date":"2026-08-22","html":"<p>The drive release: Swarm doesn't just watch agents now — it starts them, in a claimed worktree, and brokers what they're allowed to do. Plus the coordination primitives that make an autonomous run safe to leave alone: leases that renew themselves, gates that gate, and a handoff the next session reads on its own.</p>\n<h4>Added</h4>\n<ul><li><b><code>swarm run</code></b> — spawn an agent on a task. <code>swarm run --task login-form --prompt \"…\"</code> claims the task and the daemon starts <code>claude -p</code> in its worktree with stream-json on both ends. Steer it with <code>swarm run send</code>, stop it with <code>swarm run stop</code> (stdin closed, then the process registry's pid-based TERM/KILL — never by pattern), list with <code>swarm run ls</code>. The session shows in Fleet as ▶ spawned and is ingested like any other; every finished turn is a <code>run.result</code> event with cost and turns (M3.1).</li><li><b>Run from the dashboard</b> — Ready (or held) task rows on the Board get a <b>Run</b> action: a drawer with the prompt prefilled from the task, permission mode, model and max turns (⌘⏎ to submit). The spawned session opens with a stdin box to steer it and a <b>Stop</b> button (M3.3).</li><li><b>Permission broker</b> — a <code>swarm run</code> agent's tool-permission prompts go through the same rules as your interactive sessions: a rule <code>deny</code> auto-denies with the reason, an unflagged tool auto-allows so the agent can make progress, and anything the rules mark <code>ask</code> is held and surfaced on the session as an <b>Allow / Deny</b> card. No blocking on a terminal you can't see. Uses <code>--permission-prompt-tool stdio</code>; <code>POST /v1/runs/:id/permissions/:reqId</code> (M3.2).</li><li><b>Leases renew themselves.</b> A session working inside a claimed worktree extends the lease on any activity (hook or transcript growth) once it is past half-way — no more <code>swarm renew</code> in a long session. Expired leases whose worktree still holds uncommitted or unpushed work are marked <b>Orphaned</b> within a minute and open an <code>orphaned_claim</code> incident; nothing is removed automatically (M1.2).</li><li><b>Handoffs, injected on start</b> — <code>swarm handoff <task> --done … --remaining … [--files] [--verify]</code> (or <code>swarm_handoff</code>) records what the last holder leaves; <code>swarm resume</code> / <code>swarm_resume</code> reads it. The next session that starts inside that task's worktree gets it automatically as <code>SessionStart</code> context, along with what it holds and the lease left, gate status, held resources, and the repo's rule modes (M1.3).</li><li><b>Gates</b> — verification runs recorded against a task: <code>swarm gate record login-form review pass --rubric \"tests green, error paths read\"</code> (or <code>swarm_gate_record</code>). A run without a rubric is rejected; the latest run per gate decides; failed runs are never deleted and open a <code>gate_failed</code> incident. <code>.swarm.toml [gates] required = [\"review\"]</code> declares what every task must pass; the Board's Tasks grid shows ✓ / ✗ / — per gate and a <b>Recent gates</b> section lists the runs (M2.2).</li><li><b>The MCP tools finally connect.</b> <code>swarm_status</code>, <code>swarm_claim</code>, <code>swarm_next_task</code>, <code>swarm_handoff</code>, <code>swarm_gate_record</code>, <code>swarm_acquire_resource</code> and the rest are reachable from Claude Code — see the fix below.</li><li>Dashboard deep links (<code>?view=board&project=<id>&session=<id></code>) and a screenshot carousel with a lightbox on the website; <code>tools/screens.ts</code> re-captures the shots with Playwright at 2×.</li></ul><h4>Fixed</h4>\n<ul><li><b>Swarm's MCP tools were never reachable.</b> <code>swarm install</code> wrote <code>mcpServers.swarm</code> into <code>~/.claude/settings.json</code>, which Claude Code ignores — user-scope MCP servers live in <code>~/.claude.json</code> (what <code>claude mcp add -s user</code> edits). Install now registers there (and cleans the stale settings.json entry); <code>claude mcp list</code> shows <code>swarm ✔ Connected</code>. <b>Re-run <code>swarm install</code> after upgrading.</b></li><li><b>The daemon reads global config from where its state lives.</b> <code>[rules]</code> / <code>[gates]</code> / <code>[tasks]</code> in <code>~/.swarm/config.toml</code> are resolved against the daemon's home (<code>SWARM_HOME</code>), matching the DB and logs — spawned runs, which execute in a worktree without the repo's <code>.swarm.toml</code>, still see machine-wide rules.</li></ul>"},"0.4.1":{"date":"2026-08-22","html":"<h4>Fixed</h4>\n<ul><li><b>PRs view went dark under the desktop app.</b> A daemon launched from the Dock gets macOS's bare GUI <code>PATH</code>, so Homebrew's <code>gh</code> / <code>glab</code> were invisible and the forge silently returned nothing. The daemon now also looks in <code>/opt/homebrew/bin</code>, <code>/usr/local/bin</code>, Linuxbrew and <code>~/.local/bin</code>; <code>swarm doctor</code> reports forge CLI auth and warns when <code>glab</code> relies on a shell-only <code>GITLAB_TOKEN</code>.</li><li>Icons are vertically centred on their text again (<code>vertical-align: middle</code> instead of a fixed <code>-3px</code> tuned for the old type scale).</li><li>npm publish moves to <b>trusted publishing</b> (OIDC, no <code>NPM_TOKEN</code>), the same setup as fancy-menus.</li></ul>"},"0.4.0":{"date":"2026-08-22","html":"<p>The enforcement release: rules that watch file writes, not just Bash; a backlog Swarm can read; servers Swarm starts and stops by pid; and an Incidents feed you can clear.</p>\n<h4>Added</h4>\n<ul><li><b>Rules on file writes</b> — two new rules evaluated on <code>Write</code> / <code>Edit</code> / <code>MultiEdit</code> / <code>NotebookEdit</code> paths (and Bash working directories), not only Bash commands. <code>no_foreign_worktree</code> (default <code>ask</code>) stops a session from editing inside a worktree another claim holds — <em>never touch a worktree you don't hold</em> is now a hook decision, with holding inferred from the session's cwd. <code>claim_required_to_write</code> (opt-in) makes a repo's shared checkout read-only without a claim: claim a task, get a worktree, write there. Both per repo as <code>ask | deny | off</code>.</li><li><b>Incidents view</b> — the denied-action feed as its own tab: Open / All, per-rule counts, reason and session per row, <b>Ack</b> and <b>Ack all</b>; the open count sits in the nav. <code>GET /v1/incidents?open=1&project=</code>, <code>POST /v1/incidents/:seq/ack</code>, <code>POST /v1/incidents/ack</code>, <code>/v1/state.openIncidents</code>. The Board keeps a short open-only section.</li><li><b>Task source</b> — <code>.swarm.toml</code> <code>[tasks] source = \"docs/plan.md\"</code> points at a markdown file whose <code>ID | Task | Depends | Status</code> tables are the backlog (✅ / 🟡 / ⚪, dependencies by task id or milestone prefix). The Board gets a <b>Tasks</b> section (Ready / Open / All, <em>Claim</em> per row), the CLI <code>swarm tasks [--ready]</code>, and agents <code>swarm_next_task</code> — the first unclaimed task whose dependencies are done. Swarm's own roadmap is its task source. Markdown only (OQ-5 decided).</li><li><b><code>swarm serve</code> / <code>swarm proc</code></b> — <code>swarm serve start --name web -- npm run dev</code> allocates a free port (ledger + bind probe), runs the command detached with <code>PORT</code> set and logs under <code>~/.swarm/logs/<project>/</code>, registers pid + start time, and acquires the singleton — so a second <code>web</code> fails closed and the port is protected for every other session with no config. <code>serve ls | stop [name|pid]</code>, <code>proc start | ls | stop</code> for workers without a port. Stop signals registry pids only, verified by start time; nothing is ever killed by pattern. <code>POST /v1/ports/allocate</code>, <code>GET/POST/DELETE /v1/processes</code>; <b>Processes</b> section on the Board with <em>Stop</em>.</li><li><b>Star nudge</b> — once a month at most, never on first open, the dashboard asks for a GitHub star. <em>Later</em> snoozes 30 days, <em>Don't ask again</em> is final; localStorage only.</li><li><b>Sidebar drag-and-drop</b> — pinned projects reorder by dragging; the order persists on the daemon (<code>PUT /v1/projects/order</code>, <code>Project.order</code>).</li><li><b>Desktop app menu</b> — a real application menu (Swarm / Edit / View / Window): ⌘C/⌘V work, <b>View › Zoom In / Zoom Out / Actual Size</b> (<code>⌘+</code> <code>⌘−</code> <code>⌘0</code>) scale the dashboard (persisted), plus Reload and Full Screen.</li></ul><h4>Fixed</h4>\n<ul><li><b>Shared-tree rules no longer lose sight of a session mid-turn.</b> <code>shared_tree</code> / <code>destructive_git</code> keyed on a 2-minute last-seen window fed only by hooks, so a neighbour three minutes into a long turn became invisible — and its uncommitted work unguarded. Transcript growth now counts as activity (the tailer bumps <code>last_seen_at</code>), and the liveness window is the daemon's 10-minute idle threshold (<code>LIVE_WINDOW_MS</code>). A false positive costs one confirmation; a false negative cost someone's work.</li><li><code>destructive_git</code> also matches <code>git stash drop</code>, <code>git stash clear</code> and <code>git branch -D</code>.</li><li>Dashboard type scale is one step larger across the board (base 13 → 14 px; the smallest labels 10 → 11 px) — it had drifted too small, especially in the desktop app.</li><li>The <b>PRs</b> tab icon (and the branch/commit glyphs) were near-invisible at 15 px; they use the pixelarticons <em>sharp</em> variants now.</li><li>The daemon dot stayed red for up to 15 s after load on a healthy connection (the SSE stream sent nothing until its first heartbeat); the stream now flushes immediately.</li><li>The nav flashed \"Fleet\" before the restored tab was applied; session-detail event kinds (<code>userpromptsubmit</code>) no longer overflow into the message column; \"Unpinned · seen, not pinned\" keeps its spacing.</li></ul><h4>Changed — docs</h4>\n<ul><li>README and site now say what the code does: Codex CLI and Grok sessions are tailed alongside Claude Code; the requirements and architecture diagram list all three.</li><li>Rules are described as <b>guardrails against accidents, not a sandbox</b> — the guide has a new \"What rules are — and aren't\" section spelling out that a denied Bash command can be routed around (script, heredoc, direct file edit), and that worktree isolation via claims is the real fix. The site's feature cards lead with claims, rules and resources instead of Fleet and Spend.</li></ul><h4>Changed — performance</h4>\n<ul><li><b>Daemon never spawns <code>git</code> on a request.</b> Worktree status (<code>git worktree list</code> + <code>status</code>/<code>rev-list</code> per worktree, ~0.8 s across a fleet) moves to a 15 s background refresh with async <code>Bun.spawn</code>; <code>/v1/state</code> serves the cache (612 ms → ~15 ms). Claim/release invalidate it.</li><li><b>Hook round-trips are two indexed statements</b>, not two <code>git rev-parse</code> spawns plus a transcript-directory scan: <code>cwd → project</code> is cached 60 s, the inline transcript tail is debounced to once per 2 s per session (the 5 s tailer covers steady state), and subagent directories are re-listed only when their mtime moves.</li><li><b>Events store ~2 KB, not ~10 KB.</b> <code>tool_input</code> is clipped at 2 KB and <code>tool_response</code> at 4 KB in <code>payload</code> (<code>{truncated, bytes, preview}</code>), and the tool I/O is no longer duplicated in <code>raw</code>. Existing databases are rewritten once on boot and <code>VACUUM</code>ed (96 MB → 27 MB here). Retention: events older than 30 days are pruned daily (incidents kept), <code>raw</code> is cleared after 7 days.</li><li><b>Wire shape.</b> SSE frames, <code>GET /v1/events</code> replays and <code>GET /v1/sessions/:id/events</code> carry <code>seq/ts/type/projectId/sessionId/payload{hook,summary,…}</code> only — no <code>raw</code>, no tool I/O (a 5.5 MB session fetch is now ~150 KB). <code>GET /v1/events/:seq</code> returns one stored event in full; <code>?full=1</code> on the SSE replay does the same. <code>?since=0</code> replays the last 200 events, not the table.</li><li><b>Incremental session view</b> — <code>GET /v1/sessions/:id/events?after=<seq>&afterTs=<iso></code>; the dashboard appends instead of re-fetching 500 events + 500 turns on every hook.</li><li><b>Dashboard render loop</b> — one <code>requestAnimationFrame</code> scheduler, snapshot <code>seq</code> short-circuit, paused while the tab is hidden, exponential SSE reconnect backoff; session log merges two sorted lists and caches rendered rows; data-grid memoises persisted layout and uses one <code>Intl.Collator</code>; charts memoise the turn strip.</li><li>SQLite: indexes on <code>events(type, seq)</code> and <code>turns(ts)</code>, <code>mmap_size</code> 256 MB, cached prepared statements (<code>db.query</code>), <code>sessions</code>/<code>spend</code>/<code>incidents</code> memoised per write generation; <code>/v1/spend</code> is its own endpoint.</li><li>Background tick: Codex/Grok discovery every 15 s when idle; Grok <code>summary.json</code> re-read only on mtime change.</li></ul>"},"0.3.0":{"date":"2026-08-22","html":"<p>The coordination release: rules you can configure, runtime resources agents can hold, and the merge queue at the end of the loop.</p>\n<h4>Added</h4>\n<ul><li><b>Config system</b> — <code>~/.swarm/config.toml</code> (global) deep-merged with an optional <code><repo>/.swarm.toml</code>. Lenient validation: bad config can never take the daemon down. Daemon port preference is <code>SWARM_PORT</code> > config > 7777. See <code>docs/13-config.md</code>.</li><li><b>Rule engine v2</b> — every rule is per-repo configurable as <code>ask | deny | off</code>: <code>shared_tree</code>, <code>destructive_git</code>, <code>pattern_kill</code>, and the new <code>protected_ports</code> (kill/free of a configured port — <code>lsof | kill</code>, <code>fuser -k</code>, <code>kill-port</code> — is asked or denied). <code>deny</code> is returned to Claude Code as a real permission denial.</li><li><b>Incidents</b> — every non-allow decision is recorded (<code>incident.opened</code>: rule, action, command, reason), exposed at <code>GET /v1/incidents</code>, included in <code>/v1/state</code>, and shown on the Board.</li><li><b>Runtime resources (Phase 1)</b> — named singletons for what agents fight over at runtime (dev servers, databases, ports). Fail-closed acquire: holdings live while their pid runs or their lease hasn't expired; the same owner refreshes; dead holdings reap instead of blocking. Release is fail-closed too (owner required, <code>--force</code> overrides). Held ports automatically join the protected-ports rule — acquiring <code>db</code> on 5432 guards <code>lsof -ti:5432 | xargs kill</code> for every other agent, no config needed. HTTP <code>GET/POST /v1/resources</code>, <code>DELETE /v1/resources/:name</code>; MCP <code>swarm_acquire_resource</code> / <code>swarm_release_resource</code> / <code>swarm_resources</code>; CLI <code>swarm res ls|acquire|release</code>.</li><li><b>PRs view</b> — one merge queue across GitHub and GitLab. Forge detection from the git remote (ssh/https, GitLab subgroups, self-hosted), polled through the locally-authenticated <code>gh</code> / <code>glab</code> CLIs with the project root as cwd — no tokens stored, 2-minute per-project cache floor. <code>GET /v1/prs</code>, <code>POST /v1/prs/merge</code> (squash). Merge is offered only on green, mergeable, non-draft rows, behind a confirm.</li><li><b>Board view</b> — Claims, Worktrees, Resources, and Incidents move out of Fleet into their own view; Fleet shows sessions only (Live + Earlier). Last view and project selection persist across reloads.</li><li><b>Stats view</b> — <code>GET /v1/stats</code>; activity line, calendar heatmap, and streaks (daily buckets in local time, DST-immune). <code>swarm stats</code> on the CLI.</li><li><b>Data-grid everywhere</b> — Claims, Worktrees, Resources, Incidents, PRs, and all six Spend tables render through the same sortable / resizable / reorderable grid with per-column filters, a column-visibility menu, and persisted layouts. Header ticks and tooltips make the affordances discoverable.</li><li><b>Desktop: Check for Updates…</b> in the tray menu, wired to the Tauri updater with native dialogs (available / up-to-date / failed) and install-and-restart on accept.</li><li><b>Agent badge</b> on every Fleet and session row, so mixed-agent fleets are labelled consistently.</li><li><b>Website</b> — getswarm.vercel.app: OS-detected downloads from the latest GitHub release, the <code>bunx</code> one-liner, sharing tags with hero art, and (this release) rendered docs and release notes.</li><li><b>Design tokens</b> — the dashboard's CSS contains zero raw hex / rgba / font-size / duration values; the system is documented in <code>docs/12-design-tokens.md</code> with a drift grep.</li><li>Swarm now dogfoods its own rules via the repo's <code>.swarm.toml</code> (<code>shared_tree</code> / <code>destructive_git</code> deny, daemon port protected).</li></ul><h4>Fixed</h4>\n<ul><li><b>Hook resilience</b> — the PreToolUse hook falls back to the default port when <code>daemon.json</code> points at a dead daemon, so a crashed daemon no longer silently disables the guard (this was the gap behind a real <code>git add -A</code> collision).</li><li>Resource liveness: pid 0 was treated as a live process (<code>kill(0)</code> addresses the process group), so those holdings never reaped; tracked pids are now <code>> 0</code>. <code>heldPorts()</code> is one SELECT on the hook path; lazy reap on acquire, sweep on the 5 s tick. Unknown session IDs on acquire no longer mint phantom sessions.</li><li>Session view is two equal columns again (a bare <code>aside</code> selector in the sidebar-collapse CSS captured the session side panel); the log keeps your scroll position across live updates and follows the tail only when pinned to the bottom.</li><li>A pinned project whose root vanished is merged into the live same-name entry (repo renames produced duplicate sidebar rows); the sidebar <code>⋯</code> appears on hover in the count's slot and reserves no space.</li><li>Desktop: quit actually quits, window close hides (macOS convention) and the dock icon restores it, and the <code>swarmd</code> sidecar dies with the app. Dev builds serve the repo's live dashboard instead of a stale staged snapshot.</li><li>Release pipeline: npm publish is skipped cleanly when <code>NPM_TOKEN</code> is absent (since 0.4.1: trusted publishing, no token).</li></ul>"},"0.2.2":{"date":"2026-08-21","html":"<h4>Added</h4>\n<ul><li>Publishable <code>@ra3orblade/swarm</code> npm package (bundled bins + dashboard); <code>bunx @ra3orblade/swarm setup</code> onboarding.</li><li>Enterprise data-grid for Fleet with a collapsible sidebar; pixel-art icon set and bespoke empty-state illustrations; folder picker; green chart palette.</li><li>Desktop: macOS window chrome, animated pixel-logo splash, free-port daemon startup.</li></ul><h4>Fixed</h4>\n<ul><li>Release builds bundle every platform target; Linux ships <code>.deb</code> + <code>.rpm</code> (AppImage disabled until <code>linuxdeploy</code> on GitHub runners is debugged).</li></ul>"},"0.0.6":{"date":"2026-08-21","html":"<p>First signed and notarized macOS desktop build; <code>release.yml</code> became a three-OS matrix (macOS / Windows / Linux) with a native sidecar per runner.</p>"}};
|
|
2
|
+
window.RELEASE_NOTES = {"0.9.0":{"date":"2026-08-24","html":"<p>The crew release: the agents on your machine stop being strangers. They message each other and you, follow a declared workflow instead of a hopeful prompt, and every major CLI brand now shows up — Claude, Codex, Gemini, Grok. Plus the first-run and update experience a launch deserves.</p>\n<h4>Added</h4>\n<ul><li><b>Agent messaging</b> — <code>swarm_send(to, text)</code> reaches another session (id or unique prefix), whoever holds a task, or <code>\"lead\"</code> (your interactive session in the project). Delivery: on the recipient's next tool call as injected context, immediately over stdin to a spawned run, or pulled with <code>swarm_inbox</code> (which now returns answers <em>and</em> messages) — exactly once. A <b>messages</b> thread with compose box on every session page; <code>swarm msg send|ls</code> (M7.6, OQ-12 decided).</li><li><b>Workflows</b> — <code>[[workflows]] name = \"ship\" steps = [\"implement\", \"gate:tests\", \"gate:review\", \"pr\"]</code> in <code>.swarm.toml</code>; <code>swarm workflow ship <task></code> and the daemon advances it: run steps spawn an agent in the task's worktree (told what the workflow will do itself), gate steps execute — only a pass advances — and <code>pr</code> pushes and opens the pull request from the ledger. A failed step stops with a <code>workflow_failed</code> incident; a daemon restart marks in-flight workflows stopped, honestly. <b>Workflows</b> on the Board with per-step chips (M7.8).</li><li><b>Gemini CLI adapter</b> — <code>~/.gemini</code> chat recordings are discovered and priced like every other agent: sessions, turns, tokens, cost, sparkline, Timeline, Spend. Schema from upstream source; first real-session validation pending (M5.4).</li><li><b>Timeline that shows the work</b> — bars are now a faint base with a tick per turn: bursts and idle stretches are visible instead of painted over. A thin claims lane per project shows lease spans (held / expired / orphaned). Recent gates carries a per-gate pass/fail history strip; every pinned project gets a 14-day spend sparkline in the sidebar (M5.7).</li><li><b><code>swarm demo</code></b> — a seeded demo dashboard on its own home and port: four agent brands, a live lease, an orphaned claim, gate history, incidents, a question, a message, a workflow mid-flight. Tailers are off in demo mode, so it never ingests your real logs — and your real data is never touched (delete <code>~/.swarm/demo</code> to reset).</li><li><b>Update that actually updates</b> — after an upgrade the dashboard notices the newer version on disk and offers a one-click daemon restart (the daemon re-execs into the new build and the page reloads). Long-lived tabs re-check every 5 minutes.</li><li><b>First-run onboarding</b> — an empty Fleet now explains the three steps (hook in — with a live <em>not installed</em> badge —, open any agent session, watch it appear) instead of showing a blank table.</li></ul><h4>Fixed</h4>\n<ul><li>External links in the desktop app (PR titles, Documentation, feedback, the just-opened PR) open in the browser — the webview silently swallowed them before.</li><li>Section-header actions (New worktree, Collect stale, timeline ranges) are proper small buttons instead of shouting uppercase with off-baseline icons.</li><li>The timeline claims lane no longer collides with the kanban's styles; a test file that broke lint on main is formatted.</li></ul><h4>Notes</h4>\n<ul><li>Windows builds are produced by CI as before but this release had no human Windows smoke test — reports welcome.</li></ul>"},"0.8.0":{"date":"2026-08-23","html":"<p>The trust release: Swarm becomes something a team and a security reviewer can rely on, without giving up local-first. An org can pin the rules that matter and they hold even when the daemon is down; every record says who did it; the daemon has a credential; what is stored is exportable and redactable; a second agent can be the reviewer. And the dashboard stopped looking like a list of tables.</p>\n<h4>Added</h4>\n<ul><li><b>Org policy layer</b> — a third config file, <code>~/.swarm/policy.toml</code> (or <code>$SWARM_POLICY</code>), sits under global and repo config and may declare <code>locked = [\"rules.destructive_git\", \"rules.protected\", …]</code>: dotted keys or whole subtrees the layers below cannot change. A locked key keeps the policy's value, every attempt to override it is a <code>policy</code> incident, and <code>swarm doctor</code> shows which file set each value and who tried to change it. <code>GET /v1/policy</code> exposes provenance (M8.1).</li><li><b>Tamper detection</b> — on every session start the daemon checks that all ten hook entries are still in <code>~/.claude/settings.json</code> with a sane timeout, that no lower config layer fights a locked key, and that <code>SWARM_GUARD=off</code> isn't set while the policy locks rules (it is then ignored). Each finding opens a <code>policy</code> incident once; <code>swarm doctor</code> prints the same (M8.1b).</li><li><b>Fail-closed for locked rules</b> — while the policy locks any rule the daemon keeps <code>~/.swarm/policy.cache.json</code> (locked modes + a snapshot of live sessions and held worktrees, integrity-hashed). If the daemon is unreachable on a tool call, the hook shim enforces exactly those rules from the cache; everything else still fails open (M8.1c, OQ-3 resolved).</li><li><b>Who did it</b> — every ledger record and every event carries an <code>actor</code> (<code>human</code> / <code>agent</code> / <code>run</code> / <code>daemon</code> + id); existing rows were back-filled from the owner strings clients always sent. Schema changes now go through versioned migrations (<code>/v1/health</code> and <code>doctor</code> report the schema version) (M8.2a).</li><li><b>Daemon token</b> — the daemon creates <code>~/.swarm/token</code> on first start; the CLI, MCP server, hook shim and <code>swarm ui</code> send it. Local callers may still omit it by default; <code>[daemon] auth = \"required\"</code> makes every call carry it, a wrong token is always refused, and anything that isn't loopback always needs it (M8.2b).</li><li><b>Audit log + export</b> — the ledger-changing subset of events (claims, worktrees, PRs, questions, dispatch, resources, processes, gates, handoffs, permissions, incidents, run results, session start/end), with actor: <code>swarm audit export [--since 30d] [-p] [--type …] [--format jsonl|csv|json]</code> and <code>GET /v1/audit</code>. Retention is split — <code>[events] retain_days = 30</code> for chatter, <code>[audit] retain_days = 0</code> (forever) for audit rows (M8.2c).</li><li><b>Privacy on ingest</b> — <code>[privacy] store_prompts = false</code> keeps the event but not the prompt text, <code>store_reasoning = false</code> keeps token counts but not assistant text, <code>redact = [\"ACME-[0-9]+\"]</code> scrubs stored strings; API-key-looking tokens and <code>Bearer …</code> credentials are always redacted. Global-only keys an org can lock (M8.2c).</li><li><b>Review as a gate</b> — <code>[gates.review] builtin = \"review\"</code> (optional <code>model</code>, <code>timeout</code>) spawns a read-only <code>claude -p</code> over the worktree's diff with a fixed rubric. The verdict is derived from the findings (any blocker/major fails, whatever the reviewer claims), findings are the evidence, and a reviewer that times out or won't answer in JSON is a fail with that reason. Same registry, logs, incidents and triggers as executed gates (M7.9).</li><li><b>Project settings</b> — sidebar menu → <b>Settings…</b>: name, icon (any emoji — a quick row, a browse-all grid, or the OS picker — or an image file, downsized to a square), a color slot, pinned. The glyph replaces the folder icon everywhere the project appears.</li><li><b>Board that is a board</b> — a KPI strip (live / held / worktrees / ready / incidents), tasks as a kanban (Ready · In progress · Blocked · Done), and a worktree map of tiles grouped by project and colored by live / dirty / unpushed / merged. Tasks and Worktrees keep a Cards | Table toggle.</li><li><b>Row menus</b> — every row on the Board, PRs and Incidents has one menu (hover kebab, right-click, or Enter) carrying its actions — open, diff, PR, run, claim, gates, release, stop, ack, codify, merge, copy — instead of inline links; destructive ones last and confirmed. Menus are wider and labels never ellipsize.</li></ul><h4>Changed</h4>\n<ul><li><code>doctor</code> reports the daemon and schema version, per-event hook coverage, and the policy file with its locked keys.</li><li><code>/v1/health</code> reports <code>schema</code> and <code>auth</code>.</li></ul><h4>Fixed</h4>\n<ul><li>Scratch repositories under the OS temp dir (test fixtures, spawned-run clones) no longer appear as projects in the sidebar.</li><li>Fleet's <code>now</code> column had no room; ended sessions show their last assistant line instead of \"session ended\".</li><li>Incidents show the command's gist (the leading <code>cd … &&</code> stripped) and <code>(removed)</code> instead of a raw id for a deleted project; Spend's attribution tables lost their empty first column.</li><li>Emoji tiles and the icon preview no longer clip in the desktop app's WebKit view.</li></ul>"},"0.7.0":{"date":"2026-08-23","html":"<p>The orchestrate release: Swarm runs a task end to end on its own and you stay in control. New worktrees start warm, gates execute instead of being vouched for, <code>swarm dispatch</code> hands ready tasks to autonomous runs whose outcome is derived from the ledger, an agent that needs a human decision can ask for one, and a budget keeps the bill in bounds.</p>\n<h4>Added</h4>\n<ul><li><b>Budgets</b> — <code>[budget] daily = 25</code> / <code>weekly = 100</code> in <code>.swarm.toml</code> puts a spend ceiling on a repo, judged from the same transcript-priced numbers as the Spend view. At <code>warn_at</code> (80%) a <code>budget</code> incident opens; past 100% <code>on_exceed</code> decides: <code>\"warn\"</code>, <code>\"ask\"</code> (every Bash / Edit / Write in the repo asks first), or <code>\"stop\"</code> (spawned runs stopped, dispatch queue cleared). One incident per level per day; a budget tile on the Spend view.</li><li><b>Run profiles</b> — <code>swarm run --profile no-edits|read-only</code> (and the Run / Dispatch drawers, <code>[dispatch] profile</code>) narrow what a spawned agent may do: <code>no-edits</code> keeps the shell but not the file tools, <code>read-only</code> keeps only read and search.</li><li><b><code>swarm_context</code></b> — an agent can re-read what Swarm told it at session start, current as of now (holds, lease left, handoff, gates, resources, rule modes), plus answers to its questions. <code>GET /v1/context</code>. And <code>swarm install</code> now registers the same MCP server with <b>Codex CLI</b> and <b>Gemini CLI</b> when they're installed, so they get the <code>swarm_*</code> tools too (M7.10).</li><li><b>Ask the human</b> — an agent that hits a decision only a person can make calls <code>swarm_ask</code> (with optional suggested answers). The question shows on the session page under <b>waiting on you</b> with the options as buttons, the session gets an <b>Asking</b> badge on Fleet, and a desktop notification fires. Answer there, or <code>swarm answer <id> <text></code>; <code>swarm questions</code> lists what's open. The answer reaches the agent by itself — stdin for a spawned run, <code>[swarm]</code> context on the next tool call for an interactive session, <code>swarm_inbox</code> on demand — and a session starting later in the same task's worktree is told about open questions and answers that never arrived (M7.7).</li><li><b>Dispatch</b> — <code>swarm dispatch --ready</code> (or pick tasks; the <b>Dispatch</b> chip on the Board's Tasks; <code>swarm_dispatch</code> from a lead agent) hands ready tasks to autonomous runs: each gets its own claim and worktree and a <code>claude -p</code> run told to work there, run the gates, hand off and open the PR; <code>[dispatch] max_parallel</code> (default 2) run at once per repo, the rest queue. When a run ends Swarm derives the outcome from the ledger — executable gates re-run by the daemon, PR looked up on the forge — and reports <b>done</b>, <b>gates-failed</b>, <b>no-pr</b>, <b>crashed</b> or <b>stopped</b>; anything short of done opens a <code>dispatch_failed</code> incident and keeps the claim for you to resume or release. A dispatched run never edits the task list. <code>swarm dispatch status | clear</code>, <code>GET/POST/DELETE /v1/dispatch</code>, a <b>Dispatch</b> section on the Board (M7.5).</li><li><b>Diff and Open PR</b> — every worktree row on the Board (and a session page whose cwd is a worktree) gets <b>Diff</b>: the commits and files it carries beyond the main checkout's branch, uncommitted and untracked changes included, with a coloured unified diff per file. <b>PR</b> pushes the branch and opens a pull request (<code>gh</code>) or merge request (<code>glab</code>) prefilled from the task's title, the latest handoff, the required gates as a checklist and the file list — editable before it goes; refuses uncommitted changes, reuses an open PR for the branch. <code>swarm wt diff</code>, <code>swarm pr open [--dry-run]</code>, MCP <code>swarm_pr_open</code>; a <code>pr.opened</code> event on the Timeline (M7.3).</li><li><b>Gates that run themselves</b> — a gate with a command in <code>.swarm.toml</code> (<code>[gates.tests] cmd = \"bun test\"</code>, optional <code>timeout</code> / <code>cwd</code>) is executed rather than vouched for: <code>swarm gate run <task></code> (or <code>swarm_gate_run</code> from the agent, or <b>Gates</b> on a held task row on the Board) runs every required gate that has a command inside the task's worktree and records the verdict — exit 0 passes, the rubric is the command and how it ended, the evidence is the output tail, the log lives in <code>~/.swarm/logs</code>. Runs go through the process registry and are killed at <code>timeout</code>. When a session in a held worktree ends, the daemon runs them on its own and writes the verdicts into that session's auto-handoff (<code>[gates] auto = \"session-end\" | \"stop\" | \"off\"</code>) (M7.4).</li><li><b>Worktrees without a task</b> — <code>swarm wt create <name></code> makes a worktree for a spike or a review checkout (under <code>~/.swarm/worktrees/<project>/</code>, branch <code>wt/<name></code>, bootstrapped like a claim); <code>swarm wt</code> lists every worktree with <b>drift</b> against the main checkout's branch (<em>N behind</em>, <em>merged</em>); <code>swarm wt open</code> opens it with <code>[worktree] open = \"code {path}\"</code> or the file manager; <code>swarm wt rm</code> removes it with the same refusals as <code>release</code> (dirty, unpushed, never the main checkout, never a held claim); <code>swarm wt gc [--apply]</code> finds worktrees whose branch was merged or whose claim was released and the folder left behind. The Board's Worktrees section gets the drift column, <b>Open</b> / <b>Remove</b> per row, <b>New worktree</b> and <b>Collect stale</b> (M7.2).</li><li><b>Warm worktrees</b> — <code>.swarm.toml [worktree] copy = [\".env.local\"]</code> and <code>setup = \"bun install\"</code> bootstrap every new worktree: the files are copied from the main checkout as the claim is made and <code>setup</code> runs inside the worktree in the background (log in <code>~/.swarm/logs/<project>/bootstrap-<task>.log</code>, a <code>worktree.bootstrapped</code> event on the Timeline). <code>swarm run</code> waits for it before starting the agent; an interactive <code>swarm claim</code> prints the log path and returns at once. A failing setup opens a <code>bootstrap_failed</code> incident but never takes the claim away. Paths are repo-relative only (M7.1).</li></ul><h4>Fixed</h4>\n<ul><li>The Fleet agent badge no longer renders a stray \"…\" after the pill: badge-only cells clip instead of ellipsizing, and the column got a few more pixels.</li></ul>"},"0.6.0":{"date":"2026-08-23","html":"<p>The learn release: the data Swarm has been collecting starts paying back. Replay what an agent did, see what each task cost, turn an incident into a rule, resume a session that died, try a rule on history before switching it on, read your backlog from GitHub or Linear, and search everything Swarm remembers.</p>\n<h4>Added</h4>\n<ul><li><b>Session Replay</b> — a <b>Replay</b> button on any session steps through its tool calls one at a time, showing the full input and output of each (Prev/Next, a slider, ←/→ keys). See exactly what an agent did, in order (M4.1).</li><li><b>Cost by task</b> — the Spend view attributes cost and tokens to each task (matched to a claim by the session's worktree), plus a <b>Context budget</b> table ranking sessions by how much context they re-processed — a signal for agents re-reading the same material. <code>GET /v1/attribution</code> (M4.2).</li><li><b>Codify an incident</b> — the Incidents feed has a <b>Codify</b> action that turns an incident into a <code>.swarm.toml</code> rule snippet and a CLAUDE.md lesson, both copyable. A rule that keeps firing as <code>ask</code> suggests hardening to <code>deny</code> (M4.3).</li><li><b>Desktop notifications</b> — opt-in native notifications (settings menu) for a spawned run waiting on a permission, or a claim orphaned with unfinished work; clicking opens the Allow/Deny card or the Board. Quiet while you're looking at the dashboard (M4.7).</li><li><b>Auto-handoff, and resume where it died</b> — whenever a session working in a claimed worktree pauses or ends, Swarm derives a handoff from what it did: files edited, the last verification-looking command, the last request, the last thing it said. One <code>auto:</code> handoff per session, replaced on every pause, silenced by a handoff left on purpose. An ended session's page gets <b>Resume where it died</b>, which spawns a run on the task from that handoff plus the session's last actions; <code>swarm run resume <session-id></code>; <code>GET/POST /v1/sessions/:id/resume</code> (M4.4).</li><li><b>Rule dry-run</b> — <b>Dry-run rules</b> on the Incidents view replays a project's recorded tool calls through the rules under modes you pick: what would have been asked or denied, per rule, before you switch anything on. It also flags <b>flaky signals</b> — a rule that keeps firing on the same command that is then allowed through anyway. Nothing is recorded. <code>swarm rules dryrun [--set rule=mode,…]</code>; <code>GET /v1/rules/dryrun</code> (M4.6).</li><li><b>GitHub Issues and Linear as task sources</b> — <code>[tasks] source = \"github\"</code> reads the repo's issues through the logged-in <code>gh</code> (optional <code>labels</code> filter); <code>source = \"linear\"</code> reads Linear through its API with <code>LINEAR_API_KEY</code> from the daemon's environment (optional <code>team</code>). Both land in the Board's Tasks, <code>swarm tasks</code> and <code>swarm_next_task</code> like a markdown backlog: closed/completed is done, in-progress is active, <em>depends on #n</em> / <em>blocked by</em> become dependencies. Read-only; no credential stored (M4.8).</li><li><b>What's New in the app</b> — the dashboard shows the release notes for the running version: a <b>What's New</b> item in the settings menu, in the desktop app's <b>Swarm</b> menu, and in the tray. It also opens once on its own the first time you run a new version. Notes are parsed from <code>CHANGELOG.md</code> into <code>release-notes.js</code> at build time, so they work offline with no repo checkout.</li><li>The desktop <b>Check for Updates…</b> is in the system menu bar (Swarm menu), not only the tray.</li></ul>"},"0.5.0":{"date":"2026-08-22","html":"<p>The drive release: Swarm doesn't just watch agents now — it starts them, in a claimed worktree, and brokers what they're allowed to do. Plus the coordination primitives that make an autonomous run safe to leave alone: leases that renew themselves, gates that gate, and a handoff the next session reads on its own.</p>\n<h4>Added</h4>\n<ul><li><b><code>swarm run</code></b> — spawn an agent on a task. <code>swarm run --task login-form --prompt \"…\"</code> claims the task and the daemon starts <code>claude -p</code> in its worktree with stream-json on both ends. Steer it with <code>swarm run send</code>, stop it with <code>swarm run stop</code> (stdin closed, then the process registry's pid-based TERM/KILL — never by pattern), list with <code>swarm run ls</code>. The session shows in Fleet as ▶ spawned and is ingested like any other; every finished turn is a <code>run.result</code> event with cost and turns (M3.1).</li><li><b>Run from the dashboard</b> — Ready (or held) task rows on the Board get a <b>Run</b> action: a drawer with the prompt prefilled from the task, permission mode, model and max turns (⌘⏎ to submit). The spawned session opens with a stdin box to steer it and a <b>Stop</b> button (M3.3).</li><li><b>Permission broker</b> — a <code>swarm run</code> agent's tool-permission prompts go through the same rules as your interactive sessions: a rule <code>deny</code> auto-denies with the reason, an unflagged tool auto-allows so the agent can make progress, and anything the rules mark <code>ask</code> is held and surfaced on the session as an <b>Allow / Deny</b> card. No blocking on a terminal you can't see. Uses <code>--permission-prompt-tool stdio</code>; <code>POST /v1/runs/:id/permissions/:reqId</code> (M3.2).</li><li><b>Leases renew themselves.</b> A session working inside a claimed worktree extends the lease on any activity (hook or transcript growth) once it is past half-way — no more <code>swarm renew</code> in a long session. Expired leases whose worktree still holds uncommitted or unpushed work are marked <b>Orphaned</b> within a minute and open an <code>orphaned_claim</code> incident; nothing is removed automatically (M1.2).</li><li><b>Handoffs, injected on start</b> — <code>swarm handoff <task> --done … --remaining … [--files] [--verify]</code> (or <code>swarm_handoff</code>) records what the last holder leaves; <code>swarm resume</code> / <code>swarm_resume</code> reads it. The next session that starts inside that task's worktree gets it automatically as <code>SessionStart</code> context, along with what it holds and the lease left, gate status, held resources, and the repo's rule modes (M1.3).</li><li><b>Gates</b> — verification runs recorded against a task: <code>swarm gate record login-form review pass --rubric \"tests green, error paths read\"</code> (or <code>swarm_gate_record</code>). A run without a rubric is rejected; the latest run per gate decides; failed runs are never deleted and open a <code>gate_failed</code> incident. <code>.swarm.toml [gates] required = [\"review\"]</code> declares what every task must pass; the Board's Tasks grid shows ✓ / ✗ / — per gate and a <b>Recent gates</b> section lists the runs (M2.2).</li><li><b>The MCP tools finally connect.</b> <code>swarm_status</code>, <code>swarm_claim</code>, <code>swarm_next_task</code>, <code>swarm_handoff</code>, <code>swarm_gate_record</code>, <code>swarm_acquire_resource</code> and the rest are reachable from Claude Code — see the fix below.</li><li>Dashboard deep links (<code>?view=board&project=<id>&session=<id></code>) and a screenshot carousel with a lightbox on the website; <code>tools/screens.ts</code> re-captures the shots with Playwright at 2×.</li></ul><h4>Fixed</h4>\n<ul><li><b>Swarm's MCP tools were never reachable.</b> <code>swarm install</code> wrote <code>mcpServers.swarm</code> into <code>~/.claude/settings.json</code>, which Claude Code ignores — user-scope MCP servers live in <code>~/.claude.json</code> (what <code>claude mcp add -s user</code> edits). Install now registers there (and cleans the stale settings.json entry); <code>claude mcp list</code> shows <code>swarm ✔ Connected</code>. <b>Re-run <code>swarm install</code> after upgrading.</b></li><li><b>The daemon reads global config from where its state lives.</b> <code>[rules]</code> / <code>[gates]</code> / <code>[tasks]</code> in <code>~/.swarm/config.toml</code> are resolved against the daemon's home (<code>SWARM_HOME</code>), matching the DB and logs — spawned runs, which execute in a worktree without the repo's <code>.swarm.toml</code>, still see machine-wide rules.</li></ul>"},"0.4.1":{"date":"2026-08-22","html":"<h4>Fixed</h4>\n<ul><li><b>PRs view went dark under the desktop app.</b> A daemon launched from the Dock gets macOS's bare GUI <code>PATH</code>, so Homebrew's <code>gh</code> / <code>glab</code> were invisible and the forge silently returned nothing. The daemon now also looks in <code>/opt/homebrew/bin</code>, <code>/usr/local/bin</code>, Linuxbrew and <code>~/.local/bin</code>; <code>swarm doctor</code> reports forge CLI auth and warns when <code>glab</code> relies on a shell-only <code>GITLAB_TOKEN</code>.</li><li>Icons are vertically centred on their text again (<code>vertical-align: middle</code> instead of a fixed <code>-3px</code> tuned for the old type scale).</li><li>npm publish moves to <b>trusted publishing</b> (OIDC, no <code>NPM_TOKEN</code>), the same setup as fancy-menus.</li></ul>"},"0.4.0":{"date":"2026-08-22","html":"<p>The enforcement release: rules that watch file writes, not just Bash; a backlog Swarm can read; servers Swarm starts and stops by pid; and an Incidents feed you can clear.</p>\n<h4>Added</h4>\n<ul><li><b>Rules on file writes</b> — two new rules evaluated on <code>Write</code> / <code>Edit</code> / <code>MultiEdit</code> / <code>NotebookEdit</code> paths (and Bash working directories), not only Bash commands. <code>no_foreign_worktree</code> (default <code>ask</code>) stops a session from editing inside a worktree another claim holds — <em>never touch a worktree you don't hold</em> is now a hook decision, with holding inferred from the session's cwd. <code>claim_required_to_write</code> (opt-in) makes a repo's shared checkout read-only without a claim: claim a task, get a worktree, write there. Both per repo as <code>ask | deny | off</code>.</li><li><b>Incidents view</b> — the denied-action feed as its own tab: Open / All, per-rule counts, reason and session per row, <b>Ack</b> and <b>Ack all</b>; the open count sits in the nav. <code>GET /v1/incidents?open=1&project=</code>, <code>POST /v1/incidents/:seq/ack</code>, <code>POST /v1/incidents/ack</code>, <code>/v1/state.openIncidents</code>. The Board keeps a short open-only section.</li><li><b>Task source</b> — <code>.swarm.toml</code> <code>[tasks] source = \"docs/plan.md\"</code> points at a markdown file whose <code>ID | Task | Depends | Status</code> tables are the backlog (✅ / 🟡 / ⚪, dependencies by task id or milestone prefix). The Board gets a <b>Tasks</b> section (Ready / Open / All, <em>Claim</em> per row), the CLI <code>swarm tasks [--ready]</code>, and agents <code>swarm_next_task</code> — the first unclaimed task whose dependencies are done. Swarm's own roadmap is its task source. Markdown only (OQ-5 decided).</li><li><b><code>swarm serve</code> / <code>swarm proc</code></b> — <code>swarm serve start --name web -- npm run dev</code> allocates a free port (ledger + bind probe), runs the command detached with <code>PORT</code> set and logs under <code>~/.swarm/logs/<project>/</code>, registers pid + start time, and acquires the singleton — so a second <code>web</code> fails closed and the port is protected for every other session with no config. <code>serve ls | stop [name|pid]</code>, <code>proc start | ls | stop</code> for workers without a port. Stop signals registry pids only, verified by start time; nothing is ever killed by pattern. <code>POST /v1/ports/allocate</code>, <code>GET/POST/DELETE /v1/processes</code>; <b>Processes</b> section on the Board with <em>Stop</em>.</li><li><b>Star nudge</b> — once a month at most, never on first open, the dashboard asks for a GitHub star. <em>Later</em> snoozes 30 days, <em>Don't ask again</em> is final; localStorage only.</li><li><b>Sidebar drag-and-drop</b> — pinned projects reorder by dragging; the order persists on the daemon (<code>PUT /v1/projects/order</code>, <code>Project.order</code>).</li><li><b>Desktop app menu</b> — a real application menu (Swarm / Edit / View / Window): ⌘C/⌘V work, <b>View › Zoom In / Zoom Out / Actual Size</b> (<code>⌘+</code> <code>⌘−</code> <code>⌘0</code>) scale the dashboard (persisted), plus Reload and Full Screen.</li></ul><h4>Fixed</h4>\n<ul><li><b>Shared-tree rules no longer lose sight of a session mid-turn.</b> <code>shared_tree</code> / <code>destructive_git</code> keyed on a 2-minute last-seen window fed only by hooks, so a neighbour three minutes into a long turn became invisible — and its uncommitted work unguarded. Transcript growth now counts as activity (the tailer bumps <code>last_seen_at</code>), and the liveness window is the daemon's 10-minute idle threshold (<code>LIVE_WINDOW_MS</code>). A false positive costs one confirmation; a false negative cost someone's work.</li><li><code>destructive_git</code> also matches <code>git stash drop</code>, <code>git stash clear</code> and <code>git branch -D</code>.</li><li>Dashboard type scale is one step larger across the board (base 13 → 14 px; the smallest labels 10 → 11 px) — it had drifted too small, especially in the desktop app.</li><li>The <b>PRs</b> tab icon (and the branch/commit glyphs) were near-invisible at 15 px; they use the pixelarticons <em>sharp</em> variants now.</li><li>The daemon dot stayed red for up to 15 s after load on a healthy connection (the SSE stream sent nothing until its first heartbeat); the stream now flushes immediately.</li><li>The nav flashed \"Fleet\" before the restored tab was applied; session-detail event kinds (<code>userpromptsubmit</code>) no longer overflow into the message column; \"Unpinned · seen, not pinned\" keeps its spacing.</li></ul><h4>Changed — docs</h4>\n<ul><li>README and site now say what the code does: Codex CLI and Grok sessions are tailed alongside Claude Code; the requirements and architecture diagram list all three.</li><li>Rules are described as <b>guardrails against accidents, not a sandbox</b> — the guide has a new \"What rules are — and aren't\" section spelling out that a denied Bash command can be routed around (script, heredoc, direct file edit), and that worktree isolation via claims is the real fix. The site's feature cards lead with claims, rules and resources instead of Fleet and Spend.</li></ul><h4>Changed — performance</h4>\n<ul><li><b>Daemon never spawns <code>git</code> on a request.</b> Worktree status (<code>git worktree list</code> + <code>status</code>/<code>rev-list</code> per worktree, ~0.8 s across a fleet) moves to a 15 s background refresh with async <code>Bun.spawn</code>; <code>/v1/state</code> serves the cache (612 ms → ~15 ms). Claim/release invalidate it.</li><li><b>Hook round-trips are two indexed statements</b>, not two <code>git rev-parse</code> spawns plus a transcript-directory scan: <code>cwd → project</code> is cached 60 s, the inline transcript tail is debounced to once per 2 s per session (the 5 s tailer covers steady state), and subagent directories are re-listed only when their mtime moves.</li><li><b>Events store ~2 KB, not ~10 KB.</b> <code>tool_input</code> is clipped at 2 KB and <code>tool_response</code> at 4 KB in <code>payload</code> (<code>{truncated, bytes, preview}</code>), and the tool I/O is no longer duplicated in <code>raw</code>. Existing databases are rewritten once on boot and <code>VACUUM</code>ed (96 MB → 27 MB here). Retention: events older than 30 days are pruned daily (incidents kept), <code>raw</code> is cleared after 7 days.</li><li><b>Wire shape.</b> SSE frames, <code>GET /v1/events</code> replays and <code>GET /v1/sessions/:id/events</code> carry <code>seq/ts/type/projectId/sessionId/payload{hook,summary,…}</code> only — no <code>raw</code>, no tool I/O (a 5.5 MB session fetch is now ~150 KB). <code>GET /v1/events/:seq</code> returns one stored event in full; <code>?full=1</code> on the SSE replay does the same. <code>?since=0</code> replays the last 200 events, not the table.</li><li><b>Incremental session view</b> — <code>GET /v1/sessions/:id/events?after=<seq>&afterTs=<iso></code>; the dashboard appends instead of re-fetching 500 events + 500 turns on every hook.</li><li><b>Dashboard render loop</b> — one <code>requestAnimationFrame</code> scheduler, snapshot <code>seq</code> short-circuit, paused while the tab is hidden, exponential SSE reconnect backoff; session log merges two sorted lists and caches rendered rows; data-grid memoises persisted layout and uses one <code>Intl.Collator</code>; charts memoise the turn strip.</li><li>SQLite: indexes on <code>events(type, seq)</code> and <code>turns(ts)</code>, <code>mmap_size</code> 256 MB, cached prepared statements (<code>db.query</code>), <code>sessions</code>/<code>spend</code>/<code>incidents</code> memoised per write generation; <code>/v1/spend</code> is its own endpoint.</li><li>Background tick: Codex/Grok discovery every 15 s when idle; Grok <code>summary.json</code> re-read only on mtime change.</li></ul>"},"0.3.0":{"date":"2026-08-22","html":"<p>The coordination release: rules you can configure, runtime resources agents can hold, and the merge queue at the end of the loop.</p>\n<h4>Added</h4>\n<ul><li><b>Config system</b> — <code>~/.swarm/config.toml</code> (global) deep-merged with an optional <code><repo>/.swarm.toml</code>. Lenient validation: bad config can never take the daemon down. Daemon port preference is <code>SWARM_PORT</code> > config > 7777. See <code>docs/13-config.md</code>.</li><li><b>Rule engine v2</b> — every rule is per-repo configurable as <code>ask | deny | off</code>: <code>shared_tree</code>, <code>destructive_git</code>, <code>pattern_kill</code>, and the new <code>protected_ports</code> (kill/free of a configured port — <code>lsof | kill</code>, <code>fuser -k</code>, <code>kill-port</code> — is asked or denied). <code>deny</code> is returned to Claude Code as a real permission denial.</li><li><b>Incidents</b> — every non-allow decision is recorded (<code>incident.opened</code>: rule, action, command, reason), exposed at <code>GET /v1/incidents</code>, included in <code>/v1/state</code>, and shown on the Board.</li><li><b>Runtime resources (Phase 1)</b> — named singletons for what agents fight over at runtime (dev servers, databases, ports). Fail-closed acquire: holdings live while their pid runs or their lease hasn't expired; the same owner refreshes; dead holdings reap instead of blocking. Release is fail-closed too (owner required, <code>--force</code> overrides). Held ports automatically join the protected-ports rule — acquiring <code>db</code> on 5432 guards <code>lsof -ti:5432 | xargs kill</code> for every other agent, no config needed. HTTP <code>GET/POST /v1/resources</code>, <code>DELETE /v1/resources/:name</code>; MCP <code>swarm_acquire_resource</code> / <code>swarm_release_resource</code> / <code>swarm_resources</code>; CLI <code>swarm res ls|acquire|release</code>.</li><li><b>PRs view</b> — one merge queue across GitHub and GitLab. Forge detection from the git remote (ssh/https, GitLab subgroups, self-hosted), polled through the locally-authenticated <code>gh</code> / <code>glab</code> CLIs with the project root as cwd — no tokens stored, 2-minute per-project cache floor. <code>GET /v1/prs</code>, <code>POST /v1/prs/merge</code> (squash). Merge is offered only on green, mergeable, non-draft rows, behind a confirm.</li><li><b>Board view</b> — Claims, Worktrees, Resources, and Incidents move out of Fleet into their own view; Fleet shows sessions only (Live + Earlier). Last view and project selection persist across reloads.</li><li><b>Stats view</b> — <code>GET /v1/stats</code>; activity line, calendar heatmap, and streaks (daily buckets in local time, DST-immune). <code>swarm stats</code> on the CLI.</li><li><b>Data-grid everywhere</b> — Claims, Worktrees, Resources, Incidents, PRs, and all six Spend tables render through the same sortable / resizable / reorderable grid with per-column filters, a column-visibility menu, and persisted layouts. Header ticks and tooltips make the affordances discoverable.</li><li><b>Desktop: Check for Updates…</b> in the tray menu, wired to the Tauri updater with native dialogs (available / up-to-date / failed) and install-and-restart on accept.</li><li><b>Agent badge</b> on every Fleet and session row, so mixed-agent fleets are labelled consistently.</li><li><b>Website</b> — getswarm.vercel.app: OS-detected downloads from the latest GitHub release, the <code>bunx</code> one-liner, sharing tags with hero art, and (this release) rendered docs and release notes.</li><li><b>Design tokens</b> — the dashboard's CSS contains zero raw hex / rgba / font-size / duration values; the system is documented in <code>docs/12-design-tokens.md</code> with a drift grep.</li><li>Swarm now dogfoods its own rules via the repo's <code>.swarm.toml</code> (<code>shared_tree</code> / <code>destructive_git</code> deny, daemon port protected).</li></ul><h4>Fixed</h4>\n<ul><li><b>Hook resilience</b> — the PreToolUse hook falls back to the default port when <code>daemon.json</code> points at a dead daemon, so a crashed daemon no longer silently disables the guard (this was the gap behind a real <code>git add -A</code> collision).</li><li>Resource liveness: pid 0 was treated as a live process (<code>kill(0)</code> addresses the process group), so those holdings never reaped; tracked pids are now <code>> 0</code>. <code>heldPorts()</code> is one SELECT on the hook path; lazy reap on acquire, sweep on the 5 s tick. Unknown session IDs on acquire no longer mint phantom sessions.</li><li>Session view is two equal columns again (a bare <code>aside</code> selector in the sidebar-collapse CSS captured the session side panel); the log keeps your scroll position across live updates and follows the tail only when pinned to the bottom.</li><li>A pinned project whose root vanished is merged into the live same-name entry (repo renames produced duplicate sidebar rows); the sidebar <code>⋯</code> appears on hover in the count's slot and reserves no space.</li><li>Desktop: quit actually quits, window close hides (macOS convention) and the dock icon restores it, and the <code>swarmd</code> sidecar dies with the app. Dev builds serve the repo's live dashboard instead of a stale staged snapshot.</li><li>Release pipeline: npm publish is skipped cleanly when <code>NPM_TOKEN</code> is absent (since 0.4.1: trusted publishing, no token).</li></ul>"},"0.2.2":{"date":"2026-08-21","html":"<h4>Added</h4>\n<ul><li>Publishable <code>@ra3orblade/swarm</code> npm package (bundled bins + dashboard); <code>bunx @ra3orblade/swarm setup</code> onboarding.</li><li>Enterprise data-grid for Fleet with a collapsible sidebar; pixel-art icon set and bespoke empty-state illustrations; folder picker; green chart palette.</li><li>Desktop: macOS window chrome, animated pixel-logo splash, free-port daemon startup.</li></ul><h4>Fixed</h4>\n<ul><li>Release builds bundle every platform target; Linux ships <code>.deb</code> + <code>.rpm</code> (AppImage disabled until <code>linuxdeploy</code> on GitHub runners is debugged).</li></ul>"},"0.0.6":{"date":"2026-08-21","html":"<p>First signed and notarized macOS desktop build; <code>release.yml</code> became a three-OS matrix (macOS / Windows / Linux) with a native sidecar per runner.</p>"}};
|