@ra3orblade/swarm 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/swarm-hook.js +5 -1
- package/dist/swarm-mcp.js +35 -6
- package/dist/swarm.js +355 -11
- package/dist/swarmd.js +2126 -88
- package/package.json +1 -1
- package/web/app.js +333 -28
- package/web/index.html +64 -12
- package/web/release-notes.js +1 -1
- package/web/viz.js +67 -4
package/dist/swarmd.js
CHANGED
|
@@ -3,9 +3,33 @@
|
|
|
3
3
|
|
|
4
4
|
// packages/client/src/daemon.ts
|
|
5
5
|
import { randomBytes } from "crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
// packages/client/src/bins.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { dirname, resolve } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var SRC = {
|
|
15
|
+
swarm: "cli",
|
|
16
|
+
swarmd: "daemon",
|
|
17
|
+
"swarm-hook": "hook",
|
|
18
|
+
"swarm-mcp": "mcp"
|
|
19
|
+
};
|
|
20
|
+
function resolveBin(name, from = import.meta.url) {
|
|
21
|
+
const here = dirname(fileURLToPath(from));
|
|
22
|
+
const candidates = [
|
|
23
|
+
resolve(here, `../../${SRC[name]}/src/bin.ts`),
|
|
24
|
+
resolve(here, `${name}.js`)
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates)
|
|
27
|
+
if (existsSync(c))
|
|
28
|
+
return ["bun", c];
|
|
29
|
+
return [name];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/client/src/daemon.ts
|
|
9
33
|
function swarmHome() {
|
|
10
34
|
return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
|
|
11
35
|
}
|
|
@@ -41,6 +65,9 @@ function ensureToken(home = swarmHome()) {
|
|
|
41
65
|
function clearDaemonInfo() {
|
|
42
66
|
rmSync(infoFile(), { force: true });
|
|
43
67
|
}
|
|
68
|
+
function daemonCommand() {
|
|
69
|
+
return resolveBin("swarmd");
|
|
70
|
+
}
|
|
44
71
|
// packages/core/src/actor.ts
|
|
45
72
|
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
46
73
|
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
@@ -78,6 +105,150 @@ function actorFromColumns(kind, id, session) {
|
|
|
78
105
|
a.session = session;
|
|
79
106
|
return a;
|
|
80
107
|
}
|
|
108
|
+
// packages/core/src/adapters/aider/history.ts
|
|
109
|
+
var djb2 = (s) => {
|
|
110
|
+
let h = 5381;
|
|
111
|
+
for (let i = 0;i < s.length; i++)
|
|
112
|
+
h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
|
|
113
|
+
return h.toString(36);
|
|
114
|
+
};
|
|
115
|
+
var toks = (s) => {
|
|
116
|
+
if (!s)
|
|
117
|
+
return 0;
|
|
118
|
+
const n = Number.parseFloat(s.replaceAll(",", ""));
|
|
119
|
+
return Math.round(s.trim().endsWith("k") ? n * 1000 : n);
|
|
120
|
+
};
|
|
121
|
+
var HEADER = /^# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
|
122
|
+
var MODEL = /^> Model: (\S+) with /;
|
|
123
|
+
var TOKENS = /^(?:> )?Tokens: ([\d.,]+k?) sent(?:, ([\d.,]+k?) cache write)?(?:, ([\d.,]+k?) cache hit)?, ([\d.,]+k?) received\./;
|
|
124
|
+
var COST = /Cost: \$([\d.]+(?:e-?\d+)?) message/;
|
|
125
|
+
var EDIT = /^> Applied edit to (.+)/;
|
|
126
|
+
var COMMIT = /^> Commit [0-9a-f]{6,}/;
|
|
127
|
+
function parseAiderHistory(chunk, seed, carry) {
|
|
128
|
+
const segments = [];
|
|
129
|
+
let cur = carry ? {
|
|
130
|
+
sessionId: carry.sessionId,
|
|
131
|
+
startMs: carry.startMs,
|
|
132
|
+
model: carry.model,
|
|
133
|
+
title: carry.title,
|
|
134
|
+
turns: [],
|
|
135
|
+
c: { ...carry }
|
|
136
|
+
} : null;
|
|
137
|
+
const closeTurn = (t, cost) => {
|
|
138
|
+
if (!cur)
|
|
139
|
+
return;
|
|
140
|
+
t.cost = cost;
|
|
141
|
+
cur.turns.push(t);
|
|
142
|
+
cur.c.turns++;
|
|
143
|
+
cur.c.text = "";
|
|
144
|
+
cur.c.tools = [];
|
|
145
|
+
cur.c.pending = null;
|
|
146
|
+
};
|
|
147
|
+
const flushPending = () => {
|
|
148
|
+
if (cur?.c.pending)
|
|
149
|
+
closeTurn(cur.c.pending, null);
|
|
150
|
+
};
|
|
151
|
+
for (const line of chunk.split(`
|
|
152
|
+
`)) {
|
|
153
|
+
const h = line.match(HEADER);
|
|
154
|
+
if (h) {
|
|
155
|
+
flushPending();
|
|
156
|
+
if (cur)
|
|
157
|
+
segments.push(cur);
|
|
158
|
+
const stamp = h[1] ?? "";
|
|
159
|
+
const startMs = Date.parse(stamp.replace(" ", "T"));
|
|
160
|
+
const sessionId = `aider-${djb2(`${seed}|${stamp}`)}`;
|
|
161
|
+
cur = {
|
|
162
|
+
sessionId,
|
|
163
|
+
startMs,
|
|
164
|
+
model: null,
|
|
165
|
+
title: null,
|
|
166
|
+
turns: [],
|
|
167
|
+
c: {
|
|
168
|
+
sessionId,
|
|
169
|
+
startMs,
|
|
170
|
+
model: null,
|
|
171
|
+
title: null,
|
|
172
|
+
turns: 0,
|
|
173
|
+
text: "",
|
|
174
|
+
tools: [],
|
|
175
|
+
pending: null
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!cur)
|
|
181
|
+
continue;
|
|
182
|
+
const c = cur.c;
|
|
183
|
+
if (c.pending) {
|
|
184
|
+
const cost = line.match(COST);
|
|
185
|
+
if (cost) {
|
|
186
|
+
closeTurn(c.pending, Number.parseFloat(cost[1] ?? "0"));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
flushPending();
|
|
190
|
+
}
|
|
191
|
+
const m = line.match(MODEL);
|
|
192
|
+
if (m) {
|
|
193
|
+
cur.model = (m[1] ?? "").split("/").pop() || null;
|
|
194
|
+
c.model = cur.model;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const tk = line.match(TOKENS);
|
|
198
|
+
if (tk) {
|
|
199
|
+
const cacheRead = toks(tk[3]);
|
|
200
|
+
const turn = {
|
|
201
|
+
id: `${c.sessionId}-t${c.turns}`,
|
|
202
|
+
ts: new Date(c.startMs + c.turns * 1000).toISOString(),
|
|
203
|
+
model: c.model ?? "aider",
|
|
204
|
+
usage: {
|
|
205
|
+
input: Math.max(0, toks(tk[1]) - cacheRead),
|
|
206
|
+
output: toks(tk[4]),
|
|
207
|
+
cacheWrite: toks(tk[2]),
|
|
208
|
+
cacheWrite1h: 0,
|
|
209
|
+
cacheRead,
|
|
210
|
+
thinking: 0
|
|
211
|
+
},
|
|
212
|
+
text: c.text,
|
|
213
|
+
tools: c.tools,
|
|
214
|
+
effort: null,
|
|
215
|
+
sidechain: false
|
|
216
|
+
};
|
|
217
|
+
const cost = line.match(COST);
|
|
218
|
+
if (cost)
|
|
219
|
+
closeTurn(turn, Number.parseFloat(cost[1] ?? "0"));
|
|
220
|
+
else
|
|
221
|
+
c.pending = turn;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (line.startsWith("#### ")) {
|
|
225
|
+
const t = line.slice(5).trim();
|
|
226
|
+
if (t && !cur.title) {
|
|
227
|
+
cur.title = t.slice(0, 80);
|
|
228
|
+
c.title = cur.title;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (EDIT.test(line)) {
|
|
233
|
+
c.tools.push("edit");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (COMMIT.test(line)) {
|
|
237
|
+
c.tools.push("commit");
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (line.startsWith(">"))
|
|
241
|
+
continue;
|
|
242
|
+
if (line.trim() && c.text.length < 400)
|
|
243
|
+
c.text = `${c.text}${c.text ? `
|
|
244
|
+
` : ""}${line}`.slice(0, 400);
|
|
245
|
+
}
|
|
246
|
+
if (cur)
|
|
247
|
+
segments.push(cur);
|
|
248
|
+
const last = cur ? { ...cur.c } : null;
|
|
249
|
+
return { segments, carry: last };
|
|
250
|
+
}
|
|
251
|
+
|
|
81
252
|
// packages/core/src/adapters/claude-code/transcript.ts
|
|
82
253
|
function parseTranscriptChunk(chunk) {
|
|
83
254
|
const out = {
|
|
@@ -218,6 +389,79 @@ function parseCodexRollout(chunk) {
|
|
|
218
389
|
return out;
|
|
219
390
|
}
|
|
220
391
|
|
|
392
|
+
// packages/core/src/adapters/gemini/chats.ts
|
|
393
|
+
function partText(content) {
|
|
394
|
+
if (typeof content === "string")
|
|
395
|
+
return content.slice(0, 400);
|
|
396
|
+
const parts = Array.isArray(content) ? content : [content];
|
|
397
|
+
let out = "";
|
|
398
|
+
for (const p of parts) {
|
|
399
|
+
if (typeof p === "string")
|
|
400
|
+
out += p;
|
|
401
|
+
else if (p && typeof p === "object" && typeof p.text === "string")
|
|
402
|
+
out += p.text;
|
|
403
|
+
if (out.length >= 400)
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
return out.slice(0, 400);
|
|
407
|
+
}
|
|
408
|
+
function parseGeminiChat(chunk) {
|
|
409
|
+
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
410
|
+
let subagent = false;
|
|
411
|
+
for (const raw of chunk.split(`
|
|
412
|
+
`)) {
|
|
413
|
+
if (!raw.trim())
|
|
414
|
+
continue;
|
|
415
|
+
let d = null;
|
|
416
|
+
try {
|
|
417
|
+
d = JSON.parse(raw);
|
|
418
|
+
} catch {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (d.$set) {
|
|
422
|
+
const set = d.$set;
|
|
423
|
+
if (typeof set.summary === "string")
|
|
424
|
+
out.title = set.summary;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (d.sessionId && d.projectHash !== undefined) {
|
|
428
|
+
out.sessionId = d.sessionId;
|
|
429
|
+
if (Array.isArray(d.directories) && typeof d.directories[0] === "string")
|
|
430
|
+
out.cwd = d.directories[0];
|
|
431
|
+
if (typeof d.summary === "string")
|
|
432
|
+
out.title = d.summary;
|
|
433
|
+
if (d.kind === "subagent")
|
|
434
|
+
subagent = true;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (d.type !== "gemini" || !d.id)
|
|
438
|
+
continue;
|
|
439
|
+
const t = d.tokens ?? {};
|
|
440
|
+
const cacheRead = t.cached ?? 0;
|
|
441
|
+
const turn = {
|
|
442
|
+
id: `${out.sessionId ?? "gemini"}-${d.id}`,
|
|
443
|
+
ts: d.timestamp ?? new Date(0).toISOString(),
|
|
444
|
+
model: d.model ?? out.model ?? "gemini-2.5-pro",
|
|
445
|
+
usage: {
|
|
446
|
+
input: Math.max(0, (t.input ?? 0) - cacheRead),
|
|
447
|
+
output: (t.output ?? 0) + (t.tool ?? 0),
|
|
448
|
+
cacheWrite: 0,
|
|
449
|
+
cacheWrite1h: 0,
|
|
450
|
+
cacheRead,
|
|
451
|
+
thinking: t.thoughts ?? 0
|
|
452
|
+
},
|
|
453
|
+
text: partText(d.content),
|
|
454
|
+
tools: (d.toolCalls ?? []).map((c) => c.name ?? c.displayName ?? c.tool ?? "").filter(Boolean),
|
|
455
|
+
effort: null,
|
|
456
|
+
sidechain: subagent
|
|
457
|
+
};
|
|
458
|
+
if (d.model)
|
|
459
|
+
out.model = d.model;
|
|
460
|
+
out.turns.push(turn);
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
221
465
|
// packages/core/src/adapters/grok/updates.ts
|
|
222
466
|
function parseGrokUpdates(chunk) {
|
|
223
467
|
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
@@ -285,6 +529,62 @@ function parseGrokUpdates(chunk) {
|
|
|
285
529
|
}
|
|
286
530
|
return out;
|
|
287
531
|
}
|
|
532
|
+
|
|
533
|
+
// packages/core/src/adapters/opencode/db.ts
|
|
534
|
+
var ocModel = (d) => {
|
|
535
|
+
if (typeof d.model === "string")
|
|
536
|
+
return d.model;
|
|
537
|
+
if (d.model && typeof d.model === "object" && typeof d.model.id === "string")
|
|
538
|
+
return d.model.id;
|
|
539
|
+
return typeof d.modelID === "string" ? d.modelID : null;
|
|
540
|
+
};
|
|
541
|
+
var ocTs = (t, fallbackMs) => {
|
|
542
|
+
if (typeof t === "number")
|
|
543
|
+
return new Date(t).toISOString();
|
|
544
|
+
if (typeof t === "string" && !Number.isNaN(Date.parse(t)))
|
|
545
|
+
return new Date(t).toISOString();
|
|
546
|
+
return new Date(fallbackMs).toISOString();
|
|
547
|
+
};
|
|
548
|
+
function opencodeTurn(sessionId, msgId, data, fallbackMs = 0, sidechain = false) {
|
|
549
|
+
let d;
|
|
550
|
+
try {
|
|
551
|
+
d = JSON.parse(data);
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if ((d.type ?? d.role) !== "assistant")
|
|
556
|
+
return null;
|
|
557
|
+
const t = d.tokens ?? {};
|
|
558
|
+
let text = "";
|
|
559
|
+
const tools = [];
|
|
560
|
+
for (const p of Array.isArray(d.content) ? d.content : []) {
|
|
561
|
+
if (p?.type === "text" && typeof p.text === "string" && text.length < 400)
|
|
562
|
+
text = `${text}${p.text}`.slice(0, 400);
|
|
563
|
+
else if (p?.type === "tool") {
|
|
564
|
+
const name = p.tool ?? p.name;
|
|
565
|
+
if (name)
|
|
566
|
+
tools.push(name);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
id: `${sessionId}-${msgId}`,
|
|
571
|
+
ts: ocTs(d.time?.created, fallbackMs),
|
|
572
|
+
model: ocModel(d) ?? "opencode",
|
|
573
|
+
usage: {
|
|
574
|
+
input: t.input ?? 0,
|
|
575
|
+
output: t.output ?? 0,
|
|
576
|
+
cacheWrite: t.cache?.write ?? 0,
|
|
577
|
+
cacheWrite1h: 0,
|
|
578
|
+
cacheRead: t.cache?.read ?? 0,
|
|
579
|
+
thinking: t.reasoning ?? 0
|
|
580
|
+
},
|
|
581
|
+
text,
|
|
582
|
+
tools,
|
|
583
|
+
effort: null,
|
|
584
|
+
sidechain,
|
|
585
|
+
cost: typeof d.cost === "number" && d.cost > 0 ? d.cost : null
|
|
586
|
+
};
|
|
587
|
+
}
|
|
288
588
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
289
589
|
var HOOK_EVENTS = [
|
|
290
590
|
"SessionStart",
|
|
@@ -421,7 +721,9 @@ var AUDIT_TYPES = new Set([
|
|
|
421
721
|
"permission.resolved",
|
|
422
722
|
"incident.opened",
|
|
423
723
|
"incident.acked",
|
|
424
|
-
"run.result"
|
|
724
|
+
"run.result",
|
|
725
|
+
"workflow.started",
|
|
726
|
+
"workflow.finished"
|
|
425
727
|
]);
|
|
426
728
|
var isAuditType = (t) => AUDIT_TYPES.has(t);
|
|
427
729
|
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
@@ -594,16 +896,82 @@ function runProfile(name) {
|
|
|
594
896
|
return RUN_PROFILES[name] ?? null;
|
|
595
897
|
}
|
|
596
898
|
// packages/core/src/config.ts
|
|
597
|
-
import { existsSync as
|
|
899
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
598
900
|
import { join as join2 } from "path";
|
|
901
|
+
|
|
902
|
+
// packages/core/src/workflows.ts
|
|
903
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
904
|
+
function isRecord(v) {
|
|
905
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
906
|
+
}
|
|
907
|
+
function parseWorkflows(raw) {
|
|
908
|
+
const out = {};
|
|
909
|
+
if (!Array.isArray(raw))
|
|
910
|
+
return out;
|
|
911
|
+
for (const w of raw) {
|
|
912
|
+
if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
|
|
913
|
+
continue;
|
|
914
|
+
if (!Array.isArray(w.steps) || !w.steps.length)
|
|
915
|
+
continue;
|
|
916
|
+
const prompts = isRecord(w.prompts) ? w.prompts : {};
|
|
917
|
+
const steps = [];
|
|
918
|
+
for (const s of w.steps) {
|
|
919
|
+
if (typeof s !== "string" || !s.trim()) {
|
|
920
|
+
steps.length = 0;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
const t = s.trim();
|
|
924
|
+
if (t === "pr")
|
|
925
|
+
steps.push({ kind: "pr" });
|
|
926
|
+
else if (t.startsWith("gate:")) {
|
|
927
|
+
const gate = t.slice(5);
|
|
928
|
+
if (!NAME_RE.test(gate)) {
|
|
929
|
+
steps.length = 0;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
steps.push({ kind: "gate", gate });
|
|
933
|
+
} else if (NAME_RE.test(t)) {
|
|
934
|
+
const p = prompts[t];
|
|
935
|
+
steps.push({
|
|
936
|
+
kind: "run",
|
|
937
|
+
name: t,
|
|
938
|
+
prompt: typeof p === "string" && p.trim() ? p.trim() : null
|
|
939
|
+
});
|
|
940
|
+
} else {
|
|
941
|
+
steps.length = 0;
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (steps.length)
|
|
946
|
+
out[w.name] = { name: w.name, steps };
|
|
947
|
+
}
|
|
948
|
+
return out;
|
|
949
|
+
}
|
|
950
|
+
function workflowStepPrompt(step, task, ctx) {
|
|
951
|
+
if (step.prompt)
|
|
952
|
+
return step.prompt.replaceAll("{task}", task.id).replaceAll("{title}", task.title ?? "");
|
|
953
|
+
return [
|
|
954
|
+
`Task ${task.id}: ${task.title}`,
|
|
955
|
+
"",
|
|
956
|
+
`You are the "${step.name}" step of the "${ctx.workflow}" workflow. Work only inside this worktree; commit and push as you go.`,
|
|
957
|
+
ctx.remaining.length ? `After you finish, the workflow itself runs: ${ctx.remaining.join(" \u2192 ")}. Do not do those yourself.` : "You are the last step.",
|
|
958
|
+
"When done, call swarm_handoff with what was done and what remains."
|
|
959
|
+
].join(`
|
|
960
|
+
`);
|
|
961
|
+
}
|
|
962
|
+
function stepLabel(s) {
|
|
963
|
+
return s.kind === "run" ? s.name : s.kind === "gate" ? `gate:${s.gate}` : "pr";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// packages/core/src/config.ts
|
|
599
967
|
var DEFAULT_GATE_TIMEOUT_S = 900;
|
|
600
968
|
var AUTO_MODES = ["session-end", "stop", "off"];
|
|
601
969
|
function parseGateDefs(gates) {
|
|
602
970
|
const out = {};
|
|
603
|
-
if (!
|
|
971
|
+
if (!isRecord2(gates))
|
|
604
972
|
return out;
|
|
605
973
|
for (const [name, v] of Object.entries(gates)) {
|
|
606
|
-
if (!
|
|
974
|
+
if (!isRecord2(v))
|
|
607
975
|
continue;
|
|
608
976
|
const builtin = v.builtin === "review" ? "review" : null;
|
|
609
977
|
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
@@ -626,7 +994,11 @@ var DEFAULT_CONFIG = {
|
|
|
626
994
|
daemon: { port: 7777, auth: "loopback-optional" },
|
|
627
995
|
tasks: { source: null, labels: [], team: null },
|
|
628
996
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
997
|
+
workflows: {},
|
|
629
998
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
999
|
+
models: { allow: [] },
|
|
1000
|
+
notify: { webhook: null },
|
|
1001
|
+
team: { url: null, forward: ["ledger", "cost"], interval: 5 },
|
|
630
1002
|
events: { retain_days: 30 },
|
|
631
1003
|
audit: { retain_days: 0 },
|
|
632
1004
|
privacy: DEFAULT_PRIVACY,
|
|
@@ -650,11 +1022,11 @@ var DEFAULT_CONFIG = {
|
|
|
650
1022
|
}
|
|
651
1023
|
};
|
|
652
1024
|
var MODES = ["ask", "deny", "off"];
|
|
653
|
-
function
|
|
1025
|
+
function isRecord2(v) {
|
|
654
1026
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
655
1027
|
}
|
|
656
1028
|
function merge(a, b) {
|
|
657
|
-
if (!
|
|
1029
|
+
if (!isRecord2(a) || !isRecord2(b))
|
|
658
1030
|
return b === undefined ? a : b;
|
|
659
1031
|
const out = { ...a };
|
|
660
1032
|
for (const [k, v] of Object.entries(b))
|
|
@@ -721,6 +1093,27 @@ function validate(c) {
|
|
|
721
1093
|
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
722
1094
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
723
1095
|
},
|
|
1096
|
+
workflows: parseWorkflows(c.workflows),
|
|
1097
|
+
notify: {
|
|
1098
|
+
webhook: (() => {
|
|
1099
|
+
const w = c.notify?.webhook;
|
|
1100
|
+
return typeof w === "string" && /^https?:\/\//.test(w.trim()) ? w.trim() : null;
|
|
1101
|
+
})()
|
|
1102
|
+
},
|
|
1103
|
+
models: {
|
|
1104
|
+
allow: Array.isArray(c.models?.allow) ? c.models.allow.filter((m) => typeof m === "string" && m.trim() !== "") : []
|
|
1105
|
+
},
|
|
1106
|
+
team: (() => {
|
|
1107
|
+
const t = c.team ?? {};
|
|
1108
|
+
const url = typeof t.url === "string" && /^https?:\/\//.test(t.url.trim()) ? t.url.trim().replace(/\/+$/, "") : null;
|
|
1109
|
+
const iv = Number(t.interval);
|
|
1110
|
+
const KINDS = ["ledger", "cost", "transcripts"];
|
|
1111
|
+
return {
|
|
1112
|
+
url,
|
|
1113
|
+
forward: Array.isArray(t.forward) ? t.forward.filter((k) => typeof k === "string" && KINDS.includes(k)) : ["ledger", "cost"],
|
|
1114
|
+
interval: Number.isFinite(iv) && iv >= 1 && iv <= 300 ? Math.round(iv) : 5
|
|
1115
|
+
};
|
|
1116
|
+
})(),
|
|
724
1117
|
events: {
|
|
725
1118
|
retain_days: days(c.events?.retain_days, 30)
|
|
726
1119
|
},
|
|
@@ -760,7 +1153,7 @@ function validate(c) {
|
|
|
760
1153
|
};
|
|
761
1154
|
}
|
|
762
1155
|
function leafPaths(v, prefix = "") {
|
|
763
|
-
if (!
|
|
1156
|
+
if (!isRecord2(v))
|
|
764
1157
|
return prefix ? [prefix] : [];
|
|
765
1158
|
const keys = Object.keys(v);
|
|
766
1159
|
if (keys.length === 0)
|
|
@@ -770,7 +1163,7 @@ function leafPaths(v, prefix = "") {
|
|
|
770
1163
|
function getPath(v, path) {
|
|
771
1164
|
let cur = v;
|
|
772
1165
|
for (const seg of path.split(".")) {
|
|
773
|
-
if (!
|
|
1166
|
+
if (!isRecord2(cur))
|
|
774
1167
|
return;
|
|
775
1168
|
cur = cur[seg];
|
|
776
1169
|
}
|
|
@@ -780,7 +1173,7 @@ function setPath(obj, path, value) {
|
|
|
780
1173
|
const segs = path.split(".");
|
|
781
1174
|
let cur = obj;
|
|
782
1175
|
for (const seg of segs.slice(0, -1)) {
|
|
783
|
-
if (!
|
|
1176
|
+
if (!isRecord2(cur[seg]))
|
|
784
1177
|
cur[seg] = {};
|
|
785
1178
|
cur = cur[seg];
|
|
786
1179
|
}
|
|
@@ -788,7 +1181,7 @@ function setPath(obj, path, value) {
|
|
|
788
1181
|
}
|
|
789
1182
|
var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
|
|
790
1183
|
function readLayer(path) {
|
|
791
|
-
return
|
|
1184
|
+
return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
|
|
792
1185
|
}
|
|
793
1186
|
function loadConfigDetailed(opts = {}) {
|
|
794
1187
|
const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
|
|
@@ -1274,11 +1667,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
|
|
|
1274
1667
|
`) };
|
|
1275
1668
|
}
|
|
1276
1669
|
// packages/core/src/gates.ts
|
|
1277
|
-
var
|
|
1670
|
+
var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
1278
1671
|
function validateGateRun(input) {
|
|
1279
1672
|
if (!input.task?.trim())
|
|
1280
1673
|
return { ok: false, reason: "task is required" };
|
|
1281
|
-
if (!
|
|
1674
|
+
if (!NAME_RE2.test(input.gate ?? ""))
|
|
1282
1675
|
return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
|
|
1283
1676
|
if (input.verdict !== "pass" && input.verdict !== "fail")
|
|
1284
1677
|
return { ok: false, reason: 'verdict must be "pass" or "fail"' };
|
|
@@ -1335,6 +1728,41 @@ function executedGateInput(task, gate, cmd, outcome) {
|
|
|
1335
1728
|
evidence: evidenceTail(outcome.output) || null
|
|
1336
1729
|
};
|
|
1337
1730
|
}
|
|
1731
|
+
// packages/core/src/graphs.ts
|
|
1732
|
+
function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
1733
|
+
const files = new Map;
|
|
1734
|
+
const sessions = new Map;
|
|
1735
|
+
for (const r of rows) {
|
|
1736
|
+
if (!r.path || !r.sessionId)
|
|
1737
|
+
continue;
|
|
1738
|
+
const f = files.get(r.path) ?? { readers: new Set, writers: new Set };
|
|
1739
|
+
const s = sessions.get(r.sessionId) ?? { files: new Set, writes: 0 };
|
|
1740
|
+
if (writeTools.has(r.tool)) {
|
|
1741
|
+
f.writers.add(r.sessionId);
|
|
1742
|
+
s.writes++;
|
|
1743
|
+
} else
|
|
1744
|
+
f.readers.add(r.sessionId);
|
|
1745
|
+
s.files.add(r.path);
|
|
1746
|
+
files.set(r.path, f);
|
|
1747
|
+
sessions.set(r.sessionId, s);
|
|
1748
|
+
}
|
|
1749
|
+
const out = [...files.entries()].map(([path, f]) => {
|
|
1750
|
+
const writers = [...f.writers].sort();
|
|
1751
|
+
const readers = [...f.readers].filter((id) => !f.writers.has(id)).sort();
|
|
1752
|
+
const touchers = writers.length + readers.length;
|
|
1753
|
+
return { path, readers, writers, contested: touchers >= 2 && writers.length >= 1 };
|
|
1754
|
+
});
|
|
1755
|
+
out.sort((a, b) => Number(b.contested) - Number(a.contested) || b.readers.length + b.writers.length - (a.readers.length + a.writers.length) || a.path.localeCompare(b.path));
|
|
1756
|
+
return {
|
|
1757
|
+
sessions: [...sessions.entries()].map(([id, s]) => ({
|
|
1758
|
+
id,
|
|
1759
|
+
files: s.files.size,
|
|
1760
|
+
writes: s.writes
|
|
1761
|
+
})),
|
|
1762
|
+
files: out,
|
|
1763
|
+
contested: out.filter((f) => f.contested).length
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1338
1766
|
// packages/core/src/ledger.ts
|
|
1339
1767
|
var DEFAULT_LEASE_MINUTES = 45;
|
|
1340
1768
|
function isExpired(claim, now) {
|
|
@@ -1639,6 +2067,121 @@ function parseMemoryQuery(q) {
|
|
|
1639
2067
|
}
|
|
1640
2068
|
return { match: terms.join(" "), kind, task };
|
|
1641
2069
|
}
|
|
2070
|
+
// packages/core/src/messages.ts
|
|
2071
|
+
var MESSAGE_MAX = 4000;
|
|
2072
|
+
function validateMessage(text) {
|
|
2073
|
+
if (typeof text !== "string" || !text.trim())
|
|
2074
|
+
return { ok: false, reason: "message text is required" };
|
|
2075
|
+
const t = text.trim();
|
|
2076
|
+
if (t.length > MESSAGE_MAX)
|
|
2077
|
+
return { ok: false, reason: `message is over ${MESSAGE_MAX} chars` };
|
|
2078
|
+
return { ok: true, text: t };
|
|
2079
|
+
}
|
|
2080
|
+
function parseTo(to) {
|
|
2081
|
+
if (typeof to !== "string" || !to.trim())
|
|
2082
|
+
return null;
|
|
2083
|
+
const t = to.trim();
|
|
2084
|
+
if (t === "lead")
|
|
2085
|
+
return { kind: "lead" };
|
|
2086
|
+
if (/^[0-9a-f]{8}(-[0-9a-f-]{4,28})?$/i.test(t))
|
|
2087
|
+
return { kind: "session", id: t };
|
|
2088
|
+
return { kind: "task", task: t };
|
|
2089
|
+
}
|
|
2090
|
+
function formatMessages(ms) {
|
|
2091
|
+
if (!ms.length)
|
|
2092
|
+
return null;
|
|
2093
|
+
const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
|
|
2094
|
+
return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
|
|
2095
|
+
${lines.join(`
|
|
2096
|
+
`)}
|
|
2097
|
+
Reply with swarm_send if a reply is expected.`;
|
|
2098
|
+
}
|
|
2099
|
+
// packages/core/src/outcomes.ts
|
|
2100
|
+
var DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
|
|
2101
|
+
var median = (xs) => {
|
|
2102
|
+
if (!xs.length)
|
|
2103
|
+
return null;
|
|
2104
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
2105
|
+
const mid = Math.floor(s.length / 2);
|
|
2106
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
2107
|
+
};
|
|
2108
|
+
function scorecard(key, rows) {
|
|
2109
|
+
const merged = rows.filter((r) => r.outcome === "merged");
|
|
2110
|
+
const reverted = rows.filter((r) => r.outcome === "reverted");
|
|
2111
|
+
const open = rows.filter((r) => r.outcome === "open");
|
|
2112
|
+
const noPr = rows.filter((r) => r.outcome === "no-pr");
|
|
2113
|
+
const finished = merged.length + reverted.length + noPr.length;
|
|
2114
|
+
const mergedCost = merged.reduce((a, r) => a + r.costUsd, 0);
|
|
2115
|
+
return {
|
|
2116
|
+
key,
|
|
2117
|
+
branches: rows.length,
|
|
2118
|
+
merged: merged.length,
|
|
2119
|
+
reverted: reverted.length,
|
|
2120
|
+
open: open.length,
|
|
2121
|
+
noPr: noPr.length,
|
|
2122
|
+
mergeRate: finished ? merged.length / finished : null,
|
|
2123
|
+
medianLeadHours: median(merged.map((r) => r.leadHours).filter((x) => x != null)),
|
|
2124
|
+
costPerMerge: merged.length ? mergedCost / merged.length : null
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function outcomeReport(sessions, prs, revertedShas) {
|
|
2128
|
+
const byBranch = new Map;
|
|
2129
|
+
for (const s of sessions) {
|
|
2130
|
+
if (!s.branch || DEFAULT_BRANCHES.has(s.branch))
|
|
2131
|
+
continue;
|
|
2132
|
+
const a = byBranch.get(s.branch) ?? [];
|
|
2133
|
+
a.push(s);
|
|
2134
|
+
byBranch.set(s.branch, a);
|
|
2135
|
+
}
|
|
2136
|
+
const prByBranch = new Map;
|
|
2137
|
+
for (const pr of prs) {
|
|
2138
|
+
const prev = prByBranch.get(pr.branch);
|
|
2139
|
+
if (!prev || pr.state === "merged" && prev.state !== "merged" || pr.state === prev.state && pr.number > prev.number)
|
|
2140
|
+
prByBranch.set(pr.branch, pr);
|
|
2141
|
+
}
|
|
2142
|
+
const rows = [...byBranch.entries()].map(([branch, ss]) => {
|
|
2143
|
+
const dominant = [...ss].sort((a, b) => (b.costUsd ?? 0) - (a.costUsd ?? 0) || a.startedAt.localeCompare(b.startedAt))[0];
|
|
2144
|
+
const pr = prByBranch.get(branch) ?? null;
|
|
2145
|
+
const wasReverted = (sha) => {
|
|
2146
|
+
if (!sha)
|
|
2147
|
+
return false;
|
|
2148
|
+
const s = sha.toLowerCase();
|
|
2149
|
+
for (const r of revertedShas)
|
|
2150
|
+
if (s.startsWith(r) || r.startsWith(s))
|
|
2151
|
+
return true;
|
|
2152
|
+
return false;
|
|
2153
|
+
};
|
|
2154
|
+
const outcome = !pr ? "no-pr" : pr.state === "open" ? "open" : wasReverted(pr.mergeSha) ? "reverted" : "merged";
|
|
2155
|
+
const firstStart = ss.map((s) => s.startedAt).sort()[0];
|
|
2156
|
+
const leadHours = outcome === "merged" && pr?.mergedAt ? Math.max(0, (new Date(pr.mergedAt).getTime() - new Date(firstStart).getTime()) / 3600000) : null;
|
|
2157
|
+
return {
|
|
2158
|
+
branch,
|
|
2159
|
+
outcome,
|
|
2160
|
+
prNumber: pr?.number ?? null,
|
|
2161
|
+
title: pr?.title ?? null,
|
|
2162
|
+
url: pr?.url ?? null,
|
|
2163
|
+
mergedAt: pr?.mergedAt ?? null,
|
|
2164
|
+
leadHours,
|
|
2165
|
+
sessions: ss.map((s) => s.id),
|
|
2166
|
+
model: dominant.model,
|
|
2167
|
+
agent: dominant.agent,
|
|
2168
|
+
costUsd: ss.reduce((a, s) => a + (s.costUsd ?? 0), 0)
|
|
2169
|
+
};
|
|
2170
|
+
});
|
|
2171
|
+
rows.sort((a, b) => (b.mergedAt ?? "").localeCompare(a.mergedAt ?? "") || a.branch.localeCompare(b.branch));
|
|
2172
|
+
const group = (key) => {
|
|
2173
|
+
const m = new Map;
|
|
2174
|
+
for (const r of rows) {
|
|
2175
|
+
const k = key(r) ?? "unknown";
|
|
2176
|
+
m.set(k, [...m.get(k) ?? [], r]);
|
|
2177
|
+
}
|
|
2178
|
+
return [...m.entries()].map(([k, rs]) => scorecard(k, rs)).sort((a, b) => b.branches - a.branches);
|
|
2179
|
+
};
|
|
2180
|
+
return { branches: rows, byModel: group((r) => r.model), byAgent: group((r) => r.agent) };
|
|
2181
|
+
}
|
|
2182
|
+
function parseReverts(gitLog) {
|
|
2183
|
+
return new Set([...gitLog.matchAll(/This reverts commit ([0-9a-f]{7,40})/gi)].map((m) => m[1].toLowerCase()));
|
|
2184
|
+
}
|
|
1642
2185
|
// packages/core/src/policy.ts
|
|
1643
2186
|
import { createHash } from "crypto";
|
|
1644
2187
|
var HOOK_MARK = "swarm-hook";
|
|
@@ -1979,6 +2522,49 @@ ${lines.join(`
|
|
|
1979
2522
|
`)}` : ""}`
|
|
1980
2523
|
};
|
|
1981
2524
|
}
|
|
2525
|
+
// packages/core/src/stall.ts
|
|
2526
|
+
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
2527
|
+
function toolResponseErrored(resp) {
|
|
2528
|
+
if (typeof resp === "string")
|
|
2529
|
+
return /^\s*error[:\s]/i.test(resp);
|
|
2530
|
+
if (!resp || typeof resp !== "object")
|
|
2531
|
+
return false;
|
|
2532
|
+
const r = resp;
|
|
2533
|
+
if (r.is_error === true || r.isError === true)
|
|
2534
|
+
return true;
|
|
2535
|
+
if (r.success === false)
|
|
2536
|
+
return true;
|
|
2537
|
+
if (r.interrupted === true)
|
|
2538
|
+
return true;
|
|
2539
|
+
if (typeof r.error === "string" && r.error.length > 0)
|
|
2540
|
+
return true;
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
function detectStall(calls, opts = {}) {
|
|
2544
|
+
const o = { ...STALL_DEFAULTS, ...opts };
|
|
2545
|
+
const tail = calls.slice(-o.window);
|
|
2546
|
+
const last = tail.at(-1);
|
|
2547
|
+
if (!last)
|
|
2548
|
+
return null;
|
|
2549
|
+
let run = 0;
|
|
2550
|
+
let runErrors = 0;
|
|
2551
|
+
for (let i = tail.length - 1;i >= 0; i--) {
|
|
2552
|
+
const c = tail[i];
|
|
2553
|
+
if (!c || c.tool !== last.tool || c.input !== last.input)
|
|
2554
|
+
break;
|
|
2555
|
+
run++;
|
|
2556
|
+
if (c.errored)
|
|
2557
|
+
runErrors++;
|
|
2558
|
+
}
|
|
2559
|
+
if (run >= o.repeat && runErrors >= o.repeatErrors)
|
|
2560
|
+
return { kind: "repeat", reason: `repeating a failing ${last.tool} call \xD7${run}` };
|
|
2561
|
+
let streak = 0;
|
|
2562
|
+
for (let i = tail.length - 1;i >= 0 && tail[i]?.errored; i--)
|
|
2563
|
+
streak++;
|
|
2564
|
+
if (streak >= o.errors)
|
|
2565
|
+
return { kind: "errors", reason: `${streak} tool calls failing in a row` };
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
1982
2568
|
// packages/core/src/tasks.ts
|
|
1983
2569
|
var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
|
|
1984
2570
|
var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
|
|
@@ -2138,6 +2724,35 @@ function linearIssuesQuery(teamKey, first = 200) {
|
|
|
2138
2724
|
inverseRelations { nodes { type issue { identifier } } }
|
|
2139
2725
|
} } }`;
|
|
2140
2726
|
}
|
|
2727
|
+
// packages/core/src/team.ts
|
|
2728
|
+
import { createPublicKey, verify as nodeVerify } from "crypto";
|
|
2729
|
+
function modelAllowed(model, allow) {
|
|
2730
|
+
if (!allow.length)
|
|
2731
|
+
return true;
|
|
2732
|
+
return allow.some((g) => {
|
|
2733
|
+
const re = new RegExp(`^${g.trim().split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`, "i");
|
|
2734
|
+
return re.test(model);
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
function verifyPolicySignature(toml, signatureB64, publicKeyB64) {
|
|
2738
|
+
try {
|
|
2739
|
+
return nodeVerify(null, Buffer.from(toml), createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" }), Buffer.from(signatureB64, "base64"));
|
|
2740
|
+
} catch {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function clusterProjectKey(remoteUrl) {
|
|
2745
|
+
if (!remoteUrl)
|
|
2746
|
+
return null;
|
|
2747
|
+
const url = remoteUrl.trim();
|
|
2748
|
+
const m = url.match(/^https?:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/) ?? url.match(/^(?:ssh:\/\/)?(?:[^@/]+@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/);
|
|
2749
|
+
if (!m?.[1] || !m[2])
|
|
2750
|
+
return null;
|
|
2751
|
+
const host = m[1].toLowerCase();
|
|
2752
|
+
if (host.includes(" ") || !host.includes("."))
|
|
2753
|
+
return null;
|
|
2754
|
+
return `${host}/${m[2]}`;
|
|
2755
|
+
}
|
|
2141
2756
|
// packages/core/src/worktree.ts
|
|
2142
2757
|
import { join as join3 } from "path";
|
|
2143
2758
|
function planBootstrap(cfg, repoRoot, worktree) {
|
|
@@ -2212,10 +2827,10 @@ function planGc(worktrees, claims) {
|
|
|
2212
2827
|
return out;
|
|
2213
2828
|
}
|
|
2214
2829
|
// packages/daemon/src/app.ts
|
|
2215
|
-
import { existsSync as
|
|
2830
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
2216
2831
|
import { homedir as homedir4 } from "os";
|
|
2217
|
-
import { dirname as
|
|
2218
|
-
import { fileURLToPath } from "url";
|
|
2832
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2833
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2219
2834
|
|
|
2220
2835
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
2221
2836
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -4088,7 +4703,7 @@ class Dispatcher {
|
|
|
4088
4703
|
}
|
|
4089
4704
|
|
|
4090
4705
|
// packages/daemon/src/forge.ts
|
|
4091
|
-
import { existsSync as
|
|
4706
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4092
4707
|
import { homedir as homedir2 } from "os";
|
|
4093
4708
|
import { join as join4 } from "path";
|
|
4094
4709
|
var EXTRA_BIN_DIRS = [
|
|
@@ -4106,7 +4721,7 @@ function findBin(name) {
|
|
|
4106
4721
|
return onPath;
|
|
4107
4722
|
for (const d of EXTRA_BIN_DIRS) {
|
|
4108
4723
|
const p = join4(d, name);
|
|
4109
|
-
if (
|
|
4724
|
+
if (existsSync4(p))
|
|
4110
4725
|
return p;
|
|
4111
4726
|
}
|
|
4112
4727
|
return null;
|
|
@@ -4144,6 +4759,64 @@ class ForgeService {
|
|
|
4144
4759
|
}
|
|
4145
4760
|
}));
|
|
4146
4761
|
}
|
|
4762
|
+
outcomeCache = new Map;
|
|
4763
|
+
async merged(projectId, root) {
|
|
4764
|
+
const hit = this.outcomeCache.get(projectId);
|
|
4765
|
+
if (hit && Date.now() - hit.at < 600000)
|
|
4766
|
+
return hit;
|
|
4767
|
+
let merged = [];
|
|
4768
|
+
const remote = this.remote(root);
|
|
4769
|
+
if (remote?.forge === "github") {
|
|
4770
|
+
const out = await this.run([
|
|
4771
|
+
"gh",
|
|
4772
|
+
"pr",
|
|
4773
|
+
"list",
|
|
4774
|
+
"--state",
|
|
4775
|
+
"merged",
|
|
4776
|
+
"--limit",
|
|
4777
|
+
"200",
|
|
4778
|
+
"--json",
|
|
4779
|
+
"number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
|
|
4780
|
+
], root);
|
|
4781
|
+
if (out)
|
|
4782
|
+
merged = JSON.parse(out).map((r) => ({
|
|
4783
|
+
branch: String(r.headRefName ?? ""),
|
|
4784
|
+
number: Number(r.number ?? 0),
|
|
4785
|
+
title: String(r.title ?? ""),
|
|
4786
|
+
url: String(r.url ?? ""),
|
|
4787
|
+
createdAt: r.createdAt ?? null,
|
|
4788
|
+
mergedAt: r.mergedAt ?? null,
|
|
4789
|
+
mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
|
|
4790
|
+
}));
|
|
4791
|
+
} else if (remote?.forge === "gitlab") {
|
|
4792
|
+
const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
|
|
4793
|
+
if (out)
|
|
4794
|
+
merged = JSON.parse(out).map((r) => ({
|
|
4795
|
+
branch: String(r.source_branch ?? ""),
|
|
4796
|
+
number: Number(r.iid ?? 0),
|
|
4797
|
+
title: String(r.title ?? ""),
|
|
4798
|
+
url: String(r.web_url ?? ""),
|
|
4799
|
+
createdAt: r.created_at ?? null,
|
|
4800
|
+
mergedAt: r.merged_at ?? null,
|
|
4801
|
+
mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
|
|
4802
|
+
}));
|
|
4803
|
+
}
|
|
4804
|
+
const log = Bun.spawnSync([
|
|
4805
|
+
"git",
|
|
4806
|
+
"-C",
|
|
4807
|
+
root,
|
|
4808
|
+
"log",
|
|
4809
|
+
"--grep",
|
|
4810
|
+
"This reverts commit",
|
|
4811
|
+
"--format=%B",
|
|
4812
|
+
"-n",
|
|
4813
|
+
"300"
|
|
4814
|
+
]);
|
|
4815
|
+
const reverted = log.exitCode === 0 ? [...parseReverts(new TextDecoder().decode(log.stdout))] : [];
|
|
4816
|
+
const entry = { at: Date.now(), merged, reverted };
|
|
4817
|
+
this.outcomeCache.set(projectId, entry);
|
|
4818
|
+
return entry;
|
|
4819
|
+
}
|
|
4147
4820
|
remote(root) {
|
|
4148
4821
|
const r = Bun.spawnSync(["git", "-C", root, "remote", "get-url", "origin"]);
|
|
4149
4822
|
if (r.exitCode !== 0)
|
|
@@ -4376,6 +5049,16 @@ function currentBranch(cwd) {
|
|
|
4376
5049
|
branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
|
|
4377
5050
|
return branchCache.get(cwd)?.v ?? null;
|
|
4378
5051
|
}
|
|
5052
|
+
var originCache = new Map;
|
|
5053
|
+
function originUrl(root) {
|
|
5054
|
+
const hit = originCache.get(root);
|
|
5055
|
+
const now = Date.now();
|
|
5056
|
+
if (hit && now - hit.t < 300000)
|
|
5057
|
+
return hit.v;
|
|
5058
|
+
const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
|
|
5059
|
+
originCache.set(root, { v, t: now });
|
|
5060
|
+
return v;
|
|
5061
|
+
}
|
|
4379
5062
|
function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
|
|
4380
5063
|
const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
|
|
4381
5064
|
const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
|
|
@@ -4507,6 +5190,14 @@ class Runner {
|
|
|
4507
5190
|
return { ok: false, reason: "unknown project" };
|
|
4508
5191
|
if (!input.prompt.trim())
|
|
4509
5192
|
return { ok: false, reason: "prompt is required" };
|
|
5193
|
+
if (input.model) {
|
|
5194
|
+
const allow = this.store.config(input.projectId).models.allow;
|
|
5195
|
+
if (!modelAllowed(input.model, allow))
|
|
5196
|
+
return {
|
|
5197
|
+
ok: false,
|
|
5198
|
+
reason: `model "${input.model}" is not in [models] allow (${allow.join(", ")})`
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
4510
5201
|
if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
|
|
4511
5202
|
return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
|
|
4512
5203
|
if (this.get(input.task)?.projectId === input.projectId)
|
|
@@ -4805,7 +5496,8 @@ class Runner {
|
|
|
4805
5496
|
import { Database } from "bun:sqlite";
|
|
4806
5497
|
import {
|
|
4807
5498
|
closeSync,
|
|
4808
|
-
|
|
5499
|
+
copyFileSync,
|
|
5500
|
+
existsSync as existsSync6,
|
|
4809
5501
|
mkdirSync as mkdirSync4,
|
|
4810
5502
|
openSync as openSync3,
|
|
4811
5503
|
readdirSync,
|
|
@@ -4817,12 +5509,12 @@ import {
|
|
|
4817
5509
|
unlinkSync,
|
|
4818
5510
|
writeFileSync as writeFileSync2
|
|
4819
5511
|
} from "fs";
|
|
4820
|
-
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
4821
|
-
import { basename, dirname as
|
|
5512
|
+
import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
|
|
5513
|
+
import { basename, dirname as dirname3, join as join8 } from "path";
|
|
4822
5514
|
|
|
4823
5515
|
// packages/daemon/src/bootstrap.ts
|
|
4824
|
-
import { cpSync, existsSync as
|
|
4825
|
-
import { dirname, join as join7 } from "path";
|
|
5516
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
|
|
5517
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
4826
5518
|
function runBootstrap(plan, opts) {
|
|
4827
5519
|
const logDir = join7(opts.home, "logs", opts.projectId);
|
|
4828
5520
|
mkdirSync3(logDir, { recursive: true });
|
|
@@ -4830,12 +5522,12 @@ function runBootstrap(plan, opts) {
|
|
|
4830
5522
|
const copied = [];
|
|
4831
5523
|
const skipped = [];
|
|
4832
5524
|
for (const c of plan.copies) {
|
|
4833
|
-
if (!
|
|
5525
|
+
if (!existsSync5(c.from)) {
|
|
4834
5526
|
skipped.push(c.rel);
|
|
4835
5527
|
continue;
|
|
4836
5528
|
}
|
|
4837
5529
|
try {
|
|
4838
|
-
mkdirSync3(
|
|
5530
|
+
mkdirSync3(dirname2(c.to), { recursive: true });
|
|
4839
5531
|
cpSync(c.from, c.to, { recursive: true, force: true });
|
|
4840
5532
|
copied.push(c.rel);
|
|
4841
5533
|
} catch (e) {
|
|
@@ -4927,14 +5619,14 @@ class TaskSources {
|
|
|
4927
5619
|
`)[0] ?? code}`);
|
|
4928
5620
|
return normalizeGithubIssues(JSON.parse(out));
|
|
4929
5621
|
}
|
|
4930
|
-
async linear(
|
|
5622
|
+
async linear(team2) {
|
|
4931
5623
|
const key = this.env.LINEAR_API_KEY;
|
|
4932
5624
|
if (!key)
|
|
4933
5625
|
throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
|
|
4934
5626
|
const r = await fetch("https://api.linear.app/graphql", {
|
|
4935
5627
|
method: "POST",
|
|
4936
5628
|
headers: { "content-type": "application/json", authorization: key },
|
|
4937
|
-
body: JSON.stringify({ query: linearIssuesQuery(
|
|
5629
|
+
body: JSON.stringify({ query: linearIssuesQuery(team2) })
|
|
4938
5630
|
});
|
|
4939
5631
|
if (!r.ok)
|
|
4940
5632
|
throw new Error(`Linear API ${r.status}`);
|
|
@@ -4960,11 +5652,12 @@ CREATE INDEX IF NOT EXISTS events_type_seq ON events(type, seq);
|
|
|
4960
5652
|
CREATE TABLE IF NOT EXISTS turns (
|
|
4961
5653
|
id TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, ts TEXT, model TEXT, effort TEXT, sidechain INTEGER,
|
|
4962
5654
|
input INTEGER, output INTEGER, cache_write INTEGER, cache_write_1h INTEGER, cache_read INTEGER, thinking INTEGER,
|
|
4963
|
-
cost_usd REAL, text TEXT, tools TEXT
|
|
5655
|
+
cost_usd REAL, cost_fixed INTEGER DEFAULT 0, text TEXT, tools TEXT
|
|
4964
5656
|
);
|
|
4965
5657
|
CREATE INDEX IF NOT EXISTS turns_session ON turns(session_id, ts);
|
|
4966
5658
|
CREATE INDEX IF NOT EXISTS turns_ts ON turns(ts);
|
|
4967
5659
|
CREATE TABLE IF NOT EXISTS tails (path TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, offset INTEGER);
|
|
5660
|
+
CREATE TABLE IF NOT EXISTS outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, payload TEXT, created_at TEXT);
|
|
4968
5661
|
CREATE TABLE IF NOT EXISTS resources (
|
|
4969
5662
|
name TEXT, project_id TEXT, kind TEXT, owner TEXT, session_id TEXT,
|
|
4970
5663
|
pid INTEGER, port INTEGER, acquired_at TEXT, expires_at TEXT, released INTEGER DEFAULT 0,
|
|
@@ -4997,6 +5690,12 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
4997
5690
|
answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
|
|
4998
5691
|
);
|
|
4999
5692
|
CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
|
|
5693
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
5694
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
|
|
5695
|
+
step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
|
|
5696
|
+
started_at TEXT, updated_at TEXT, ended_at TEXT, actor_kind TEXT, actor_id TEXT
|
|
5697
|
+
);
|
|
5698
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_proj ON workflow_runs(project_id, id);
|
|
5000
5699
|
CREATE TABLE IF NOT EXISTS claims (
|
|
5001
5700
|
project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
|
|
5002
5701
|
acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
|
|
@@ -5024,9 +5723,13 @@ class Store {
|
|
|
5024
5723
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
|
|
5025
5724
|
this.db.exec(SCHEMA);
|
|
5026
5725
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
5726
|
+
this.ensureColumn("claims", "team_state", "TEXT");
|
|
5727
|
+
this.ensureColumn("turns", "cost_fixed", "INTEGER DEFAULT 0");
|
|
5027
5728
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5028
5729
|
this.ensureColumn("projects", "icon", "TEXT");
|
|
5029
5730
|
this.ensureColumn("projects", "color", "TEXT");
|
|
5731
|
+
this.ensureColumn("messages", "to_kind", "TEXT");
|
|
5732
|
+
this.ensureColumn("messages", "from_session", "TEXT");
|
|
5030
5733
|
this.migrate();
|
|
5031
5734
|
this.migrateProjectsJson(join8(home, "projects.json"));
|
|
5032
5735
|
this.reconcileMovedProjects();
|
|
@@ -5047,6 +5750,12 @@ class Store {
|
|
|
5047
5750
|
setMeta(key, value) {
|
|
5048
5751
|
this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
5049
5752
|
}
|
|
5753
|
+
metaValue(key) {
|
|
5754
|
+
return this.meta(key) || null;
|
|
5755
|
+
}
|
|
5756
|
+
setMetaValue(key, value) {
|
|
5757
|
+
this.setMeta(key, value);
|
|
5758
|
+
}
|
|
5050
5759
|
slimExistingEvents() {
|
|
5051
5760
|
if (this.meta("events_slim") === "1")
|
|
5052
5761
|
return;
|
|
@@ -5076,9 +5785,9 @@ class Store {
|
|
|
5076
5785
|
reconcileMovedProjects() {
|
|
5077
5786
|
const all = this.projects();
|
|
5078
5787
|
for (const stale of all) {
|
|
5079
|
-
if (
|
|
5788
|
+
if (existsSync6(stale.root))
|
|
5080
5789
|
continue;
|
|
5081
|
-
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name &&
|
|
5790
|
+
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
|
|
5082
5791
|
if (live.length !== 1)
|
|
5083
5792
|
continue;
|
|
5084
5793
|
this.mergeProject(stale.id, live[0].id);
|
|
@@ -5162,7 +5871,7 @@ class Store {
|
|
|
5162
5871
|
return actorFrom(owner, sessionId, { user: osUser(), runId });
|
|
5163
5872
|
}
|
|
5164
5873
|
migrateProjectsJson(file) {
|
|
5165
|
-
if (!
|
|
5874
|
+
if (!existsSync6(file))
|
|
5166
5875
|
return;
|
|
5167
5876
|
try {
|
|
5168
5877
|
const list = JSON.parse(readFileSync3(file, "utf8"));
|
|
@@ -5177,7 +5886,7 @@ class Store {
|
|
|
5177
5886
|
const hit = this.topCache.get(cwd);
|
|
5178
5887
|
if (hit && Date.now() - hit.t < 1e4)
|
|
5179
5888
|
return hit.v;
|
|
5180
|
-
const v = cwd &&
|
|
5889
|
+
const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
|
|
5181
5890
|
this.topCache.set(cwd, { v, t: Date.now() });
|
|
5182
5891
|
return v;
|
|
5183
5892
|
}
|
|
@@ -5383,7 +6092,7 @@ class Store {
|
|
|
5383
6092
|
}));
|
|
5384
6093
|
}
|
|
5385
6094
|
sessionContext(cwd) {
|
|
5386
|
-
if (!cwd || !
|
|
6095
|
+
if (!cwd || !existsSync6(cwd))
|
|
5387
6096
|
return null;
|
|
5388
6097
|
const toplevel = this.toplevel(cwd);
|
|
5389
6098
|
const project = this.resolveProject(cwd);
|
|
@@ -5523,7 +6232,160 @@ class Store {
|
|
|
5523
6232
|
return qs;
|
|
5524
6233
|
}
|
|
5525
6234
|
answerContext(sessionId) {
|
|
5526
|
-
|
|
6235
|
+
const parts = [
|
|
6236
|
+
formatAnswers(this.inbox(sessionId)),
|
|
6237
|
+
formatMessages(this.messageInbox(sessionId))
|
|
6238
|
+
];
|
|
6239
|
+
const out = parts.filter(Boolean);
|
|
6240
|
+
return out.length ? out.join(`
|
|
6241
|
+
`) : null;
|
|
6242
|
+
}
|
|
6243
|
+
wfInsert(projectId, task, workflow, steps, actor2) {
|
|
6244
|
+
const now = new Date().toISOString();
|
|
6245
|
+
const r = this.db.query(`INSERT INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, started_at, updated_at, actor_kind, actor_id)
|
|
6246
|
+
VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor2.kind, actor2.id);
|
|
6247
|
+
this.touch();
|
|
6248
|
+
return Number(r.lastInsertRowid);
|
|
6249
|
+
}
|
|
6250
|
+
wfUpdate(id, patch) {
|
|
6251
|
+
const sets = ["updated_at = ?"];
|
|
6252
|
+
const args = [new Date().toISOString()];
|
|
6253
|
+
if (patch.step !== undefined) {
|
|
6254
|
+
sets.push("step = ?");
|
|
6255
|
+
args.push(patch.step);
|
|
6256
|
+
}
|
|
6257
|
+
if (patch.stepLabel !== undefined) {
|
|
6258
|
+
sets.push("step_label = ?");
|
|
6259
|
+
args.push(patch.stepLabel);
|
|
6260
|
+
}
|
|
6261
|
+
if (patch.state !== undefined) {
|
|
6262
|
+
sets.push("state = ?");
|
|
6263
|
+
args.push(patch.state);
|
|
6264
|
+
}
|
|
6265
|
+
if (patch.detail !== undefined) {
|
|
6266
|
+
sets.push("detail = ?");
|
|
6267
|
+
args.push(patch.detail);
|
|
6268
|
+
}
|
|
6269
|
+
if (patch.runId !== undefined) {
|
|
6270
|
+
sets.push("run_id = ?");
|
|
6271
|
+
args.push(patch.runId);
|
|
6272
|
+
}
|
|
6273
|
+
if (patch.ended) {
|
|
6274
|
+
sets.push("ended_at = ?");
|
|
6275
|
+
args.push(new Date().toISOString());
|
|
6276
|
+
}
|
|
6277
|
+
this.db.query(`UPDATE workflow_runs SET ${sets.join(", ")} WHERE id = ?`).run(...args, id);
|
|
6278
|
+
this.touch();
|
|
6279
|
+
}
|
|
6280
|
+
wfRuns(projectId, limit = 50) {
|
|
6281
|
+
return this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map(rowToWorkflowRun);
|
|
6282
|
+
}
|
|
6283
|
+
wfActive(projectId, task) {
|
|
6284
|
+
const r = this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? AND task = ? AND state = 'running' ORDER BY id DESC LIMIT 1").get(projectId, task);
|
|
6285
|
+
return r ? rowToWorkflowRun(r) : null;
|
|
6286
|
+
}
|
|
6287
|
+
wfSweepOrphans() {
|
|
6288
|
+
this.db.query("UPDATE workflow_runs SET state = 'stopped', detail = COALESCE(detail, 'daemon restarted mid-workflow'), ended_at = ? WHERE state = 'running'").run(new Date().toISOString());
|
|
6289
|
+
}
|
|
6290
|
+
send(projectId, input) {
|
|
6291
|
+
if (!this.project(projectId))
|
|
6292
|
+
return { ok: false, error: "unknown project" };
|
|
6293
|
+
const v = validateMessage(input.text);
|
|
6294
|
+
if (!v.ok)
|
|
6295
|
+
return { ok: false, error: v.reason };
|
|
6296
|
+
const to = parseTo(input.to);
|
|
6297
|
+
if (!to)
|
|
6298
|
+
return { ok: false, error: 'to must be a session id, a task, or "lead"' };
|
|
6299
|
+
let sessionId = null;
|
|
6300
|
+
let task = null;
|
|
6301
|
+
if (to.kind === "session") {
|
|
6302
|
+
sessionId = this.knownSession(to.id) ?? this.sessionByPrefix(to.id);
|
|
6303
|
+
if (!sessionId)
|
|
6304
|
+
return { ok: false, error: `unknown session ${to.id}` };
|
|
6305
|
+
} else if (to.kind === "task") {
|
|
6306
|
+
task = to.task;
|
|
6307
|
+
sessionId = this.sessionForTask(projectId, to.task);
|
|
6308
|
+
} else {
|
|
6309
|
+
sessionId = this.leadSession(projectId);
|
|
6310
|
+
}
|
|
6311
|
+
const createdAt = new Date().toISOString();
|
|
6312
|
+
const from = input.from ?? (input.fromSession ? `agent ${input.fromSession.slice(0, 8)}` : null);
|
|
6313
|
+
const r = this.db.query(`INSERT INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
6314
|
+
VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?)`).run(projectId, sessionId, task, v.text, from, createdAt, to.kind, input.fromSession ?? null);
|
|
6315
|
+
const message = this.message(Number(r.lastInsertRowid));
|
|
6316
|
+
this.append({
|
|
6317
|
+
ts: createdAt,
|
|
6318
|
+
type: "message.sent",
|
|
6319
|
+
projectId,
|
|
6320
|
+
sessionId: input.fromSession ?? null,
|
|
6321
|
+
actor: this.actorFor(input.from ?? null, input.fromSession ?? null),
|
|
6322
|
+
payload: {
|
|
6323
|
+
id: message.id,
|
|
6324
|
+
to: input.to,
|
|
6325
|
+
task,
|
|
6326
|
+
recipient: sessionId,
|
|
6327
|
+
text: v.text.slice(0, 400),
|
|
6328
|
+
summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
|
|
6329
|
+
}
|
|
6330
|
+
});
|
|
6331
|
+
return { ok: true, message };
|
|
6332
|
+
}
|
|
6333
|
+
message(id) {
|
|
6334
|
+
const r = this.db.query("SELECT * FROM messages WHERE id = ? AND kind = 'message'").get(id);
|
|
6335
|
+
return r ? rowToMessage(r) : null;
|
|
6336
|
+
}
|
|
6337
|
+
messages(opts = {}) {
|
|
6338
|
+
const where = ["kind = 'message'"];
|
|
6339
|
+
const args = [];
|
|
6340
|
+
if (opts.projectId) {
|
|
6341
|
+
where.push("project_id = ?");
|
|
6342
|
+
args.push(opts.projectId);
|
|
6343
|
+
}
|
|
6344
|
+
if (opts.sessionId) {
|
|
6345
|
+
where.push("(session_id = ? OR from_session = ?)");
|
|
6346
|
+
args.push(opts.sessionId, opts.sessionId);
|
|
6347
|
+
}
|
|
6348
|
+
if (opts.task) {
|
|
6349
|
+
where.push("task = ?");
|
|
6350
|
+
args.push(opts.task);
|
|
6351
|
+
}
|
|
6352
|
+
args.push(opts.limit ?? 100);
|
|
6353
|
+
return this.db.query(`SELECT * FROM messages WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`).all(...args).map(rowToMessage);
|
|
6354
|
+
}
|
|
6355
|
+
messageInbox(sessionId, opts = {}) {
|
|
6356
|
+
if (!sessionId)
|
|
6357
|
+
return [];
|
|
6358
|
+
const s = this.db.query("SELECT project_id, kind, cwd FROM sessions WHERE id = ?").get(sessionId);
|
|
6359
|
+
if (!s)
|
|
6360
|
+
return [];
|
|
6361
|
+
const task = this.heldClaimsWithWorktree().find((c) => isInside(s.cwd, c.worktree))?.task ?? null;
|
|
6362
|
+
const rows = this.db.query(`SELECT * FROM messages WHERE kind = 'message' AND delivered_at IS NULL AND from_session IS NOT ?
|
|
6363
|
+
AND (session_id = ?
|
|
6364
|
+
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
6365
|
+
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
6366
|
+
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
6367
|
+
const ms = rows.map(rowToMessage);
|
|
6368
|
+
if (ms.length && !opts.peek)
|
|
6369
|
+
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms.map((m) => m.id));
|
|
6370
|
+
return ms;
|
|
6371
|
+
}
|
|
6372
|
+
markMessageDelivered(id, sessionId) {
|
|
6373
|
+
this.db.query("UPDATE messages SET delivered_at = ?, session_id = COALESCE(?, session_id) WHERE id = ? AND delivered_at IS NULL").run(new Date().toISOString(), sessionId, id);
|
|
6374
|
+
}
|
|
6375
|
+
leadSession(projectId) {
|
|
6376
|
+
const r = this.db.query("SELECT id FROM sessions WHERE project_id = ? AND kind = 'interactive' AND state != 'ended' ORDER BY last_seen_at DESC LIMIT 1").get(projectId);
|
|
6377
|
+
return r?.id ?? null;
|
|
6378
|
+
}
|
|
6379
|
+
sessionByPrefix(prefix) {
|
|
6380
|
+
const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
|
|
6381
|
+
return rows.length === 1 ? rows[0]?.id ?? null : null;
|
|
6382
|
+
}
|
|
6383
|
+
sessionForTask(projectId, task) {
|
|
6384
|
+
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
6385
|
+
if (!claim?.worktree)
|
|
6386
|
+
return null;
|
|
6387
|
+
const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
|
|
6388
|
+
return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
|
|
5527
6389
|
}
|
|
5528
6390
|
questionContext(task, projectId) {
|
|
5529
6391
|
if (!task)
|
|
@@ -5600,13 +6462,13 @@ class Store {
|
|
|
5600
6462
|
};
|
|
5601
6463
|
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
5602
6464
|
const worktree2 = claim?.worktree;
|
|
5603
|
-
if (!worktree2 || !
|
|
6465
|
+
if (!worktree2 || !existsSync6(worktree2))
|
|
5604
6466
|
return {
|
|
5605
6467
|
ok: false,
|
|
5606
6468
|
reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
|
|
5607
6469
|
};
|
|
5608
6470
|
const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
|
|
5609
|
-
if (!
|
|
6471
|
+
if (!existsSync6(cwd))
|
|
5610
6472
|
return { ok: false, reason: `gate cwd ${cwd} does not exist` };
|
|
5611
6473
|
const key = `${projectId}:${task}:${gate}`;
|
|
5612
6474
|
if (this.gateJobs.has(key))
|
|
@@ -5947,7 +6809,7 @@ ${err}
|
|
|
5947
6809
|
error = e.error;
|
|
5948
6810
|
} else {
|
|
5949
6811
|
const path = join8(p.root, source);
|
|
5950
|
-
if (!
|
|
6812
|
+
if (!existsSync6(path))
|
|
5951
6813
|
return { source, required: this.requiredGates(projectId), tasks: [] };
|
|
5952
6814
|
const mtime = statSync(path).mtimeMs;
|
|
5953
6815
|
let md = this.taskCache.get(projectId);
|
|
@@ -5996,7 +6858,7 @@ ${err}
|
|
|
5996
6858
|
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
5997
6859
|
try {
|
|
5998
6860
|
if (!hasLockedRules(loaded)) {
|
|
5999
|
-
if (
|
|
6861
|
+
if (existsSync6(file))
|
|
6000
6862
|
unlinkSync(file);
|
|
6001
6863
|
return;
|
|
6002
6864
|
}
|
|
@@ -6012,13 +6874,13 @@ ${err}
|
|
|
6012
6874
|
claudeSettings() {
|
|
6013
6875
|
const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
|
|
6014
6876
|
try {
|
|
6015
|
-
return
|
|
6877
|
+
return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
|
|
6016
6878
|
} catch {
|
|
6017
6879
|
return null;
|
|
6018
6880
|
}
|
|
6019
6881
|
}
|
|
6020
6882
|
checkPolicy(cwd, sessionId) {
|
|
6021
|
-
const project =
|
|
6883
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6022
6884
|
const repoRoot = project?.root ?? null;
|
|
6023
6885
|
const loaded = this.policyFor(repoRoot);
|
|
6024
6886
|
const settings = this.claudeSettings();
|
|
@@ -6043,7 +6905,7 @@ ${err}
|
|
|
6043
6905
|
return findings;
|
|
6044
6906
|
}
|
|
6045
6907
|
evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
|
|
6046
|
-
if (BUDGET_ASK_TOOLS.has(tool) && cwd &&
|
|
6908
|
+
if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
|
|
6047
6909
|
const project = this.resolveProject(cwd);
|
|
6048
6910
|
const b = this.budgetFor(project.id);
|
|
6049
6911
|
if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
|
|
@@ -6054,6 +6916,18 @@ ${err}
|
|
|
6054
6916
|
};
|
|
6055
6917
|
return { decision: d, display: input.command ?? input.file_path ?? tool };
|
|
6056
6918
|
}
|
|
6919
|
+
const tb = this.teamBudgets().find((x) => x.level === "exceeded" && x.on_exceed === "ask" && (x.scope !== "project" || x.key === this.clusterKeyFor(project.id)));
|
|
6920
|
+
if (tb) {
|
|
6921
|
+
const label = tb.scope === "org" ? "the org" : `${tb.scope} ${tb.key}`;
|
|
6922
|
+
return {
|
|
6923
|
+
decision: {
|
|
6924
|
+
action: "ask",
|
|
6925
|
+
rule: "budget",
|
|
6926
|
+
reason: `team ${tb.kind} budget for ${label} is exceeded ($${tb.spent.toFixed(2)} of $${tb.limit}) \u2014 the team's on_exceed = "ask": confirm each change, or have an admin raise it (POST /t1/budgets)`
|
|
6927
|
+
},
|
|
6928
|
+
display: input.command ?? input.file_path ?? tool
|
|
6929
|
+
};
|
|
6930
|
+
}
|
|
6057
6931
|
}
|
|
6058
6932
|
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
6059
6933
|
const cmd = tool === "Bash" ? input.command : undefined;
|
|
@@ -6134,7 +7008,7 @@ ${err}
|
|
|
6134
7008
|
return this.openIncident(d, cwd, id, cmd);
|
|
6135
7009
|
}
|
|
6136
7010
|
openIncident(d, cwd, sessionId, command) {
|
|
6137
|
-
const project = cwd &&
|
|
7011
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6138
7012
|
this.append({
|
|
6139
7013
|
ts: new Date().toISOString(),
|
|
6140
7014
|
type: "incident.opened",
|
|
@@ -6185,7 +7059,7 @@ ${err}
|
|
|
6185
7059
|
}
|
|
6186
7060
|
}
|
|
6187
7061
|
const report = dryRunRules(calls, modes, {
|
|
6188
|
-
toplevel: (cwd) => cwd &&
|
|
7062
|
+
toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
|
|
6189
7063
|
claims: this.heldWorktrees()
|
|
6190
7064
|
});
|
|
6191
7065
|
return { ...report, modes };
|
|
@@ -6201,7 +7075,7 @@ ${err}
|
|
|
6201
7075
|
this.prices = { ...PRICES };
|
|
6202
7076
|
for (const f of ["pricing.litellm.json", "pricing.json"]) {
|
|
6203
7077
|
const p = join8(this.home, f);
|
|
6204
|
-
if (!
|
|
7078
|
+
if (!existsSync6(p))
|
|
6205
7079
|
continue;
|
|
6206
7080
|
try {
|
|
6207
7081
|
const j = JSON.parse(readFileSync3(p, "utf8"));
|
|
@@ -6221,7 +7095,7 @@ ${err}
|
|
|
6221
7095
|
this.reprice();
|
|
6222
7096
|
}
|
|
6223
7097
|
reprice() {
|
|
6224
|
-
const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns").all();
|
|
7098
|
+
const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns WHERE cost_fixed IS NOT 1").all();
|
|
6225
7099
|
const up = this.db.query("UPDATE turns SET cost_usd = ? WHERE id = ?");
|
|
6226
7100
|
const tx = this.db.transaction(() => {
|
|
6227
7101
|
for (const r of rows)
|
|
@@ -6382,13 +7256,70 @@ ${err}
|
|
|
6382
7256
|
if (stored.type === "incident.opened")
|
|
6383
7257
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
6384
7258
|
this.projectSession(stored);
|
|
7259
|
+
if (stored.type === "incident.opened") {
|
|
7260
|
+
const webhook = this.policyFor(null).config.notify.webhook;
|
|
7261
|
+
if (webhook) {
|
|
7262
|
+
const p2 = stored.payload ?? {};
|
|
7263
|
+
const project = this.project(stored.projectId)?.name ?? stored.projectId;
|
|
7264
|
+
fetch(webhook, {
|
|
7265
|
+
method: "POST",
|
|
7266
|
+
headers: { "content-type": "application/json" },
|
|
7267
|
+
body: JSON.stringify({
|
|
7268
|
+
text: `Swarm incident \xB7 ${p2.rule ?? "?"} \xB7 ${project}
|
|
7269
|
+
${p2.command ?? ""}
|
|
7270
|
+
${p2.reason ?? ""}`.trim(),
|
|
7271
|
+
rule: p2.rule,
|
|
7272
|
+
project,
|
|
7273
|
+
sessionId: stored.sessionId,
|
|
7274
|
+
ts: stored.ts
|
|
7275
|
+
}),
|
|
7276
|
+
signal: AbortSignal.timeout(5000)
|
|
7277
|
+
}).catch(() => {});
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
const team2 = this.policyFor(null).config.team;
|
|
7281
|
+
if (team2.url && team2.forward.includes("ledger") && isAuditType(stored.type)) {
|
|
7282
|
+
this.db.query("INSERT INTO outbox (kind, payload, created_at) VALUES ('event', ?, ?)").run(JSON.stringify({
|
|
7283
|
+
seq: stored.seq,
|
|
7284
|
+
ts: stored.ts,
|
|
7285
|
+
type: stored.type,
|
|
7286
|
+
projectId: stored.projectId,
|
|
7287
|
+
sessionId: stored.sessionId,
|
|
7288
|
+
actor: actor2,
|
|
7289
|
+
payload: slim.payload ?? null
|
|
7290
|
+
}), stored.ts);
|
|
7291
|
+
}
|
|
6385
7292
|
this.touch();
|
|
6386
7293
|
const wire = toWire(stored);
|
|
6387
7294
|
for (const l of this.listeners)
|
|
6388
7295
|
l(wire);
|
|
6389
7296
|
return stored;
|
|
6390
7297
|
}
|
|
6391
|
-
|
|
7298
|
+
outboxPending(limit = 200) {
|
|
7299
|
+
return this.db.query("SELECT seq, kind, payload FROM outbox ORDER BY seq LIMIT ?").all(limit);
|
|
7300
|
+
}
|
|
7301
|
+
outboxAck(upTo) {
|
|
7302
|
+
this.db.query("DELETE FROM outbox WHERE seq <= ?").run(upTo);
|
|
7303
|
+
}
|
|
7304
|
+
outboxStatus() {
|
|
7305
|
+
const r = this.db.query("SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM outbox").get();
|
|
7306
|
+
return { pending: r.n, oldest: r.oldest };
|
|
7307
|
+
}
|
|
7308
|
+
machineIdentity() {
|
|
7309
|
+
let id = this.meta("machine_id");
|
|
7310
|
+
if (!id) {
|
|
7311
|
+
id = crypto.randomUUID();
|
|
7312
|
+
this.setMeta("machine_id", id);
|
|
7313
|
+
}
|
|
7314
|
+
return { id, name: hostname() };
|
|
7315
|
+
}
|
|
7316
|
+
spendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
7317
|
+
return this.db.query(`SELECT s.project_id AS projectId, COALESCE(s.agent, 'claude-code') AS agent, t.model AS model,
|
|
7318
|
+
SUM(t.cost_usd) AS cost, SUM(t.input + t.cache_write + t.cache_read) AS tokensIn, SUM(t.output) AS tokensOut
|
|
7319
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
7320
|
+
WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, agent, t.model`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
|
|
7321
|
+
}
|
|
7322
|
+
audit(opts = {}) {
|
|
6392
7323
|
const where = [`type IN (${AUDIT_TYPES_SQL})`];
|
|
6393
7324
|
const args = [];
|
|
6394
7325
|
if (opts.since) {
|
|
@@ -6426,10 +7357,10 @@ ${err}
|
|
|
6426
7357
|
if (typeof raw2.cwd === "string")
|
|
6427
7358
|
this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
|
|
6428
7359
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
|
|
6429
|
-
const project =
|
|
7360
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6430
7361
|
const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
|
|
6431
7362
|
if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
|
|
6432
|
-
if (
|
|
7363
|
+
if (existsSync6(cwd)) {
|
|
6433
7364
|
this.autoHandoff(e.sessionId, cwd);
|
|
6434
7365
|
this.autoGate(event, e.sessionId, cwd);
|
|
6435
7366
|
}
|
|
@@ -6475,7 +7406,7 @@ ${err}
|
|
|
6475
7406
|
return;
|
|
6476
7407
|
const p = e.payload;
|
|
6477
7408
|
const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
|
|
6478
|
-
const branch = p.cwd &&
|
|
7409
|
+
const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
|
|
6479
7410
|
if (!row) {
|
|
6480
7411
|
this.db.query("INSERT INTO sessions (id, project_id, kind, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, 'active')").run(e.sessionId, e.projectId, p.cwd ?? "", branch, e.ts, e.ts, p.summary ?? e.type, e.type);
|
|
6481
7412
|
}
|
|
@@ -6515,13 +7446,13 @@ ${err}
|
|
|
6515
7446
|
persistTurns(sessionId, agentId, turns) {
|
|
6516
7447
|
const privacy = this.policyFor(null).config.privacy;
|
|
6517
7448
|
const res = this.redactions();
|
|
6518
|
-
const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
|
|
6519
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7449
|
+
const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, cost_fixed, text, tools)
|
|
7450
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6520
7451
|
ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
|
|
6521
|
-
cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
|
|
7452
|
+
cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, cost_fixed=excluded.cost_fixed, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
|
|
6522
7453
|
const tx = this.db.transaction((ts) => {
|
|
6523
7454
|
for (const t of ts) {
|
|
6524
|
-
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
|
|
7455
|
+
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, t.cost ?? costUsd(t.model, t.usage, this.prices), t.cost != null ? 1 : 0, privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
|
|
6525
7456
|
}
|
|
6526
7457
|
});
|
|
6527
7458
|
if (turns.length)
|
|
@@ -6547,10 +7478,10 @@ ${err}
|
|
|
6547
7478
|
}
|
|
6548
7479
|
tailSession(sessionId) {
|
|
6549
7480
|
const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
|
|
6550
|
-
if (!s?.transcript_path || !
|
|
7481
|
+
if (!s?.transcript_path || !existsSync6(s.transcript_path))
|
|
6551
7482
|
return 0;
|
|
6552
7483
|
let n = this.tailFile(s.transcript_path, sessionId, null);
|
|
6553
|
-
const subDir = join8(
|
|
7484
|
+
const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
|
|
6554
7485
|
for (const f of this.subagentFiles(subDir)) {
|
|
6555
7486
|
n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
|
|
6556
7487
|
}
|
|
@@ -6618,7 +7549,7 @@ ${err}
|
|
|
6618
7549
|
return out;
|
|
6619
7550
|
}
|
|
6620
7551
|
tailCodex(windowMs = 3 * 24 * 60 * 60000) {
|
|
6621
|
-
if (!
|
|
7552
|
+
if (!existsSync6(this.codexRoot()))
|
|
6622
7553
|
return 0;
|
|
6623
7554
|
let n = 0;
|
|
6624
7555
|
for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
|
|
@@ -6630,9 +7561,54 @@ ${err}
|
|
|
6630
7561
|
return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
|
|
6631
7562
|
}
|
|
6632
7563
|
grokSummary = new Map;
|
|
7564
|
+
tailGemini(windowMs = 3 * 24 * 60 * 60000) {
|
|
7565
|
+
const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
|
|
7566
|
+
if (!existsSync6(root))
|
|
7567
|
+
return 0;
|
|
7568
|
+
const since = Date.now() - windowMs;
|
|
7569
|
+
const ls = (p) => {
|
|
7570
|
+
try {
|
|
7571
|
+
return readdirSync(p);
|
|
7572
|
+
} catch {
|
|
7573
|
+
return [];
|
|
7574
|
+
}
|
|
7575
|
+
};
|
|
7576
|
+
let n = 0;
|
|
7577
|
+
const ingestDir = (dir) => {
|
|
7578
|
+
for (const f of ls(dir)) {
|
|
7579
|
+
const path = join8(dir, f);
|
|
7580
|
+
if (!f.endsWith(".jsonl")) {
|
|
7581
|
+
try {
|
|
7582
|
+
if (statSync(path).isDirectory()) {
|
|
7583
|
+
for (const g of ls(path))
|
|
7584
|
+
if (g.endsWith(".jsonl"))
|
|
7585
|
+
ingestFile(join8(path, g));
|
|
7586
|
+
}
|
|
7587
|
+
} catch {}
|
|
7588
|
+
continue;
|
|
7589
|
+
}
|
|
7590
|
+
ingestFile(path);
|
|
7591
|
+
}
|
|
7592
|
+
};
|
|
7593
|
+
const ingestFile = (path) => {
|
|
7594
|
+
try {
|
|
7595
|
+
if (statSync(path).mtimeMs < since)
|
|
7596
|
+
return;
|
|
7597
|
+
} catch {
|
|
7598
|
+
return;
|
|
7599
|
+
}
|
|
7600
|
+
n += this.ingestLog(path, "gemini", parseGeminiChat);
|
|
7601
|
+
};
|
|
7602
|
+
for (const hash of ls(root)) {
|
|
7603
|
+
const chats = join8(root, hash, "chats");
|
|
7604
|
+
if (existsSync6(chats))
|
|
7605
|
+
ingestDir(chats);
|
|
7606
|
+
}
|
|
7607
|
+
return n;
|
|
7608
|
+
}
|
|
6633
7609
|
tailGrok(windowMs = 3 * 24 * 60 * 60000) {
|
|
6634
7610
|
const root = this.grokRoot();
|
|
6635
|
-
if (!
|
|
7611
|
+
if (!existsSync6(root))
|
|
6636
7612
|
return 0;
|
|
6637
7613
|
const since = Date.now() - windowMs;
|
|
6638
7614
|
const ls = (p) => {
|
|
@@ -6655,7 +7631,7 @@ ${err}
|
|
|
6655
7631
|
const cwdDir = join8(root, enc);
|
|
6656
7632
|
for (const sid of ls(cwdDir)) {
|
|
6657
7633
|
const path = join8(cwdDir, sid, "updates.jsonl");
|
|
6658
|
-
if (!
|
|
7634
|
+
if (!existsSync6(path))
|
|
6659
7635
|
continue;
|
|
6660
7636
|
try {
|
|
6661
7637
|
if (statSync(path).mtimeMs < since)
|
|
@@ -6686,6 +7662,160 @@ ${err}
|
|
|
6686
7662
|
}
|
|
6687
7663
|
return n;
|
|
6688
7664
|
}
|
|
7665
|
+
heldClaimsForSync() {
|
|
7666
|
+
return this.db.query("SELECT project_id, task, acquired_at, expires_at, actor_kind, actor_id, team_state FROM claims WHERE state = 'held'").all().map((r) => ({
|
|
7667
|
+
projectId: r.project_id,
|
|
7668
|
+
task: r.task,
|
|
7669
|
+
acquiredAt: r.acquired_at,
|
|
7670
|
+
expiresAt: r.expires_at,
|
|
7671
|
+
actorKind: r.actor_kind ?? null,
|
|
7672
|
+
actorId: r.actor_id ?? null,
|
|
7673
|
+
teamState: r.team_state ?? null
|
|
7674
|
+
}));
|
|
7675
|
+
}
|
|
7676
|
+
markClaimTeamState(projectId, task, state) {
|
|
7677
|
+
this.db.query("UPDATE claims SET team_state = ? WHERE project_id = ? AND task = ?").run(state, projectId, task);
|
|
7678
|
+
}
|
|
7679
|
+
revokeClaimConflict(projectId, task, holder) {
|
|
7680
|
+
const row = this.db.query("SELECT state FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
|
|
7681
|
+
if (row?.state !== "held")
|
|
7682
|
+
return;
|
|
7683
|
+
const now = new Date().toISOString();
|
|
7684
|
+
this.db.query("UPDATE claims SET state = 'released', released_at = ?, team_state = 'conflict' WHERE project_id = ? AND task = ?").run(now, projectId, task);
|
|
7685
|
+
this.append({
|
|
7686
|
+
ts: now,
|
|
7687
|
+
type: "claim.released",
|
|
7688
|
+
projectId,
|
|
7689
|
+
sessionId: null,
|
|
7690
|
+
payload: { task, summary: `revoked \u2014 the team ledger holds ${task} on ${holder}` }
|
|
7691
|
+
});
|
|
7692
|
+
this.append({
|
|
7693
|
+
ts: now,
|
|
7694
|
+
type: "incident.opened",
|
|
7695
|
+
projectId,
|
|
7696
|
+
sessionId: null,
|
|
7697
|
+
payload: {
|
|
7698
|
+
rule: "claim_conflict",
|
|
7699
|
+
action: "revoked",
|
|
7700
|
+
command: task,
|
|
7701
|
+
reason: `the team daemon holds ${task} for ${holder}; the local claim was revoked \u2014 the worktree is untouched`
|
|
7702
|
+
}
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
aiderCarries = new Map;
|
|
7706
|
+
recoverAiderCarry(sessionId) {
|
|
7707
|
+
const s = this.db.query("SELECT started_at, model, title FROM sessions WHERE id = ?").get(sessionId);
|
|
7708
|
+
if (!s)
|
|
7709
|
+
return null;
|
|
7710
|
+
const t = this.db.query("SELECT COUNT(*) AS n FROM turns WHERE session_id = ?").get(sessionId);
|
|
7711
|
+
return {
|
|
7712
|
+
sessionId,
|
|
7713
|
+
startMs: Date.parse(s.started_at) || 0,
|
|
7714
|
+
model: s.model,
|
|
7715
|
+
title: s.title,
|
|
7716
|
+
turns: t.n,
|
|
7717
|
+
text: "",
|
|
7718
|
+
tools: [],
|
|
7719
|
+
pending: null
|
|
7720
|
+
};
|
|
7721
|
+
}
|
|
7722
|
+
tailAider(windowMs = 3 * 24 * 60 * 60000) {
|
|
7723
|
+
const roots = this.db.query("SELECT DISTINCT root FROM projects WHERE root IS NOT NULL AND root != ''").all();
|
|
7724
|
+
let n = 0;
|
|
7725
|
+
for (const { root } of roots) {
|
|
7726
|
+
const path = join8(root, ".aider.chat.history.md");
|
|
7727
|
+
let mtime;
|
|
7728
|
+
try {
|
|
7729
|
+
mtime = statSync(path).mtimeMs;
|
|
7730
|
+
} catch {
|
|
7731
|
+
continue;
|
|
7732
|
+
}
|
|
7733
|
+
if (mtime < Date.now() - windowMs)
|
|
7734
|
+
continue;
|
|
7735
|
+
const row = this.db.query("SELECT offset, session_id FROM tails WHERE path = ?").get(path);
|
|
7736
|
+
const r = this.readFrom(path, row?.offset ?? 0);
|
|
7737
|
+
if (!r)
|
|
7738
|
+
continue;
|
|
7739
|
+
let carry = this.aiderCarries.get(path) ?? null;
|
|
7740
|
+
if (!carry && row?.session_id)
|
|
7741
|
+
carry = this.recoverAiderCarry(row.session_id);
|
|
7742
|
+
const { segments, carry: next } = parseAiderHistory(r.chunk, path, carry);
|
|
7743
|
+
this.aiderCarries.set(path, next);
|
|
7744
|
+
const lastSeg = segments.at(-1);
|
|
7745
|
+
for (const seg of segments) {
|
|
7746
|
+
this.ensureAgentSession(seg.sessionId, "aider", root, seg.startMs || mtime);
|
|
7747
|
+
this.persistTurns(seg.sessionId, null, seg.turns);
|
|
7748
|
+
const live = seg === lastSeg && Date.now() - mtime < 90000;
|
|
7749
|
+
const lastSeen = new Date(seg === lastSeg ? mtime : seg.startMs + seg.turns.length * 1000).toISOString();
|
|
7750
|
+
const lastText = [...seg.turns].reverse().find((t) => t.text)?.text ?? null;
|
|
7751
|
+
this.db.query("UPDATE sessions SET title = COALESCE(title, ?), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(seg.title, seg.model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, seg.sessionId);
|
|
7752
|
+
n += seg.turns.length;
|
|
7753
|
+
}
|
|
7754
|
+
this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, ?, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset, session_id = excluded.session_id").run(path, lastSeg?.sessionId ?? row?.session_id ?? null, r.next);
|
|
7755
|
+
}
|
|
7756
|
+
return n;
|
|
7757
|
+
}
|
|
7758
|
+
ocDbs = new Map;
|
|
7759
|
+
tailOpencode(windowMs = 3 * 24 * 60 * 60000) {
|
|
7760
|
+
const dir = process.env.SWARM_OPENCODE_DIR ?? join8(process.env.XDG_DATA_HOME ?? join8(homedir3(), ".local", "share"), "opencode");
|
|
7761
|
+
let files;
|
|
7762
|
+
try {
|
|
7763
|
+
files = readdirSync(dir).filter((f) => /^opencode[^/]*\.db$/.test(f));
|
|
7764
|
+
} catch {
|
|
7765
|
+
return 0;
|
|
7766
|
+
}
|
|
7767
|
+
let n = 0;
|
|
7768
|
+
for (const f of files) {
|
|
7769
|
+
const path = join8(dir, f);
|
|
7770
|
+
let db = this.ocDbs.get(path);
|
|
7771
|
+
if (!db) {
|
|
7772
|
+
try {
|
|
7773
|
+
db = new Database(path, { readonly: true });
|
|
7774
|
+
} catch {
|
|
7775
|
+
continue;
|
|
7776
|
+
}
|
|
7777
|
+
this.ocDbs.set(path, db);
|
|
7778
|
+
}
|
|
7779
|
+
const row = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path);
|
|
7780
|
+
const lower = Math.max(row?.offset ?? 0, Date.now() - windowMs);
|
|
7781
|
+
let rows;
|
|
7782
|
+
try {
|
|
7783
|
+
rows = db.query(`SELECT m.id, m.session_id, m.time_created, m.time_updated, m.data,
|
|
7784
|
+
s.directory, s.title, s.parent_id
|
|
7785
|
+
FROM message m JOIN session s ON s.id = m.session_id
|
|
7786
|
+
WHERE m.time_updated > ? ORDER BY m.time_updated ASC LIMIT 2000`).all(lower);
|
|
7787
|
+
} catch {
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
if (!rows.length)
|
|
7791
|
+
continue;
|
|
7792
|
+
let cursor = lower;
|
|
7793
|
+
const bySession = new Map;
|
|
7794
|
+
for (const m of rows) {
|
|
7795
|
+
cursor = Math.max(cursor, m.time_updated ?? 0);
|
|
7796
|
+
const g = bySession.get(m.session_id) ?? { rows: [], last: 0 };
|
|
7797
|
+
g.rows.push(m);
|
|
7798
|
+
g.last = Math.max(g.last, m.time_updated ?? m.time_created ?? 0);
|
|
7799
|
+
bySession.set(m.session_id, g);
|
|
7800
|
+
}
|
|
7801
|
+
for (const [sid, g] of bySession) {
|
|
7802
|
+
const first = g.rows[0];
|
|
7803
|
+
if (!first)
|
|
7804
|
+
continue;
|
|
7805
|
+
this.ensureAgentSession(sid, "opencode", first.directory ?? "", first.time_created ?? g.last);
|
|
7806
|
+
const turns = g.rows.map((m) => opencodeTurn(sid, m.id, m.data, m.time_created ?? 0, first.parent_id != null)).filter((t) => t != null);
|
|
7807
|
+
this.persistTurns(sid, null, turns);
|
|
7808
|
+
const live = Date.now() - g.last < 90000;
|
|
7809
|
+
const lastSeen = new Date(g.last).toISOString();
|
|
7810
|
+
const lastText = [...turns].reverse().find((t) => t.text)?.text ?? null;
|
|
7811
|
+
const model = [...turns].reverse().find((t) => t.model !== "opencode")?.model ?? null;
|
|
7812
|
+
this.db.query("UPDATE sessions SET title = COALESCE(?, title), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(first.title, model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, sid);
|
|
7813
|
+
n += turns.length;
|
|
7814
|
+
}
|
|
7815
|
+
this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, NULL, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset").run(path, cursor);
|
|
7816
|
+
}
|
|
7817
|
+
return n;
|
|
7818
|
+
}
|
|
6689
7819
|
ingestLog(path, agent, parse, cwdHint, titleHint) {
|
|
6690
7820
|
const off = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path) ?? { offset: 0 };
|
|
6691
7821
|
const r = this.readFrom(path, off.offset);
|
|
@@ -6714,9 +7844,9 @@ ${err}
|
|
|
6714
7844
|
ensureAgentSession(sid, agent, cwd, mtime) {
|
|
6715
7845
|
if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
|
|
6716
7846
|
return;
|
|
6717
|
-
const project = cwd &&
|
|
7847
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6718
7848
|
const ts = new Date(mtime).toISOString();
|
|
6719
|
-
this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd &&
|
|
7849
|
+
this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd && existsSync6(cwd) ? currentBranch(cwd) : null, ts, ts);
|
|
6720
7850
|
}
|
|
6721
7851
|
claimRows(projectId) {
|
|
6722
7852
|
return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
|
|
@@ -6762,9 +7892,9 @@ ${err}
|
|
|
6762
7892
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
6763
7893
|
const branch = `task/${task}`;
|
|
6764
7894
|
const worktree2 = this.worktreePath(projectId, task);
|
|
6765
|
-
if (
|
|
7895
|
+
if (existsSync6(worktree2))
|
|
6766
7896
|
return { ok: false, error: `${worktree2} already exists; release ${task} first` };
|
|
6767
|
-
mkdirSync4(
|
|
7897
|
+
mkdirSync4(dirname3(worktree2), { recursive: true });
|
|
6768
7898
|
const created = worktreeAdd(p.root, worktree2, branch, baseRef);
|
|
6769
7899
|
if (!created)
|
|
6770
7900
|
return { ok: false, error: `git worktree add failed for ${task}` };
|
|
@@ -6867,6 +7997,77 @@ ${err}
|
|
|
6867
7997
|
expiresAt: r.expires_at
|
|
6868
7998
|
}));
|
|
6869
7999
|
}
|
|
8000
|
+
collisions(projectId) {
|
|
8001
|
+
const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8002
|
+
const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
|
|
8003
|
+
WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [cutoff, projectId] : [cutoff]);
|
|
8004
|
+
if (!live.length)
|
|
8005
|
+
return { sessions: [], files: [], contested: 0 };
|
|
8006
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool, json_extract(payload,'$.toolInput.file_path') AS path
|
|
8007
|
+
FROM events WHERE type = 'tool.requested' AND session_id IN (${live.map(() => "?").join(",")})
|
|
8008
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL`).all(...live.map((s) => s.id));
|
|
8009
|
+
const g = collisionGraph(rows.map((r) => ({ sessionId: r.session_id, tool: r.tool ?? "", path: r.path ?? "" })));
|
|
8010
|
+
const meta = new Map(live.map((s) => [s.id, s]));
|
|
8011
|
+
return {
|
|
8012
|
+
...g,
|
|
8013
|
+
sessions: g.sessions.map((s) => {
|
|
8014
|
+
const m = meta.get(s.id);
|
|
8015
|
+
return {
|
|
8016
|
+
...s,
|
|
8017
|
+
title: m?.title ?? null,
|
|
8018
|
+
agent: m?.agent ?? "claude-code",
|
|
8019
|
+
projectId: m?.project_id ?? null
|
|
8020
|
+
};
|
|
8021
|
+
})
|
|
8022
|
+
};
|
|
8023
|
+
}
|
|
8024
|
+
stalls = new Map;
|
|
8025
|
+
checkStalls() {
|
|
8026
|
+
const live = this.db.query("SELECT id, project_id FROM sessions WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?").all(new Date(Date.now() - IDLE_MS).toISOString());
|
|
8027
|
+
const liveIds = new Set(live.map((s) => s.id));
|
|
8028
|
+
for (const id of [...this.stalls.keys()])
|
|
8029
|
+
if (!liveIds.has(id))
|
|
8030
|
+
this.stalls.delete(id);
|
|
8031
|
+
let flagged = 0;
|
|
8032
|
+
for (const s of live) {
|
|
8033
|
+
const rows = this.db.query("SELECT payload FROM events WHERE session_id = ? AND type = 'tool.completed' ORDER BY seq DESC LIMIT 12").all(s.id);
|
|
8034
|
+
const calls = rows.reverse().map((r) => {
|
|
8035
|
+
let p = {};
|
|
8036
|
+
try {
|
|
8037
|
+
p = JSON.parse(r.payload || "{}");
|
|
8038
|
+
} catch {}
|
|
8039
|
+
return {
|
|
8040
|
+
tool: typeof p.tool === "string" ? p.tool : "?",
|
|
8041
|
+
input: JSON.stringify(p.toolInput ?? null),
|
|
8042
|
+
errored: toolResponseErrored(p.toolResponse),
|
|
8043
|
+
ts: ""
|
|
8044
|
+
};
|
|
8045
|
+
});
|
|
8046
|
+
const stall2 = detectStall(calls);
|
|
8047
|
+
if (!stall2) {
|
|
8048
|
+
this.stalls.delete(s.id);
|
|
8049
|
+
continue;
|
|
8050
|
+
}
|
|
8051
|
+
flagged++;
|
|
8052
|
+
const prev = this.stalls.get(s.id);
|
|
8053
|
+
this.stalls.set(s.id, stall2);
|
|
8054
|
+
if (prev?.kind === stall2.kind)
|
|
8055
|
+
continue;
|
|
8056
|
+
this.append({
|
|
8057
|
+
ts: new Date().toISOString(),
|
|
8058
|
+
type: "session.stuck",
|
|
8059
|
+
projectId: s.project_id,
|
|
8060
|
+
sessionId: s.id,
|
|
8061
|
+
payload: {
|
|
8062
|
+
kind: stall2.kind,
|
|
8063
|
+
reason: stall2.reason,
|
|
8064
|
+
summary: `session looks stuck \u2014 ${stall2.reason}`
|
|
8065
|
+
}
|
|
8066
|
+
});
|
|
8067
|
+
this.touch();
|
|
8068
|
+
}
|
|
8069
|
+
return flagged;
|
|
8070
|
+
}
|
|
6870
8071
|
sweepOrphans() {
|
|
6871
8072
|
const now = Date.now();
|
|
6872
8073
|
let n = 0;
|
|
@@ -6874,7 +8075,7 @@ ${err}
|
|
|
6874
8075
|
for (const c of this.claimRows(p.id)) {
|
|
6875
8076
|
if (c.state !== "held" || isActive(c, now))
|
|
6876
8077
|
continue;
|
|
6877
|
-
const exists = c.worktree ?
|
|
8078
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6878
8079
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6879
8080
|
if (reapAction(c, now, exists, work) !== "keep-orphaned")
|
|
6880
8081
|
continue;
|
|
@@ -6930,7 +8131,7 @@ ${err}
|
|
|
6930
8131
|
if (!row)
|
|
6931
8132
|
return { ok: false, error: `no claim on ${task}` };
|
|
6932
8133
|
const worktree2 = row.worktree ?? "";
|
|
6933
|
-
if (worktree2 &&
|
|
8134
|
+
if (worktree2 && existsSync6(worktree2)) {
|
|
6934
8135
|
const work = heldWork(worktree2);
|
|
6935
8136
|
const can = canRelease(work, force);
|
|
6936
8137
|
if (!can.ok)
|
|
@@ -6965,7 +8166,7 @@ ${err}
|
|
|
6965
8166
|
continue;
|
|
6966
8167
|
if (isActive({ ...c, state: "held" }, now))
|
|
6967
8168
|
continue;
|
|
6968
|
-
const exists = c.worktree ?
|
|
8169
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6969
8170
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6970
8171
|
const action = reapAction({ ...c, state: "held" }, now, exists, work);
|
|
6971
8172
|
if (action === "not-expired")
|
|
@@ -7064,9 +8265,9 @@ ${err}
|
|
|
7064
8265
|
if (!slug || slug === "." || slug === "..")
|
|
7065
8266
|
return { ok: false, error: "bad worktree name" };
|
|
7066
8267
|
const path = this.worktreePath(projectId, slug);
|
|
7067
|
-
if (
|
|
8268
|
+
if (existsSync6(path))
|
|
7068
8269
|
return { ok: false, error: `${path} already exists` };
|
|
7069
|
-
mkdirSync4(
|
|
8270
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
7070
8271
|
const br = branch?.trim() || `wt/${slug}`;
|
|
7071
8272
|
const created = worktreeAdd(p.root, path, br, baseRef);
|
|
7072
8273
|
if (!created)
|
|
@@ -7244,6 +8445,7 @@ ${err}
|
|
|
7244
8445
|
lastType: r.last_type,
|
|
7245
8446
|
lastText: r.last_text ?? null,
|
|
7246
8447
|
state,
|
|
8448
|
+
stuck: state === "active" || state === "waiting" ? this.stalls.get(r.id)?.reason ?? null : null,
|
|
7247
8449
|
toolCalls: r.tool_calls,
|
|
7248
8450
|
subagents: r.subagents,
|
|
7249
8451
|
turns: r.turns,
|
|
@@ -7308,8 +8510,120 @@ ${err}
|
|
|
7308
8510
|
fn(p.id, b.status);
|
|
7309
8511
|
this.touch();
|
|
7310
8512
|
}
|
|
8513
|
+
for (const b of this.teamBudgets()) {
|
|
8514
|
+
if (b.level === "ok")
|
|
8515
|
+
continue;
|
|
8516
|
+
const mapKey = `team:${b.scope}:${b.key}`;
|
|
8517
|
+
const seen = `${day}:${b.level}`;
|
|
8518
|
+
const affected = b.scope === "project" ? this.projects().filter((p) => this.clusterKeyFor(p.id) === b.key) : this.projects();
|
|
8519
|
+
if (this.budgetNotified.get(mapKey) !== seen) {
|
|
8520
|
+
this.budgetNotified.set(mapKey, seen);
|
|
8521
|
+
const label = b.scope === "org" ? "the org" : `${b.scope} ${b.key}`;
|
|
8522
|
+
this.append({
|
|
8523
|
+
ts: new Date().toISOString(),
|
|
8524
|
+
type: "incident.opened",
|
|
8525
|
+
projectId: affected[0]?.id ?? "p_unknown",
|
|
8526
|
+
sessionId: null,
|
|
8527
|
+
payload: {
|
|
8528
|
+
rule: "budget",
|
|
8529
|
+
action: b.level === "exceeded" ? b.on_exceed : "warn",
|
|
8530
|
+
command: `team ${b.kind ?? ""} budget \xB7 ${label}`,
|
|
8531
|
+
reason: b.level === "exceeded" ? `${label} spent $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling set on the team daemon. ${b.on_exceed === "stop" ? "Spawned runs were stopped." : b.on_exceed === "ask" ? "Every Bash/Edit/Write now asks first." : "An admin can raise it via POST /t1/budgets."}` : `${label} is at $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling \u2014 approaching the team's limit`
|
|
8532
|
+
}
|
|
8533
|
+
});
|
|
8534
|
+
this.touch();
|
|
8535
|
+
}
|
|
8536
|
+
if (b.level === "exceeded" && b.on_exceed === "stop")
|
|
8537
|
+
for (const p of affected)
|
|
8538
|
+
for (const fn of this.budgetListeners)
|
|
8539
|
+
fn(p.id, {
|
|
8540
|
+
level: "exceeded",
|
|
8541
|
+
kind: b.kind === "daily" ? "daily" : "weekly",
|
|
8542
|
+
spent: b.spent,
|
|
8543
|
+
limit: b.limit,
|
|
8544
|
+
pct: b.limit ? b.spent / b.limit : 1,
|
|
8545
|
+
daily: { spent: b.spent, limit: b.limit, pct: 1 },
|
|
8546
|
+
weekly: { spent: 0, limit: null, pct: 0 }
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
7311
8549
|
return out;
|
|
7312
8550
|
}
|
|
8551
|
+
teamBudgets() {
|
|
8552
|
+
try {
|
|
8553
|
+
return JSON.parse(this.metaValue("team_budget") ?? "[]");
|
|
8554
|
+
} catch {
|
|
8555
|
+
return [];
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
backupTo(destDir) {
|
|
8559
|
+
mkdirSync4(destDir, { recursive: true });
|
|
8560
|
+
const files = [];
|
|
8561
|
+
const dbDest = join8(destDir, "swarm.db");
|
|
8562
|
+
if (existsSync6(dbDest))
|
|
8563
|
+
unlinkSync(dbDest);
|
|
8564
|
+
this.db.exec(`VACUUM INTO '${dbDest.replaceAll("'", "''")}'`);
|
|
8565
|
+
files.push("swarm.db");
|
|
8566
|
+
for (const f of [
|
|
8567
|
+
"config.toml",
|
|
8568
|
+
"policy.toml",
|
|
8569
|
+
"policy.sig.json",
|
|
8570
|
+
"token",
|
|
8571
|
+
"pricing.json",
|
|
8572
|
+
"pricing.litellm.json",
|
|
8573
|
+
"team-token"
|
|
8574
|
+
]) {
|
|
8575
|
+
const src = join8(this.home, f);
|
|
8576
|
+
if (!existsSync6(src))
|
|
8577
|
+
continue;
|
|
8578
|
+
copyFileSync(src, join8(destDir, f));
|
|
8579
|
+
files.push(f);
|
|
8580
|
+
}
|
|
8581
|
+
return { dest: destDir, files };
|
|
8582
|
+
}
|
|
8583
|
+
clusterKeyCache = new Map;
|
|
8584
|
+
clusterKeyFor(projectId) {
|
|
8585
|
+
const hit = this.clusterKeyCache.get(projectId);
|
|
8586
|
+
if (hit)
|
|
8587
|
+
return hit;
|
|
8588
|
+
const root = this.db.query("SELECT root FROM projects WHERE id = ?").get(projectId)?.root;
|
|
8589
|
+
const key = root && clusterProjectKey(originUrl(root)) || `local:${projectId}`;
|
|
8590
|
+
this.clusterKeyCache.set(projectId, key);
|
|
8591
|
+
return key;
|
|
8592
|
+
}
|
|
8593
|
+
taskSpendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
8594
|
+
return this.db.query(`SELECT s.project_id AS projectId, c.task AS task, SUM(t.cost_usd) AS cost
|
|
8595
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
8596
|
+
JOIN claims c ON c.project_id = s.project_id AND c.worktree != '' AND (s.cwd = c.worktree OR s.cwd LIKE c.worktree || '/%')
|
|
8597
|
+
WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, c.task`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
|
|
8598
|
+
}
|
|
8599
|
+
modelFlagged = new Set;
|
|
8600
|
+
checkModels() {
|
|
8601
|
+
const since = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8602
|
+
const rows = this.db.query("SELECT id, project_id, model FROM sessions WHERE model IS NOT NULL AND model != '' AND state != 'ended' AND last_seen_at > ?").all(since);
|
|
8603
|
+
let n = 0;
|
|
8604
|
+
for (const s of rows) {
|
|
8605
|
+
if (this.modelFlagged.has(s.id))
|
|
8606
|
+
continue;
|
|
8607
|
+
const allow = this.config(s.project_id).models.allow;
|
|
8608
|
+
if (!allow.length || modelAllowed(s.model, allow))
|
|
8609
|
+
continue;
|
|
8610
|
+
this.modelFlagged.add(s.id);
|
|
8611
|
+
n++;
|
|
8612
|
+
this.append({
|
|
8613
|
+
ts: new Date().toISOString(),
|
|
8614
|
+
type: "incident.opened",
|
|
8615
|
+
projectId: s.project_id,
|
|
8616
|
+
sessionId: s.id,
|
|
8617
|
+
payload: {
|
|
8618
|
+
rule: "model_allowlist",
|
|
8619
|
+
action: "observed",
|
|
8620
|
+
command: s.model,
|
|
8621
|
+
reason: `session runs on "${s.model}", outside [models] allow (${allow.join(", ")}) \u2014 nothing was interrupted; spawned runs on this model are refused`
|
|
8622
|
+
}
|
|
8623
|
+
});
|
|
8624
|
+
}
|
|
8625
|
+
return n;
|
|
8626
|
+
}
|
|
7313
8627
|
spend() {
|
|
7314
8628
|
const dayStart = new Date;
|
|
7315
8629
|
dayStart.setHours(0, 0, 0, 0);
|
|
@@ -7751,6 +9065,48 @@ ${err}
|
|
|
7751
9065
|
seq() {
|
|
7752
9066
|
return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
|
|
7753
9067
|
}
|
|
9068
|
+
timelineDetail(hours, projectId) {
|
|
9069
|
+
const from = new Date(Date.now() - Math.min(Math.max(hours, 1), 168) * 3600000).toISOString();
|
|
9070
|
+
const args = [from];
|
|
9071
|
+
let filter = "";
|
|
9072
|
+
if (projectId) {
|
|
9073
|
+
filter = " AND s.project_id = ?";
|
|
9074
|
+
args.push(projectId);
|
|
9075
|
+
}
|
|
9076
|
+
const rows = this.db.query(`SELECT t.session_id AS sid, t.ts FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
9077
|
+
WHERE t.ts >= ? AND t.sidechain = 0${filter} ORDER BY t.ts LIMIT 20000`).all(...args);
|
|
9078
|
+
const turns = {};
|
|
9079
|
+
for (const r of rows) {
|
|
9080
|
+
turns[r.sid] ??= [];
|
|
9081
|
+
turns[r.sid]?.push(new Date(r.ts).getTime());
|
|
9082
|
+
}
|
|
9083
|
+
const claims = this.claims().filter((c) => (!projectId || c.projectId === projectId) && c.state !== "released").map((c) => ({
|
|
9084
|
+
projectId: c.projectId,
|
|
9085
|
+
task: c.task,
|
|
9086
|
+
owner: c.owner,
|
|
9087
|
+
state: c.state,
|
|
9088
|
+
acquiredAt: c.acquiredAt,
|
|
9089
|
+
expiresAt: c.expiresAt
|
|
9090
|
+
}));
|
|
9091
|
+
return { turns, claims };
|
|
9092
|
+
}
|
|
9093
|
+
spendSparks() {
|
|
9094
|
+
const from = localDayIso(-13);
|
|
9095
|
+
const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
|
|
9096
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
9097
|
+
const days2 = [];
|
|
9098
|
+
for (let i = 13;i >= 0; i--)
|
|
9099
|
+
days2.push(localDayIso(-i).slice(0, 10));
|
|
9100
|
+
const out = {};
|
|
9101
|
+
for (const r of rows) {
|
|
9102
|
+
out[r.pid] ??= new Array(14).fill(0);
|
|
9103
|
+
const arr = out[r.pid];
|
|
9104
|
+
const i = days2.indexOf(r.day);
|
|
9105
|
+
if (i >= 0)
|
|
9106
|
+
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
9107
|
+
}
|
|
9108
|
+
return out;
|
|
9109
|
+
}
|
|
7754
9110
|
snapshot() {
|
|
7755
9111
|
const worktrees = {};
|
|
7756
9112
|
const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
|
|
@@ -7761,6 +9117,7 @@ ${err}
|
|
|
7761
9117
|
worktrees,
|
|
7762
9118
|
sessions: this.memoised("sessions", 2000, () => this.sessions()),
|
|
7763
9119
|
spend: this.memoised("spend", 30000, () => this.spend()),
|
|
9120
|
+
spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
|
|
7764
9121
|
claims: this.claims(),
|
|
7765
9122
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
7766
9123
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
@@ -7858,15 +9215,428 @@ function isScratchRoot(root) {
|
|
|
7858
9215
|
const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
|
|
7859
9216
|
return tmp.some((t) => root === t || root.startsWith(`${t}/`));
|
|
7860
9217
|
}
|
|
9218
|
+
function rowToMessage(r) {
|
|
9219
|
+
return {
|
|
9220
|
+
id: r.id,
|
|
9221
|
+
projectId: r.project_id,
|
|
9222
|
+
task: r.task ?? null,
|
|
9223
|
+
sessionId: r.session_id ?? null,
|
|
9224
|
+
toKind: r.to_kind ?? "session",
|
|
9225
|
+
from: r.asked_by ?? null,
|
|
9226
|
+
fromSession: r.from_session ?? null,
|
|
9227
|
+
text: r.text,
|
|
9228
|
+
createdAt: r.created_at,
|
|
9229
|
+
deliveredAt: r.delivered_at ?? null
|
|
9230
|
+
};
|
|
9231
|
+
}
|
|
9232
|
+
function rowToWorkflowRun(r) {
|
|
9233
|
+
return {
|
|
9234
|
+
id: r.id,
|
|
9235
|
+
projectId: r.project_id,
|
|
9236
|
+
task: r.task,
|
|
9237
|
+
workflow: r.workflow,
|
|
9238
|
+
step: r.step,
|
|
9239
|
+
stepLabel: r.step_label ?? "",
|
|
9240
|
+
steps: JSON.parse(r.steps ?? "[]"),
|
|
9241
|
+
state: r.state,
|
|
9242
|
+
detail: r.detail ?? null,
|
|
9243
|
+
runId: r.run_id ?? null,
|
|
9244
|
+
startedAt: r.started_at,
|
|
9245
|
+
updatedAt: r.updated_at,
|
|
9246
|
+
endedAt: r.ended_at ?? null
|
|
9247
|
+
};
|
|
9248
|
+
}
|
|
9249
|
+
function localDayIso(offsetDays) {
|
|
9250
|
+
const d = new Date;
|
|
9251
|
+
d.setHours(0, 0, 0, 0);
|
|
9252
|
+
d.setDate(d.getDate() + offsetDays);
|
|
9253
|
+
return d.toISOString();
|
|
9254
|
+
}
|
|
9255
|
+
|
|
9256
|
+
// packages/daemon/src/team.ts
|
|
9257
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
9258
|
+
import { join as join9 } from "path";
|
|
9259
|
+
var SPEND_EVERY_MS = 60000;
|
|
9260
|
+
var MAX_BACKOFF_MS = 300000;
|
|
9261
|
+
var POLICY_EVERY_MS = 300000;
|
|
9262
|
+
|
|
9263
|
+
class TeamForwarder {
|
|
9264
|
+
store;
|
|
9265
|
+
version;
|
|
9266
|
+
lastTry = 0;
|
|
9267
|
+
backoffMs = 0;
|
|
9268
|
+
lastSpend = 0;
|
|
9269
|
+
constructor(store, version) {
|
|
9270
|
+
this.store = store;
|
|
9271
|
+
this.version = version;
|
|
9272
|
+
}
|
|
9273
|
+
projectKey(projectId) {
|
|
9274
|
+
return this.store.clusterKeyFor(projectId);
|
|
9275
|
+
}
|
|
9276
|
+
status() {
|
|
9277
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
9278
|
+
const box = this.store.outboxStatus();
|
|
9279
|
+
return {
|
|
9280
|
+
configured: team2.url != null,
|
|
9281
|
+
url: team2.url,
|
|
9282
|
+
forward: team2.forward,
|
|
9283
|
+
pending: box.pending,
|
|
9284
|
+
oldest: box.oldest,
|
|
9285
|
+
lastAckAt: this.store.metaValue("team_last_ack") ?? null,
|
|
9286
|
+
lastError: this.store.metaValue("team_last_error") ?? null,
|
|
9287
|
+
machine: this.store.machineIdentity(),
|
|
9288
|
+
authed: this.store.metaValue("team_machine_token") != null
|
|
9289
|
+
};
|
|
9290
|
+
}
|
|
9291
|
+
async tick(now = Date.now()) {
|
|
9292
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
9293
|
+
if (!team2.url)
|
|
9294
|
+
return 0;
|
|
9295
|
+
if (now - this.lastTry < team2.interval * 1000 + this.backoffMs)
|
|
9296
|
+
return 0;
|
|
9297
|
+
this.lastTry = now;
|
|
9298
|
+
const records = [];
|
|
9299
|
+
const events = team2.forward.includes("ledger") ? this.store.outboxPending() : [];
|
|
9300
|
+
for (const e of events) {
|
|
9301
|
+
const body = JSON.parse(e.payload);
|
|
9302
|
+
if (typeof body.projectId === "string")
|
|
9303
|
+
body.projectKey = this.projectKey(body.projectId);
|
|
9304
|
+
records.push({ seq: e.seq, kind: e.kind, body });
|
|
9305
|
+
}
|
|
9306
|
+
let spendRows = 0;
|
|
9307
|
+
if (team2.forward.includes("cost") && now - this.lastSpend > SPEND_EVERY_MS) {
|
|
9308
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
9309
|
+
for (const r of this.store.spendRollup(day)) {
|
|
9310
|
+
records.push({
|
|
9311
|
+
seq: 0,
|
|
9312
|
+
kind: "spend",
|
|
9313
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
9314
|
+
});
|
|
9315
|
+
spendRows++;
|
|
9316
|
+
}
|
|
9317
|
+
for (const r of this.store.taskSpendRollup(day)) {
|
|
9318
|
+
records.push({
|
|
9319
|
+
seq: 0,
|
|
9320
|
+
kind: "spend_task",
|
|
9321
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
9322
|
+
});
|
|
9323
|
+
spendRows++;
|
|
9324
|
+
}
|
|
9325
|
+
}
|
|
9326
|
+
const held = this.store.heldClaimsForSync();
|
|
9327
|
+
if (!records.length && !held.length)
|
|
9328
|
+
return 0;
|
|
9329
|
+
const machine = { ...this.store.machineIdentity(), version: this.version };
|
|
9330
|
+
const token = this.store.metaValue("team_machine_token");
|
|
9331
|
+
const headers = {
|
|
9332
|
+
"content-type": "application/json",
|
|
9333
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
9334
|
+
};
|
|
9335
|
+
const base = this.store.policyFor(null).config.team.url;
|
|
9336
|
+
try {
|
|
9337
|
+
if (records.length) {
|
|
9338
|
+
const req = { machine, records };
|
|
9339
|
+
const res = await fetch(`${base}/t1/ingest`, {
|
|
9340
|
+
method: "POST",
|
|
9341
|
+
headers,
|
|
9342
|
+
body: JSON.stringify(req),
|
|
9343
|
+
signal: AbortSignal.timeout(1e4)
|
|
9344
|
+
});
|
|
9345
|
+
if (!res.ok)
|
|
9346
|
+
throw new Error(`ingest ${res.status}`);
|
|
9347
|
+
const reply = await res.json();
|
|
9348
|
+
if (reply.ack > 0)
|
|
9349
|
+
this.store.outboxAck(reply.ack);
|
|
9350
|
+
if (spendRows)
|
|
9351
|
+
this.lastSpend = now;
|
|
9352
|
+
}
|
|
9353
|
+
if (held.length) {
|
|
9354
|
+
const claims = held.map((c) => ({
|
|
9355
|
+
projectKey: this.projectKey(c.projectId),
|
|
9356
|
+
task: c.task,
|
|
9357
|
+
acquiredAt: c.acquiredAt,
|
|
9358
|
+
expiresAt: c.expiresAt,
|
|
9359
|
+
actor: c.actorKind && c.actorId ? { kind: c.actorKind, id: c.actorId } : undefined
|
|
9360
|
+
}));
|
|
9361
|
+
const res = await fetch(`${base}/t1/claims`, {
|
|
9362
|
+
method: "POST",
|
|
9363
|
+
headers,
|
|
9364
|
+
body: JSON.stringify({ machine, claims }),
|
|
9365
|
+
signal: AbortSignal.timeout(1e4)
|
|
9366
|
+
});
|
|
9367
|
+
if (!res.ok)
|
|
9368
|
+
throw new Error(`claims ${res.status}`);
|
|
9369
|
+
const reply = await res.json();
|
|
9370
|
+
for (const r of reply.results) {
|
|
9371
|
+
const local = held.find((c) => c.task === r.task && this.projectKey(c.projectId) === r.projectKey);
|
|
9372
|
+
if (!local)
|
|
9373
|
+
continue;
|
|
9374
|
+
if (r.status === "ok") {
|
|
9375
|
+
if (local.teamState !== "registered")
|
|
9376
|
+
this.store.markClaimTeamState(local.projectId, local.task, "registered");
|
|
9377
|
+
} else
|
|
9378
|
+
this.store.revokeClaimConflict(local.projectId, local.task, r.holder);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
try {
|
|
9382
|
+
const res = await fetch(`${base}/t1/budget`, {
|
|
9383
|
+
headers,
|
|
9384
|
+
signal: AbortSignal.timeout(1e4)
|
|
9385
|
+
});
|
|
9386
|
+
if (res.ok) {
|
|
9387
|
+
const { budgets } = await res.json();
|
|
9388
|
+
this.store.setMetaValue("team_budget", JSON.stringify(budgets ?? []));
|
|
9389
|
+
}
|
|
9390
|
+
} catch {}
|
|
9391
|
+
this.backoffMs = 0;
|
|
9392
|
+
this.store.setMetaValue("team_last_ack", new Date(now).toISOString());
|
|
9393
|
+
this.store.setMetaValue("team_last_error", "");
|
|
9394
|
+
await this.syncPolicy(base, headers, now);
|
|
9395
|
+
return records.length + held.length;
|
|
9396
|
+
} catch (e) {
|
|
9397
|
+
this.backoffMs = Math.min(this.backoffMs ? this.backoffMs * 2 : 5000, MAX_BACKOFF_MS);
|
|
9398
|
+
this.store.setMetaValue("team_last_error", e.message);
|
|
9399
|
+
return 0;
|
|
9400
|
+
}
|
|
9401
|
+
}
|
|
9402
|
+
lastPolicy = 0;
|
|
9403
|
+
async syncPolicy(base, headers, now) {
|
|
9404
|
+
if (now - this.lastPolicy < POLICY_EVERY_MS)
|
|
9405
|
+
return;
|
|
9406
|
+
this.lastPolicy = now;
|
|
9407
|
+
try {
|
|
9408
|
+
const res = await fetch(`${base}/t1/policy`, {
|
|
9409
|
+
headers,
|
|
9410
|
+
signal: AbortSignal.timeout(1e4)
|
|
9411
|
+
});
|
|
9412
|
+
if (!res.ok)
|
|
9413
|
+
return;
|
|
9414
|
+
const { policy: policy2 } = await res.json();
|
|
9415
|
+
if (!policy2)
|
|
9416
|
+
return;
|
|
9417
|
+
let pinned = this.store.metaValue("team_policy_pubkey");
|
|
9418
|
+
if (!pinned) {
|
|
9419
|
+
pinned = policy2.publicKey;
|
|
9420
|
+
this.store.setMetaValue("team_policy_pubkey", pinned);
|
|
9421
|
+
}
|
|
9422
|
+
if (!verifyPolicySignature(policy2.toml, policy2.signature, pinned)) {
|
|
9423
|
+
this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
|
|
9424
|
+
return;
|
|
9425
|
+
}
|
|
9426
|
+
const file = join9(this.store.home, "policy.toml");
|
|
9427
|
+
const prev = this.store.metaValue("team_policy_sig");
|
|
9428
|
+
if (prev === policy2.signature)
|
|
9429
|
+
return;
|
|
9430
|
+
writeFileSync3(file, policy2.toml, { mode: 384 });
|
|
9431
|
+
writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
|
|
9432
|
+
signature: policy2.signature,
|
|
9433
|
+
publicKey: pinned,
|
|
9434
|
+
fetchedAt: new Date(now).toISOString(),
|
|
9435
|
+
url: base
|
|
9436
|
+
}), { mode: 384 });
|
|
9437
|
+
this.store.setMetaValue("team_policy_sig", policy2.signature);
|
|
9438
|
+
} catch {}
|
|
9439
|
+
}
|
|
9440
|
+
}
|
|
9441
|
+
|
|
9442
|
+
// packages/daemon/src/workflow.ts
|
|
9443
|
+
class WorkflowEngine {
|
|
9444
|
+
store;
|
|
9445
|
+
runner;
|
|
9446
|
+
forge;
|
|
9447
|
+
active = new Map;
|
|
9448
|
+
constructor(store, runner, forge2) {
|
|
9449
|
+
this.store = store;
|
|
9450
|
+
this.runner = runner;
|
|
9451
|
+
this.forge = forge2;
|
|
9452
|
+
store.wfSweepOrphans();
|
|
9453
|
+
runner.onEnd((run2) => void this.onRunEnd(run2));
|
|
9454
|
+
}
|
|
9455
|
+
start(projectId, task, workflow, opts = {}) {
|
|
9456
|
+
const def = this.store.config(projectId).workflows[workflow];
|
|
9457
|
+
if (!def) {
|
|
9458
|
+
const known = Object.keys(this.store.config(projectId).workflows);
|
|
9459
|
+
return {
|
|
9460
|
+
ok: false,
|
|
9461
|
+
error: `unknown workflow ${workflow}${known.length ? ` \u2014 this repo declares: ${known.join(", ")}` : " \u2014 declare [[workflows]] in .swarm.toml"}`
|
|
9462
|
+
};
|
|
9463
|
+
}
|
|
9464
|
+
const key = `${projectId}:${task}`;
|
|
9465
|
+
if (this.active.has(key) || this.store.wfActive(projectId, task))
|
|
9466
|
+
return { ok: false, error: `a workflow is already running on ${task}` };
|
|
9467
|
+
const title = this.store.tasks(projectId)?.tasks.find((t) => t.id === task)?.title ?? task;
|
|
9468
|
+
const owner = opts.owner ?? "workflow";
|
|
9469
|
+
const id = this.store.wfInsert(projectId, task, workflow, def.steps.map(stepLabel), this.store.actorFor(owner, opts.sessionId ?? null));
|
|
9470
|
+
const w = { id, projectId, task, title, def, step: 0, runId: null, owner };
|
|
9471
|
+
this.active.set(key, w);
|
|
9472
|
+
this.store.append({
|
|
9473
|
+
ts: new Date().toISOString(),
|
|
9474
|
+
type: "workflow.started",
|
|
9475
|
+
projectId,
|
|
9476
|
+
sessionId: opts.sessionId ?? null,
|
|
9477
|
+
payload: {
|
|
9478
|
+
id,
|
|
9479
|
+
task,
|
|
9480
|
+
workflow,
|
|
9481
|
+
steps: def.steps.map(stepLabel),
|
|
9482
|
+
summary: `workflow ${workflow} on ${task}: ${def.steps.map(stepLabel).join(" \u2192 ")}`
|
|
9483
|
+
}
|
|
9484
|
+
});
|
|
9485
|
+
this.advance(w);
|
|
9486
|
+
return { ok: true, id };
|
|
9487
|
+
}
|
|
9488
|
+
status(projectId) {
|
|
9489
|
+
return this.store.wfRuns(projectId);
|
|
9490
|
+
}
|
|
9491
|
+
stop(projectId, task) {
|
|
9492
|
+
const key = `${projectId}:${task}`;
|
|
9493
|
+
const w = this.active.get(key);
|
|
9494
|
+
if (!w)
|
|
9495
|
+
return { ok: false, error: `no running workflow on ${task}` };
|
|
9496
|
+
if (w.runId)
|
|
9497
|
+
this.runner.stop(w.runId);
|
|
9498
|
+
this.finish(w, "stopped", `stopped at ${this.label(w)}`);
|
|
9499
|
+
return { ok: true };
|
|
9500
|
+
}
|
|
9501
|
+
label(w) {
|
|
9502
|
+
const s = w.def.steps[w.step];
|
|
9503
|
+
return s ? stepLabel(s) : "done";
|
|
9504
|
+
}
|
|
9505
|
+
async advance(w) {
|
|
9506
|
+
while (w.step < w.def.steps.length) {
|
|
9507
|
+
const s = w.def.steps[w.step];
|
|
9508
|
+
this.store.wfUpdate(w.id, { step: w.step, stepLabel: stepLabel(s), runId: null });
|
|
9509
|
+
this.step(w, `step ${w.step + 1}/${w.def.steps.length}: ${stepLabel(s)}`);
|
|
9510
|
+
if (s.kind === "run") {
|
|
9511
|
+
const cfg = this.store.config(w.projectId).dispatch;
|
|
9512
|
+
const remaining = w.def.steps.slice(w.step + 1).map(stepLabel);
|
|
9513
|
+
const r = await this.runner.start({
|
|
9514
|
+
projectId: w.projectId,
|
|
9515
|
+
task: w.task,
|
|
9516
|
+
prompt: workflowStepPrompt(s, { id: w.task, title: w.title }, { workflow: w.def.name, remaining }),
|
|
9517
|
+
owner: w.owner,
|
|
9518
|
+
permissionMode: cfg.permission_mode ?? "acceptEdits",
|
|
9519
|
+
model: cfg.model ?? undefined,
|
|
9520
|
+
maxTurns: cfg.max_turns ?? undefined,
|
|
9521
|
+
profile: cfg.profile ?? undefined
|
|
9522
|
+
});
|
|
9523
|
+
if (!r.ok)
|
|
9524
|
+
return this.fail(w, `could not start ${stepLabel(s)}: ${r.reason}`);
|
|
9525
|
+
w.runId = r.run.id;
|
|
9526
|
+
this.store.wfUpdate(w.id, { runId: r.run.id });
|
|
9527
|
+
return;
|
|
9528
|
+
}
|
|
9529
|
+
if (s.kind === "gate") {
|
|
9530
|
+
const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
|
|
9531
|
+
const run2 = r.runs.find((x) => x.gate === s.gate);
|
|
9532
|
+
if (!run2)
|
|
9533
|
+
return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
|
|
9534
|
+
if (run2.verdict !== "pass")
|
|
9535
|
+
return this.fail(w, `gate ${s.gate} failed \u2014 ${run2.rubric}`);
|
|
9536
|
+
w.step++;
|
|
9537
|
+
continue;
|
|
9538
|
+
}
|
|
9539
|
+
const d = await this.store.prDraftFor(w.projectId, w.task);
|
|
9540
|
+
if (!d.ok)
|
|
9541
|
+
return this.fail(w, `pr: ${d.error}`);
|
|
9542
|
+
const pr = await this.forge.openPR(w.projectId, d.worktree, {
|
|
9543
|
+
title: d.title,
|
|
9544
|
+
body: d.body,
|
|
9545
|
+
isDraft: false
|
|
9546
|
+
});
|
|
9547
|
+
if (!pr.ok)
|
|
9548
|
+
return this.fail(w, `pr: ${pr.error}`);
|
|
9549
|
+
this.store.recordPrOpened(w.projectId, d.task, d.worktree.path, pr.url);
|
|
9550
|
+
this.store.wfUpdate(w.id, { detail: `PR ${pr.url}` });
|
|
9551
|
+
w.step++;
|
|
9552
|
+
}
|
|
9553
|
+
this.finish(w, "done", null);
|
|
9554
|
+
}
|
|
9555
|
+
async onRunEnd(run2) {
|
|
9556
|
+
const w = this.active.get(`${run2.projectId}:${run2.task}`);
|
|
9557
|
+
if (!w || w.runId !== run2.id)
|
|
9558
|
+
return;
|
|
9559
|
+
w.runId = null;
|
|
9560
|
+
if (run2.stopped)
|
|
9561
|
+
return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
|
|
9562
|
+
if (run2.exitCode !== 0 || run2.result?.isError)
|
|
9563
|
+
return this.fail(w, `${this.label(w)} exited ${run2.exitCode}${run2.result?.isError ? " (error)" : ""} \u2014 log: ${run2.log}`);
|
|
9564
|
+
w.step++;
|
|
9565
|
+
this.advance(w);
|
|
9566
|
+
}
|
|
9567
|
+
step(w, summary) {
|
|
9568
|
+
this.store.append({
|
|
9569
|
+
ts: new Date().toISOString(),
|
|
9570
|
+
type: "workflow.step",
|
|
9571
|
+
projectId: w.projectId,
|
|
9572
|
+
sessionId: null,
|
|
9573
|
+
payload: {
|
|
9574
|
+
id: w.id,
|
|
9575
|
+
task: w.task,
|
|
9576
|
+
workflow: w.def.name,
|
|
9577
|
+
step: w.step,
|
|
9578
|
+
label: this.label(w),
|
|
9579
|
+
summary: `workflow ${w.def.name} on ${w.task} \u2014 ${summary}`
|
|
9580
|
+
}
|
|
9581
|
+
});
|
|
9582
|
+
}
|
|
9583
|
+
fail(w, detail) {
|
|
9584
|
+
this.store.wfUpdate(w.id, { state: "failed", detail, ended: true });
|
|
9585
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
9586
|
+
this.store.append({
|
|
9587
|
+
ts: new Date().toISOString(),
|
|
9588
|
+
type: "workflow.finished",
|
|
9589
|
+
projectId: w.projectId,
|
|
9590
|
+
sessionId: null,
|
|
9591
|
+
payload: {
|
|
9592
|
+
id: w.id,
|
|
9593
|
+
task: w.task,
|
|
9594
|
+
workflow: w.def.name,
|
|
9595
|
+
outcome: "failed",
|
|
9596
|
+
detail,
|
|
9597
|
+
summary: `workflow ${w.def.name} on ${w.task} failed at ${this.label(w)}: ${detail.slice(0, 160)}`
|
|
9598
|
+
}
|
|
9599
|
+
});
|
|
9600
|
+
this.store.append({
|
|
9601
|
+
ts: new Date().toISOString(),
|
|
9602
|
+
type: "incident.opened",
|
|
9603
|
+
projectId: w.projectId,
|
|
9604
|
+
sessionId: null,
|
|
9605
|
+
payload: {
|
|
9606
|
+
rule: "workflow_failed",
|
|
9607
|
+
action: "failed",
|
|
9608
|
+
command: `${w.task} \xB7 ${w.def.name} \xB7 ${this.label(w)}`,
|
|
9609
|
+
reason: detail.slice(0, 400)
|
|
9610
|
+
}
|
|
9611
|
+
});
|
|
9612
|
+
}
|
|
9613
|
+
finish(w, state, detail) {
|
|
9614
|
+
this.store.wfUpdate(w.id, { state, ...detail !== null ? { detail } : {}, ended: true });
|
|
9615
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
9616
|
+
this.store.append({
|
|
9617
|
+
ts: new Date().toISOString(),
|
|
9618
|
+
type: "workflow.finished",
|
|
9619
|
+
projectId: w.projectId,
|
|
9620
|
+
sessionId: null,
|
|
9621
|
+
payload: {
|
|
9622
|
+
id: w.id,
|
|
9623
|
+
task: w.task,
|
|
9624
|
+
workflow: w.def.name,
|
|
9625
|
+
outcome: state,
|
|
9626
|
+
summary: `workflow ${w.def.name} on ${w.task}: ${state}${detail ? ` \u2014 ${detail}` : ""}`
|
|
9627
|
+
}
|
|
9628
|
+
});
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
7861
9631
|
|
|
7862
9632
|
// packages/daemon/src/app.ts
|
|
7863
|
-
var VERSION = "0.
|
|
9633
|
+
var VERSION = "0.10.0";
|
|
7864
9634
|
var WEB_DIR = (() => {
|
|
7865
9635
|
if (process.env.SWARM_WEB_DIR)
|
|
7866
9636
|
return process.env.SWARM_WEB_DIR;
|
|
7867
|
-
const here =
|
|
7868
|
-
const dev =
|
|
7869
|
-
return
|
|
9637
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
9638
|
+
const dev = join10(here, "../../web/public");
|
|
9639
|
+
return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
|
|
7870
9640
|
})();
|
|
7871
9641
|
var REPLAY_TAIL = 200;
|
|
7872
9642
|
var wireCache = new WeakMap;
|
|
@@ -7880,13 +9650,40 @@ function wireJson(e) {
|
|
|
7880
9650
|
}
|
|
7881
9651
|
function hookRepoRoot(store, raw2) {
|
|
7882
9652
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
|
|
7883
|
-
return cwd &&
|
|
9653
|
+
return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
|
|
9654
|
+
}
|
|
9655
|
+
function claudeSettings() {
|
|
9656
|
+
try {
|
|
9657
|
+
const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
|
|
9658
|
+
return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
|
|
9659
|
+
} catch {
|
|
9660
|
+
return null;
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
function diskVersion() {
|
|
9664
|
+
try {
|
|
9665
|
+
const entry = daemonCommand().at(-1);
|
|
9666
|
+
if (!entry || !existsSync7(entry))
|
|
9667
|
+
return null;
|
|
9668
|
+
for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
|
|
9669
|
+
if (!existsSync7(f))
|
|
9670
|
+
continue;
|
|
9671
|
+
const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
|
|
9672
|
+
if (m?.[1])
|
|
9673
|
+
return m[1];
|
|
9674
|
+
}
|
|
9675
|
+
return null;
|
|
9676
|
+
} catch {
|
|
9677
|
+
return null;
|
|
9678
|
+
}
|
|
7884
9679
|
}
|
|
7885
|
-
function createApp(store = new Store) {
|
|
9680
|
+
function createApp(store = new Store, hooks2 = {}) {
|
|
7886
9681
|
const app = new Hono2;
|
|
7887
9682
|
const forge2 = new ForgeService(store);
|
|
7888
9683
|
const runner = new Runner(store, store.home);
|
|
7889
9684
|
const dispatcher = new Dispatcher(store, runner, forge2);
|
|
9685
|
+
const workflows2 = new WorkflowEngine(store, runner, forge2);
|
|
9686
|
+
const team2 = new TeamForwarder(store, VERSION);
|
|
7890
9687
|
store.onBudgetStop((projectId) => {
|
|
7891
9688
|
dispatcher.clear(projectId);
|
|
7892
9689
|
for (const run2 of runner.list(projectId))
|
|
@@ -7908,6 +9705,8 @@ function createApp(store = new Store) {
|
|
|
7908
9705
|
return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
|
|
7909
9706
|
});
|
|
7910
9707
|
app.get("/v1/health", (c) => c.json({
|
|
9708
|
+
disk: diskVersion(),
|
|
9709
|
+
hooksInstalled: hookCoverage(claudeSettings()).complete,
|
|
7911
9710
|
ok: true,
|
|
7912
9711
|
version: VERSION,
|
|
7913
9712
|
schema: store.schemaVersion(),
|
|
@@ -7942,13 +9741,13 @@ function createApp(store = new Store) {
|
|
|
7942
9741
|
const q = c.req.query("path");
|
|
7943
9742
|
let dir;
|
|
7944
9743
|
try {
|
|
7945
|
-
dir = realpathSync3(q &&
|
|
9744
|
+
dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
|
|
7946
9745
|
} catch {
|
|
7947
9746
|
dir = homedir4();
|
|
7948
9747
|
}
|
|
7949
9748
|
try {
|
|
7950
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo:
|
|
7951
|
-
const parent =
|
|
9749
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join10(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
9750
|
+
const parent = dirname4(dir);
|
|
7952
9751
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
7953
9752
|
} catch (e) {
|
|
7954
9753
|
return c.json({ error: e.message, path: dir }, 400);
|
|
@@ -7956,10 +9755,44 @@ function createApp(store = new Store) {
|
|
|
7956
9755
|
});
|
|
7957
9756
|
app.get("/v1/state", (c) => c.json(store.snapshot()));
|
|
7958
9757
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
9758
|
+
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
7959
9759
|
app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
|
|
7960
9760
|
open: c.req.query("open") === "1",
|
|
7961
9761
|
projectId: c.req.query("project") || undefined
|
|
7962
9762
|
})));
|
|
9763
|
+
app.get("/v1/outcomes", async (c) => {
|
|
9764
|
+
const project = c.req.query("project") || undefined;
|
|
9765
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
9766
|
+
id: s.id,
|
|
9767
|
+
branch: s.branch,
|
|
9768
|
+
model: s.model,
|
|
9769
|
+
agent: s.agent,
|
|
9770
|
+
costUsd: s.costUsd,
|
|
9771
|
+
startedAt: s.startedAt
|
|
9772
|
+
}));
|
|
9773
|
+
const prs = [];
|
|
9774
|
+
const reverted = new Set;
|
|
9775
|
+
for (const p of store.projects().filter((x) => !project || x.id === project)) {
|
|
9776
|
+
const o = await forge2.merged(p.id, p.root);
|
|
9777
|
+
for (const m of o.merged)
|
|
9778
|
+
prs.push({ ...m, state: "merged" });
|
|
9779
|
+
for (const sha of o.reverted)
|
|
9780
|
+
reverted.add(sha);
|
|
9781
|
+
}
|
|
9782
|
+
for (const pr of forge2.prs())
|
|
9783
|
+
if (!project || pr.projectId === project)
|
|
9784
|
+
prs.push({
|
|
9785
|
+
branch: pr.branch,
|
|
9786
|
+
number: pr.number,
|
|
9787
|
+
state: "open",
|
|
9788
|
+
title: pr.title,
|
|
9789
|
+
url: pr.url,
|
|
9790
|
+
createdAt: pr.createdAt,
|
|
9791
|
+
mergedAt: null,
|
|
9792
|
+
mergeSha: null
|
|
9793
|
+
});
|
|
9794
|
+
return c.json(outcomeReport(sessions, prs, reverted));
|
|
9795
|
+
});
|
|
7963
9796
|
app.get("/v1/memory", (c) => {
|
|
7964
9797
|
const q = c.req.query("q") ?? "";
|
|
7965
9798
|
const kind = c.req.query("kind");
|
|
@@ -7973,6 +9806,26 @@ function createApp(store = new Store) {
|
|
|
7973
9806
|
})
|
|
7974
9807
|
});
|
|
7975
9808
|
});
|
|
9809
|
+
app.post("/v1/backup", async (c) => {
|
|
9810
|
+
const b = await c.req.json().catch(() => ({}));
|
|
9811
|
+
if (typeof b.dest !== "string" || !b.dest.startsWith("/"))
|
|
9812
|
+
return c.json({ error: "dest must be an absolute path" }, 400);
|
|
9813
|
+
try {
|
|
9814
|
+
return c.json(store.backupTo(b.dest));
|
|
9815
|
+
} catch (e) {
|
|
9816
|
+
return c.json({ error: e.message }, 500);
|
|
9817
|
+
}
|
|
9818
|
+
});
|
|
9819
|
+
app.get("/v1/team", (c) => c.json(team2.status()));
|
|
9820
|
+
app.post("/v1/team/credentials", async (c) => {
|
|
9821
|
+
const b = await c.req.json().catch(() => ({}));
|
|
9822
|
+
if (typeof b.token !== "string" || !b.token)
|
|
9823
|
+
return c.json({ error: "token required" }, 400);
|
|
9824
|
+
store.setMetaValue("team_machine_token", b.token);
|
|
9825
|
+
if (typeof b.policyPublicKey === "string" && b.policyPublicKey)
|
|
9826
|
+
store.setMetaValue("team_policy_pubkey", b.policyPublicKey);
|
|
9827
|
+
return c.json({ ok: true, machine: store.machineIdentity() });
|
|
9828
|
+
});
|
|
7976
9829
|
app.get("/v1/policy", (c) => {
|
|
7977
9830
|
const id = c.req.query("project");
|
|
7978
9831
|
const p = id ? store.project(id) : null;
|
|
@@ -7998,6 +9851,12 @@ function createApp(store = new Store) {
|
|
|
7998
9851
|
const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
|
|
7999
9852
|
return c.body(formatAudit(rows, format), 200, { "content-type": ct });
|
|
8000
9853
|
});
|
|
9854
|
+
app.post("/v1/daemon/restart", (c) => {
|
|
9855
|
+
if (!hooks2.restart)
|
|
9856
|
+
return c.json({ error: "not restartable in this environment" }, 501);
|
|
9857
|
+
setTimeout(() => hooks2.restart?.(), 50);
|
|
9858
|
+
return c.json({ ok: true, restarting: true });
|
|
9859
|
+
});
|
|
8001
9860
|
app.get("/v1/rules/dryrun", (c) => {
|
|
8002
9861
|
const projectId = c.req.query("project");
|
|
8003
9862
|
if (!projectId)
|
|
@@ -8132,6 +9991,58 @@ function createApp(store = new Store) {
|
|
|
8132
9991
|
return c.json(r, r.ok ? 200 : 409);
|
|
8133
9992
|
});
|
|
8134
9993
|
app.get("/v1/inbox", (c) => c.json(store.inbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
9994
|
+
app.get("/v1/workflows", (c) => {
|
|
9995
|
+
const project = c.req.query("project");
|
|
9996
|
+
if (!project)
|
|
9997
|
+
return c.json({ error: "project required" }, 400);
|
|
9998
|
+
return c.json({ defs: store.config(project).workflows, runs: workflows2.status(project) });
|
|
9999
|
+
});
|
|
10000
|
+
app.post("/v1/workflows", async (c) => {
|
|
10001
|
+
const b = await c.req.json().catch(() => ({}));
|
|
10002
|
+
if (!b.projectId || !b.task || !b.workflow)
|
|
10003
|
+
return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
|
|
10004
|
+
const r = workflows2.start(b.projectId, b.task, b.workflow, {
|
|
10005
|
+
...b.owner ? { owner: b.owner } : {},
|
|
10006
|
+
sessionId: b.sessionId ?? null
|
|
10007
|
+
});
|
|
10008
|
+
return c.json(r, r.ok ? 201 : 409);
|
|
10009
|
+
});
|
|
10010
|
+
app.post("/v1/workflows/stop", async (c) => {
|
|
10011
|
+
const b = await c.req.json().catch(() => ({}));
|
|
10012
|
+
if (!b.projectId || !b.task)
|
|
10013
|
+
return c.json({ ok: false, error: "projectId and task required" }, 400);
|
|
10014
|
+
const r = workflows2.stop(b.projectId, b.task);
|
|
10015
|
+
return c.json(r, r.ok ? 200 : 404);
|
|
10016
|
+
});
|
|
10017
|
+
app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
|
|
10018
|
+
app.get("/v1/messages", (c) => c.json(store.messages({
|
|
10019
|
+
...c.req.query("project") ? { projectId: c.req.query("project") } : {},
|
|
10020
|
+
...c.req.query("session") ? { sessionId: c.req.query("session") } : {},
|
|
10021
|
+
...c.req.query("task") ? { task: c.req.query("task") } : {},
|
|
10022
|
+
limit: Number(c.req.query("limit")) || 100
|
|
10023
|
+
})));
|
|
10024
|
+
app.get("/v1/messages/inbox", (c) => c.json(store.messageInbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
10025
|
+
app.post("/v1/messages", async (c) => {
|
|
10026
|
+
const b = await c.req.json().catch(() => ({}));
|
|
10027
|
+
if (!b.projectId)
|
|
10028
|
+
return c.json({ ok: false, error: "projectId required" }, 400);
|
|
10029
|
+
const r = store.send(b.projectId, {
|
|
10030
|
+
to: b.to,
|
|
10031
|
+
text: b.text,
|
|
10032
|
+
from: b.from ?? null,
|
|
10033
|
+
fromSession: b.sessionId ?? null
|
|
10034
|
+
});
|
|
10035
|
+
if (!r.ok)
|
|
10036
|
+
return c.json(r, 400);
|
|
10037
|
+
const m = r.message;
|
|
10038
|
+
const run2 = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
|
|
10039
|
+
if (run2 && !run2.endedAt) {
|
|
10040
|
+
const sent = runner.send(run2.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
|
|
10041
|
+
if (sent.ok)
|
|
10042
|
+
store.markMessageDelivered(m.id, run2.sessionId);
|
|
10043
|
+
}
|
|
10044
|
+
return c.json({ ok: true, message: store.message(m.id) }, 201);
|
|
10045
|
+
});
|
|
8135
10046
|
app.get("/v1/dispatch", (c) => {
|
|
8136
10047
|
const project = c.req.query("project");
|
|
8137
10048
|
if (!project)
|
|
@@ -8453,7 +10364,7 @@ function createApp(store = new Store) {
|
|
|
8453
10364
|
await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
|
|
8454
10365
|
}
|
|
8455
10366
|
await stream2.writeSSE({ event: "ping", data: "" });
|
|
8456
|
-
await new Promise((
|
|
10367
|
+
await new Promise((resolve2) => {
|
|
8457
10368
|
const off = store.subscribe((e) => {
|
|
8458
10369
|
stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
|
|
8459
10370
|
});
|
|
@@ -8461,28 +10372,117 @@ function createApp(store = new Store) {
|
|
|
8461
10372
|
stream2.onAbort(() => {
|
|
8462
10373
|
clearInterval(beat);
|
|
8463
10374
|
off();
|
|
8464
|
-
|
|
10375
|
+
resolve2();
|
|
8465
10376
|
});
|
|
8466
10377
|
});
|
|
8467
10378
|
});
|
|
8468
10379
|
});
|
|
8469
|
-
app.get("/", (c) => c.html(readFileSync4(
|
|
10380
|
+
app.get("/", (c) => c.html(readFileSync4(join10(WEB_DIR, "index.html"), "utf8")));
|
|
8470
10381
|
const MIME = { js: "text/javascript", css: "text/css" };
|
|
8471
10382
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
8472
10383
|
const f = c.req.param("file");
|
|
8473
|
-
const p =
|
|
8474
|
-
if (!
|
|
10384
|
+
const p = join10(WEB_DIR, f);
|
|
10385
|
+
if (!existsSync7(p))
|
|
8475
10386
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
8476
10387
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
8477
10388
|
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
8478
10389
|
});
|
|
8479
10390
|
});
|
|
8480
|
-
return { app, store, forge: forge2, runner, dispatcher };
|
|
10391
|
+
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
|
|
10392
|
+
}
|
|
10393
|
+
|
|
10394
|
+
// packages/daemon/src/demo.ts
|
|
10395
|
+
var H = 3600000;
|
|
10396
|
+
var iso = (msAgo) => new Date(Date.now() - msAgo).toISOString();
|
|
10397
|
+
function isEmpty(store) {
|
|
10398
|
+
return !store.db.query("SELECT 1 FROM sessions LIMIT 1").get();
|
|
10399
|
+
}
|
|
10400
|
+
function seedDemo(store) {
|
|
10401
|
+
const db = store.db;
|
|
10402
|
+
const project = (id, name, root, icon, color) => db.query("INSERT OR IGNORE INTO projects (id, root, common_dir, name, discovered, created_at, icon, color) VALUES (?, ?, ?, ?, 0, ?, ?, ?)").run(id, root, `${root}/.git`, name, iso(90 * 24 * H), icon, color);
|
|
10403
|
+
project("p_demo1", "acme-app", "/work/acme-app", "\uD83D\uDED2", "c3");
|
|
10404
|
+
project("p_demo2", "acme-site", "/work/acme-site", "\uD83C\uDF10", "c5");
|
|
10405
|
+
const session = (id, pid, agent, title, cwd, branch, startedAgo, lastAgo, state, model) => {
|
|
10406
|
+
db.query(`INSERT OR IGNORE INTO sessions (id, project_id, kind, agent, cwd, branch, title, model, started_at, last_seen_at, last, last_type, last_text, state, tool_calls)
|
|
10407
|
+
VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, ?, ?, 'tool.completed', ?, ?, ?)`).run(id, pid, agent, cwd, branch, title, model, iso(startedAgo), iso(lastAgo), state === "active" ? "Bash bun test" : "session ended", state === "active" ? "Running the suite before handing off." : "Done \u2014 PR opened, gates green.", state, 40 + Math.floor(Math.random() * 200));
|
|
10408
|
+
let t = startedAgo;
|
|
10409
|
+
let i = 0;
|
|
10410
|
+
while (t > lastAgo) {
|
|
10411
|
+
const out = 300 + Math.floor(Math.random() * 4000);
|
|
10412
|
+
const read = 50000 + Math.floor(Math.random() * 900000);
|
|
10413
|
+
db.query(`INSERT OR IGNORE INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
|
|
10414
|
+
VALUES (?, ?, NULL, ?, ?, NULL, 0, ?, ?, ?, 0, ?, ?, ?, ?, '["Bash","Edit"]')`).run(`${id}-t${i}`, id, iso(t), model, 800 + i * 97 % 2000, out, 12000, read, i % 3 === 0 ? 900 : 0, 0.02 + out / 1e6 * 15 + read / 1e6 * 0.3, i % 4 === 0 ? "Tests are green; tightening the error path next." : "");
|
|
10415
|
+
t -= (8 + i * 13 % 30) * 60000;
|
|
10416
|
+
i++;
|
|
10417
|
+
}
|
|
10418
|
+
};
|
|
10419
|
+
session("demo-s1", "p_demo1", "claude-code", "Checkout flow refactor", "/work/acme-app-wt/checkout", "task/checkout", 5 * H, 2 * 60000, "active", "claude-fable-5");
|
|
10420
|
+
session("demo-s2", "p_demo1", "codex", "Fix flaky cart tests", "/work/acme-app", "main", 7 * H, 3 * H, "ended", "gpt-5.2-codex");
|
|
10421
|
+
session("demo-s3", "p_demo1", "gemini", "Payment webhook audit", "/work/acme-app", "main", 26 * H, 22 * H, "ended", "gemini-2.5-pro");
|
|
10422
|
+
session("demo-s4", "p_demo2", "grok", "Landing page rewrite", "/work/acme-site", "task/landing", 30 * H, 25 * H, "ended", "grok-4");
|
|
10423
|
+
session("demo-s5", "p_demo2", "claude-code", "SEO metadata sweep", "/work/acme-site", "main", 50 * H, 47 * H, "ended", "claude-sonnet-5");
|
|
10424
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
10425
|
+
VALUES ('p_demo1', 'checkout', 'demo-s1', '/work/acme-app-wt/checkout', 'task/checkout', ?, ?, NULL, 'held', 'agent', 'demo-s1')`).run(iso(5 * H), iso(-30 * 60000));
|
|
10426
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
10427
|
+
VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
|
|
10428
|
+
const gate = (task, gate2, verdict, rubric, ago, sid) => db.query(`INSERT OR IGNORE INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
10429
|
+
VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate2, verdict, rubric, sid, iso(ago));
|
|
10430
|
+
gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
|
|
10431
|
+
gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
|
|
10432
|
+
gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
|
|
10433
|
+
gate("webhooks", "tests", "pass", "ran `bun test` \u2014 exit 0 in 22s", 22 * H, "demo-s3");
|
|
10434
|
+
const ev = (type, ago, sid, payload) => store.append({
|
|
10435
|
+
ts: iso(ago),
|
|
10436
|
+
type,
|
|
10437
|
+
projectId: "p_demo1",
|
|
10438
|
+
sessionId: sid,
|
|
10439
|
+
payload
|
|
10440
|
+
});
|
|
10441
|
+
ev("incident.opened", 4 * H, "demo-s1", {
|
|
10442
|
+
rule: "pattern_kill",
|
|
10443
|
+
action: "ask",
|
|
10444
|
+
command: "pkill -f vite",
|
|
10445
|
+
reason: "This kills processes by command pattern \u2014 other agents' dev servers match too."
|
|
10446
|
+
});
|
|
10447
|
+
ev("incident.opened", 26 * H, "demo-s3", {
|
|
10448
|
+
rule: "shared_tree",
|
|
10449
|
+
action: "deny",
|
|
10450
|
+
command: "git reset --hard",
|
|
10451
|
+
reason: "Another session (demo-s2) is active in this same checkout."
|
|
10452
|
+
});
|
|
10453
|
+
ev("claim.acquired", 5 * H, "demo-s1", {
|
|
10454
|
+
task: "checkout",
|
|
10455
|
+
owner: "demo-s1",
|
|
10456
|
+
summary: "claim checkout"
|
|
10457
|
+
});
|
|
10458
|
+
ev("pr.opened", 30 * 60000, "demo-s1", {
|
|
10459
|
+
task: "checkout",
|
|
10460
|
+
url: "https://github.com/acme/app/pull/128",
|
|
10461
|
+
summary: "PR #128 opened for checkout"
|
|
10462
|
+
});
|
|
10463
|
+
ev("question.asked", 20 * 60000, "demo-s1", {
|
|
10464
|
+
id: 1,
|
|
10465
|
+
task: "checkout",
|
|
10466
|
+
text: "Coupon codes: keep the legacy endpoint alive for one release, or cut over now?",
|
|
10467
|
+
options: ["Keep one release", "Cut over"],
|
|
10468
|
+
summary: "question #1"
|
|
10469
|
+
});
|
|
10470
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, options, asked_by, created_at)
|
|
10471
|
+
VALUES ('p_demo1', 'demo-s1', 'checkout', 'question', 'Coupon codes: keep the legacy endpoint alive for one release, or cut over now?', '["Keep one release","Cut over"]', 'demo-s1', ?)`).run(iso(20 * 60000));
|
|
10472
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
10473
|
+
VALUES ('p_demo1', 'demo-s1', 'checkout', 'message', 'Cart tests are green again \u2014 rebasing on main is safe now.', 'agent demo-s2', ?, 'task', 'demo-s2')`).run(iso(50 * 60000));
|
|
10474
|
+
db.query(`INSERT OR IGNORE INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
10475
|
+
VALUES ('p_demo1', 'webhooks', 'Signature validation + retries done', 'Dead-letter queue wiring', '["src/webhooks.ts","src/queue.ts"]', 'bun test \u2014 118 pass', 'auto:demo-s3', 'demo-s3', ?, 'daemon', 'daemon')`).run(iso(22 * H));
|
|
10476
|
+
db.query(`INSERT OR IGNORE INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, detail, started_at, updated_at, ended_at, actor_kind, actor_id)
|
|
10477
|
+
VALUES ('p_demo1', 'checkout', 'ship', 2, 'gate:review', '["implement","gate:tests","gate:review","pr"]', 'running', NULL, ?, ?, NULL, 'human', 'demo')`).run(iso(2 * H), iso(10 * 60000));
|
|
8481
10478
|
}
|
|
8482
10479
|
|
|
8483
10480
|
// packages/daemon/src/bin.ts
|
|
8484
10481
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
8485
|
-
var
|
|
10482
|
+
var appHooks = {};
|
|
10483
|
+
var { app, store, runner, team: team2 } = createApp(new Store, appHooks);
|
|
10484
|
+
if (process.env.SWARM_DEMO === "1" && isEmpty(store))
|
|
10485
|
+
seedDemo(store);
|
|
8486
10486
|
function serve() {
|
|
8487
10487
|
const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
|
|
8488
10488
|
try {
|
|
@@ -8494,21 +10494,54 @@ function serve() {
|
|
|
8494
10494
|
return bind(0);
|
|
8495
10495
|
}
|
|
8496
10496
|
}
|
|
8497
|
-
var server
|
|
10497
|
+
var server;
|
|
10498
|
+
var restart = () => {
|
|
10499
|
+
console.error("swarmd: restarting into the version on disk\u2026");
|
|
10500
|
+
try {
|
|
10501
|
+
clearInterval(tailer);
|
|
10502
|
+
clearInterval(wtRefresh);
|
|
10503
|
+
clearInterval(pruner);
|
|
10504
|
+
} catch {}
|
|
10505
|
+
try {
|
|
10506
|
+
server.stop(true);
|
|
10507
|
+
} catch {}
|
|
10508
|
+
clearDaemonInfo();
|
|
10509
|
+
const [cmd, ...args] = daemonCommand();
|
|
10510
|
+
if (cmd)
|
|
10511
|
+
Bun.spawn([cmd, ...args], {
|
|
10512
|
+
stdin: "ignore",
|
|
10513
|
+
stdout: "ignore",
|
|
10514
|
+
stderr: "ignore",
|
|
10515
|
+
env: { ...process.env }
|
|
10516
|
+
}).unref();
|
|
10517
|
+
setTimeout(() => process.exit(0), 100);
|
|
10518
|
+
};
|
|
10519
|
+
appHooks.restart = restart;
|
|
10520
|
+
server = serve();
|
|
8498
10521
|
var port = server.port ?? DEFAULT_PORT2;
|
|
8499
10522
|
ensureToken();
|
|
8500
10523
|
writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
|
|
8501
10524
|
var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
|
|
8502
10525
|
var backfillMs = backfillDays * 24 * 60 * 60000;
|
|
8503
|
-
|
|
8504
|
-
|
|
10526
|
+
var DEMO = process.env.SWARM_DEMO === "1";
|
|
10527
|
+
if (!DEMO) {
|
|
10528
|
+
store.tailCodex(backfillMs);
|
|
10529
|
+
store.tailGrok(backfillMs);
|
|
10530
|
+
store.tailGemini(backfillMs);
|
|
10531
|
+
store.tailAider(backfillMs);
|
|
10532
|
+
store.tailOpencode(backfillMs);
|
|
10533
|
+
}
|
|
8505
10534
|
var tick = 0;
|
|
8506
10535
|
var tailer = setInterval(() => {
|
|
8507
10536
|
tick++;
|
|
8508
|
-
|
|
8509
|
-
|
|
10537
|
+
if (!DEMO)
|
|
10538
|
+
store.tailActive();
|
|
10539
|
+
if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
|
|
8510
10540
|
store.tailCodex();
|
|
8511
10541
|
store.tailGrok();
|
|
10542
|
+
store.tailGemini();
|
|
10543
|
+
store.tailAider();
|
|
10544
|
+
store.tailOpencode();
|
|
8512
10545
|
}
|
|
8513
10546
|
store.reapResources();
|
|
8514
10547
|
store.reapProcesses();
|
|
@@ -8516,6 +10549,11 @@ var tailer = setInterval(() => {
|
|
|
8516
10549
|
store.sweepOrphans();
|
|
8517
10550
|
if (tick % 6 === 0)
|
|
8518
10551
|
store.checkBudgets();
|
|
10552
|
+
if (tick % 12 === 0)
|
|
10553
|
+
store.checkModels();
|
|
10554
|
+
if (tick % 2 === 0)
|
|
10555
|
+
store.checkStalls();
|
|
10556
|
+
team2.tick();
|
|
8519
10557
|
}, 5000);
|
|
8520
10558
|
store.refreshAllWorktrees();
|
|
8521
10559
|
var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);
|