@ra3orblade/swarm 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -10
- package/dist/swarm-hook.js +283 -1
- package/dist/swarm-mcp.js +53 -8
- package/dist/swarm.js +606 -49
- package/dist/swarmd.js +1899 -132
- package/package.json +1 -1
- package/web/app.js +533 -133
- package/web/index.html +106 -3
- package/web/menus.js +1 -1
- package/web/release-notes.js +1 -1
- package/web/viz.js +20 -3
package/dist/swarmd.js
CHANGED
|
@@ -2,9 +2,34 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/client/src/daemon.ts
|
|
5
|
-
import {
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
7
|
import { homedir } from "os";
|
|
7
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
|
|
8
33
|
function swarmHome() {
|
|
9
34
|
return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
|
|
10
35
|
}
|
|
@@ -18,9 +43,68 @@ function writeDaemonInfo(info) {
|
|
|
18
43
|
`);
|
|
19
44
|
return full;
|
|
20
45
|
}
|
|
46
|
+
var tokenFile = (home = swarmHome()) => join(home, "token");
|
|
47
|
+
function readToken(home = swarmHome()) {
|
|
48
|
+
try {
|
|
49
|
+
const t = readFileSync(tokenFile(home), "utf8").trim();
|
|
50
|
+
return /^[a-f0-9]{64}$/.test(t) ? t : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function ensureToken(home = swarmHome()) {
|
|
56
|
+
const cur = readToken(home);
|
|
57
|
+
if (cur)
|
|
58
|
+
return cur;
|
|
59
|
+
mkdirSync(home, { recursive: true });
|
|
60
|
+
const t = randomBytes(32).toString("hex");
|
|
61
|
+
writeFileSync(tokenFile(home), `${t}
|
|
62
|
+
`, { mode: 384 });
|
|
63
|
+
return t;
|
|
64
|
+
}
|
|
21
65
|
function clearDaemonInfo() {
|
|
22
66
|
rmSync(infoFile(), { force: true });
|
|
23
67
|
}
|
|
68
|
+
function daemonCommand() {
|
|
69
|
+
return resolveBin("swarmd");
|
|
70
|
+
}
|
|
71
|
+
// packages/core/src/actor.ts
|
|
72
|
+
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
73
|
+
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
74
|
+
function actorFrom(owner, sessionId, opts = {}) {
|
|
75
|
+
const o = (owner ?? "").trim();
|
|
76
|
+
const sid = sessionId?.trim() || undefined;
|
|
77
|
+
if (opts.runId)
|
|
78
|
+
return { kind: "run", id: opts.runId, session: sid };
|
|
79
|
+
if (o.startsWith("auto:"))
|
|
80
|
+
return { kind: "daemon", id: "daemon", session: sid };
|
|
81
|
+
if (DAEMON_ALIASES.has(o))
|
|
82
|
+
return { kind: "daemon", id: "daemon" };
|
|
83
|
+
if (o === "agent" || o.startsWith("agent:") || o.startsWith("session:"))
|
|
84
|
+
return {
|
|
85
|
+
kind: "agent",
|
|
86
|
+
id: sid ?? o.replace(/^(agent|session):/, "") ?? "agent",
|
|
87
|
+
session: sid
|
|
88
|
+
};
|
|
89
|
+
if (!o || HUMAN_ALIASES.has(o)) {
|
|
90
|
+
if (!o && sid)
|
|
91
|
+
return { kind: "agent", id: sid, session: sid };
|
|
92
|
+
return { kind: "human", id: opts.user?.trim() || "me" };
|
|
93
|
+
}
|
|
94
|
+
if (sid && o === sid)
|
|
95
|
+
return { kind: "agent", id: sid, session: sid };
|
|
96
|
+
return { kind: "human", id: o };
|
|
97
|
+
}
|
|
98
|
+
function actorFromColumns(kind, id, session) {
|
|
99
|
+
if (!kind || !id)
|
|
100
|
+
return null;
|
|
101
|
+
if (!["human", "agent", "run", "daemon"].includes(kind))
|
|
102
|
+
return null;
|
|
103
|
+
const a = { kind, id };
|
|
104
|
+
if (session && kind !== "human" && kind !== "daemon")
|
|
105
|
+
a.session = session;
|
|
106
|
+
return a;
|
|
107
|
+
}
|
|
24
108
|
// packages/core/src/adapters/claude-code/transcript.ts
|
|
25
109
|
function parseTranscriptChunk(chunk) {
|
|
26
110
|
const out = {
|
|
@@ -161,6 +245,79 @@ function parseCodexRollout(chunk) {
|
|
|
161
245
|
return out;
|
|
162
246
|
}
|
|
163
247
|
|
|
248
|
+
// packages/core/src/adapters/gemini/chats.ts
|
|
249
|
+
function partText(content) {
|
|
250
|
+
if (typeof content === "string")
|
|
251
|
+
return content.slice(0, 400);
|
|
252
|
+
const parts = Array.isArray(content) ? content : [content];
|
|
253
|
+
let out = "";
|
|
254
|
+
for (const p of parts) {
|
|
255
|
+
if (typeof p === "string")
|
|
256
|
+
out += p;
|
|
257
|
+
else if (p && typeof p === "object" && typeof p.text === "string")
|
|
258
|
+
out += p.text;
|
|
259
|
+
if (out.length >= 400)
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
return out.slice(0, 400);
|
|
263
|
+
}
|
|
264
|
+
function parseGeminiChat(chunk) {
|
|
265
|
+
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
266
|
+
let subagent = false;
|
|
267
|
+
for (const raw of chunk.split(`
|
|
268
|
+
`)) {
|
|
269
|
+
if (!raw.trim())
|
|
270
|
+
continue;
|
|
271
|
+
let d = null;
|
|
272
|
+
try {
|
|
273
|
+
d = JSON.parse(raw);
|
|
274
|
+
} catch {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (d.$set) {
|
|
278
|
+
const set = d.$set;
|
|
279
|
+
if (typeof set.summary === "string")
|
|
280
|
+
out.title = set.summary;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (d.sessionId && d.projectHash !== undefined) {
|
|
284
|
+
out.sessionId = d.sessionId;
|
|
285
|
+
if (Array.isArray(d.directories) && typeof d.directories[0] === "string")
|
|
286
|
+
out.cwd = d.directories[0];
|
|
287
|
+
if (typeof d.summary === "string")
|
|
288
|
+
out.title = d.summary;
|
|
289
|
+
if (d.kind === "subagent")
|
|
290
|
+
subagent = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (d.type !== "gemini" || !d.id)
|
|
294
|
+
continue;
|
|
295
|
+
const t = d.tokens ?? {};
|
|
296
|
+
const cacheRead = t.cached ?? 0;
|
|
297
|
+
const turn = {
|
|
298
|
+
id: `${out.sessionId ?? "gemini"}-${d.id}`,
|
|
299
|
+
ts: d.timestamp ?? new Date(0).toISOString(),
|
|
300
|
+
model: d.model ?? out.model ?? "gemini-2.5-pro",
|
|
301
|
+
usage: {
|
|
302
|
+
input: Math.max(0, (t.input ?? 0) - cacheRead),
|
|
303
|
+
output: (t.output ?? 0) + (t.tool ?? 0),
|
|
304
|
+
cacheWrite: 0,
|
|
305
|
+
cacheWrite1h: 0,
|
|
306
|
+
cacheRead,
|
|
307
|
+
thinking: t.thoughts ?? 0
|
|
308
|
+
},
|
|
309
|
+
text: partText(d.content),
|
|
310
|
+
tools: (d.toolCalls ?? []).map((c) => c.name ?? c.displayName ?? c.tool ?? "").filter(Boolean),
|
|
311
|
+
effort: null,
|
|
312
|
+
sidechain: subagent
|
|
313
|
+
};
|
|
314
|
+
if (d.model)
|
|
315
|
+
out.model = d.model;
|
|
316
|
+
out.turns.push(turn);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
164
321
|
// packages/core/src/adapters/grok/updates.ts
|
|
165
322
|
function parseGrokUpdates(chunk) {
|
|
166
323
|
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
@@ -229,6 +386,18 @@ function parseGrokUpdates(chunk) {
|
|
|
229
386
|
return out;
|
|
230
387
|
}
|
|
231
388
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
389
|
+
var HOOK_EVENTS = [
|
|
390
|
+
"SessionStart",
|
|
391
|
+
"UserPromptSubmit",
|
|
392
|
+
"PreToolUse",
|
|
393
|
+
"PostToolUse",
|
|
394
|
+
"SubagentStart",
|
|
395
|
+
"SubagentStop",
|
|
396
|
+
"Stop",
|
|
397
|
+
"SessionEnd",
|
|
398
|
+
"Notification",
|
|
399
|
+
"PreCompact"
|
|
400
|
+
];
|
|
232
401
|
var MAP = {
|
|
233
402
|
SessionStart: "session.started",
|
|
234
403
|
UserPromptSubmit: "prompt.submitted",
|
|
@@ -322,6 +491,157 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
|
|
|
322
491
|
payload.prompt = raw.prompt;
|
|
323
492
|
return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
|
|
324
493
|
}
|
|
494
|
+
// packages/core/src/audit.ts
|
|
495
|
+
var AUDIT_TYPES = new Set([
|
|
496
|
+
"session.started",
|
|
497
|
+
"session.ended",
|
|
498
|
+
"tool.denied",
|
|
499
|
+
"claim.acquired",
|
|
500
|
+
"claim.renewed",
|
|
501
|
+
"claim.released",
|
|
502
|
+
"claim.expired",
|
|
503
|
+
"claim.orphaned",
|
|
504
|
+
"worktree.created",
|
|
505
|
+
"worktree.removed",
|
|
506
|
+
"worktree.bootstrapped",
|
|
507
|
+
"pr.opened",
|
|
508
|
+
"question.asked",
|
|
509
|
+
"question.answered",
|
|
510
|
+
"dispatch.queued",
|
|
511
|
+
"dispatch.started",
|
|
512
|
+
"dispatch.finished",
|
|
513
|
+
"resource.acquired",
|
|
514
|
+
"resource.released",
|
|
515
|
+
"resource.reaped",
|
|
516
|
+
"process.started",
|
|
517
|
+
"process.exited",
|
|
518
|
+
"gate.recorded",
|
|
519
|
+
"handoff.recorded",
|
|
520
|
+
"permission.requested",
|
|
521
|
+
"permission.resolved",
|
|
522
|
+
"incident.opened",
|
|
523
|
+
"incident.acked",
|
|
524
|
+
"run.result",
|
|
525
|
+
"workflow.started",
|
|
526
|
+
"workflow.finished"
|
|
527
|
+
]);
|
|
528
|
+
var isAuditType = (t) => AUDIT_TYPES.has(t);
|
|
529
|
+
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
530
|
+
var DEFAULT_PRIVACY = {
|
|
531
|
+
store_prompts: true,
|
|
532
|
+
store_reasoning: true,
|
|
533
|
+
redact: []
|
|
534
|
+
};
|
|
535
|
+
var BUILTIN_REDACT = [
|
|
536
|
+
/\b(sk|pk|rk|ghp|gho|ghu|ghs|xoxb|xoxp|AKIA)[A-Za-z0-9_-]{16,}\b/g,
|
|
537
|
+
/\bBearer\s+[A-Za-z0-9._-]{20,}/g
|
|
538
|
+
];
|
|
539
|
+
function compileRedactions(patterns) {
|
|
540
|
+
const out = [...BUILTIN_REDACT];
|
|
541
|
+
for (const p of patterns) {
|
|
542
|
+
try {
|
|
543
|
+
out.push(new RegExp(p, "g"));
|
|
544
|
+
} catch {}
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
function redactValue(v, res) {
|
|
549
|
+
if (!res.length)
|
|
550
|
+
return v;
|
|
551
|
+
if (typeof v === "string")
|
|
552
|
+
return redactString(v, res);
|
|
553
|
+
if (Array.isArray(v)) {
|
|
554
|
+
let changed = false;
|
|
555
|
+
const out = v.map((x) => {
|
|
556
|
+
const r = redactValue(x, res);
|
|
557
|
+
if (r !== x)
|
|
558
|
+
changed = true;
|
|
559
|
+
return r;
|
|
560
|
+
});
|
|
561
|
+
return changed ? out : v;
|
|
562
|
+
}
|
|
563
|
+
if (v && typeof v === "object") {
|
|
564
|
+
let changed = false;
|
|
565
|
+
const out = {};
|
|
566
|
+
for (const [k, x] of Object.entries(v)) {
|
|
567
|
+
const r = redactValue(x, res);
|
|
568
|
+
if (r !== x)
|
|
569
|
+
changed = true;
|
|
570
|
+
out[k] = r;
|
|
571
|
+
}
|
|
572
|
+
return changed ? out : v;
|
|
573
|
+
}
|
|
574
|
+
return v;
|
|
575
|
+
}
|
|
576
|
+
function redactString(s, res) {
|
|
577
|
+
let out = s;
|
|
578
|
+
for (const re of res) {
|
|
579
|
+
re.lastIndex = 0;
|
|
580
|
+
if (re.test(out)) {
|
|
581
|
+
re.lastIndex = 0;
|
|
582
|
+
out = out.replace(re, "[redacted]");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
var AUDIT_COLUMNS = [
|
|
588
|
+
"seq",
|
|
589
|
+
"ts",
|
|
590
|
+
"type",
|
|
591
|
+
"projectId",
|
|
592
|
+
"sessionId",
|
|
593
|
+
"actorKind",
|
|
594
|
+
"actorId",
|
|
595
|
+
"summary",
|
|
596
|
+
"payload"
|
|
597
|
+
];
|
|
598
|
+
function auditRow(e) {
|
|
599
|
+
const p = e.payload ?? {};
|
|
600
|
+
const summary = typeof p.summary === "string" ? p.summary : typeof p.reason === "string" ? p.reason : typeof p.command === "string" ? p.command : typeof p.task === "string" ? String(p.task) : "";
|
|
601
|
+
return {
|
|
602
|
+
seq: e.seq ?? 0,
|
|
603
|
+
ts: e.ts,
|
|
604
|
+
type: e.type,
|
|
605
|
+
projectId: e.projectId,
|
|
606
|
+
sessionId: e.sessionId,
|
|
607
|
+
actorKind: e.actor?.kind ?? null,
|
|
608
|
+
actorId: e.actor?.id ?? null,
|
|
609
|
+
summary: summary.slice(0, 400),
|
|
610
|
+
payload: e.payload ?? null
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
var csvCell = (v) => {
|
|
614
|
+
const s = v == null ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
615
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
616
|
+
};
|
|
617
|
+
function formatAudit(rows, format) {
|
|
618
|
+
if (format === "json")
|
|
619
|
+
return JSON.stringify(rows);
|
|
620
|
+
if (format === "csv") {
|
|
621
|
+
const lines = [
|
|
622
|
+
AUDIT_COLUMNS.join(","),
|
|
623
|
+
...rows.map((r) => AUDIT_COLUMNS.map((c) => csvCell(r[c])).join(","))
|
|
624
|
+
];
|
|
625
|
+
return `${lines.join(`
|
|
626
|
+
`)}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
return `${rows.map((r) => JSON.stringify(r)).join(`
|
|
630
|
+
`)}${rows.length ? `
|
|
631
|
+
` : ""}`;
|
|
632
|
+
}
|
|
633
|
+
function sinceToIso(since, now = Date.now()) {
|
|
634
|
+
if (!since)
|
|
635
|
+
return null;
|
|
636
|
+
const m = /^(\d+)([dhm])$/.exec(since.trim());
|
|
637
|
+
if (m) {
|
|
638
|
+
const n = Number(m[1]);
|
|
639
|
+
const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
|
|
640
|
+
return new Date(now - n * ms).toISOString();
|
|
641
|
+
}
|
|
642
|
+
const t = Date.parse(since);
|
|
643
|
+
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
644
|
+
}
|
|
325
645
|
// packages/core/src/budget.ts
|
|
326
646
|
function budgetStatus(spent, cfg) {
|
|
327
647
|
const part = (s, l) => ({
|
|
@@ -376,33 +696,109 @@ function runProfile(name) {
|
|
|
376
696
|
return RUN_PROFILES[name] ?? null;
|
|
377
697
|
}
|
|
378
698
|
// packages/core/src/config.ts
|
|
379
|
-
import { existsSync as
|
|
699
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
380
700
|
import { join as join2 } from "path";
|
|
701
|
+
|
|
702
|
+
// packages/core/src/workflows.ts
|
|
703
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
704
|
+
function isRecord(v) {
|
|
705
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
706
|
+
}
|
|
707
|
+
function parseWorkflows(raw) {
|
|
708
|
+
const out = {};
|
|
709
|
+
if (!Array.isArray(raw))
|
|
710
|
+
return out;
|
|
711
|
+
for (const w of raw) {
|
|
712
|
+
if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
|
|
713
|
+
continue;
|
|
714
|
+
if (!Array.isArray(w.steps) || !w.steps.length)
|
|
715
|
+
continue;
|
|
716
|
+
const prompts = isRecord(w.prompts) ? w.prompts : {};
|
|
717
|
+
const steps = [];
|
|
718
|
+
for (const s of w.steps) {
|
|
719
|
+
if (typeof s !== "string" || !s.trim()) {
|
|
720
|
+
steps.length = 0;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
const t = s.trim();
|
|
724
|
+
if (t === "pr")
|
|
725
|
+
steps.push({ kind: "pr" });
|
|
726
|
+
else if (t.startsWith("gate:")) {
|
|
727
|
+
const gate = t.slice(5);
|
|
728
|
+
if (!NAME_RE.test(gate)) {
|
|
729
|
+
steps.length = 0;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
steps.push({ kind: "gate", gate });
|
|
733
|
+
} else if (NAME_RE.test(t)) {
|
|
734
|
+
const p = prompts[t];
|
|
735
|
+
steps.push({
|
|
736
|
+
kind: "run",
|
|
737
|
+
name: t,
|
|
738
|
+
prompt: typeof p === "string" && p.trim() ? p.trim() : null
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
steps.length = 0;
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (steps.length)
|
|
746
|
+
out[w.name] = { name: w.name, steps };
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
function workflowStepPrompt(step, task, ctx) {
|
|
751
|
+
if (step.prompt)
|
|
752
|
+
return step.prompt.replaceAll("{task}", task.id).replaceAll("{title}", task.title ?? "");
|
|
753
|
+
return [
|
|
754
|
+
`Task ${task.id}: ${task.title}`,
|
|
755
|
+
"",
|
|
756
|
+
`You are the "${step.name}" step of the "${ctx.workflow}" workflow. Work only inside this worktree; commit and push as you go.`,
|
|
757
|
+
ctx.remaining.length ? `After you finish, the workflow itself runs: ${ctx.remaining.join(" \u2192 ")}. Do not do those yourself.` : "You are the last step.",
|
|
758
|
+
"When done, call swarm_handoff with what was done and what remains."
|
|
759
|
+
].join(`
|
|
760
|
+
`);
|
|
761
|
+
}
|
|
762
|
+
function stepLabel(s) {
|
|
763
|
+
return s.kind === "run" ? s.name : s.kind === "gate" ? `gate:${s.gate}` : "pr";
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// packages/core/src/config.ts
|
|
381
767
|
var DEFAULT_GATE_TIMEOUT_S = 900;
|
|
382
768
|
var AUTO_MODES = ["session-end", "stop", "off"];
|
|
383
769
|
function parseGateDefs(gates) {
|
|
384
770
|
const out = {};
|
|
385
|
-
if (!
|
|
771
|
+
if (!isRecord2(gates))
|
|
386
772
|
return out;
|
|
387
773
|
for (const [name, v] of Object.entries(gates)) {
|
|
388
|
-
if (!
|
|
774
|
+
if (!isRecord2(v))
|
|
775
|
+
continue;
|
|
776
|
+
const builtin = v.builtin === "review" ? "review" : null;
|
|
777
|
+
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
778
|
+
if (!cmd && !builtin)
|
|
389
779
|
continue;
|
|
390
780
|
if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
|
|
391
781
|
continue;
|
|
392
782
|
const t = Number(v.timeout);
|
|
393
783
|
out[name] = {
|
|
394
|
-
cmd:
|
|
395
|
-
timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : DEFAULT_GATE_TIMEOUT_S,
|
|
396
|
-
cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null
|
|
784
|
+
cmd: builtin ? "" : cmd,
|
|
785
|
+
timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
|
|
786
|
+
cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
|
|
787
|
+
builtin,
|
|
788
|
+
model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
|
|
397
789
|
};
|
|
398
790
|
}
|
|
399
791
|
return out;
|
|
400
792
|
}
|
|
401
793
|
var DEFAULT_CONFIG = {
|
|
402
|
-
daemon: { port: 7777 },
|
|
794
|
+
daemon: { port: 7777, auth: "loopback-optional" },
|
|
403
795
|
tasks: { source: null, labels: [], team: null },
|
|
404
796
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
797
|
+
workflows: {},
|
|
405
798
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
799
|
+
events: { retain_days: 30 },
|
|
800
|
+
audit: { retain_days: 0 },
|
|
801
|
+
privacy: DEFAULT_PRIVACY,
|
|
406
802
|
dispatch: {
|
|
407
803
|
max_parallel: 2,
|
|
408
804
|
permission_mode: null,
|
|
@@ -423,11 +819,11 @@ var DEFAULT_CONFIG = {
|
|
|
423
819
|
}
|
|
424
820
|
};
|
|
425
821
|
var MODES = ["ask", "deny", "off"];
|
|
426
|
-
function
|
|
822
|
+
function isRecord2(v) {
|
|
427
823
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
428
824
|
}
|
|
429
825
|
function merge(a, b) {
|
|
430
|
-
if (!
|
|
826
|
+
if (!isRecord2(a) || !isRecord2(b))
|
|
431
827
|
return b === undefined ? a : b;
|
|
432
828
|
const out = { ...a };
|
|
433
829
|
for (const [k, v] of Object.entries(b))
|
|
@@ -450,6 +846,10 @@ function isRepoRelative(f) {
|
|
|
450
846
|
return false;
|
|
451
847
|
return !t.split(/[/\\]/).some((seg) => seg === "..");
|
|
452
848
|
}
|
|
849
|
+
var days = (v, fallback) => {
|
|
850
|
+
const n = Number(v);
|
|
851
|
+
return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
|
|
852
|
+
};
|
|
453
853
|
function validate(c) {
|
|
454
854
|
const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
|
|
455
855
|
const port = Number(c.daemon?.port);
|
|
@@ -470,7 +870,10 @@ function validate(c) {
|
|
|
470
870
|
const auto = rawGates?.auto;
|
|
471
871
|
return {
|
|
472
872
|
...c,
|
|
473
|
-
daemon: {
|
|
873
|
+
daemon: {
|
|
874
|
+
port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
|
|
875
|
+
auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
|
|
876
|
+
},
|
|
474
877
|
tasks: {
|
|
475
878
|
source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
|
|
476
879
|
labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
|
|
@@ -487,6 +890,18 @@ function validate(c) {
|
|
|
487
890
|
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
488
891
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
489
892
|
},
|
|
893
|
+
workflows: parseWorkflows(c.workflows),
|
|
894
|
+
events: {
|
|
895
|
+
retain_days: days(c.events?.retain_days, 30)
|
|
896
|
+
},
|
|
897
|
+
audit: {
|
|
898
|
+
retain_days: days(c.audit?.retain_days, 0)
|
|
899
|
+
},
|
|
900
|
+
privacy: {
|
|
901
|
+
store_prompts: c.privacy?.store_prompts !== false,
|
|
902
|
+
store_reasoning: c.privacy?.store_reasoning !== false,
|
|
903
|
+
redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
|
|
904
|
+
},
|
|
490
905
|
dispatch: {
|
|
491
906
|
max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
|
|
492
907
|
permission_mode: str(d.permission_mode),
|
|
@@ -514,18 +929,84 @@ function validate(c) {
|
|
|
514
929
|
}
|
|
515
930
|
};
|
|
516
931
|
}
|
|
517
|
-
function
|
|
932
|
+
function leafPaths(v, prefix = "") {
|
|
933
|
+
if (!isRecord2(v))
|
|
934
|
+
return prefix ? [prefix] : [];
|
|
935
|
+
const keys = Object.keys(v);
|
|
936
|
+
if (keys.length === 0)
|
|
937
|
+
return prefix ? [prefix] : [];
|
|
938
|
+
return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
|
|
939
|
+
}
|
|
940
|
+
function getPath(v, path) {
|
|
941
|
+
let cur = v;
|
|
942
|
+
for (const seg of path.split(".")) {
|
|
943
|
+
if (!isRecord2(cur))
|
|
944
|
+
return;
|
|
945
|
+
cur = cur[seg];
|
|
946
|
+
}
|
|
947
|
+
return cur;
|
|
948
|
+
}
|
|
949
|
+
function setPath(obj, path, value) {
|
|
950
|
+
const segs = path.split(".");
|
|
951
|
+
let cur = obj;
|
|
952
|
+
for (const seg of segs.slice(0, -1)) {
|
|
953
|
+
if (!isRecord2(cur[seg]))
|
|
954
|
+
cur[seg] = {};
|
|
955
|
+
cur = cur[seg];
|
|
956
|
+
}
|
|
957
|
+
cur[segs[segs.length - 1]] = value;
|
|
958
|
+
}
|
|
959
|
+
var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
|
|
960
|
+
function readLayer(path) {
|
|
961
|
+
return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
|
|
962
|
+
}
|
|
963
|
+
function loadConfigDetailed(opts = {}) {
|
|
518
964
|
const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
|
|
965
|
+
const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
|
|
966
|
+
const policyRaw = readLayer(policyPath);
|
|
967
|
+
const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
|
|
968
|
+
const policy = { ...policyRaw ?? {} };
|
|
969
|
+
delete policy.locked;
|
|
970
|
+
const layers = [
|
|
971
|
+
["policy", policyRaw ? policy : null],
|
|
972
|
+
["global", readLayer(join2(home, "config.toml"))],
|
|
973
|
+
["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
|
|
974
|
+
];
|
|
975
|
+
const provenance = {};
|
|
976
|
+
for (const p of leafPaths(DEFAULT_CONFIG))
|
|
977
|
+
provenance[p] = "default";
|
|
978
|
+
const overridden = [];
|
|
519
979
|
let cfg = DEFAULT_CONFIG;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
980
|
+
for (const [layer, raw] of layers) {
|
|
981
|
+
if (!raw)
|
|
982
|
+
continue;
|
|
983
|
+
for (const p of leafPaths(raw)) {
|
|
984
|
+
const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
|
|
985
|
+
if (lock)
|
|
986
|
+
overridden.push({ key: p, layer, attempted: getPath(raw, p) });
|
|
987
|
+
else
|
|
988
|
+
provenance[p] = layer;
|
|
989
|
+
}
|
|
990
|
+
cfg = merge(cfg, raw);
|
|
991
|
+
}
|
|
992
|
+
if (overridden.length) {
|
|
993
|
+
const out = structuredClone(cfg);
|
|
994
|
+
for (const { key } of overridden) {
|
|
995
|
+
const fromPolicy = getPath(policy, key);
|
|
996
|
+
setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
|
|
997
|
+
provenance[key] = fromPolicy === undefined ? "default" : "policy";
|
|
998
|
+
}
|
|
999
|
+
cfg = out;
|
|
527
1000
|
}
|
|
528
|
-
return
|
|
1001
|
+
return {
|
|
1002
|
+
config: validate(cfg),
|
|
1003
|
+
provenance,
|
|
1004
|
+
overridden,
|
|
1005
|
+
policy: { path: policyRaw ? policyPath : null, locked }
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function loadConfig(opts = {}) {
|
|
1009
|
+
return loadConfigDetailed(opts).config;
|
|
529
1010
|
}
|
|
530
1011
|
// packages/core/src/dispatch.ts
|
|
531
1012
|
function planDispatch(tasks, wanted, opts) {
|
|
@@ -963,11 +1444,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
|
|
|
963
1444
|
`) };
|
|
964
1445
|
}
|
|
965
1446
|
// packages/core/src/gates.ts
|
|
966
|
-
var
|
|
1447
|
+
var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
967
1448
|
function validateGateRun(input) {
|
|
968
1449
|
if (!input.task?.trim())
|
|
969
1450
|
return { ok: false, reason: "task is required" };
|
|
970
|
-
if (!
|
|
1451
|
+
if (!NAME_RE2.test(input.gate ?? ""))
|
|
971
1452
|
return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
|
|
972
1453
|
if (input.verdict !== "pass" && input.verdict !== "fail")
|
|
973
1454
|
return { ok: false, reason: 'verdict must be "pass" or "fail"' };
|
|
@@ -1328,6 +1809,121 @@ function parseMemoryQuery(q) {
|
|
|
1328
1809
|
}
|
|
1329
1810
|
return { match: terms.join(" "), kind, task };
|
|
1330
1811
|
}
|
|
1812
|
+
// packages/core/src/messages.ts
|
|
1813
|
+
var MESSAGE_MAX = 4000;
|
|
1814
|
+
function validateMessage(text) {
|
|
1815
|
+
if (typeof text !== "string" || !text.trim())
|
|
1816
|
+
return { ok: false, reason: "message text is required" };
|
|
1817
|
+
const t = text.trim();
|
|
1818
|
+
if (t.length > MESSAGE_MAX)
|
|
1819
|
+
return { ok: false, reason: `message is over ${MESSAGE_MAX} chars` };
|
|
1820
|
+
return { ok: true, text: t };
|
|
1821
|
+
}
|
|
1822
|
+
function parseTo(to) {
|
|
1823
|
+
if (typeof to !== "string" || !to.trim())
|
|
1824
|
+
return null;
|
|
1825
|
+
const t = to.trim();
|
|
1826
|
+
if (t === "lead")
|
|
1827
|
+
return { kind: "lead" };
|
|
1828
|
+
if (/^[0-9a-f]{8}(-[0-9a-f-]{4,28})?$/i.test(t))
|
|
1829
|
+
return { kind: "session", id: t };
|
|
1830
|
+
return { kind: "task", task: t };
|
|
1831
|
+
}
|
|
1832
|
+
function formatMessages(ms) {
|
|
1833
|
+
if (!ms.length)
|
|
1834
|
+
return null;
|
|
1835
|
+
const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
|
|
1836
|
+
return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
|
|
1837
|
+
${lines.join(`
|
|
1838
|
+
`)}
|
|
1839
|
+
Reply with swarm_send if a reply is expected.`;
|
|
1840
|
+
}
|
|
1841
|
+
// packages/core/src/policy.ts
|
|
1842
|
+
import { createHash } from "crypto";
|
|
1843
|
+
var HOOK_MARK = "swarm-hook";
|
|
1844
|
+
var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
|
|
1845
|
+
var MIN_HOOK_TIMEOUT_S = 5;
|
|
1846
|
+
function hookCoverage(settings) {
|
|
1847
|
+
const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
|
|
1848
|
+
const missing = [];
|
|
1849
|
+
const short = [];
|
|
1850
|
+
for (const ev of HOOK_EVENTS) {
|
|
1851
|
+
const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
|
|
1852
|
+
const ours = groups.flatMap((g) => {
|
|
1853
|
+
const list = g?.hooks;
|
|
1854
|
+
return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
|
|
1855
|
+
});
|
|
1856
|
+
if (!ours.length)
|
|
1857
|
+
missing.push(ev);
|
|
1858
|
+
else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
|
|
1859
|
+
short.push(ev);
|
|
1860
|
+
}
|
|
1861
|
+
return { missing, short, complete: !missing.length && !short.length };
|
|
1862
|
+
}
|
|
1863
|
+
var hasLockedRules = (loaded) => loaded.policy.locked.some((k) => k === "rules" || k.startsWith("rules."));
|
|
1864
|
+
function policyFindings(input) {
|
|
1865
|
+
const out = [];
|
|
1866
|
+
const repo = input.repoRoot ?? "";
|
|
1867
|
+
for (const o of input.loaded.overridden)
|
|
1868
|
+
out.push({
|
|
1869
|
+
key: `override:${repo}:${o.layer}:${o.key}`,
|
|
1870
|
+
subject: `${o.layer === "repo" ? ".swarm.toml" : "config.toml"} ${o.key}`,
|
|
1871
|
+
reason: `locked by policy; ${o.layer} config tried to set ${JSON.stringify(o.attempted)}`
|
|
1872
|
+
});
|
|
1873
|
+
const cov = input.coverage;
|
|
1874
|
+
if (cov && !cov.complete) {
|
|
1875
|
+
if (cov.missing.length)
|
|
1876
|
+
out.push({
|
|
1877
|
+
key: `hooks:missing:${cov.missing.join(",")}`,
|
|
1878
|
+
subject: `hooks ${cov.missing.join(", ")}`,
|
|
1879
|
+
reason: "swarm hook entry removed from settings.json \u2014 run: swarm install"
|
|
1880
|
+
});
|
|
1881
|
+
if (cov.short.length)
|
|
1882
|
+
out.push({
|
|
1883
|
+
key: `hooks:short:${cov.short.join(",")}`,
|
|
1884
|
+
subject: `hooks ${cov.short.join(", ")}`,
|
|
1885
|
+
reason: `hook timeout below ${MIN_HOOK_TIMEOUT_S}s \u2014 run: swarm install`
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (input.guardOff && hasLockedRules(input.loaded))
|
|
1889
|
+
out.push({
|
|
1890
|
+
key: "guard:off",
|
|
1891
|
+
subject: "SWARM_GUARD=off",
|
|
1892
|
+
reason: "policy locks rules; SWARM_GUARD=off is ignored for locked rules"
|
|
1893
|
+
});
|
|
1894
|
+
return out;
|
|
1895
|
+
}
|
|
1896
|
+
var POLICY_CACHE_VERSION = 1;
|
|
1897
|
+
var POLICY_CACHE_FILE = "policy.cache.json";
|
|
1898
|
+
var RULE_KEYS = [
|
|
1899
|
+
"shared_tree",
|
|
1900
|
+
"destructive_git",
|
|
1901
|
+
"pattern_kill",
|
|
1902
|
+
"protected_ports",
|
|
1903
|
+
"no_foreign_worktree",
|
|
1904
|
+
"claim_required_to_write"
|
|
1905
|
+
];
|
|
1906
|
+
var lockedKey = (locked, key) => locked.some((l) => l === "rules" || key === l || key.startsWith(`${l}.`));
|
|
1907
|
+
function offlineModes(loaded) {
|
|
1908
|
+
const locked = loaded.policy.locked;
|
|
1909
|
+
const out = { ...DEFAULT_MODES, protected: { ports: [] } };
|
|
1910
|
+
for (const k of RULE_KEYS)
|
|
1911
|
+
out[k] = lockedKey(locked, `rules.${k}`) ? loaded.config.rules[k] : "off";
|
|
1912
|
+
if (lockedKey(locked, "rules.protected.ports"))
|
|
1913
|
+
out.protected = { ports: [...loaded.config.rules.protected.ports] };
|
|
1914
|
+
return out;
|
|
1915
|
+
}
|
|
1916
|
+
var digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
|
|
1917
|
+
function buildPolicyCache(loaded, sessions, worktrees, now = new Date) {
|
|
1918
|
+
const body = {
|
|
1919
|
+
version: POLICY_CACHE_VERSION,
|
|
1920
|
+
writtenAt: now.toISOString(),
|
|
1921
|
+
modes: offlineModes(loaded),
|
|
1922
|
+
sessions,
|
|
1923
|
+
worktrees
|
|
1924
|
+
};
|
|
1925
|
+
return { ...body, sha256: digest(body) };
|
|
1926
|
+
}
|
|
1331
1927
|
// packages/core/src/pricing.ts
|
|
1332
1928
|
var PRICES = {
|
|
1333
1929
|
"claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
@@ -1474,6 +2070,114 @@ function acquireRefusalMessage(holder) {
|
|
|
1474
2070
|
const via = isTrackedPid(holder.pid) ? `pid ${holder.pid}` : holder.expiresAt ? `lease until ${holder.expiresAt}` : "unbounded";
|
|
1475
2071
|
return `Resource "${holder.name}" is held by ${holder.owner} (${via}).` + ` Pick another name, coordinate with the holder, or wait for release/reap.`;
|
|
1476
2072
|
}
|
|
2073
|
+
// packages/core/src/review.ts
|
|
2074
|
+
var REVIEW_RUBRIC = "review: no blocker/major findings \u2014 correctness bugs, data loss, security, broken invariants (never kill by pattern, never touch a worktree you don't hold, repo-agnostic), missing tests for changed behaviour";
|
|
2075
|
+
var REVIEW_PATCH_MAX = 120000;
|
|
2076
|
+
function reviewPrompt(input) {
|
|
2077
|
+
const patch = input.patch.length > REVIEW_PATCH_MAX ? `${input.patch.slice(0, REVIEW_PATCH_MAX)}
|
|
2078
|
+
|
|
2079
|
+
[\u2026 patch truncated at ${REVIEW_PATCH_MAX} chars; read the files for the rest]` : input.patch;
|
|
2080
|
+
return [
|
|
2081
|
+
`You are the review gate for task ${input.task}${input.title ? ` \u2014 ${input.title}` : ""}${input.branch ? ` (branch ${input.branch})` : ""}.`,
|
|
2082
|
+
"You are read-only: you may Read, Grep and Glob files in this worktree to understand context. Do not edit anything.",
|
|
2083
|
+
"",
|
|
2084
|
+
"Judge the diff below against this rubric and nothing else:",
|
|
2085
|
+
`- ${REVIEW_RUBRIC}`,
|
|
2086
|
+
"- A finding is a concrete defect with a file and, when possible, a line \u2014 not style, not preference.",
|
|
2087
|
+
"- Severity: blocker (must not merge), major (should not merge), minor (worth fixing), nit.",
|
|
2088
|
+
'- verdict is "fail" if and only if there is at least one blocker or major finding.',
|
|
2089
|
+
"",
|
|
2090
|
+
"Respond with ONLY a JSON object, no prose, no code fence:",
|
|
2091
|
+
'{"verdict":"pass"|"fail","summary":"one sentence","findings":[{"file":"path","line":123,"severity":"blocker|major|minor|nit","summary":"what is wrong and why"}]}',
|
|
2092
|
+
"",
|
|
2093
|
+
"Files changed:",
|
|
2094
|
+
input.stat.trim() || "(no stat)",
|
|
2095
|
+
"",
|
|
2096
|
+
"Diff:",
|
|
2097
|
+
patch.trim() || "(empty diff)"
|
|
2098
|
+
].join(`
|
|
2099
|
+
`);
|
|
2100
|
+
}
|
|
2101
|
+
function reviewArgs(prompt, opts = {}) {
|
|
2102
|
+
const args = [
|
|
2103
|
+
"-p",
|
|
2104
|
+
prompt,
|
|
2105
|
+
"--output-format",
|
|
2106
|
+
"json",
|
|
2107
|
+
"--allowedTools",
|
|
2108
|
+
"Read",
|
|
2109
|
+
"Grep",
|
|
2110
|
+
"Glob",
|
|
2111
|
+
"LS",
|
|
2112
|
+
"--disallowedTools",
|
|
2113
|
+
"Edit",
|
|
2114
|
+
"Write",
|
|
2115
|
+
"MultiEdit",
|
|
2116
|
+
"NotebookEdit",
|
|
2117
|
+
"Bash",
|
|
2118
|
+
"WebFetch",
|
|
2119
|
+
"WebSearch",
|
|
2120
|
+
"--permission-mode",
|
|
2121
|
+
"dontAsk"
|
|
2122
|
+
];
|
|
2123
|
+
if (opts.model)
|
|
2124
|
+
args.push("--model", opts.model);
|
|
2125
|
+
return args;
|
|
2126
|
+
}
|
|
2127
|
+
function parseReviewVerdict(stdout) {
|
|
2128
|
+
let text = stdout.trim();
|
|
2129
|
+
try {
|
|
2130
|
+
const env = JSON.parse(text);
|
|
2131
|
+
if (env && typeof env === "object" && typeof env.result === "string")
|
|
2132
|
+
text = env.result.trim();
|
|
2133
|
+
} catch {}
|
|
2134
|
+
text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
2135
|
+
const start = text.indexOf("{");
|
|
2136
|
+
const end = text.lastIndexOf("}");
|
|
2137
|
+
if (start < 0 || end <= start)
|
|
2138
|
+
return null;
|
|
2139
|
+
let obj;
|
|
2140
|
+
try {
|
|
2141
|
+
obj = JSON.parse(text.slice(start, end + 1));
|
|
2142
|
+
} catch {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
if (!obj || typeof obj !== "object")
|
|
2146
|
+
return null;
|
|
2147
|
+
const o = obj;
|
|
2148
|
+
const findings = Array.isArray(o.findings) ? o.findings.filter((f) => !!f && typeof f === "object").map((f) => ({
|
|
2149
|
+
file: String(f.file ?? "").slice(0, 300),
|
|
2150
|
+
line: Number.isInteger(f.line) ? f.line : null,
|
|
2151
|
+
severity: ["blocker", "major", "minor", "nit"].includes(String(f.severity)) ? String(f.severity) : "minor",
|
|
2152
|
+
summary: String(f.summary ?? "").slice(0, 600)
|
|
2153
|
+
})).filter((f) => f.summary) : [];
|
|
2154
|
+
const serious = findings.some((f) => f.severity === "blocker" || f.severity === "major");
|
|
2155
|
+
const verdict = serious ? "fail" : o.verdict === "fail" ? "fail" : "pass";
|
|
2156
|
+
return { verdict, summary: String(o.summary ?? "").slice(0, 400), findings };
|
|
2157
|
+
}
|
|
2158
|
+
function reviewGateInput(task, gate, outcome) {
|
|
2159
|
+
if (outcome.kind === "error")
|
|
2160
|
+
return {
|
|
2161
|
+
task,
|
|
2162
|
+
gate,
|
|
2163
|
+
verdict: "fail",
|
|
2164
|
+
rubric: REVIEW_RUBRIC,
|
|
2165
|
+
evidence: `reviewer did not answer: ${outcome.reason}${outcome.output ? `
|
|
2166
|
+
${outcome.output.slice(-1500)}` : ""}`
|
|
2167
|
+
};
|
|
2168
|
+
const v = outcome.verdict;
|
|
2169
|
+
const lines = v.findings.map((f) => `- [${f.severity}] ${f.file}${f.line ? `:${f.line}` : ""} \u2014 ${f.summary}`);
|
|
2170
|
+
const secs = (outcome.durationMs / 1000).toFixed(0);
|
|
2171
|
+
return {
|
|
2172
|
+
task,
|
|
2173
|
+
gate,
|
|
2174
|
+
verdict: v.verdict,
|
|
2175
|
+
rubric: REVIEW_RUBRIC,
|
|
2176
|
+
evidence: `${v.summary || (v.verdict === "pass" ? "no blocking findings" : "blocking findings")} (${v.findings.length} finding${v.findings.length === 1 ? "" : "s"}, ${secs}s)${lines.length ? `
|
|
2177
|
+
${lines.join(`
|
|
2178
|
+
`)}` : ""}`
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
1477
2181
|
// packages/core/src/tasks.ts
|
|
1478
2182
|
var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
|
|
1479
2183
|
var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
|
|
@@ -1707,10 +2411,10 @@ function planGc(worktrees, claims) {
|
|
|
1707
2411
|
return out;
|
|
1708
2412
|
}
|
|
1709
2413
|
// packages/daemon/src/app.ts
|
|
1710
|
-
import { existsSync as
|
|
2414
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
1711
2415
|
import { homedir as homedir4 } from "os";
|
|
1712
|
-
import { dirname as
|
|
1713
|
-
import { fileURLToPath } from "url";
|
|
2416
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
2417
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1714
2418
|
|
|
1715
2419
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
1716
2420
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -1918,7 +2622,7 @@ var tryDecode = (str, decoder) => {
|
|
|
1918
2622
|
}
|
|
1919
2623
|
};
|
|
1920
2624
|
var tryDecodeURI = (str) => tryDecode(str, decodeURI);
|
|
1921
|
-
var
|
|
2625
|
+
var getPath2 = (request) => {
|
|
1922
2626
|
const url = request.url;
|
|
1923
2627
|
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
1924
2628
|
let i = start;
|
|
@@ -1937,7 +2641,7 @@ var getPath = (request) => {
|
|
|
1937
2641
|
return url.slice(start, i);
|
|
1938
2642
|
};
|
|
1939
2643
|
var getPathNoStrict = (request) => {
|
|
1940
|
-
const result =
|
|
2644
|
+
const result = getPath2(request);
|
|
1941
2645
|
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
|
|
1942
2646
|
};
|
|
1943
2647
|
var mergePath = (base, sub, ...rest) => {
|
|
@@ -2459,7 +3163,7 @@ var Hono = class _Hono {
|
|
|
2459
3163
|
};
|
|
2460
3164
|
const { strict, ...optionsWithoutStrict } = options;
|
|
2461
3165
|
Object.assign(this, optionsWithoutStrict);
|
|
2462
|
-
this.getPath = strict ?? true ? options.getPath ??
|
|
3166
|
+
this.getPath = strict ?? true ? options.getPath ?? getPath2 : getPathNoStrict;
|
|
2463
3167
|
}
|
|
2464
3168
|
#clone() {
|
|
2465
3169
|
const clone = new _Hono({
|
|
@@ -3583,7 +4287,7 @@ class Dispatcher {
|
|
|
3583
4287
|
}
|
|
3584
4288
|
|
|
3585
4289
|
// packages/daemon/src/forge.ts
|
|
3586
|
-
import { existsSync as
|
|
4290
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3587
4291
|
import { homedir as homedir2 } from "os";
|
|
3588
4292
|
import { join as join4 } from "path";
|
|
3589
4293
|
var EXTRA_BIN_DIRS = [
|
|
@@ -3601,7 +4305,7 @@ function findBin(name) {
|
|
|
3601
4305
|
return onPath;
|
|
3602
4306
|
for (const d of EXTRA_BIN_DIRS) {
|
|
3603
4307
|
const p = join4(d, name);
|
|
3604
|
-
if (
|
|
4308
|
+
if (existsSync4(p))
|
|
3605
4309
|
return p;
|
|
3606
4310
|
}
|
|
3607
4311
|
return null;
|
|
@@ -4300,7 +5004,7 @@ class Runner {
|
|
|
4300
5004
|
import { Database } from "bun:sqlite";
|
|
4301
5005
|
import {
|
|
4302
5006
|
closeSync,
|
|
4303
|
-
existsSync as
|
|
5007
|
+
existsSync as existsSync6,
|
|
4304
5008
|
mkdirSync as mkdirSync4,
|
|
4305
5009
|
openSync as openSync3,
|
|
4306
5010
|
readdirSync,
|
|
@@ -4309,14 +5013,15 @@ import {
|
|
|
4309
5013
|
realpathSync as realpathSync2,
|
|
4310
5014
|
renameSync,
|
|
4311
5015
|
statSync,
|
|
5016
|
+
unlinkSync,
|
|
4312
5017
|
writeFileSync as writeFileSync2
|
|
4313
5018
|
} from "fs";
|
|
4314
|
-
import { homedir as homedir3 } from "os";
|
|
4315
|
-
import { basename, dirname as
|
|
5019
|
+
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
5020
|
+
import { basename, dirname as dirname3, join as join8 } from "path";
|
|
4316
5021
|
|
|
4317
5022
|
// packages/daemon/src/bootstrap.ts
|
|
4318
|
-
import { cpSync, existsSync as
|
|
4319
|
-
import { dirname, join as join7 } from "path";
|
|
5023
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
|
|
5024
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
4320
5025
|
function runBootstrap(plan, opts) {
|
|
4321
5026
|
const logDir = join7(opts.home, "logs", opts.projectId);
|
|
4322
5027
|
mkdirSync3(logDir, { recursive: true });
|
|
@@ -4324,12 +5029,12 @@ function runBootstrap(plan, opts) {
|
|
|
4324
5029
|
const copied = [];
|
|
4325
5030
|
const skipped = [];
|
|
4326
5031
|
for (const c of plan.copies) {
|
|
4327
|
-
if (!
|
|
5032
|
+
if (!existsSync5(c.from)) {
|
|
4328
5033
|
skipped.push(c.rel);
|
|
4329
5034
|
continue;
|
|
4330
5035
|
}
|
|
4331
5036
|
try {
|
|
4332
|
-
mkdirSync3(
|
|
5037
|
+
mkdirSync3(dirname2(c.to), { recursive: true });
|
|
4333
5038
|
cpSync(c.from, c.to, { recursive: true, force: true });
|
|
4334
5039
|
copied.push(c.rel);
|
|
4335
5040
|
} catch (e) {
|
|
@@ -4491,6 +5196,12 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
4491
5196
|
answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
|
|
4492
5197
|
);
|
|
4493
5198
|
CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
|
|
5199
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
5200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
|
|
5201
|
+
step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
|
|
5202
|
+
started_at TEXT, updated_at TEXT, ended_at TEXT, actor_kind TEXT, actor_id TEXT
|
|
5203
|
+
);
|
|
5204
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_proj ON workflow_runs(project_id, id);
|
|
4494
5205
|
CREATE TABLE IF NOT EXISTS claims (
|
|
4495
5206
|
project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
|
|
4496
5207
|
acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
|
|
@@ -4519,6 +5230,11 @@ class Store {
|
|
|
4519
5230
|
this.db.exec(SCHEMA);
|
|
4520
5231
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
4521
5232
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5233
|
+
this.ensureColumn("projects", "icon", "TEXT");
|
|
5234
|
+
this.ensureColumn("projects", "color", "TEXT");
|
|
5235
|
+
this.ensureColumn("messages", "to_kind", "TEXT");
|
|
5236
|
+
this.ensureColumn("messages", "from_session", "TEXT");
|
|
5237
|
+
this.migrate();
|
|
4522
5238
|
this.migrateProjectsJson(join8(home, "projects.json"));
|
|
4523
5239
|
this.reconcileMovedProjects();
|
|
4524
5240
|
this.slimExistingEvents();
|
|
@@ -4567,9 +5283,9 @@ class Store {
|
|
|
4567
5283
|
reconcileMovedProjects() {
|
|
4568
5284
|
const all = this.projects();
|
|
4569
5285
|
for (const stale of all) {
|
|
4570
|
-
if (
|
|
5286
|
+
if (existsSync6(stale.root))
|
|
4571
5287
|
continue;
|
|
4572
|
-
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name &&
|
|
5288
|
+
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
|
|
4573
5289
|
if (live.length !== 1)
|
|
4574
5290
|
continue;
|
|
4575
5291
|
this.mergeProject(stale.id, live[0].id);
|
|
@@ -4601,8 +5317,59 @@ class Store {
|
|
|
4601
5317
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
|
|
4602
5318
|
}
|
|
4603
5319
|
}
|
|
5320
|
+
static SCHEMA_VERSION = 1;
|
|
5321
|
+
schemaVersion() {
|
|
5322
|
+
return Number(this.meta("schema_version") ?? 0);
|
|
5323
|
+
}
|
|
5324
|
+
migrate() {
|
|
5325
|
+
const steps = [
|
|
5326
|
+
(db) => {
|
|
5327
|
+
for (const t of [
|
|
5328
|
+
"events",
|
|
5329
|
+
"claims",
|
|
5330
|
+
"resources",
|
|
5331
|
+
"processes",
|
|
5332
|
+
"handoffs",
|
|
5333
|
+
"gates",
|
|
5334
|
+
"incident_acks",
|
|
5335
|
+
"sessions"
|
|
5336
|
+
]) {
|
|
5337
|
+
this.ensureColumn(t, "actor_kind", "TEXT");
|
|
5338
|
+
this.ensureColumn(t, "actor_id", "TEXT");
|
|
5339
|
+
}
|
|
5340
|
+
const user = osUser();
|
|
5341
|
+
const fill = (table, ownerCol, sessionCol, key) => {
|
|
5342
|
+
const rows = db.query(`SELECT rowid AS rid, ${ownerCol ?? "NULL"} AS owner, ${sessionCol ?? "NULL"} AS sid FROM ${table} WHERE actor_kind IS NULL`).all();
|
|
5343
|
+
const upd = db.query(`UPDATE ${table} SET actor_kind = ?, actor_id = ? WHERE rowid = ?`);
|
|
5344
|
+
for (const r of rows) {
|
|
5345
|
+
const a = actorFrom(r.owner, r.sid, { user });
|
|
5346
|
+
upd.run(a.kind, a.id, r.rid);
|
|
5347
|
+
}
|
|
5348
|
+
return `${key}:${rows.length}`;
|
|
5349
|
+
};
|
|
5350
|
+
fill("claims", "owner", null, "claims");
|
|
5351
|
+
fill("resources", "owner", "session_id", "resources");
|
|
5352
|
+
fill("processes", "owner", "session_id", "processes");
|
|
5353
|
+
fill("handoffs", "by", "session_id", "handoffs");
|
|
5354
|
+
fill("gates", "NULL", "session_id", "gates");
|
|
5355
|
+
fill("incident_acks", "'dashboard'", null, "acks");
|
|
5356
|
+
fill("sessions", "NULL", "id", "sessions");
|
|
5357
|
+
fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
|
|
5358
|
+
}
|
|
5359
|
+
];
|
|
5360
|
+
for (let v = this.schemaVersion();v < steps.length; v++) {
|
|
5361
|
+
const step = steps[v];
|
|
5362
|
+
this.db.transaction(() => {
|
|
5363
|
+
step(this.db);
|
|
5364
|
+
this.setMeta("schema_version", String(v + 1));
|
|
5365
|
+
})();
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
actorFor(owner, sessionId, runId) {
|
|
5369
|
+
return actorFrom(owner, sessionId, { user: osUser(), runId });
|
|
5370
|
+
}
|
|
4604
5371
|
migrateProjectsJson(file) {
|
|
4605
|
-
if (!
|
|
5372
|
+
if (!existsSync6(file))
|
|
4606
5373
|
return;
|
|
4607
5374
|
try {
|
|
4608
5375
|
const list = JSON.parse(readFileSync3(file, "utf8"));
|
|
@@ -4617,11 +5384,12 @@ class Store {
|
|
|
4617
5384
|
const hit = this.topCache.get(cwd);
|
|
4618
5385
|
if (hit && Date.now() - hit.t < 1e4)
|
|
4619
5386
|
return hit.v;
|
|
4620
|
-
const v = cwd &&
|
|
5387
|
+
const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
|
|
4621
5388
|
this.topCache.set(cwd, { v, t: Date.now() });
|
|
4622
5389
|
return v;
|
|
4623
5390
|
}
|
|
4624
|
-
|
|
5391
|
+
policyCache = new Map;
|
|
5392
|
+
policySeen = new Set;
|
|
4625
5393
|
preregisterSpawnedSession(id, projectId, cwd, task) {
|
|
4626
5394
|
const now = new Date().toISOString();
|
|
4627
5395
|
this.db.query(`INSERT INTO sessions (id, project_id, kind, cwd, started_at, last_seen_at, last, last_type, state, title)
|
|
@@ -4650,8 +5418,8 @@ class Store {
|
|
|
4650
5418
|
createdAt: new Date().toISOString()
|
|
4651
5419
|
};
|
|
4652
5420
|
const sessionId = this.knownSession(h.sessionId);
|
|
4653
|
-
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
|
|
4654
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt);
|
|
5421
|
+
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
5422
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt, ...actorCols(this.actorFor(handoff.by, sessionId)));
|
|
4655
5423
|
this.remember(handoffDoc(projectId, Number(ins.lastInsertRowid), handoff, sessionId));
|
|
4656
5424
|
this.append({
|
|
4657
5425
|
ts: handoff.createdAt,
|
|
@@ -4701,8 +5469,8 @@ class Store {
|
|
|
4701
5469
|
if (!h)
|
|
4702
5470
|
return null;
|
|
4703
5471
|
this.db.query("DELETE FROM handoffs WHERE project_id = ? AND task = ? AND session_id = ? AND by LIKE 'auto%'").run(held.projectId, held.task, sessionId);
|
|
4704
|
-
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
|
|
4705
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt);
|
|
5472
|
+
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
5473
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt, ...actorCols(this.actorFor(h.by, sessionId)));
|
|
4706
5474
|
this.remember(handoffDoc(held.projectId, Number(ins.lastInsertRowid), h, sessionId));
|
|
4707
5475
|
this.touch();
|
|
4708
5476
|
return h;
|
|
@@ -4822,7 +5590,7 @@ class Store {
|
|
|
4822
5590
|
}));
|
|
4823
5591
|
}
|
|
4824
5592
|
sessionContext(cwd) {
|
|
4825
|
-
if (!cwd || !
|
|
5593
|
+
if (!cwd || !existsSync6(cwd))
|
|
4826
5594
|
return null;
|
|
4827
5595
|
const toplevel = this.toplevel(cwd);
|
|
4828
5596
|
const project = this.resolveProject(cwd);
|
|
@@ -4962,7 +5730,160 @@ class Store {
|
|
|
4962
5730
|
return qs;
|
|
4963
5731
|
}
|
|
4964
5732
|
answerContext(sessionId) {
|
|
4965
|
-
|
|
5733
|
+
const parts = [
|
|
5734
|
+
formatAnswers(this.inbox(sessionId)),
|
|
5735
|
+
formatMessages(this.messageInbox(sessionId))
|
|
5736
|
+
];
|
|
5737
|
+
const out = parts.filter(Boolean);
|
|
5738
|
+
return out.length ? out.join(`
|
|
5739
|
+
`) : null;
|
|
5740
|
+
}
|
|
5741
|
+
wfInsert(projectId, task, workflow, steps, actor2) {
|
|
5742
|
+
const now = new Date().toISOString();
|
|
5743
|
+
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)
|
|
5744
|
+
VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor2.kind, actor2.id);
|
|
5745
|
+
this.touch();
|
|
5746
|
+
return Number(r.lastInsertRowid);
|
|
5747
|
+
}
|
|
5748
|
+
wfUpdate(id, patch) {
|
|
5749
|
+
const sets = ["updated_at = ?"];
|
|
5750
|
+
const args = [new Date().toISOString()];
|
|
5751
|
+
if (patch.step !== undefined) {
|
|
5752
|
+
sets.push("step = ?");
|
|
5753
|
+
args.push(patch.step);
|
|
5754
|
+
}
|
|
5755
|
+
if (patch.stepLabel !== undefined) {
|
|
5756
|
+
sets.push("step_label = ?");
|
|
5757
|
+
args.push(patch.stepLabel);
|
|
5758
|
+
}
|
|
5759
|
+
if (patch.state !== undefined) {
|
|
5760
|
+
sets.push("state = ?");
|
|
5761
|
+
args.push(patch.state);
|
|
5762
|
+
}
|
|
5763
|
+
if (patch.detail !== undefined) {
|
|
5764
|
+
sets.push("detail = ?");
|
|
5765
|
+
args.push(patch.detail);
|
|
5766
|
+
}
|
|
5767
|
+
if (patch.runId !== undefined) {
|
|
5768
|
+
sets.push("run_id = ?");
|
|
5769
|
+
args.push(patch.runId);
|
|
5770
|
+
}
|
|
5771
|
+
if (patch.ended) {
|
|
5772
|
+
sets.push("ended_at = ?");
|
|
5773
|
+
args.push(new Date().toISOString());
|
|
5774
|
+
}
|
|
5775
|
+
this.db.query(`UPDATE workflow_runs SET ${sets.join(", ")} WHERE id = ?`).run(...args, id);
|
|
5776
|
+
this.touch();
|
|
5777
|
+
}
|
|
5778
|
+
wfRuns(projectId, limit = 50) {
|
|
5779
|
+
return this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map(rowToWorkflowRun);
|
|
5780
|
+
}
|
|
5781
|
+
wfActive(projectId, task) {
|
|
5782
|
+
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);
|
|
5783
|
+
return r ? rowToWorkflowRun(r) : null;
|
|
5784
|
+
}
|
|
5785
|
+
wfSweepOrphans() {
|
|
5786
|
+
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());
|
|
5787
|
+
}
|
|
5788
|
+
send(projectId, input) {
|
|
5789
|
+
if (!this.project(projectId))
|
|
5790
|
+
return { ok: false, error: "unknown project" };
|
|
5791
|
+
const v = validateMessage(input.text);
|
|
5792
|
+
if (!v.ok)
|
|
5793
|
+
return { ok: false, error: v.reason };
|
|
5794
|
+
const to = parseTo(input.to);
|
|
5795
|
+
if (!to)
|
|
5796
|
+
return { ok: false, error: 'to must be a session id, a task, or "lead"' };
|
|
5797
|
+
let sessionId = null;
|
|
5798
|
+
let task = null;
|
|
5799
|
+
if (to.kind === "session") {
|
|
5800
|
+
sessionId = this.knownSession(to.id) ?? this.sessionByPrefix(to.id);
|
|
5801
|
+
if (!sessionId)
|
|
5802
|
+
return { ok: false, error: `unknown session ${to.id}` };
|
|
5803
|
+
} else if (to.kind === "task") {
|
|
5804
|
+
task = to.task;
|
|
5805
|
+
sessionId = this.sessionForTask(projectId, to.task);
|
|
5806
|
+
} else {
|
|
5807
|
+
sessionId = this.leadSession(projectId);
|
|
5808
|
+
}
|
|
5809
|
+
const createdAt = new Date().toISOString();
|
|
5810
|
+
const from = input.from ?? (input.fromSession ? `agent ${input.fromSession.slice(0, 8)}` : null);
|
|
5811
|
+
const r = this.db.query(`INSERT INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
5812
|
+
VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?)`).run(projectId, sessionId, task, v.text, from, createdAt, to.kind, input.fromSession ?? null);
|
|
5813
|
+
const message = this.message(Number(r.lastInsertRowid));
|
|
5814
|
+
this.append({
|
|
5815
|
+
ts: createdAt,
|
|
5816
|
+
type: "message.sent",
|
|
5817
|
+
projectId,
|
|
5818
|
+
sessionId: input.fromSession ?? null,
|
|
5819
|
+
actor: this.actorFor(input.from ?? null, input.fromSession ?? null),
|
|
5820
|
+
payload: {
|
|
5821
|
+
id: message.id,
|
|
5822
|
+
to: input.to,
|
|
5823
|
+
task,
|
|
5824
|
+
recipient: sessionId,
|
|
5825
|
+
text: v.text.slice(0, 400),
|
|
5826
|
+
summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
|
|
5827
|
+
}
|
|
5828
|
+
});
|
|
5829
|
+
return { ok: true, message };
|
|
5830
|
+
}
|
|
5831
|
+
message(id) {
|
|
5832
|
+
const r = this.db.query("SELECT * FROM messages WHERE id = ? AND kind = 'message'").get(id);
|
|
5833
|
+
return r ? rowToMessage(r) : null;
|
|
5834
|
+
}
|
|
5835
|
+
messages(opts = {}) {
|
|
5836
|
+
const where = ["kind = 'message'"];
|
|
5837
|
+
const args = [];
|
|
5838
|
+
if (opts.projectId) {
|
|
5839
|
+
where.push("project_id = ?");
|
|
5840
|
+
args.push(opts.projectId);
|
|
5841
|
+
}
|
|
5842
|
+
if (opts.sessionId) {
|
|
5843
|
+
where.push("(session_id = ? OR from_session = ?)");
|
|
5844
|
+
args.push(opts.sessionId, opts.sessionId);
|
|
5845
|
+
}
|
|
5846
|
+
if (opts.task) {
|
|
5847
|
+
where.push("task = ?");
|
|
5848
|
+
args.push(opts.task);
|
|
5849
|
+
}
|
|
5850
|
+
args.push(opts.limit ?? 100);
|
|
5851
|
+
return this.db.query(`SELECT * FROM messages WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`).all(...args).map(rowToMessage);
|
|
5852
|
+
}
|
|
5853
|
+
messageInbox(sessionId, opts = {}) {
|
|
5854
|
+
if (!sessionId)
|
|
5855
|
+
return [];
|
|
5856
|
+
const s = this.db.query("SELECT project_id, kind, cwd FROM sessions WHERE id = ?").get(sessionId);
|
|
5857
|
+
if (!s)
|
|
5858
|
+
return [];
|
|
5859
|
+
const task = this.heldClaimsWithWorktree().find((c) => isInside(s.cwd, c.worktree))?.task ?? null;
|
|
5860
|
+
const rows = this.db.query(`SELECT * FROM messages WHERE kind = 'message' AND delivered_at IS NULL AND from_session IS NOT ?
|
|
5861
|
+
AND (session_id = ?
|
|
5862
|
+
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
5863
|
+
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
5864
|
+
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
5865
|
+
const ms = rows.map(rowToMessage);
|
|
5866
|
+
if (ms.length && !opts.peek)
|
|
5867
|
+
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));
|
|
5868
|
+
return ms;
|
|
5869
|
+
}
|
|
5870
|
+
markMessageDelivered(id, sessionId) {
|
|
5871
|
+
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);
|
|
5872
|
+
}
|
|
5873
|
+
leadSession(projectId) {
|
|
5874
|
+
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);
|
|
5875
|
+
return r?.id ?? null;
|
|
5876
|
+
}
|
|
5877
|
+
sessionByPrefix(prefix) {
|
|
5878
|
+
const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
|
|
5879
|
+
return rows.length === 1 ? rows[0]?.id ?? null : null;
|
|
5880
|
+
}
|
|
5881
|
+
sessionForTask(projectId, task) {
|
|
5882
|
+
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
5883
|
+
if (!claim?.worktree)
|
|
5884
|
+
return null;
|
|
5885
|
+
const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
|
|
5886
|
+
return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
|
|
4966
5887
|
}
|
|
4967
5888
|
questionContext(task, projectId) {
|
|
4968
5889
|
if (!task)
|
|
@@ -5039,13 +5960,13 @@ class Store {
|
|
|
5039
5960
|
};
|
|
5040
5961
|
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
5041
5962
|
const worktree2 = claim?.worktree;
|
|
5042
|
-
if (!worktree2 || !
|
|
5963
|
+
if (!worktree2 || !existsSync6(worktree2))
|
|
5043
5964
|
return {
|
|
5044
5965
|
ok: false,
|
|
5045
5966
|
reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
|
|
5046
5967
|
};
|
|
5047
5968
|
const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
|
|
5048
|
-
if (!
|
|
5969
|
+
if (!existsSync6(cwd))
|
|
5049
5970
|
return { ok: false, reason: `gate cwd ${cwd} does not exist` };
|
|
5050
5971
|
const key = `${projectId}:${task}:${gate}`;
|
|
5051
5972
|
if (this.gateJobs.has(key))
|
|
@@ -5054,6 +5975,8 @@ class Store {
|
|
|
5054
5975
|
const logDir = join8(this.home, "logs", projectId);
|
|
5055
5976
|
mkdirSync4(logDir, { recursive: true });
|
|
5056
5977
|
const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
|
|
5978
|
+
if (def.builtin === "review")
|
|
5979
|
+
return this.runReviewGate(projectId, task, gate, def, { worktree: worktree2, cwd, key, log }, opts);
|
|
5057
5980
|
writeFileSync2(log, `$ ${def.cmd}
|
|
5058
5981
|
# cwd ${cwd} \xB7 ${new Date().toISOString()}
|
|
5059
5982
|
`);
|
|
@@ -5133,6 +6056,134 @@ class Store {
|
|
|
5133
6056
|
this.gateJobs.set(key, done);
|
|
5134
6057
|
return { ok: true, pid: proc.pid, log, done };
|
|
5135
6058
|
}
|
|
6059
|
+
runReviewGate(projectId, task, gate, def, where, opts) {
|
|
6060
|
+
const bin = findBin("claude");
|
|
6061
|
+
if (!bin)
|
|
6062
|
+
return { ok: false, reason: "claude CLI not found \u2014 the review gate needs Claude Code" };
|
|
6063
|
+
const p = this.project(projectId);
|
|
6064
|
+
if (!p)
|
|
6065
|
+
return { ok: false, reason: "unknown project" };
|
|
6066
|
+
const started = Date.now();
|
|
6067
|
+
const record = (input) => {
|
|
6068
|
+
const run2 = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
|
|
6069
|
+
return run2.ok ? run2.run : null;
|
|
6070
|
+
};
|
|
6071
|
+
const done = (async () => {
|
|
6072
|
+
let diffText = "";
|
|
6073
|
+
let stat = "";
|
|
6074
|
+
try {
|
|
6075
|
+
const diff = await worktreeDiff(p.root, where.worktree);
|
|
6076
|
+
stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
6077
|
+
`);
|
|
6078
|
+
diffText = await worktreePatch(where.worktree, diff.base);
|
|
6079
|
+
} catch (e) {
|
|
6080
|
+
return record(reviewGateInput(task, gate, {
|
|
6081
|
+
kind: "error",
|
|
6082
|
+
reason: `diff failed: ${e.message}`,
|
|
6083
|
+
durationMs: Date.now() - started
|
|
6084
|
+
}));
|
|
6085
|
+
}
|
|
6086
|
+
if (!diffText.trim())
|
|
6087
|
+
return record(reviewGateInput(task, gate, {
|
|
6088
|
+
kind: "verdict",
|
|
6089
|
+
durationMs: Date.now() - started,
|
|
6090
|
+
verdict: { verdict: "pass", summary: "nothing to review \u2014 empty diff", findings: [] }
|
|
6091
|
+
}));
|
|
6092
|
+
const taskRow = this.tasks(projectId)?.tasks.find((t) => t.id === task) ?? null;
|
|
6093
|
+
const w = this.findWorktree(projectId, where.worktree);
|
|
6094
|
+
const prompt = reviewPrompt({
|
|
6095
|
+
task,
|
|
6096
|
+
title: taskRow?.title ?? null,
|
|
6097
|
+
branch: w?.branch ?? null,
|
|
6098
|
+
stat,
|
|
6099
|
+
patch: diffText
|
|
6100
|
+
});
|
|
6101
|
+
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
6102
|
+
# cwd ${where.cwd} \xB7 ${new Date().toISOString()}
|
|
6103
|
+
`);
|
|
6104
|
+
let proc;
|
|
6105
|
+
try {
|
|
6106
|
+
proc = Bun.spawn([bin, ...reviewArgs(prompt, { model: def.model })], {
|
|
6107
|
+
cwd: where.cwd,
|
|
6108
|
+
stdin: "ignore",
|
|
6109
|
+
stdout: "pipe",
|
|
6110
|
+
stderr: "pipe",
|
|
6111
|
+
env: {
|
|
6112
|
+
...process.env,
|
|
6113
|
+
SWARM_WORKTREE: where.worktree,
|
|
6114
|
+
SWARM_TASK: task,
|
|
6115
|
+
SWARM_GATE: gate,
|
|
6116
|
+
CLAUDE_CODE_DISABLE_AUTOUPDATE: "1"
|
|
6117
|
+
}
|
|
6118
|
+
});
|
|
6119
|
+
} catch (e) {
|
|
6120
|
+
return record(reviewGateInput(task, gate, {
|
|
6121
|
+
kind: "error",
|
|
6122
|
+
reason: e.message,
|
|
6123
|
+
durationMs: Date.now() - started
|
|
6124
|
+
}));
|
|
6125
|
+
}
|
|
6126
|
+
const reg = this.registerProcess({
|
|
6127
|
+
pid: proc.pid,
|
|
6128
|
+
projectId,
|
|
6129
|
+
sessionId: opts.sessionId ?? null,
|
|
6130
|
+
kind: "gate",
|
|
6131
|
+
name: `gate:${task}:${gate}`,
|
|
6132
|
+
cwd: where.cwd,
|
|
6133
|
+
cmd: "claude -p (review)",
|
|
6134
|
+
owner: opts.owner ?? "daemon",
|
|
6135
|
+
log: where.log
|
|
6136
|
+
});
|
|
6137
|
+
let timedOut = false;
|
|
6138
|
+
const timer = setTimeout(() => {
|
|
6139
|
+
timedOut = true;
|
|
6140
|
+
try {
|
|
6141
|
+
proc.kill("SIGTERM");
|
|
6142
|
+
setTimeout(() => {
|
|
6143
|
+
try {
|
|
6144
|
+
proc.kill("SIGKILL");
|
|
6145
|
+
} catch {}
|
|
6146
|
+
}, 5000).unref();
|
|
6147
|
+
} catch {}
|
|
6148
|
+
}, def.timeout * 1000);
|
|
6149
|
+
const [out, err] = await Promise.all([
|
|
6150
|
+
new Response(proc.stdout).text(),
|
|
6151
|
+
new Response(proc.stderr).text()
|
|
6152
|
+
]);
|
|
6153
|
+
const code = await proc.exited;
|
|
6154
|
+
clearTimeout(timer);
|
|
6155
|
+
try {
|
|
6156
|
+
writeFileSync2(where.log, `${readFileSync3(where.log, "utf8")}${out}
|
|
6157
|
+
${err}
|
|
6158
|
+
# exit ${timedOut ? "timeout" : code} \xB7 ${((Date.now() - started) / 1000).toFixed(0)}s
|
|
6159
|
+
`);
|
|
6160
|
+
} catch {}
|
|
6161
|
+
if (reg.ok)
|
|
6162
|
+
this.processes(projectId);
|
|
6163
|
+
const durationMs = Date.now() - started;
|
|
6164
|
+
if (timedOut)
|
|
6165
|
+
return record(reviewGateInput(task, gate, {
|
|
6166
|
+
kind: "error",
|
|
6167
|
+
reason: `timed out after ${def.timeout}s`,
|
|
6168
|
+
durationMs,
|
|
6169
|
+
output: err
|
|
6170
|
+
}));
|
|
6171
|
+
const verdict = parseReviewVerdict(out);
|
|
6172
|
+
if (!verdict)
|
|
6173
|
+
return record(reviewGateInput(task, gate, {
|
|
6174
|
+
kind: "error",
|
|
6175
|
+
reason: code === 0 ? "no JSON verdict in the reply" : `claude exited ${code}`,
|
|
6176
|
+
durationMs,
|
|
6177
|
+
output: err || out
|
|
6178
|
+
}));
|
|
6179
|
+
return record(reviewGateInput(task, gate, { kind: "verdict", verdict, durationMs }));
|
|
6180
|
+
})().finally(() => {
|
|
6181
|
+
this.gateJobs.delete(where.key);
|
|
6182
|
+
this.touch();
|
|
6183
|
+
});
|
|
6184
|
+
this.gateJobs.set(where.key, done);
|
|
6185
|
+
return { ok: true, pid: 0, log: where.log, done };
|
|
6186
|
+
}
|
|
5136
6187
|
async runGates(projectId, task, gates2, opts = {}) {
|
|
5137
6188
|
const cfg = this.gateDefs(projectId);
|
|
5138
6189
|
const names = gates2?.length ? gates2 : (cfg?.required ?? []).filter((g) => cfg?.defs[g]);
|
|
@@ -5202,8 +6253,8 @@ class Store {
|
|
|
5202
6253
|
return v;
|
|
5203
6254
|
const createdAt = new Date().toISOString();
|
|
5204
6255
|
const sessionId = this.knownSession(input.sessionId);
|
|
5205
|
-
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at)
|
|
5206
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt);
|
|
6256
|
+
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
6257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
|
|
5207
6258
|
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
5208
6259
|
this.remember(gateDoc(projectId, run2.id, run2, sessionId));
|
|
5209
6260
|
this.append({
|
|
@@ -5256,7 +6307,7 @@ class Store {
|
|
|
5256
6307
|
error = e.error;
|
|
5257
6308
|
} else {
|
|
5258
6309
|
const path = join8(p.root, source);
|
|
5259
|
-
if (!
|
|
6310
|
+
if (!existsSync6(path))
|
|
5260
6311
|
return { source, required: this.requiredGates(projectId), tasks: [] };
|
|
5261
6312
|
const mtime = statSync(path).mtimeMs;
|
|
5262
6313
|
let md = this.taskCache.get(projectId);
|
|
@@ -5289,16 +6340,70 @@ class Store {
|
|
|
5289
6340
|
return { source, required, tasks: board, error };
|
|
5290
6341
|
}
|
|
5291
6342
|
rulesFor(repoRoot) {
|
|
6343
|
+
return this.policyFor(repoRoot).config.rules;
|
|
6344
|
+
}
|
|
6345
|
+
policyFor(repoRoot) {
|
|
5292
6346
|
const key = repoRoot ?? "";
|
|
5293
|
-
const hit = this.
|
|
6347
|
+
const hit = this.policyCache.get(key);
|
|
5294
6348
|
if (hit && Date.now() - hit.at < 30000)
|
|
5295
|
-
return hit.
|
|
5296
|
-
const
|
|
5297
|
-
this.
|
|
5298
|
-
|
|
6349
|
+
return hit.loaded;
|
|
6350
|
+
const loaded = loadConfigDetailed({ repoRoot, home: this.home });
|
|
6351
|
+
this.policyCache.set(key, { at: Date.now(), loaded });
|
|
6352
|
+
this.writePolicyCache(loaded);
|
|
6353
|
+
return loaded;
|
|
6354
|
+
}
|
|
6355
|
+
writePolicyCache(loaded) {
|
|
6356
|
+
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
6357
|
+
try {
|
|
6358
|
+
if (!hasLockedRules(loaded)) {
|
|
6359
|
+
if (existsSync6(file))
|
|
6360
|
+
unlinkSync(file);
|
|
6361
|
+
return;
|
|
6362
|
+
}
|
|
6363
|
+
const cache = buildPolicyCache(loaded, this.liveSessions(), this.heldWorktrees());
|
|
6364
|
+
writeFileSync2(file, JSON.stringify(cache), { mode: 384 });
|
|
6365
|
+
} catch (e) {
|
|
6366
|
+
console.error(`swarm: policy cache: ${e.message}`);
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
guardDisabled(repoRoot) {
|
|
6370
|
+
return process.env.SWARM_GUARD === "off" && !hasLockedRules(this.policyFor(repoRoot));
|
|
6371
|
+
}
|
|
6372
|
+
claudeSettings() {
|
|
6373
|
+
const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
|
|
6374
|
+
try {
|
|
6375
|
+
return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
|
|
6376
|
+
} catch {
|
|
6377
|
+
return null;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
checkPolicy(cwd, sessionId) {
|
|
6381
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6382
|
+
const repoRoot = project?.root ?? null;
|
|
6383
|
+
const loaded = this.policyFor(repoRoot);
|
|
6384
|
+
const settings = this.claudeSettings();
|
|
6385
|
+
const findings = policyFindings({
|
|
6386
|
+
loaded,
|
|
6387
|
+
coverage: settings === null ? null : hookCoverage(settings),
|
|
6388
|
+
guardOff: process.env.SWARM_GUARD === "off",
|
|
6389
|
+
repoRoot
|
|
6390
|
+
});
|
|
6391
|
+
for (const f of findings) {
|
|
6392
|
+
if (this.policySeen.has(f.key))
|
|
6393
|
+
continue;
|
|
6394
|
+
this.policySeen.add(f.key);
|
|
6395
|
+
this.append({
|
|
6396
|
+
ts: new Date().toISOString(),
|
|
6397
|
+
type: "incident.opened",
|
|
6398
|
+
projectId: project?.id ?? "p_unknown",
|
|
6399
|
+
sessionId,
|
|
6400
|
+
payload: { rule: "policy", action: "tampered", command: f.subject, reason: f.reason }
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
return findings;
|
|
5299
6404
|
}
|
|
5300
6405
|
evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
|
|
5301
|
-
if (BUDGET_ASK_TOOLS.has(tool) && cwd &&
|
|
6406
|
+
if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
|
|
5302
6407
|
const project = this.resolveProject(cwd);
|
|
5303
6408
|
const b = this.budgetFor(project.id);
|
|
5304
6409
|
if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
|
|
@@ -5389,7 +6494,7 @@ class Store {
|
|
|
5389
6494
|
return this.openIncident(d, cwd, id, cmd);
|
|
5390
6495
|
}
|
|
5391
6496
|
openIncident(d, cwd, sessionId, command) {
|
|
5392
|
-
const project = cwd &&
|
|
6497
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
5393
6498
|
this.append({
|
|
5394
6499
|
ts: new Date().toISOString(),
|
|
5395
6500
|
type: "incident.opened",
|
|
@@ -5440,7 +6545,7 @@ class Store {
|
|
|
5440
6545
|
}
|
|
5441
6546
|
}
|
|
5442
6547
|
const report = dryRunRules(calls, modes, {
|
|
5443
|
-
toplevel: (cwd) => cwd &&
|
|
6548
|
+
toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
|
|
5444
6549
|
claims: this.heldWorktrees()
|
|
5445
6550
|
});
|
|
5446
6551
|
return { ...report, modes };
|
|
@@ -5456,7 +6561,7 @@ class Store {
|
|
|
5456
6561
|
this.prices = { ...PRICES };
|
|
5457
6562
|
for (const f of ["pricing.litellm.json", "pricing.json"]) {
|
|
5458
6563
|
const p = join8(this.home, f);
|
|
5459
|
-
if (!
|
|
6564
|
+
if (!existsSync6(p))
|
|
5460
6565
|
continue;
|
|
5461
6566
|
try {
|
|
5462
6567
|
const j = JSON.parse(readFileSync3(p, "utf8"));
|
|
@@ -5498,6 +6603,8 @@ class Store {
|
|
|
5498
6603
|
name: r.name,
|
|
5499
6604
|
discovered: Boolean(r.discovered),
|
|
5500
6605
|
order: typeof r.sort_order === "number" ? r.sort_order : null,
|
|
6606
|
+
icon: r.icon ?? null,
|
|
6607
|
+
color: r.color ?? null,
|
|
5501
6608
|
createdAt: r.created_at
|
|
5502
6609
|
}));
|
|
5503
6610
|
}
|
|
@@ -5512,6 +6619,8 @@ class Store {
|
|
|
5512
6619
|
name: r.name,
|
|
5513
6620
|
discovered: Boolean(r.discovered),
|
|
5514
6621
|
order: typeof r.sort_order === "number" ? r.sort_order : null,
|
|
6622
|
+
icon: r.icon ?? null,
|
|
6623
|
+
color: r.color ?? null,
|
|
5515
6624
|
createdAt: r.created_at
|
|
5516
6625
|
};
|
|
5517
6626
|
}
|
|
@@ -5549,6 +6658,8 @@ class Store {
|
|
|
5549
6658
|
...ident,
|
|
5550
6659
|
discovered: !explicit,
|
|
5551
6660
|
order: null,
|
|
6661
|
+
icon: null,
|
|
6662
|
+
color: null,
|
|
5552
6663
|
createdAt: new Date().toISOString()
|
|
5553
6664
|
};
|
|
5554
6665
|
if (name)
|
|
@@ -5569,8 +6680,24 @@ class Store {
|
|
|
5569
6680
|
return;
|
|
5570
6681
|
if (patch.pinned !== undefined)
|
|
5571
6682
|
this.db.query("UPDATE projects SET discovered = ? WHERE id = ?").run(patch.pinned ? 0 : 1, id);
|
|
5572
|
-
if (patch.name)
|
|
5573
|
-
this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name, id);
|
|
6683
|
+
if (patch.name?.trim())
|
|
6684
|
+
this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name.trim(), id);
|
|
6685
|
+
if (patch.icon !== undefined) {
|
|
6686
|
+
const icon = (patch.icon ?? "").trim();
|
|
6687
|
+
const isImage = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(icon);
|
|
6688
|
+
if (!isImage && [...icon].length > 4)
|
|
6689
|
+
return;
|
|
6690
|
+
if (isImage && icon.length > 24000)
|
|
6691
|
+
return;
|
|
6692
|
+
this.db.query("UPDATE projects SET icon = ? WHERE id = ?").run(icon || null, id);
|
|
6693
|
+
}
|
|
6694
|
+
if (patch.color !== undefined) {
|
|
6695
|
+
const color = (patch.color ?? "").trim();
|
|
6696
|
+
if (color && !/^c[1-7]$/.test(color))
|
|
6697
|
+
return;
|
|
6698
|
+
this.db.query("UPDATE projects SET color = ? WHERE id = ?").run(color || null, id);
|
|
6699
|
+
}
|
|
6700
|
+
this.touch();
|
|
5574
6701
|
return this.project(id);
|
|
5575
6702
|
}
|
|
5576
6703
|
reorderProjects(ids) {
|
|
@@ -5589,10 +6716,29 @@ class Store {
|
|
|
5589
6716
|
this.touch();
|
|
5590
6717
|
return this.db.query("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
|
|
5591
6718
|
}
|
|
5592
|
-
|
|
6719
|
+
redactions() {
|
|
6720
|
+
const cfg = this.policyFor(null).config.privacy;
|
|
6721
|
+
const key = cfg.redact.join("\x00");
|
|
6722
|
+
if (this.redactCache?.key !== key)
|
|
6723
|
+
this.redactCache = { key, res: compileRedactions(cfg.redact) };
|
|
6724
|
+
return this.redactCache.res;
|
|
6725
|
+
}
|
|
6726
|
+
redactCache = null;
|
|
6727
|
+
append(e0) {
|
|
6728
|
+
const privacy = this.policyFor(null).config.privacy;
|
|
6729
|
+
let e = e0;
|
|
6730
|
+
if (!privacy.store_prompts && e.type === "prompt.submitted" && e.payload && typeof e.payload === "object") {
|
|
6731
|
+
const { prompt: _p, ...rest } = e.payload;
|
|
6732
|
+
e = { ...e, payload: { ...rest, prompt: "[not stored]" } };
|
|
6733
|
+
}
|
|
6734
|
+
const res = this.redactions();
|
|
6735
|
+
if (res.length)
|
|
6736
|
+
e = { ...e, payload: redactValue(e.payload, res), raw: redactValue(e.raw, res) };
|
|
5593
6737
|
const slim = slimForStorage(e);
|
|
5594
|
-
const
|
|
5595
|
-
const
|
|
6738
|
+
const p = e.payload ?? {};
|
|
6739
|
+
const actor2 = e.actor ?? this.actorFor(typeof p.owner === "string" ? p.owner : typeof p.by === "string" ? p.by : null, e.sessionId);
|
|
6740
|
+
const r = this.db.query("INSERT INTO events (ts, type, project_id, session_id, payload, raw, actor_kind, actor_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.ts, e.type, e.projectId, e.sessionId, JSON.stringify(slim.payload ?? null), slim.raw === undefined ? null : JSON.stringify(slim.raw), actor2.kind, actor2.id);
|
|
6741
|
+
const stored = { ...e, actor: actor2, seq: Number(r.lastInsertRowid) };
|
|
5596
6742
|
if (stored.type === "incident.opened")
|
|
5597
6743
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
5598
6744
|
this.projectSession(stored);
|
|
@@ -5602,9 +6748,34 @@ class Store {
|
|
|
5602
6748
|
l(wire);
|
|
5603
6749
|
return stored;
|
|
5604
6750
|
}
|
|
5605
|
-
|
|
5606
|
-
const
|
|
5607
|
-
const
|
|
6751
|
+
audit(opts = {}) {
|
|
6752
|
+
const where = [`type IN (${AUDIT_TYPES_SQL})`];
|
|
6753
|
+
const args = [];
|
|
6754
|
+
if (opts.since) {
|
|
6755
|
+
where.push("ts >= ?");
|
|
6756
|
+
args.push(opts.since);
|
|
6757
|
+
}
|
|
6758
|
+
if (opts.projectId) {
|
|
6759
|
+
where.push("project_id = ?");
|
|
6760
|
+
args.push(opts.projectId);
|
|
6761
|
+
}
|
|
6762
|
+
if (opts.type && isAuditType(opts.type)) {
|
|
6763
|
+
where.push("type = ?");
|
|
6764
|
+
args.push(opts.type);
|
|
6765
|
+
}
|
|
6766
|
+
const limit = Math.min(Math.max(opts.limit ?? 1e4, 1), 1e5);
|
|
6767
|
+
const rows = this.db.query(`SELECT * FROM (SELECT ${WIRE_COLS} FROM events WHERE ${where.join(" AND ")} ORDER BY seq DESC LIMIT ?) ORDER BY seq`).all(...args, limit);
|
|
6768
|
+
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
6769
|
+
}
|
|
6770
|
+
prune(days2) {
|
|
6771
|
+
const cfg = this.policyFor(null).config;
|
|
6772
|
+
const chatter = days2 ?? cfg.events.retain_days;
|
|
6773
|
+
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
6774
|
+
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
6775
|
+
if (cfg.audit.retain_days > 0) {
|
|
6776
|
+
const acut = new Date(Date.now() - cfg.audit.retain_days * 86400000).toISOString();
|
|
6777
|
+
n += this.db.query(`DELETE FROM events WHERE ts < ? AND type IN (${AUDIT_TYPES_SQL}) AND type != 'incident.opened'`).run(acut).changes;
|
|
6778
|
+
}
|
|
5608
6779
|
const old = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
5609
6780
|
this.db.query("UPDATE events SET raw = NULL WHERE ts < ? AND raw IS NOT NULL").run(old);
|
|
5610
6781
|
if (n > 0)
|
|
@@ -5615,10 +6786,10 @@ class Store {
|
|
|
5615
6786
|
if (typeof raw2.cwd === "string")
|
|
5616
6787
|
this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
|
|
5617
6788
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
|
|
5618
|
-
const project =
|
|
6789
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
5619
6790
|
const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
|
|
5620
6791
|
if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
|
|
5621
|
-
if (
|
|
6792
|
+
if (existsSync6(cwd)) {
|
|
5622
6793
|
this.autoHandoff(e.sessionId, cwd);
|
|
5623
6794
|
this.autoGate(event, e.sessionId, cwd);
|
|
5624
6795
|
}
|
|
@@ -5664,7 +6835,7 @@ class Store {
|
|
|
5664
6835
|
return;
|
|
5665
6836
|
const p = e.payload;
|
|
5666
6837
|
const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
|
|
5667
|
-
const branch = p.cwd &&
|
|
6838
|
+
const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
|
|
5668
6839
|
if (!row) {
|
|
5669
6840
|
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);
|
|
5670
6841
|
}
|
|
@@ -5702,13 +6873,15 @@ class Store {
|
|
|
5702
6873
|
}
|
|
5703
6874
|
}
|
|
5704
6875
|
persistTurns(sessionId, agentId, turns) {
|
|
6876
|
+
const privacy = this.policyFor(null).config.privacy;
|
|
6877
|
+
const res = this.redactions();
|
|
5705
6878
|
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)
|
|
5706
6879
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5707
6880
|
ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
|
|
5708
6881
|
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`);
|
|
5709
6882
|
const tx = this.db.transaction((ts) => {
|
|
5710
6883
|
for (const t of ts) {
|
|
5711
|
-
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), t.text, JSON.stringify(t.tools));
|
|
6884
|
+
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));
|
|
5712
6885
|
}
|
|
5713
6886
|
});
|
|
5714
6887
|
if (turns.length)
|
|
@@ -5734,10 +6907,10 @@ class Store {
|
|
|
5734
6907
|
}
|
|
5735
6908
|
tailSession(sessionId) {
|
|
5736
6909
|
const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
|
|
5737
|
-
if (!s?.transcript_path || !
|
|
6910
|
+
if (!s?.transcript_path || !existsSync6(s.transcript_path))
|
|
5738
6911
|
return 0;
|
|
5739
6912
|
let n = this.tailFile(s.transcript_path, sessionId, null);
|
|
5740
|
-
const subDir = join8(
|
|
6913
|
+
const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
|
|
5741
6914
|
for (const f of this.subagentFiles(subDir)) {
|
|
5742
6915
|
n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
|
|
5743
6916
|
}
|
|
@@ -5805,7 +6978,7 @@ class Store {
|
|
|
5805
6978
|
return out;
|
|
5806
6979
|
}
|
|
5807
6980
|
tailCodex(windowMs = 3 * 24 * 60 * 60000) {
|
|
5808
|
-
if (!
|
|
6981
|
+
if (!existsSync6(this.codexRoot()))
|
|
5809
6982
|
return 0;
|
|
5810
6983
|
let n = 0;
|
|
5811
6984
|
for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
|
|
@@ -5817,9 +6990,54 @@ class Store {
|
|
|
5817
6990
|
return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
|
|
5818
6991
|
}
|
|
5819
6992
|
grokSummary = new Map;
|
|
6993
|
+
tailGemini(windowMs = 3 * 24 * 60 * 60000) {
|
|
6994
|
+
const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
|
|
6995
|
+
if (!existsSync6(root))
|
|
6996
|
+
return 0;
|
|
6997
|
+
const since = Date.now() - windowMs;
|
|
6998
|
+
const ls = (p) => {
|
|
6999
|
+
try {
|
|
7000
|
+
return readdirSync(p);
|
|
7001
|
+
} catch {
|
|
7002
|
+
return [];
|
|
7003
|
+
}
|
|
7004
|
+
};
|
|
7005
|
+
let n = 0;
|
|
7006
|
+
const ingestDir = (dir) => {
|
|
7007
|
+
for (const f of ls(dir)) {
|
|
7008
|
+
const path = join8(dir, f);
|
|
7009
|
+
if (!f.endsWith(".jsonl")) {
|
|
7010
|
+
try {
|
|
7011
|
+
if (statSync(path).isDirectory()) {
|
|
7012
|
+
for (const g of ls(path))
|
|
7013
|
+
if (g.endsWith(".jsonl"))
|
|
7014
|
+
ingestFile(join8(path, g));
|
|
7015
|
+
}
|
|
7016
|
+
} catch {}
|
|
7017
|
+
continue;
|
|
7018
|
+
}
|
|
7019
|
+
ingestFile(path);
|
|
7020
|
+
}
|
|
7021
|
+
};
|
|
7022
|
+
const ingestFile = (path) => {
|
|
7023
|
+
try {
|
|
7024
|
+
if (statSync(path).mtimeMs < since)
|
|
7025
|
+
return;
|
|
7026
|
+
} catch {
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
n += this.ingestLog(path, "gemini", parseGeminiChat);
|
|
7030
|
+
};
|
|
7031
|
+
for (const hash of ls(root)) {
|
|
7032
|
+
const chats = join8(root, hash, "chats");
|
|
7033
|
+
if (existsSync6(chats))
|
|
7034
|
+
ingestDir(chats);
|
|
7035
|
+
}
|
|
7036
|
+
return n;
|
|
7037
|
+
}
|
|
5820
7038
|
tailGrok(windowMs = 3 * 24 * 60 * 60000) {
|
|
5821
7039
|
const root = this.grokRoot();
|
|
5822
|
-
if (!
|
|
7040
|
+
if (!existsSync6(root))
|
|
5823
7041
|
return 0;
|
|
5824
7042
|
const since = Date.now() - windowMs;
|
|
5825
7043
|
const ls = (p) => {
|
|
@@ -5842,7 +7060,7 @@ class Store {
|
|
|
5842
7060
|
const cwdDir = join8(root, enc);
|
|
5843
7061
|
for (const sid of ls(cwdDir)) {
|
|
5844
7062
|
const path = join8(cwdDir, sid, "updates.jsonl");
|
|
5845
|
-
if (!
|
|
7063
|
+
if (!existsSync6(path))
|
|
5846
7064
|
continue;
|
|
5847
7065
|
try {
|
|
5848
7066
|
if (statSync(path).mtimeMs < since)
|
|
@@ -5901,9 +7119,9 @@ class Store {
|
|
|
5901
7119
|
ensureAgentSession(sid, agent, cwd, mtime) {
|
|
5902
7120
|
if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
|
|
5903
7121
|
return;
|
|
5904
|
-
const project = cwd &&
|
|
7122
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
5905
7123
|
const ts = new Date(mtime).toISOString();
|
|
5906
|
-
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 &&
|
|
7124
|
+
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);
|
|
5907
7125
|
}
|
|
5908
7126
|
claimRows(projectId) {
|
|
5909
7127
|
return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
|
|
@@ -5939,7 +7157,7 @@ class Store {
|
|
|
5939
7157
|
const p = this.project(projectId);
|
|
5940
7158
|
return join8(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
|
|
5941
7159
|
}
|
|
5942
|
-
claim(projectId, task, owner, baseRef = "HEAD") {
|
|
7160
|
+
claim(projectId, task, owner, baseRef = "HEAD", sessionId = null) {
|
|
5943
7161
|
const p = this.project(projectId);
|
|
5944
7162
|
if (!p)
|
|
5945
7163
|
return { ok: false, error: "unknown project" };
|
|
@@ -5949,24 +7167,26 @@ class Store {
|
|
|
5949
7167
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
5950
7168
|
const branch = `task/${task}`;
|
|
5951
7169
|
const worktree2 = this.worktreePath(projectId, task);
|
|
5952
|
-
if (
|
|
7170
|
+
if (existsSync6(worktree2))
|
|
5953
7171
|
return { ok: false, error: `${worktree2} already exists; release ${task} first` };
|
|
5954
|
-
mkdirSync4(
|
|
7172
|
+
mkdirSync4(dirname3(worktree2), { recursive: true });
|
|
5955
7173
|
const created = worktreeAdd(p.root, worktree2, branch, baseRef);
|
|
5956
7174
|
if (!created)
|
|
5957
7175
|
return { ok: false, error: `git worktree add failed for ${task}` };
|
|
5958
7176
|
this.invalidateWorktrees(projectId);
|
|
5959
7177
|
const expiresAt = nextExpiry(now);
|
|
5960
7178
|
const acquiredAt = new Date(now).toISOString();
|
|
5961
|
-
this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state)
|
|
5962
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held')
|
|
7179
|
+
this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
7180
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held', ?, ?)
|
|
5963
7181
|
ON CONFLICT(project_id, task) DO UPDATE SET owner=excluded.owner, worktree=excluded.worktree, branch=excluded.branch,
|
|
5964
|
-
acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held'
|
|
7182
|
+
acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held',
|
|
7183
|
+
actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(projectId, task, owner, created, branch, acquiredAt, expiresAt, ...actorCols(this.actorFor(owner, sessionId)));
|
|
5965
7184
|
this.append({
|
|
5966
7185
|
ts: acquiredAt,
|
|
5967
7186
|
type: "claim.acquired",
|
|
5968
7187
|
projectId,
|
|
5969
|
-
sessionId
|
|
7188
|
+
sessionId,
|
|
7189
|
+
actor: this.actorFor(owner, sessionId),
|
|
5970
7190
|
payload: { task, owner, worktree: created, branch, summary: `claim ${task} by ${owner}` }
|
|
5971
7191
|
});
|
|
5972
7192
|
const bootstrap = this.bootstrapWorktree(projectId, task, p.root, created);
|
|
@@ -6059,7 +7279,7 @@ class Store {
|
|
|
6059
7279
|
for (const c of this.claimRows(p.id)) {
|
|
6060
7280
|
if (c.state !== "held" || isActive(c, now))
|
|
6061
7281
|
continue;
|
|
6062
|
-
const exists = c.worktree ?
|
|
7282
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6063
7283
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6064
7284
|
if (reapAction(c, now, exists, work) !== "keep-orphaned")
|
|
6065
7285
|
continue;
|
|
@@ -6115,7 +7335,7 @@ class Store {
|
|
|
6115
7335
|
if (!row)
|
|
6116
7336
|
return { ok: false, error: `no claim on ${task}` };
|
|
6117
7337
|
const worktree2 = row.worktree ?? "";
|
|
6118
|
-
if (worktree2 &&
|
|
7338
|
+
if (worktree2 && existsSync6(worktree2)) {
|
|
6119
7339
|
const work = heldWork(worktree2);
|
|
6120
7340
|
const can = canRelease(work, force);
|
|
6121
7341
|
if (!can.ok)
|
|
@@ -6150,7 +7370,7 @@ class Store {
|
|
|
6150
7370
|
continue;
|
|
6151
7371
|
if (isActive({ ...c, state: "held" }, now))
|
|
6152
7372
|
continue;
|
|
6153
|
-
const exists = c.worktree ?
|
|
7373
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6154
7374
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6155
7375
|
const action = reapAction({ ...c, state: "held" }, now, exists, work);
|
|
6156
7376
|
if (action === "not-expired")
|
|
@@ -6249,9 +7469,9 @@ class Store {
|
|
|
6249
7469
|
if (!slug || slug === "." || slug === "..")
|
|
6250
7470
|
return { ok: false, error: "bad worktree name" };
|
|
6251
7471
|
const path = this.worktreePath(projectId, slug);
|
|
6252
|
-
if (
|
|
7472
|
+
if (existsSync6(path))
|
|
6253
7473
|
return { ok: false, error: `${path} already exists` };
|
|
6254
|
-
mkdirSync4(
|
|
7474
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
6255
7475
|
const br = branch?.trim() || `wt/${slug}`;
|
|
6256
7476
|
const created = worktreeAdd(p.root, path, br, baseRef);
|
|
6257
7477
|
if (!created)
|
|
@@ -6642,19 +7862,19 @@ class Store {
|
|
|
6642
7862
|
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
|
|
6643
7863
|
return r.n;
|
|
6644
7864
|
}
|
|
6645
|
-
ackIncident(seq) {
|
|
7865
|
+
ackIncident(seq, by) {
|
|
6646
7866
|
const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
|
|
6647
7867
|
if (!row)
|
|
6648
7868
|
return false;
|
|
6649
|
-
this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at) VALUES (?, ?)").run(seq, new Date().toISOString());
|
|
7869
|
+
this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id) VALUES (?, ?, ?, ?)").run(seq, new Date().toISOString(), ...actorCols(this.actorFor(by ?? "dashboard")));
|
|
6650
7870
|
this.touch();
|
|
6651
7871
|
return true;
|
|
6652
7872
|
}
|
|
6653
|
-
ackAllIncidents(projectId) {
|
|
7873
|
+
ackAllIncidents(projectId, by) {
|
|
6654
7874
|
const at = new Date().toISOString();
|
|
6655
|
-
const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at)
|
|
6656
|
-
SELECT e.seq, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
6657
|
-
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...projectId ? [projectId] : []);
|
|
7875
|
+
const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id)
|
|
7876
|
+
SELECT e.seq, ?, ?, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
7877
|
+
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...actorCols(this.actorFor(by ?? "dashboard")), ...projectId ? [projectId] : []);
|
|
6658
7878
|
this.touch();
|
|
6659
7879
|
return Number(r.changes);
|
|
6660
7880
|
}
|
|
@@ -6821,8 +8041,8 @@ class Store {
|
|
|
6821
8041
|
endedAt: null
|
|
6822
8042
|
};
|
|
6823
8043
|
this.db.query("UPDATE processes SET ended_at = ? WHERE ended_at IS NULL AND project_id = ? AND name = ?").run(p.startedAt, p.projectId, p.name);
|
|
6824
|
-
this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at)
|
|
6825
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt);
|
|
8044
|
+
this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at, actor_kind, actor_id)
|
|
8045
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt, ...actorCols(this.actorFor(p.owner, p.sessionId)));
|
|
6826
8046
|
this.append({
|
|
6827
8047
|
ts: p.startedAt,
|
|
6828
8048
|
type: "process.started",
|
|
@@ -6886,11 +8106,12 @@ class Store {
|
|
|
6886
8106
|
expiresAt,
|
|
6887
8107
|
released: false
|
|
6888
8108
|
};
|
|
6889
|
-
this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released)
|
|
6890
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
8109
|
+
this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released, actor_kind, actor_id)
|
|
8110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
6891
8111
|
ON CONFLICT(name, project_id) DO UPDATE SET
|
|
6892
8112
|
kind=excluded.kind, owner=excluded.owner, session_id=excluded.session_id, pid=excluded.pid,
|
|
6893
|
-
port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0
|
|
8113
|
+
port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0,
|
|
8114
|
+
actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(resource.name, key, resource.kind, resource.owner, resource.sessionId, resource.pid, resource.port, resource.acquiredAt, resource.expiresAt, ...actorCols(this.actorFor(resource.owner, resource.sessionId)));
|
|
6894
8115
|
this.append({
|
|
6895
8116
|
ts: resource.acquiredAt,
|
|
6896
8117
|
type: "resource.acquired",
|
|
@@ -6935,9 +8156,51 @@ class Store {
|
|
|
6935
8156
|
seq() {
|
|
6936
8157
|
return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
|
|
6937
8158
|
}
|
|
8159
|
+
timelineDetail(hours, projectId) {
|
|
8160
|
+
const from = new Date(Date.now() - Math.min(Math.max(hours, 1), 168) * 3600000).toISOString();
|
|
8161
|
+
const args = [from];
|
|
8162
|
+
let filter = "";
|
|
8163
|
+
if (projectId) {
|
|
8164
|
+
filter = " AND s.project_id = ?";
|
|
8165
|
+
args.push(projectId);
|
|
8166
|
+
}
|
|
8167
|
+
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
|
|
8168
|
+
WHERE t.ts >= ? AND t.sidechain = 0${filter} ORDER BY t.ts LIMIT 20000`).all(...args);
|
|
8169
|
+
const turns = {};
|
|
8170
|
+
for (const r of rows) {
|
|
8171
|
+
turns[r.sid] ??= [];
|
|
8172
|
+
turns[r.sid]?.push(new Date(r.ts).getTime());
|
|
8173
|
+
}
|
|
8174
|
+
const claims = this.claims().filter((c) => (!projectId || c.projectId === projectId) && c.state !== "released").map((c) => ({
|
|
8175
|
+
projectId: c.projectId,
|
|
8176
|
+
task: c.task,
|
|
8177
|
+
owner: c.owner,
|
|
8178
|
+
state: c.state,
|
|
8179
|
+
acquiredAt: c.acquiredAt,
|
|
8180
|
+
expiresAt: c.expiresAt
|
|
8181
|
+
}));
|
|
8182
|
+
return { turns, claims };
|
|
8183
|
+
}
|
|
8184
|
+
spendSparks() {
|
|
8185
|
+
const from = localDayIso(-13);
|
|
8186
|
+
const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
|
|
8187
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
8188
|
+
const days2 = [];
|
|
8189
|
+
for (let i = 13;i >= 0; i--)
|
|
8190
|
+
days2.push(localDayIso(-i).slice(0, 10));
|
|
8191
|
+
const out = {};
|
|
8192
|
+
for (const r of rows) {
|
|
8193
|
+
out[r.pid] ??= new Array(14).fill(0);
|
|
8194
|
+
const arr = out[r.pid];
|
|
8195
|
+
const i = days2.indexOf(r.day);
|
|
8196
|
+
if (i >= 0)
|
|
8197
|
+
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
8198
|
+
}
|
|
8199
|
+
return out;
|
|
8200
|
+
}
|
|
6938
8201
|
snapshot() {
|
|
6939
8202
|
const worktrees = {};
|
|
6940
|
-
const projects = this.projects();
|
|
8203
|
+
const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
|
|
6941
8204
|
for (const p of projects)
|
|
6942
8205
|
worktrees[p.id] = this.worktrees(p.id);
|
|
6943
8206
|
return {
|
|
@@ -6945,6 +8208,7 @@ class Store {
|
|
|
6945
8208
|
worktrees,
|
|
6946
8209
|
sessions: this.memoised("sessions", 2000, () => this.sessions()),
|
|
6947
8210
|
spend: this.memoised("spend", 30000, () => this.spend()),
|
|
8211
|
+
spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
|
|
6948
8212
|
claims: this.claims(),
|
|
6949
8213
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
6950
8214
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
@@ -6955,7 +8219,7 @@ class Store {
|
|
|
6955
8219
|
};
|
|
6956
8220
|
}
|
|
6957
8221
|
}
|
|
6958
|
-
var WIRE_COLS = "seq, ts, type, project_id, session_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
8222
|
+
var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
6959
8223
|
var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
|
|
6960
8224
|
var TOOL_INPUT_MAX = 2048;
|
|
6961
8225
|
var TOOL_RESPONSE_MAX = 4096;
|
|
@@ -7001,7 +8265,7 @@ function toWire(e) {
|
|
|
7001
8265
|
}
|
|
7002
8266
|
function wireRowToEvent(r) {
|
|
7003
8267
|
const p = JSON.parse(r.payload ?? "null");
|
|
7004
|
-
|
|
8268
|
+
const e = {
|
|
7005
8269
|
seq: r.seq,
|
|
7006
8270
|
ts: r.ts,
|
|
7007
8271
|
type: r.type,
|
|
@@ -7009,6 +8273,10 @@ function wireRowToEvent(r) {
|
|
|
7009
8273
|
sessionId: r.session_id ?? null,
|
|
7010
8274
|
payload: p
|
|
7011
8275
|
};
|
|
8276
|
+
const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
|
|
8277
|
+
if (a)
|
|
8278
|
+
e.actor = a;
|
|
8279
|
+
return e;
|
|
7012
8280
|
}
|
|
7013
8281
|
function rowToEvent(r) {
|
|
7014
8282
|
const e = {
|
|
@@ -7021,17 +8289,259 @@ function rowToEvent(r) {
|
|
|
7021
8289
|
};
|
|
7022
8290
|
if (r.raw)
|
|
7023
8291
|
e.raw = JSON.parse(r.raw);
|
|
8292
|
+
const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
|
|
8293
|
+
if (a)
|
|
8294
|
+
e.actor = a;
|
|
7024
8295
|
return e;
|
|
7025
8296
|
}
|
|
8297
|
+
function osUser() {
|
|
8298
|
+
try {
|
|
8299
|
+
return userInfo().username || process.env.USER || "me";
|
|
8300
|
+
} catch {
|
|
8301
|
+
return process.env.USER || "me";
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8304
|
+
var actorCols = (a) => [a.kind, a.id];
|
|
8305
|
+
function isScratchRoot(root) {
|
|
8306
|
+
const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
|
|
8307
|
+
return tmp.some((t) => root === t || root.startsWith(`${t}/`));
|
|
8308
|
+
}
|
|
8309
|
+
function rowToMessage(r) {
|
|
8310
|
+
return {
|
|
8311
|
+
id: r.id,
|
|
8312
|
+
projectId: r.project_id,
|
|
8313
|
+
task: r.task ?? null,
|
|
8314
|
+
sessionId: r.session_id ?? null,
|
|
8315
|
+
toKind: r.to_kind ?? "session",
|
|
8316
|
+
from: r.asked_by ?? null,
|
|
8317
|
+
fromSession: r.from_session ?? null,
|
|
8318
|
+
text: r.text,
|
|
8319
|
+
createdAt: r.created_at,
|
|
8320
|
+
deliveredAt: r.delivered_at ?? null
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
function rowToWorkflowRun(r) {
|
|
8324
|
+
return {
|
|
8325
|
+
id: r.id,
|
|
8326
|
+
projectId: r.project_id,
|
|
8327
|
+
task: r.task,
|
|
8328
|
+
workflow: r.workflow,
|
|
8329
|
+
step: r.step,
|
|
8330
|
+
stepLabel: r.step_label ?? "",
|
|
8331
|
+
steps: JSON.parse(r.steps ?? "[]"),
|
|
8332
|
+
state: r.state,
|
|
8333
|
+
detail: r.detail ?? null,
|
|
8334
|
+
runId: r.run_id ?? null,
|
|
8335
|
+
startedAt: r.started_at,
|
|
8336
|
+
updatedAt: r.updated_at,
|
|
8337
|
+
endedAt: r.ended_at ?? null
|
|
8338
|
+
};
|
|
8339
|
+
}
|
|
8340
|
+
function localDayIso(offsetDays) {
|
|
8341
|
+
const d = new Date;
|
|
8342
|
+
d.setHours(0, 0, 0, 0);
|
|
8343
|
+
d.setDate(d.getDate() + offsetDays);
|
|
8344
|
+
return d.toISOString();
|
|
8345
|
+
}
|
|
8346
|
+
|
|
8347
|
+
// packages/daemon/src/workflow.ts
|
|
8348
|
+
class WorkflowEngine {
|
|
8349
|
+
store;
|
|
8350
|
+
runner;
|
|
8351
|
+
forge;
|
|
8352
|
+
active = new Map;
|
|
8353
|
+
constructor(store, runner, forge2) {
|
|
8354
|
+
this.store = store;
|
|
8355
|
+
this.runner = runner;
|
|
8356
|
+
this.forge = forge2;
|
|
8357
|
+
store.wfSweepOrphans();
|
|
8358
|
+
runner.onEnd((run2) => void this.onRunEnd(run2));
|
|
8359
|
+
}
|
|
8360
|
+
start(projectId, task, workflow, opts = {}) {
|
|
8361
|
+
const def = this.store.config(projectId).workflows[workflow];
|
|
8362
|
+
if (!def) {
|
|
8363
|
+
const known = Object.keys(this.store.config(projectId).workflows);
|
|
8364
|
+
return {
|
|
8365
|
+
ok: false,
|
|
8366
|
+
error: `unknown workflow ${workflow}${known.length ? ` \u2014 this repo declares: ${known.join(", ")}` : " \u2014 declare [[workflows]] in .swarm.toml"}`
|
|
8367
|
+
};
|
|
8368
|
+
}
|
|
8369
|
+
const key = `${projectId}:${task}`;
|
|
8370
|
+
if (this.active.has(key) || this.store.wfActive(projectId, task))
|
|
8371
|
+
return { ok: false, error: `a workflow is already running on ${task}` };
|
|
8372
|
+
const title = this.store.tasks(projectId)?.tasks.find((t) => t.id === task)?.title ?? task;
|
|
8373
|
+
const owner = opts.owner ?? "workflow";
|
|
8374
|
+
const id = this.store.wfInsert(projectId, task, workflow, def.steps.map(stepLabel), this.store.actorFor(owner, opts.sessionId ?? null));
|
|
8375
|
+
const w = { id, projectId, task, title, def, step: 0, runId: null, owner };
|
|
8376
|
+
this.active.set(key, w);
|
|
8377
|
+
this.store.append({
|
|
8378
|
+
ts: new Date().toISOString(),
|
|
8379
|
+
type: "workflow.started",
|
|
8380
|
+
projectId,
|
|
8381
|
+
sessionId: opts.sessionId ?? null,
|
|
8382
|
+
payload: {
|
|
8383
|
+
id,
|
|
8384
|
+
task,
|
|
8385
|
+
workflow,
|
|
8386
|
+
steps: def.steps.map(stepLabel),
|
|
8387
|
+
summary: `workflow ${workflow} on ${task}: ${def.steps.map(stepLabel).join(" \u2192 ")}`
|
|
8388
|
+
}
|
|
8389
|
+
});
|
|
8390
|
+
this.advance(w);
|
|
8391
|
+
return { ok: true, id };
|
|
8392
|
+
}
|
|
8393
|
+
status(projectId) {
|
|
8394
|
+
return this.store.wfRuns(projectId);
|
|
8395
|
+
}
|
|
8396
|
+
stop(projectId, task) {
|
|
8397
|
+
const key = `${projectId}:${task}`;
|
|
8398
|
+
const w = this.active.get(key);
|
|
8399
|
+
if (!w)
|
|
8400
|
+
return { ok: false, error: `no running workflow on ${task}` };
|
|
8401
|
+
if (w.runId)
|
|
8402
|
+
this.runner.stop(w.runId);
|
|
8403
|
+
this.finish(w, "stopped", `stopped at ${this.label(w)}`);
|
|
8404
|
+
return { ok: true };
|
|
8405
|
+
}
|
|
8406
|
+
label(w) {
|
|
8407
|
+
const s = w.def.steps[w.step];
|
|
8408
|
+
return s ? stepLabel(s) : "done";
|
|
8409
|
+
}
|
|
8410
|
+
async advance(w) {
|
|
8411
|
+
while (w.step < w.def.steps.length) {
|
|
8412
|
+
const s = w.def.steps[w.step];
|
|
8413
|
+
this.store.wfUpdate(w.id, { step: w.step, stepLabel: stepLabel(s), runId: null });
|
|
8414
|
+
this.step(w, `step ${w.step + 1}/${w.def.steps.length}: ${stepLabel(s)}`);
|
|
8415
|
+
if (s.kind === "run") {
|
|
8416
|
+
const cfg = this.store.config(w.projectId).dispatch;
|
|
8417
|
+
const remaining = w.def.steps.slice(w.step + 1).map(stepLabel);
|
|
8418
|
+
const r = await this.runner.start({
|
|
8419
|
+
projectId: w.projectId,
|
|
8420
|
+
task: w.task,
|
|
8421
|
+
prompt: workflowStepPrompt(s, { id: w.task, title: w.title }, { workflow: w.def.name, remaining }),
|
|
8422
|
+
owner: w.owner,
|
|
8423
|
+
permissionMode: cfg.permission_mode ?? "acceptEdits",
|
|
8424
|
+
model: cfg.model ?? undefined,
|
|
8425
|
+
maxTurns: cfg.max_turns ?? undefined,
|
|
8426
|
+
profile: cfg.profile ?? undefined
|
|
8427
|
+
});
|
|
8428
|
+
if (!r.ok)
|
|
8429
|
+
return this.fail(w, `could not start ${stepLabel(s)}: ${r.reason}`);
|
|
8430
|
+
w.runId = r.run.id;
|
|
8431
|
+
this.store.wfUpdate(w.id, { runId: r.run.id });
|
|
8432
|
+
return;
|
|
8433
|
+
}
|
|
8434
|
+
if (s.kind === "gate") {
|
|
8435
|
+
const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
|
|
8436
|
+
const run2 = r.runs.find((x) => x.gate === s.gate);
|
|
8437
|
+
if (!run2)
|
|
8438
|
+
return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
|
|
8439
|
+
if (run2.verdict !== "pass")
|
|
8440
|
+
return this.fail(w, `gate ${s.gate} failed \u2014 ${run2.rubric}`);
|
|
8441
|
+
w.step++;
|
|
8442
|
+
continue;
|
|
8443
|
+
}
|
|
8444
|
+
const d = await this.store.prDraftFor(w.projectId, w.task);
|
|
8445
|
+
if (!d.ok)
|
|
8446
|
+
return this.fail(w, `pr: ${d.error}`);
|
|
8447
|
+
const pr = await this.forge.openPR(w.projectId, d.worktree, {
|
|
8448
|
+
title: d.title,
|
|
8449
|
+
body: d.body,
|
|
8450
|
+
isDraft: false
|
|
8451
|
+
});
|
|
8452
|
+
if (!pr.ok)
|
|
8453
|
+
return this.fail(w, `pr: ${pr.error}`);
|
|
8454
|
+
this.store.recordPrOpened(w.projectId, d.task, d.worktree.path, pr.url);
|
|
8455
|
+
this.store.wfUpdate(w.id, { detail: `PR ${pr.url}` });
|
|
8456
|
+
w.step++;
|
|
8457
|
+
}
|
|
8458
|
+
this.finish(w, "done", null);
|
|
8459
|
+
}
|
|
8460
|
+
async onRunEnd(run2) {
|
|
8461
|
+
const w = this.active.get(`${run2.projectId}:${run2.task}`);
|
|
8462
|
+
if (!w || w.runId !== run2.id)
|
|
8463
|
+
return;
|
|
8464
|
+
w.runId = null;
|
|
8465
|
+
if (run2.stopped)
|
|
8466
|
+
return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
|
|
8467
|
+
if (run2.exitCode !== 0 || run2.result?.isError)
|
|
8468
|
+
return this.fail(w, `${this.label(w)} exited ${run2.exitCode}${run2.result?.isError ? " (error)" : ""} \u2014 log: ${run2.log}`);
|
|
8469
|
+
w.step++;
|
|
8470
|
+
this.advance(w);
|
|
8471
|
+
}
|
|
8472
|
+
step(w, summary) {
|
|
8473
|
+
this.store.append({
|
|
8474
|
+
ts: new Date().toISOString(),
|
|
8475
|
+
type: "workflow.step",
|
|
8476
|
+
projectId: w.projectId,
|
|
8477
|
+
sessionId: null,
|
|
8478
|
+
payload: {
|
|
8479
|
+
id: w.id,
|
|
8480
|
+
task: w.task,
|
|
8481
|
+
workflow: w.def.name,
|
|
8482
|
+
step: w.step,
|
|
8483
|
+
label: this.label(w),
|
|
8484
|
+
summary: `workflow ${w.def.name} on ${w.task} \u2014 ${summary}`
|
|
8485
|
+
}
|
|
8486
|
+
});
|
|
8487
|
+
}
|
|
8488
|
+
fail(w, detail) {
|
|
8489
|
+
this.store.wfUpdate(w.id, { state: "failed", detail, ended: true });
|
|
8490
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
8491
|
+
this.store.append({
|
|
8492
|
+
ts: new Date().toISOString(),
|
|
8493
|
+
type: "workflow.finished",
|
|
8494
|
+
projectId: w.projectId,
|
|
8495
|
+
sessionId: null,
|
|
8496
|
+
payload: {
|
|
8497
|
+
id: w.id,
|
|
8498
|
+
task: w.task,
|
|
8499
|
+
workflow: w.def.name,
|
|
8500
|
+
outcome: "failed",
|
|
8501
|
+
detail,
|
|
8502
|
+
summary: `workflow ${w.def.name} on ${w.task} failed at ${this.label(w)}: ${detail.slice(0, 160)}`
|
|
8503
|
+
}
|
|
8504
|
+
});
|
|
8505
|
+
this.store.append({
|
|
8506
|
+
ts: new Date().toISOString(),
|
|
8507
|
+
type: "incident.opened",
|
|
8508
|
+
projectId: w.projectId,
|
|
8509
|
+
sessionId: null,
|
|
8510
|
+
payload: {
|
|
8511
|
+
rule: "workflow_failed",
|
|
8512
|
+
action: "failed",
|
|
8513
|
+
command: `${w.task} \xB7 ${w.def.name} \xB7 ${this.label(w)}`,
|
|
8514
|
+
reason: detail.slice(0, 400)
|
|
8515
|
+
}
|
|
8516
|
+
});
|
|
8517
|
+
}
|
|
8518
|
+
finish(w, state, detail) {
|
|
8519
|
+
this.store.wfUpdate(w.id, { state, ...detail !== null ? { detail } : {}, ended: true });
|
|
8520
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
8521
|
+
this.store.append({
|
|
8522
|
+
ts: new Date().toISOString(),
|
|
8523
|
+
type: "workflow.finished",
|
|
8524
|
+
projectId: w.projectId,
|
|
8525
|
+
sessionId: null,
|
|
8526
|
+
payload: {
|
|
8527
|
+
id: w.id,
|
|
8528
|
+
task: w.task,
|
|
8529
|
+
workflow: w.def.name,
|
|
8530
|
+
outcome: state,
|
|
8531
|
+
summary: `workflow ${w.def.name} on ${w.task}: ${state}${detail ? ` \u2014 ${detail}` : ""}`
|
|
8532
|
+
}
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
7026
8536
|
|
|
7027
8537
|
// packages/daemon/src/app.ts
|
|
7028
|
-
var VERSION = "0.
|
|
8538
|
+
var VERSION = "0.9.0";
|
|
7029
8539
|
var WEB_DIR = (() => {
|
|
7030
8540
|
if (process.env.SWARM_WEB_DIR)
|
|
7031
8541
|
return process.env.SWARM_WEB_DIR;
|
|
7032
|
-
const here =
|
|
8542
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
7033
8543
|
const dev = join9(here, "../../web/public");
|
|
7034
|
-
return
|
|
8544
|
+
return existsSync7(join9(dev, "index.html")) ? dev : join9(here, "../web");
|
|
7035
8545
|
})();
|
|
7036
8546
|
var REPLAY_TAIL = 200;
|
|
7037
8547
|
var wireCache = new WeakMap;
|
|
@@ -7043,17 +8553,69 @@ function wireJson(e) {
|
|
|
7043
8553
|
}
|
|
7044
8554
|
return s;
|
|
7045
8555
|
}
|
|
7046
|
-
function
|
|
8556
|
+
function hookRepoRoot(store, raw2) {
|
|
8557
|
+
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
|
|
8558
|
+
return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
|
|
8559
|
+
}
|
|
8560
|
+
function claudeSettings() {
|
|
8561
|
+
try {
|
|
8562
|
+
const p = process.env.CLAUDE_SETTINGS ?? join9(homedir4(), ".claude", "settings.json");
|
|
8563
|
+
return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
|
|
8564
|
+
} catch {
|
|
8565
|
+
return null;
|
|
8566
|
+
}
|
|
8567
|
+
}
|
|
8568
|
+
function diskVersion() {
|
|
8569
|
+
try {
|
|
8570
|
+
const entry = daemonCommand().at(-1);
|
|
8571
|
+
if (!entry || !existsSync7(entry))
|
|
8572
|
+
return null;
|
|
8573
|
+
for (const f of [entry, join9(dirname4(entry), "app.ts")]) {
|
|
8574
|
+
if (!existsSync7(f))
|
|
8575
|
+
continue;
|
|
8576
|
+
const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
|
|
8577
|
+
if (m?.[1])
|
|
8578
|
+
return m[1];
|
|
8579
|
+
}
|
|
8580
|
+
return null;
|
|
8581
|
+
} catch {
|
|
8582
|
+
return null;
|
|
8583
|
+
}
|
|
8584
|
+
}
|
|
8585
|
+
function createApp(store = new Store, hooks2 = {}) {
|
|
7047
8586
|
const app = new Hono2;
|
|
7048
8587
|
const forge2 = new ForgeService(store);
|
|
7049
8588
|
const runner = new Runner(store, store.home);
|
|
7050
8589
|
const dispatcher = new Dispatcher(store, runner, forge2);
|
|
8590
|
+
const workflows2 = new WorkflowEngine(store, runner, forge2);
|
|
7051
8591
|
store.onBudgetStop((projectId) => {
|
|
7052
8592
|
dispatcher.clear(projectId);
|
|
7053
8593
|
for (const run2 of runner.list(projectId))
|
|
7054
8594
|
runner.stop(run2.id);
|
|
7055
8595
|
});
|
|
7056
|
-
app.
|
|
8596
|
+
app.use("/v1/*", async (c, next) => {
|
|
8597
|
+
if (c.req.path === "/v1/health")
|
|
8598
|
+
return next();
|
|
8599
|
+
const token = readToken(store.home);
|
|
8600
|
+
const given = c.req.header("authorization")?.replace(/^Bearer\s+/i, "") ?? c.req.query("token");
|
|
8601
|
+
if (token && given && given === token)
|
|
8602
|
+
return next();
|
|
8603
|
+
if (given)
|
|
8604
|
+
return c.json({ error: "unauthorized: wrong daemon token" }, 401);
|
|
8605
|
+
const ip = c.env?.requestIP?.(c.req.raw)?.address;
|
|
8606
|
+
const loopback = !ip || ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
8607
|
+
if (loopback && store.policyFor(null).config.daemon.auth !== "required")
|
|
8608
|
+
return next();
|
|
8609
|
+
return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
|
|
8610
|
+
});
|
|
8611
|
+
app.get("/v1/health", (c) => c.json({
|
|
8612
|
+
disk: diskVersion(),
|
|
8613
|
+
hooksInstalled: hookCoverage(claudeSettings()).complete,
|
|
8614
|
+
ok: true,
|
|
8615
|
+
version: VERSION,
|
|
8616
|
+
schema: store.schemaVersion(),
|
|
8617
|
+
auth: store.policyFor(null).config.daemon.auth
|
|
8618
|
+
}));
|
|
7057
8619
|
app.get("/v1/projects", (c) => c.json(store.snapshot().projects));
|
|
7058
8620
|
app.post("/v1/projects", async (c) => {
|
|
7059
8621
|
const { path, name } = await c.req.json();
|
|
@@ -7072,22 +8634,24 @@ function createApp(store = new Store) {
|
|
|
7072
8634
|
return c.json(store.reorderProjects(ids));
|
|
7073
8635
|
});
|
|
7074
8636
|
app.patch("/v1/projects/:id", async (c) => {
|
|
7075
|
-
const { pinned, name } = await c.req.json().catch(() => ({}));
|
|
7076
|
-
|
|
7077
|
-
|
|
8637
|
+
const { pinned, name, icon, color } = await c.req.json().catch(() => ({}));
|
|
8638
|
+
if (!store.project(c.req.param("id")))
|
|
8639
|
+
return c.json({ error: "not found" }, 404);
|
|
8640
|
+
const p = store.updateProject(c.req.param("id"), { pinned, name, icon, color });
|
|
8641
|
+
return p ? c.json(p) : c.json({ error: "icon is at most 4 characters; color is c1\u2026c7" }, 400);
|
|
7078
8642
|
});
|
|
7079
8643
|
app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
|
|
7080
8644
|
app.get("/v1/fs/ls", (c) => {
|
|
7081
8645
|
const q = c.req.query("path");
|
|
7082
8646
|
let dir;
|
|
7083
8647
|
try {
|
|
7084
|
-
dir = realpathSync3(q &&
|
|
8648
|
+
dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
|
|
7085
8649
|
} catch {
|
|
7086
8650
|
dir = homedir4();
|
|
7087
8651
|
}
|
|
7088
8652
|
try {
|
|
7089
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo:
|
|
7090
|
-
const parent =
|
|
8653
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join9(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
8654
|
+
const parent = dirname4(dir);
|
|
7091
8655
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
7092
8656
|
} catch (e) {
|
|
7093
8657
|
return c.json({ error: e.message, path: dir }, 400);
|
|
@@ -7112,6 +8676,37 @@ function createApp(store = new Store) {
|
|
|
7112
8676
|
})
|
|
7113
8677
|
});
|
|
7114
8678
|
});
|
|
8679
|
+
app.get("/v1/policy", (c) => {
|
|
8680
|
+
const id = c.req.query("project");
|
|
8681
|
+
const p = id ? store.project(id) : null;
|
|
8682
|
+
if (id && !p)
|
|
8683
|
+
return c.json({ error: "unknown project" }, 404);
|
|
8684
|
+
const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
8685
|
+
return c.json({ ...policy2, provenance, overridden });
|
|
8686
|
+
});
|
|
8687
|
+
app.get("/v1/audit", (c) => {
|
|
8688
|
+
const since = sinceToIso(c.req.query("since"));
|
|
8689
|
+
if (c.req.query("since") && !since)
|
|
8690
|
+
return c.json({ error: "since: use 30d / 12h / 90m or an ISO date" }, 400);
|
|
8691
|
+
const pid = c.req.query("project") || null;
|
|
8692
|
+
if (pid && !store.project(pid))
|
|
8693
|
+
return c.json({ error: "unknown project" }, 404);
|
|
8694
|
+
const rows = store.audit({
|
|
8695
|
+
since,
|
|
8696
|
+
projectId: pid,
|
|
8697
|
+
type: c.req.query("type") || null,
|
|
8698
|
+
limit: Number(c.req.query("limit")) || undefined
|
|
8699
|
+
});
|
|
8700
|
+
const format = c.req.query("format") === "csv" ? "csv" : c.req.query("format") === "jsonl" ? "jsonl" : "json";
|
|
8701
|
+
const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
|
|
8702
|
+
return c.body(formatAudit(rows, format), 200, { "content-type": ct });
|
|
8703
|
+
});
|
|
8704
|
+
app.post("/v1/daemon/restart", (c) => {
|
|
8705
|
+
if (!hooks2.restart)
|
|
8706
|
+
return c.json({ error: "not restartable in this environment" }, 501);
|
|
8707
|
+
setTimeout(() => hooks2.restart?.(), 50);
|
|
8708
|
+
return c.json({ ok: true, restarting: true });
|
|
8709
|
+
});
|
|
7115
8710
|
app.get("/v1/rules/dryrun", (c) => {
|
|
7116
8711
|
const projectId = c.req.query("project");
|
|
7117
8712
|
if (!projectId)
|
|
@@ -7125,11 +8720,11 @@ function createApp(store = new Store) {
|
|
|
7125
8720
|
});
|
|
7126
8721
|
app.post("/v1/incidents/ack", async (c) => {
|
|
7127
8722
|
const body = await c.req.json().catch(() => ({}));
|
|
7128
|
-
return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined) });
|
|
8723
|
+
return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined, body.by) });
|
|
7129
8724
|
});
|
|
7130
8725
|
app.post("/v1/incidents/:seq/ack", (c) => {
|
|
7131
8726
|
const seq = Number(c.req.param("seq"));
|
|
7132
|
-
if (!Number.isInteger(seq) || !store.ackIncident(seq))
|
|
8727
|
+
if (!Number.isInteger(seq) || !store.ackIncident(seq, c.req.query("by")))
|
|
7133
8728
|
return c.json({ ok: false, error: "no such incident" }, 404);
|
|
7134
8729
|
return c.json({ ok: true });
|
|
7135
8730
|
});
|
|
@@ -7246,6 +8841,58 @@ function createApp(store = new Store) {
|
|
|
7246
8841
|
return c.json(r, r.ok ? 200 : 409);
|
|
7247
8842
|
});
|
|
7248
8843
|
app.get("/v1/inbox", (c) => c.json(store.inbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
8844
|
+
app.get("/v1/workflows", (c) => {
|
|
8845
|
+
const project = c.req.query("project");
|
|
8846
|
+
if (!project)
|
|
8847
|
+
return c.json({ error: "project required" }, 400);
|
|
8848
|
+
return c.json({ defs: store.config(project).workflows, runs: workflows2.status(project) });
|
|
8849
|
+
});
|
|
8850
|
+
app.post("/v1/workflows", async (c) => {
|
|
8851
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8852
|
+
if (!b.projectId || !b.task || !b.workflow)
|
|
8853
|
+
return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
|
|
8854
|
+
const r = workflows2.start(b.projectId, b.task, b.workflow, {
|
|
8855
|
+
...b.owner ? { owner: b.owner } : {},
|
|
8856
|
+
sessionId: b.sessionId ?? null
|
|
8857
|
+
});
|
|
8858
|
+
return c.json(r, r.ok ? 201 : 409);
|
|
8859
|
+
});
|
|
8860
|
+
app.post("/v1/workflows/stop", async (c) => {
|
|
8861
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8862
|
+
if (!b.projectId || !b.task)
|
|
8863
|
+
return c.json({ ok: false, error: "projectId and task required" }, 400);
|
|
8864
|
+
const r = workflows2.stop(b.projectId, b.task);
|
|
8865
|
+
return c.json(r, r.ok ? 200 : 404);
|
|
8866
|
+
});
|
|
8867
|
+
app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
|
|
8868
|
+
app.get("/v1/messages", (c) => c.json(store.messages({
|
|
8869
|
+
...c.req.query("project") ? { projectId: c.req.query("project") } : {},
|
|
8870
|
+
...c.req.query("session") ? { sessionId: c.req.query("session") } : {},
|
|
8871
|
+
...c.req.query("task") ? { task: c.req.query("task") } : {},
|
|
8872
|
+
limit: Number(c.req.query("limit")) || 100
|
|
8873
|
+
})));
|
|
8874
|
+
app.get("/v1/messages/inbox", (c) => c.json(store.messageInbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
8875
|
+
app.post("/v1/messages", async (c) => {
|
|
8876
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8877
|
+
if (!b.projectId)
|
|
8878
|
+
return c.json({ ok: false, error: "projectId required" }, 400);
|
|
8879
|
+
const r = store.send(b.projectId, {
|
|
8880
|
+
to: b.to,
|
|
8881
|
+
text: b.text,
|
|
8882
|
+
from: b.from ?? null,
|
|
8883
|
+
fromSession: b.sessionId ?? null
|
|
8884
|
+
});
|
|
8885
|
+
if (!r.ok)
|
|
8886
|
+
return c.json(r, 400);
|
|
8887
|
+
const m = r.message;
|
|
8888
|
+
const run2 = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
|
|
8889
|
+
if (run2 && !run2.endedAt) {
|
|
8890
|
+
const sent = runner.send(run2.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
|
|
8891
|
+
if (sent.ok)
|
|
8892
|
+
store.markMessageDelivered(m.id, run2.sessionId);
|
|
8893
|
+
}
|
|
8894
|
+
return c.json({ ok: true, message: store.message(m.id) }, 201);
|
|
8895
|
+
});
|
|
7249
8896
|
app.get("/v1/dispatch", (c) => {
|
|
7250
8897
|
const project = c.req.query("project");
|
|
7251
8898
|
if (!project)
|
|
@@ -7377,7 +9024,7 @@ function createApp(store = new Store) {
|
|
|
7377
9024
|
const b = await c.req.json();
|
|
7378
9025
|
if (!b.projectId || !b.task)
|
|
7379
9026
|
return c.json({ error: "projectId and task required" }, 400);
|
|
7380
|
-
const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef);
|
|
9027
|
+
const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef, b.sessionId ?? null);
|
|
7381
9028
|
return c.json(r, r.ok ? 201 : 409);
|
|
7382
9029
|
});
|
|
7383
9030
|
app.post("/v1/claims/renew", async (c) => {
|
|
@@ -7524,6 +9171,7 @@ function createApp(store = new Store) {
|
|
|
7524
9171
|
const raw2 = await c.req.json().catch(() => ({}));
|
|
7525
9172
|
store.ingestHook(event, raw2);
|
|
7526
9173
|
if (event === "SessionStart" && typeof raw2.cwd === "string") {
|
|
9174
|
+
store.checkPolicy(raw2.cwd, typeof raw2.session_id === "string" ? raw2.session_id : null);
|
|
7527
9175
|
const ctx = store.sessionContext(raw2.cwd);
|
|
7528
9176
|
if (ctx)
|
|
7529
9177
|
return c.json({
|
|
@@ -7533,7 +9181,7 @@ function createApp(store = new Store) {
|
|
|
7533
9181
|
}
|
|
7534
9182
|
const sid = typeof raw2.session_id === "string" ? raw2.session_id : null;
|
|
7535
9183
|
const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
|
|
7536
|
-
if (event === "PreToolUse" &&
|
|
9184
|
+
if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw2))) {
|
|
7537
9185
|
const guard = store.guardHook(raw2);
|
|
7538
9186
|
if (guard) {
|
|
7539
9187
|
return c.json({
|
|
@@ -7566,7 +9214,7 @@ function createApp(store = new Store) {
|
|
|
7566
9214
|
await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
|
|
7567
9215
|
}
|
|
7568
9216
|
await stream2.writeSSE({ event: "ping", data: "" });
|
|
7569
|
-
await new Promise((
|
|
9217
|
+
await new Promise((resolve2) => {
|
|
7570
9218
|
const off = store.subscribe((e) => {
|
|
7571
9219
|
stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
|
|
7572
9220
|
});
|
|
@@ -7574,7 +9222,7 @@ function createApp(store = new Store) {
|
|
|
7574
9222
|
stream2.onAbort(() => {
|
|
7575
9223
|
clearInterval(beat);
|
|
7576
9224
|
off();
|
|
7577
|
-
|
|
9225
|
+
resolve2();
|
|
7578
9226
|
});
|
|
7579
9227
|
});
|
|
7580
9228
|
});
|
|
@@ -7584,18 +9232,107 @@ function createApp(store = new Store) {
|
|
|
7584
9232
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
7585
9233
|
const f = c.req.param("file");
|
|
7586
9234
|
const p = join9(WEB_DIR, f);
|
|
7587
|
-
if (!
|
|
9235
|
+
if (!existsSync7(p))
|
|
7588
9236
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
7589
9237
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
7590
9238
|
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
7591
9239
|
});
|
|
7592
9240
|
});
|
|
7593
|
-
return { app, store, forge: forge2, runner, dispatcher };
|
|
9241
|
+
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
|
|
9242
|
+
}
|
|
9243
|
+
|
|
9244
|
+
// packages/daemon/src/demo.ts
|
|
9245
|
+
var H = 3600000;
|
|
9246
|
+
var iso = (msAgo) => new Date(Date.now() - msAgo).toISOString();
|
|
9247
|
+
function isEmpty(store) {
|
|
9248
|
+
return !store.db.query("SELECT 1 FROM sessions LIMIT 1").get();
|
|
9249
|
+
}
|
|
9250
|
+
function seedDemo(store) {
|
|
9251
|
+
const db = store.db;
|
|
9252
|
+
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);
|
|
9253
|
+
project("p_demo1", "acme-app", "/work/acme-app", "\uD83D\uDED2", "c3");
|
|
9254
|
+
project("p_demo2", "acme-site", "/work/acme-site", "\uD83C\uDF10", "c5");
|
|
9255
|
+
const session = (id, pid, agent, title, cwd, branch, startedAgo, lastAgo, state, model) => {
|
|
9256
|
+
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)
|
|
9257
|
+
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));
|
|
9258
|
+
let t = startedAgo;
|
|
9259
|
+
let i = 0;
|
|
9260
|
+
while (t > lastAgo) {
|
|
9261
|
+
const out = 300 + Math.floor(Math.random() * 4000);
|
|
9262
|
+
const read = 50000 + Math.floor(Math.random() * 900000);
|
|
9263
|
+
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)
|
|
9264
|
+
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." : "");
|
|
9265
|
+
t -= (8 + i * 13 % 30) * 60000;
|
|
9266
|
+
i++;
|
|
9267
|
+
}
|
|
9268
|
+
};
|
|
9269
|
+
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");
|
|
9270
|
+
session("demo-s2", "p_demo1", "codex", "Fix flaky cart tests", "/work/acme-app", "main", 7 * H, 3 * H, "ended", "gpt-5.2-codex");
|
|
9271
|
+
session("demo-s3", "p_demo1", "gemini", "Payment webhook audit", "/work/acme-app", "main", 26 * H, 22 * H, "ended", "gemini-2.5-pro");
|
|
9272
|
+
session("demo-s4", "p_demo2", "grok", "Landing page rewrite", "/work/acme-site", "task/landing", 30 * H, 25 * H, "ended", "grok-4");
|
|
9273
|
+
session("demo-s5", "p_demo2", "claude-code", "SEO metadata sweep", "/work/acme-site", "main", 50 * H, 47 * H, "ended", "claude-sonnet-5");
|
|
9274
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
9275
|
+
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));
|
|
9276
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
9277
|
+
VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
|
|
9278
|
+
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)
|
|
9279
|
+
VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate2, verdict, rubric, sid, iso(ago));
|
|
9280
|
+
gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
|
|
9281
|
+
gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
|
|
9282
|
+
gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
|
|
9283
|
+
gate("webhooks", "tests", "pass", "ran `bun test` \u2014 exit 0 in 22s", 22 * H, "demo-s3");
|
|
9284
|
+
const ev = (type, ago, sid, payload) => store.append({
|
|
9285
|
+
ts: iso(ago),
|
|
9286
|
+
type,
|
|
9287
|
+
projectId: "p_demo1",
|
|
9288
|
+
sessionId: sid,
|
|
9289
|
+
payload
|
|
9290
|
+
});
|
|
9291
|
+
ev("incident.opened", 4 * H, "demo-s1", {
|
|
9292
|
+
rule: "pattern_kill",
|
|
9293
|
+
action: "ask",
|
|
9294
|
+
command: "pkill -f vite",
|
|
9295
|
+
reason: "This kills processes by command pattern \u2014 other agents' dev servers match too."
|
|
9296
|
+
});
|
|
9297
|
+
ev("incident.opened", 26 * H, "demo-s3", {
|
|
9298
|
+
rule: "shared_tree",
|
|
9299
|
+
action: "deny",
|
|
9300
|
+
command: "git reset --hard",
|
|
9301
|
+
reason: "Another session (demo-s2) is active in this same checkout."
|
|
9302
|
+
});
|
|
9303
|
+
ev("claim.acquired", 5 * H, "demo-s1", {
|
|
9304
|
+
task: "checkout",
|
|
9305
|
+
owner: "demo-s1",
|
|
9306
|
+
summary: "claim checkout"
|
|
9307
|
+
});
|
|
9308
|
+
ev("pr.opened", 30 * 60000, "demo-s1", {
|
|
9309
|
+
task: "checkout",
|
|
9310
|
+
url: "https://github.com/acme/app/pull/128",
|
|
9311
|
+
summary: "PR #128 opened for checkout"
|
|
9312
|
+
});
|
|
9313
|
+
ev("question.asked", 20 * 60000, "demo-s1", {
|
|
9314
|
+
id: 1,
|
|
9315
|
+
task: "checkout",
|
|
9316
|
+
text: "Coupon codes: keep the legacy endpoint alive for one release, or cut over now?",
|
|
9317
|
+
options: ["Keep one release", "Cut over"],
|
|
9318
|
+
summary: "question #1"
|
|
9319
|
+
});
|
|
9320
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, options, asked_by, created_at)
|
|
9321
|
+
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));
|
|
9322
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
9323
|
+
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));
|
|
9324
|
+
db.query(`INSERT OR IGNORE INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
9325
|
+
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));
|
|
9326
|
+
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)
|
|
9327
|
+
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));
|
|
7594
9328
|
}
|
|
7595
9329
|
|
|
7596
9330
|
// packages/daemon/src/bin.ts
|
|
7597
9331
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
7598
|
-
var
|
|
9332
|
+
var appHooks = {};
|
|
9333
|
+
var { app, store, runner } = createApp(new Store, appHooks);
|
|
9334
|
+
if (process.env.SWARM_DEMO === "1" && isEmpty(store))
|
|
9335
|
+
seedDemo(store);
|
|
7599
9336
|
function serve() {
|
|
7600
9337
|
const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
|
|
7601
9338
|
try {
|
|
@@ -7607,20 +9344,50 @@ function serve() {
|
|
|
7607
9344
|
return bind(0);
|
|
7608
9345
|
}
|
|
7609
9346
|
}
|
|
7610
|
-
var server
|
|
9347
|
+
var server;
|
|
9348
|
+
var restart = () => {
|
|
9349
|
+
console.error("swarmd: restarting into the version on disk\u2026");
|
|
9350
|
+
try {
|
|
9351
|
+
clearInterval(tailer);
|
|
9352
|
+
clearInterval(wtRefresh);
|
|
9353
|
+
clearInterval(pruner);
|
|
9354
|
+
} catch {}
|
|
9355
|
+
try {
|
|
9356
|
+
server.stop(true);
|
|
9357
|
+
} catch {}
|
|
9358
|
+
clearDaemonInfo();
|
|
9359
|
+
const [cmd, ...args] = daemonCommand();
|
|
9360
|
+
if (cmd)
|
|
9361
|
+
Bun.spawn([cmd, ...args], {
|
|
9362
|
+
stdin: "ignore",
|
|
9363
|
+
stdout: "ignore",
|
|
9364
|
+
stderr: "ignore",
|
|
9365
|
+
env: { ...process.env }
|
|
9366
|
+
}).unref();
|
|
9367
|
+
setTimeout(() => process.exit(0), 100);
|
|
9368
|
+
};
|
|
9369
|
+
appHooks.restart = restart;
|
|
9370
|
+
server = serve();
|
|
7611
9371
|
var port = server.port ?? DEFAULT_PORT2;
|
|
9372
|
+
ensureToken();
|
|
7612
9373
|
writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
|
|
7613
9374
|
var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
|
|
7614
9375
|
var backfillMs = backfillDays * 24 * 60 * 60000;
|
|
7615
|
-
|
|
7616
|
-
|
|
9376
|
+
var DEMO = process.env.SWARM_DEMO === "1";
|
|
9377
|
+
if (!DEMO) {
|
|
9378
|
+
store.tailCodex(backfillMs);
|
|
9379
|
+
store.tailGrok(backfillMs);
|
|
9380
|
+
store.tailGemini(backfillMs);
|
|
9381
|
+
}
|
|
7617
9382
|
var tick = 0;
|
|
7618
9383
|
var tailer = setInterval(() => {
|
|
7619
9384
|
tick++;
|
|
7620
|
-
|
|
7621
|
-
|
|
9385
|
+
if (!DEMO)
|
|
9386
|
+
store.tailActive();
|
|
9387
|
+
if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
|
|
7622
9388
|
store.tailCodex();
|
|
7623
9389
|
store.tailGrok();
|
|
9390
|
+
store.tailGemini();
|
|
7624
9391
|
}
|
|
7625
9392
|
store.reapResources();
|
|
7626
9393
|
store.reapProcesses();
|