@ra3orblade/swarm 0.4.1 → 0.5.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 +5 -5
- package/dist/swarm-mcp.js +87 -0
- package/dist/swarm.js +279 -8
- package/dist/swarmd.js +871 -41
- package/package.json +1 -1
- package/web/app.js +151 -13
- package/web/icons.js +2 -2
- package/web/index.html +21 -0
package/dist/swarmd.js
CHANGED
|
@@ -328,6 +328,7 @@ import { join as join2 } from "path";
|
|
|
328
328
|
var DEFAULT_CONFIG = {
|
|
329
329
|
daemon: { port: 7777 },
|
|
330
330
|
tasks: { source: null },
|
|
331
|
+
gates: { required: [] },
|
|
331
332
|
rules: {
|
|
332
333
|
shared_tree: "ask",
|
|
333
334
|
destructive_git: "ask",
|
|
@@ -459,6 +460,49 @@ function normalizeGitlab(raw, repo) {
|
|
|
459
460
|
};
|
|
460
461
|
});
|
|
461
462
|
}
|
|
463
|
+
// packages/core/src/gates.ts
|
|
464
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
465
|
+
function validateGateRun(input) {
|
|
466
|
+
if (!input.task?.trim())
|
|
467
|
+
return { ok: false, reason: "task is required" };
|
|
468
|
+
if (!NAME_RE.test(input.gate ?? ""))
|
|
469
|
+
return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
|
|
470
|
+
if (input.verdict !== "pass" && input.verdict !== "fail")
|
|
471
|
+
return { ok: false, reason: 'verdict must be "pass" or "fail"' };
|
|
472
|
+
const rubric = input.rubric?.trim() ?? "";
|
|
473
|
+
if (rubric.length < 8)
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
reason: 'rubric is required: say what was checked (e.g. "tests green, no TODOs, reviewed error paths"). A verdict without a rubric is rejected.'
|
|
477
|
+
};
|
|
478
|
+
return { ok: true };
|
|
479
|
+
}
|
|
480
|
+
function gateStatus(runs, declared = []) {
|
|
481
|
+
const byGate = new Map;
|
|
482
|
+
for (const r of runs) {
|
|
483
|
+
const list = byGate.get(r.gate) ?? [];
|
|
484
|
+
list.push(r);
|
|
485
|
+
byGate.set(r.gate, list);
|
|
486
|
+
}
|
|
487
|
+
const names = [...new Set([...declared, ...byGate.keys()])];
|
|
488
|
+
return names.map((gate) => {
|
|
489
|
+
const list = (byGate.get(gate) ?? []).sort((a, b) => a.createdAt === b.createdAt ? b.id - a.id : a.createdAt < b.createdAt ? 1 : -1);
|
|
490
|
+
const latest = list[0] ?? null;
|
|
491
|
+
return {
|
|
492
|
+
gate,
|
|
493
|
+
verdict: latest?.verdict ?? null,
|
|
494
|
+
latest,
|
|
495
|
+
runs: list.length,
|
|
496
|
+
fails: list.filter((r) => r.verdict === "fail").length
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function gatesSatisfied(runs, declared) {
|
|
501
|
+
if (!declared.length)
|
|
502
|
+
return true;
|
|
503
|
+
const st = gateStatus(runs, declared);
|
|
504
|
+
return declared.every((g) => st.find((s) => s.gate === g)?.verdict === "pass");
|
|
505
|
+
}
|
|
462
506
|
// packages/core/src/ledger.ts
|
|
463
507
|
var DEFAULT_LEASE_MINUTES = 45;
|
|
464
508
|
function isExpired(claim, now) {
|
|
@@ -495,12 +539,43 @@ function reapAction(claim, now, worktreeExists, work) {
|
|
|
495
539
|
return "keep-orphaned";
|
|
496
540
|
return "reap";
|
|
497
541
|
}
|
|
542
|
+
function shouldAutoRenew(claim, now, leaseMinutes = DEFAULT_LEASE_MINUTES) {
|
|
543
|
+
if (claim.state !== "held")
|
|
544
|
+
return false;
|
|
545
|
+
const left = new Date(claim.expiresAt).getTime() - now;
|
|
546
|
+
if (left <= 0)
|
|
547
|
+
return false;
|
|
548
|
+
return left < leaseMinutes * 60000 / 2;
|
|
549
|
+
}
|
|
498
550
|
function claimRefusalMessage(d, task) {
|
|
499
551
|
return `${task} is held by ${d.heldBy} until ${d.until}. ` + "Pick another task or coordinate with the holder \u2014 claims fail closed on purpose.";
|
|
500
552
|
}
|
|
501
553
|
function releaseRefusalMessage(d, worktree) {
|
|
502
554
|
return d.reason === "dirty" ? `${worktree} has uncommitted changes. Commit and push them, or re-run with --force to discard.` : `${worktree} has unpushed commits. Push them, or re-run with --force to discard the worktree.`;
|
|
503
555
|
}
|
|
556
|
+
function validateHandoff(h) {
|
|
557
|
+
if (!h.task?.trim())
|
|
558
|
+
return { ok: false, reason: "task is required" };
|
|
559
|
+
if (!h.done?.trim() || !h.remaining?.trim())
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
reason: "a handoff needs both `done` (what was finished) and `remaining` (what is left, in order)"
|
|
563
|
+
};
|
|
564
|
+
return { ok: true };
|
|
565
|
+
}
|
|
566
|
+
function formatHandoff(h) {
|
|
567
|
+
const lines = [
|
|
568
|
+
`[swarm] handoff on ${h.task}${h.by ? ` from ${h.by}` : ""} (${h.createdAt.slice(0, 16).replace("T", " ")}):`,
|
|
569
|
+
` done: ${h.done.trim()}`,
|
|
570
|
+
` remaining: ${h.remaining.trim()}`
|
|
571
|
+
];
|
|
572
|
+
if (h.files.length)
|
|
573
|
+
lines.push(` files: ${h.files.join(", ")}`);
|
|
574
|
+
if (h.verify)
|
|
575
|
+
lines.push(` verify: ${h.verify.trim()}`);
|
|
576
|
+
return lines.join(`
|
|
577
|
+
`);
|
|
578
|
+
}
|
|
504
579
|
// packages/core/src/pricing.ts
|
|
505
580
|
var PRICES = {
|
|
506
581
|
"claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
@@ -873,7 +948,7 @@ function taskBoard(tasks, activeClaims) {
|
|
|
873
948
|
// packages/daemon/src/app.ts
|
|
874
949
|
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
875
950
|
import { homedir as homedir4 } from "os";
|
|
876
|
-
import { dirname as dirname2, join as
|
|
951
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
877
952
|
import { fileURLToPath } from "url";
|
|
878
953
|
|
|
879
954
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
@@ -2507,7 +2582,7 @@ var EXTRA_BIN_DIRS = [
|
|
|
2507
2582
|
function findBin(name) {
|
|
2508
2583
|
if (!name)
|
|
2509
2584
|
return null;
|
|
2510
|
-
const onPath = Bun.which(name);
|
|
2585
|
+
const onPath = Bun.which(name, { PATH: process.env.PATH ?? "" });
|
|
2511
2586
|
if (onPath)
|
|
2512
2587
|
return onPath;
|
|
2513
2588
|
for (const d of EXTRA_BIN_DIRS) {
|
|
@@ -2600,13 +2675,326 @@ class ForgeService {
|
|
|
2600
2675
|
}
|
|
2601
2676
|
}
|
|
2602
2677
|
|
|
2678
|
+
// packages/daemon/src/runner.ts
|
|
2679
|
+
import { appendFileSync, mkdirSync as mkdirSync2, openSync } from "fs";
|
|
2680
|
+
import { join as join4 } from "path";
|
|
2681
|
+
var PERMISSION_MODES = [
|
|
2682
|
+
"acceptEdits",
|
|
2683
|
+
"auto",
|
|
2684
|
+
"bypassPermissions",
|
|
2685
|
+
"manual",
|
|
2686
|
+
"dontAsk",
|
|
2687
|
+
"plan"
|
|
2688
|
+
];
|
|
2689
|
+
|
|
2690
|
+
class Runner {
|
|
2691
|
+
store;
|
|
2692
|
+
home;
|
|
2693
|
+
live = new Map;
|
|
2694
|
+
constructor(store, home) {
|
|
2695
|
+
this.store = store;
|
|
2696
|
+
this.home = home;
|
|
2697
|
+
}
|
|
2698
|
+
list(projectId) {
|
|
2699
|
+
return [...this.live.values()].map((x) => x.run).filter((r) => !projectId || r.projectId === projectId);
|
|
2700
|
+
}
|
|
2701
|
+
get(idOrTask) {
|
|
2702
|
+
for (const { run: run2 } of this.live.values())
|
|
2703
|
+
if (run2.id === idOrTask || run2.sessionId === idOrTask || run2.task === idOrTask)
|
|
2704
|
+
return run2;
|
|
2705
|
+
return null;
|
|
2706
|
+
}
|
|
2707
|
+
async start(input) {
|
|
2708
|
+
const bin = findBin("claude");
|
|
2709
|
+
if (!bin)
|
|
2710
|
+
return { ok: false, reason: "claude CLI not found \u2014 install Claude Code first" };
|
|
2711
|
+
const project = this.store.project(input.projectId);
|
|
2712
|
+
if (!project)
|
|
2713
|
+
return { ok: false, reason: "unknown project" };
|
|
2714
|
+
if (!input.prompt.trim())
|
|
2715
|
+
return { ok: false, reason: "prompt is required" };
|
|
2716
|
+
if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
|
|
2717
|
+
return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
|
|
2718
|
+
if (this.get(input.task)?.projectId === input.projectId)
|
|
2719
|
+
return {
|
|
2720
|
+
ok: false,
|
|
2721
|
+
reason: `a run on ${input.task} is already live \u2014 stop it or send it input`
|
|
2722
|
+
};
|
|
2723
|
+
const held = this.store.claims(input.projectId).find((c) => c.task === input.task && c.state === "held" && c.owner === input.owner);
|
|
2724
|
+
let worktree = held?.worktree ?? "";
|
|
2725
|
+
if (!worktree) {
|
|
2726
|
+
const c = this.store.claim(input.projectId, input.task, input.owner);
|
|
2727
|
+
if (!c.ok)
|
|
2728
|
+
return { ok: false, reason: c.error };
|
|
2729
|
+
worktree = c.worktree;
|
|
2730
|
+
}
|
|
2731
|
+
const sessionId = crypto.randomUUID();
|
|
2732
|
+
const id = sessionId.slice(0, 8);
|
|
2733
|
+
const logDir = join4(this.home, "logs", project.id);
|
|
2734
|
+
mkdirSync2(logDir, { recursive: true });
|
|
2735
|
+
const log = join4(logDir, `run-${input.task.replace(/[^a-zA-Z0-9_.-]+/g, "-")}-${id}.log`);
|
|
2736
|
+
const logFd = openSync(log, "a");
|
|
2737
|
+
const args = [
|
|
2738
|
+
bin,
|
|
2739
|
+
"-p",
|
|
2740
|
+
"--output-format",
|
|
2741
|
+
"stream-json",
|
|
2742
|
+
"--input-format",
|
|
2743
|
+
"stream-json",
|
|
2744
|
+
"--verbose",
|
|
2745
|
+
"--session-id",
|
|
2746
|
+
sessionId,
|
|
2747
|
+
"--permission-prompt-tool",
|
|
2748
|
+
"stdio"
|
|
2749
|
+
];
|
|
2750
|
+
if (input.model)
|
|
2751
|
+
args.push("--model", input.model);
|
|
2752
|
+
if (input.permissionMode)
|
|
2753
|
+
args.push("--permission-mode", input.permissionMode);
|
|
2754
|
+
if (input.allowedTools?.length)
|
|
2755
|
+
args.push("--allowedTools", ...input.allowedTools);
|
|
2756
|
+
if (input.maxTurns)
|
|
2757
|
+
args.push("--max-turns", String(input.maxTurns));
|
|
2758
|
+
this.store.preregisterSpawnedSession(sessionId, project.id, worktree, input.task);
|
|
2759
|
+
const proc = Bun.spawn(args, {
|
|
2760
|
+
cwd: worktree,
|
|
2761
|
+
env: { ...process.env, SWARM_RUN_ID: id, SWARM_OWNER: input.owner },
|
|
2762
|
+
stdin: "pipe",
|
|
2763
|
+
stdout: "pipe",
|
|
2764
|
+
stderr: logFd
|
|
2765
|
+
});
|
|
2766
|
+
const run2 = {
|
|
2767
|
+
id,
|
|
2768
|
+
sessionId,
|
|
2769
|
+
projectId: project.id,
|
|
2770
|
+
task: input.task,
|
|
2771
|
+
worktree,
|
|
2772
|
+
pid: proc.pid,
|
|
2773
|
+
owner: input.owner,
|
|
2774
|
+
model: input.model ?? null,
|
|
2775
|
+
permissionMode: input.permissionMode ?? null,
|
|
2776
|
+
prompt: input.prompt,
|
|
2777
|
+
log,
|
|
2778
|
+
startedAt: new Date().toISOString(),
|
|
2779
|
+
endedAt: null,
|
|
2780
|
+
exitCode: null,
|
|
2781
|
+
result: null,
|
|
2782
|
+
pending: []
|
|
2783
|
+
};
|
|
2784
|
+
this.live.set(id, { run: run2, proc });
|
|
2785
|
+
this.store.registerProcess({
|
|
2786
|
+
pid: proc.pid,
|
|
2787
|
+
projectId: project.id,
|
|
2788
|
+
sessionId,
|
|
2789
|
+
kind: "proc",
|
|
2790
|
+
name: `run:${input.task}`,
|
|
2791
|
+
cwd: worktree,
|
|
2792
|
+
cmd: `claude -p (run ${id})`,
|
|
2793
|
+
owner: input.owner,
|
|
2794
|
+
log
|
|
2795
|
+
});
|
|
2796
|
+
this.store.append({
|
|
2797
|
+
ts: run2.startedAt,
|
|
2798
|
+
type: "session.started",
|
|
2799
|
+
projectId: project.id,
|
|
2800
|
+
sessionId,
|
|
2801
|
+
payload: { kind: "spawned", task: input.task, runId: id, summary: `swarm run ${input.task}` }
|
|
2802
|
+
});
|
|
2803
|
+
this.pump(id, proc);
|
|
2804
|
+
this.send(id, input.prompt);
|
|
2805
|
+
return { ok: true, run: run2 };
|
|
2806
|
+
}
|
|
2807
|
+
async pump(id, proc) {
|
|
2808
|
+
const entry = this.live.get(id);
|
|
2809
|
+
if (!entry || !proc.stdout || typeof proc.stdout === "number")
|
|
2810
|
+
return;
|
|
2811
|
+
const reader = proc.stdout.getReader();
|
|
2812
|
+
const dec = new TextDecoder;
|
|
2813
|
+
let buf = "";
|
|
2814
|
+
try {
|
|
2815
|
+
while (true) {
|
|
2816
|
+
const { value, done } = await reader.read();
|
|
2817
|
+
if (done)
|
|
2818
|
+
break;
|
|
2819
|
+
buf += dec.decode(value, { stream: true });
|
|
2820
|
+
let nl = buf.indexOf(`
|
|
2821
|
+
`);
|
|
2822
|
+
while (nl >= 0) {
|
|
2823
|
+
const line = buf.slice(0, nl);
|
|
2824
|
+
buf = buf.slice(nl + 1);
|
|
2825
|
+
try {
|
|
2826
|
+
appendFileSync(entry.run.log, `${line}
|
|
2827
|
+
`);
|
|
2828
|
+
} catch {}
|
|
2829
|
+
this.onLine(entry.run, line);
|
|
2830
|
+
nl = buf.indexOf(`
|
|
2831
|
+
`);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
} catch (e) {
|
|
2835
|
+
console.error("swarm run: stdout pump failed:", e.message);
|
|
2836
|
+
}
|
|
2837
|
+
const code = await proc.exited;
|
|
2838
|
+
entry.run.endedAt = new Date().toISOString();
|
|
2839
|
+
entry.run.exitCode = code;
|
|
2840
|
+
this.store.append({
|
|
2841
|
+
ts: entry.run.endedAt,
|
|
2842
|
+
type: "run.result",
|
|
2843
|
+
projectId: entry.run.projectId,
|
|
2844
|
+
sessionId: entry.run.sessionId,
|
|
2845
|
+
payload: {
|
|
2846
|
+
runId: id,
|
|
2847
|
+
task: entry.run.task,
|
|
2848
|
+
exitCode: code,
|
|
2849
|
+
final: true,
|
|
2850
|
+
...entry.run.result ?? {},
|
|
2851
|
+
summary: `run ${entry.run.task} exited ${code}`
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
this.store.endSpawnedSession(entry.run.sessionId);
|
|
2855
|
+
this.live.delete(id);
|
|
2856
|
+
}
|
|
2857
|
+
onLine(run2, line) {
|
|
2858
|
+
if (!line.startsWith("{"))
|
|
2859
|
+
return;
|
|
2860
|
+
let j;
|
|
2861
|
+
try {
|
|
2862
|
+
j = JSON.parse(line);
|
|
2863
|
+
} catch {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
if (j.type === "control_request" && j.request?.subtype === "can_use_tool") {
|
|
2867
|
+
this.onPermissionRequest(run2, j.request_id, j.request.tool_name ?? "", j.request.input ?? {});
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (j.type !== "result")
|
|
2871
|
+
return;
|
|
2872
|
+
run2.result = {
|
|
2873
|
+
costUsd: Number(j.total_cost_usd ?? 0),
|
|
2874
|
+
turns: Number(j.num_turns ?? 0),
|
|
2875
|
+
isError: Boolean(j.is_error),
|
|
2876
|
+
at: new Date().toISOString()
|
|
2877
|
+
};
|
|
2878
|
+
this.store.append({
|
|
2879
|
+
ts: run2.result.at,
|
|
2880
|
+
type: "run.result",
|
|
2881
|
+
projectId: run2.projectId,
|
|
2882
|
+
sessionId: run2.sessionId,
|
|
2883
|
+
payload: {
|
|
2884
|
+
runId: run2.id,
|
|
2885
|
+
task: run2.task,
|
|
2886
|
+
...run2.result,
|
|
2887
|
+
summary: `turn done \xB7 $${run2.result.costUsd.toFixed(2)} \xB7 ${run2.result.turns} turns${run2.result.isError ? " \xB7 error" : ""}`
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
onPermissionRequest(run2, requestId, tool, input) {
|
|
2892
|
+
const { decision, display } = this.store.evaluateTool(tool, input, run2.sessionId, run2.worktree, true);
|
|
2893
|
+
if (decision.action === "deny") {
|
|
2894
|
+
this.answerPermission(run2.id, requestId, false, `[swarm] ${decision.reason}`);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
if (decision.action === "allow") {
|
|
2898
|
+
this.answerPermission(run2.id, requestId, true);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
run2.pending.push({
|
|
2902
|
+
requestId,
|
|
2903
|
+
tool,
|
|
2904
|
+
input,
|
|
2905
|
+
display,
|
|
2906
|
+
reason: decision.reason,
|
|
2907
|
+
askedAt: new Date().toISOString()
|
|
2908
|
+
});
|
|
2909
|
+
this.store.append({
|
|
2910
|
+
ts: new Date().toISOString(),
|
|
2911
|
+
type: "permission.requested",
|
|
2912
|
+
projectId: run2.projectId,
|
|
2913
|
+
sessionId: run2.sessionId,
|
|
2914
|
+
payload: {
|
|
2915
|
+
runId: run2.id,
|
|
2916
|
+
requestId,
|
|
2917
|
+
tool,
|
|
2918
|
+
display: display.slice(0, 300),
|
|
2919
|
+
reason: decision.reason,
|
|
2920
|
+
summary: `permission: ${tool} \u2014 waiting`
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
this.store.touch();
|
|
2924
|
+
}
|
|
2925
|
+
answerPermission(runId, requestId, allow, message) {
|
|
2926
|
+
const entry = this.live.get(runId);
|
|
2927
|
+
if (!entry)
|
|
2928
|
+
return { ok: false, reason: "no live run" };
|
|
2929
|
+
const stdin = entry.proc.stdin;
|
|
2930
|
+
if (!stdin || typeof stdin === "number")
|
|
2931
|
+
return { ok: false, reason: "stdin not available" };
|
|
2932
|
+
const pend = entry.run.pending.find((p) => p.requestId === requestId);
|
|
2933
|
+
const response = allow ? { behavior: "allow", updatedInput: pend?.input ?? {} } : { behavior: "deny", message: message ?? "Denied from the Swarm dashboard" };
|
|
2934
|
+
stdin.write(`${JSON.stringify({ type: "control_response", response: { subtype: "success", request_id: requestId, response } })}
|
|
2935
|
+
`);
|
|
2936
|
+
stdin.flush();
|
|
2937
|
+
entry.run.pending = entry.run.pending.filter((p) => p.requestId !== requestId);
|
|
2938
|
+
if (pend)
|
|
2939
|
+
this.store.append({
|
|
2940
|
+
ts: new Date().toISOString(),
|
|
2941
|
+
type: "permission.resolved",
|
|
2942
|
+
projectId: entry.run.projectId,
|
|
2943
|
+
sessionId: entry.run.sessionId,
|
|
2944
|
+
payload: {
|
|
2945
|
+
runId,
|
|
2946
|
+
requestId,
|
|
2947
|
+
tool: pend.tool,
|
|
2948
|
+
allow,
|
|
2949
|
+
summary: `permission: ${pend.tool} \u2014 ${allow ? "allowed" : "denied"}`
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
this.store.touch();
|
|
2953
|
+
return { ok: true };
|
|
2954
|
+
}
|
|
2955
|
+
send(id, text) {
|
|
2956
|
+
const entry = this.live.get(id) ?? [...this.live.values()].find((x) => x.run.task === id || x.run.sessionId === id);
|
|
2957
|
+
if (!entry)
|
|
2958
|
+
return { ok: false, reason: "no live run" };
|
|
2959
|
+
const stdin = entry.proc.stdin;
|
|
2960
|
+
if (!stdin || typeof stdin === "number")
|
|
2961
|
+
return { ok: false, reason: "stdin not available" };
|
|
2962
|
+
stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: text } })}
|
|
2963
|
+
`);
|
|
2964
|
+
stdin.flush();
|
|
2965
|
+
this.store.append({
|
|
2966
|
+
ts: new Date().toISOString(),
|
|
2967
|
+
type: "prompt.submitted",
|
|
2968
|
+
projectId: entry.run.projectId,
|
|
2969
|
+
sessionId: entry.run.sessionId,
|
|
2970
|
+
payload: { prompt: text.slice(0, 400), via: "swarm run send", summary: text.slice(0, 120) }
|
|
2971
|
+
});
|
|
2972
|
+
return { ok: true };
|
|
2973
|
+
}
|
|
2974
|
+
async stop(id) {
|
|
2975
|
+
const run2 = this.get(id);
|
|
2976
|
+
if (!run2)
|
|
2977
|
+
return { ok: false, reason: "no live run" };
|
|
2978
|
+
const entry = this.live.get(run2.id);
|
|
2979
|
+
try {
|
|
2980
|
+
const stdin = entry?.proc.stdin;
|
|
2981
|
+
if (stdin && typeof stdin !== "number")
|
|
2982
|
+
stdin.end();
|
|
2983
|
+
} catch {}
|
|
2984
|
+
return this.store.stopProcess(run2.pid, run2.projectId, 5000);
|
|
2985
|
+
}
|
|
2986
|
+
async stopAll() {
|
|
2987
|
+
await Promise.all([...this.live.keys()].map((id) => this.stop(id)));
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2603
2991
|
// packages/daemon/src/store.ts
|
|
2604
2992
|
import { Database } from "bun:sqlite";
|
|
2605
2993
|
import {
|
|
2606
2994
|
closeSync,
|
|
2607
2995
|
existsSync as existsSync4,
|
|
2608
|
-
mkdirSync as
|
|
2609
|
-
openSync,
|
|
2996
|
+
mkdirSync as mkdirSync3,
|
|
2997
|
+
openSync as openSync2,
|
|
2610
2998
|
readdirSync,
|
|
2611
2999
|
readFileSync as readFileSync3,
|
|
2612
3000
|
readSync,
|
|
@@ -2616,11 +3004,11 @@ import {
|
|
|
2616
3004
|
writeFileSync as writeFileSync2
|
|
2617
3005
|
} from "fs";
|
|
2618
3006
|
import { homedir as homedir3 } from "os";
|
|
2619
|
-
import { basename, dirname, join as
|
|
3007
|
+
import { basename, dirname, join as join6 } from "path";
|
|
2620
3008
|
|
|
2621
3009
|
// packages/daemon/src/git.ts
|
|
2622
3010
|
import { realpathSync } from "fs";
|
|
2623
|
-
import { join as
|
|
3011
|
+
import { join as join5 } from "path";
|
|
2624
3012
|
function git(cwd, args) {
|
|
2625
3013
|
try {
|
|
2626
3014
|
const r = Bun.spawnSync(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
@@ -2634,7 +3022,7 @@ function gitCommonDir(cwd) {
|
|
|
2634
3022
|
if (!out)
|
|
2635
3023
|
return null;
|
|
2636
3024
|
try {
|
|
2637
|
-
return realpathSync(out.startsWith("/") ? out :
|
|
3025
|
+
return realpathSync(out.startsWith("/") ? out : join5(cwd, out));
|
|
2638
3026
|
} catch {
|
|
2639
3027
|
return null;
|
|
2640
3028
|
}
|
|
@@ -2789,6 +3177,16 @@ CREATE TABLE IF NOT EXISTS processes (
|
|
|
2789
3177
|
cwd TEXT, cmd TEXT, owner TEXT, log TEXT, started_at TEXT, ended_at TEXT
|
|
2790
3178
|
);
|
|
2791
3179
|
CREATE INDEX IF NOT EXISTS processes_live ON processes(ended_at, project_id);
|
|
3180
|
+
CREATE TABLE IF NOT EXISTS gates (
|
|
3181
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, gate TEXT, verdict TEXT,
|
|
3182
|
+
rubric TEXT, evidence TEXT, session_id TEXT, created_at TEXT
|
|
3183
|
+
);
|
|
3184
|
+
CREATE INDEX IF NOT EXISTS gates_task ON gates(project_id, task, created_at);
|
|
3185
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
3186
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, done TEXT, remaining TEXT,
|
|
3187
|
+
files TEXT, verify TEXT, by TEXT, session_id TEXT, created_at TEXT
|
|
3188
|
+
);
|
|
3189
|
+
CREATE INDEX IF NOT EXISTS handoffs_task ON handoffs(project_id, task, created_at);
|
|
2792
3190
|
CREATE TABLE IF NOT EXISTS claims (
|
|
2793
3191
|
project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
|
|
2794
3192
|
acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
|
|
@@ -2809,15 +3207,15 @@ class Store {
|
|
|
2809
3207
|
gen = 0;
|
|
2810
3208
|
memo = new Map;
|
|
2811
3209
|
constructor(home = swarmHome()) {
|
|
2812
|
-
|
|
3210
|
+
mkdirSync3(home, { recursive: true });
|
|
2813
3211
|
this.home = home;
|
|
2814
|
-
this.db = new Database(
|
|
3212
|
+
this.db = new Database(join6(home, "swarm.db"));
|
|
2815
3213
|
this.loadPricing();
|
|
2816
3214
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
|
|
2817
3215
|
this.db.exec(SCHEMA);
|
|
2818
3216
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
2819
3217
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
2820
|
-
this.migrateProjectsJson(
|
|
3218
|
+
this.migrateProjectsJson(join6(home, "projects.json"));
|
|
2821
3219
|
this.reconcileMovedProjects();
|
|
2822
3220
|
this.slimExistingEvents();
|
|
2823
3221
|
this.retypeNotificationIncidents();
|
|
@@ -2919,17 +3317,186 @@ class Store {
|
|
|
2919
3317
|
return v;
|
|
2920
3318
|
}
|
|
2921
3319
|
rulesCache = new Map;
|
|
3320
|
+
preregisterSpawnedSession(id, projectId, cwd, task) {
|
|
3321
|
+
const now = new Date().toISOString();
|
|
3322
|
+
this.db.query(`INSERT INTO sessions (id, project_id, kind, cwd, started_at, last_seen_at, last, last_type, state, title)
|
|
3323
|
+
VALUES (?, ?, 'spawned', ?, ?, ?, ?, 'session.started', 'active', ?)
|
|
3324
|
+
ON CONFLICT(id) DO UPDATE SET kind = 'spawned', project_id = excluded.project_id, cwd = excluded.cwd`).run(id, projectId, cwd, now, now, `swarm run ${task}`, `run: ${task}`);
|
|
3325
|
+
this.touch();
|
|
3326
|
+
}
|
|
3327
|
+
endSpawnedSession(id) {
|
|
3328
|
+
const now = new Date().toISOString();
|
|
3329
|
+
this.db.query("UPDATE sessions SET state = 'ended', ended_at = COALESCE(ended_at, ?), last_seen_at = ? WHERE id = ?").run(now, now, id);
|
|
3330
|
+
this.touch();
|
|
3331
|
+
}
|
|
3332
|
+
recordHandoff(projectId, h) {
|
|
3333
|
+
if (!this.project(projectId))
|
|
3334
|
+
return { ok: false, reason: "unknown project" };
|
|
3335
|
+
const v = validateHandoff(h);
|
|
3336
|
+
if (!v.ok)
|
|
3337
|
+
return v;
|
|
3338
|
+
const handoff = {
|
|
3339
|
+
task: h.task.trim(),
|
|
3340
|
+
done: h.done.trim(),
|
|
3341
|
+
remaining: h.remaining.trim(),
|
|
3342
|
+
files: (h.files ?? []).map((f) => f.trim()).filter(Boolean).slice(0, 50),
|
|
3343
|
+
verify: h.verify?.trim() || null,
|
|
3344
|
+
by: h.by?.trim() || null,
|
|
3345
|
+
createdAt: new Date().toISOString()
|
|
3346
|
+
};
|
|
3347
|
+
const sessionId = this.knownSession(h.sessionId);
|
|
3348
|
+
this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
|
|
3349
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt);
|
|
3350
|
+
this.append({
|
|
3351
|
+
ts: handoff.createdAt,
|
|
3352
|
+
type: "handoff.recorded",
|
|
3353
|
+
projectId,
|
|
3354
|
+
sessionId,
|
|
3355
|
+
payload: { task: handoff.task, by: handoff.by, summary: `handoff on ${handoff.task}` }
|
|
3356
|
+
});
|
|
3357
|
+
this.touch();
|
|
3358
|
+
return { ok: true, handoff };
|
|
3359
|
+
}
|
|
3360
|
+
latestHandoff(projectId, task) {
|
|
3361
|
+
const r = this.db.query("SELECT * FROM handoffs WHERE project_id = ? AND task = ? ORDER BY id DESC LIMIT 1").get(projectId, task);
|
|
3362
|
+
if (!r)
|
|
3363
|
+
return null;
|
|
3364
|
+
return {
|
|
3365
|
+
task: r.task,
|
|
3366
|
+
done: r.done,
|
|
3367
|
+
remaining: r.remaining,
|
|
3368
|
+
files: JSON.parse(r.files || "[]"),
|
|
3369
|
+
verify: r.verify ?? null,
|
|
3370
|
+
by: r.by ?? null,
|
|
3371
|
+
createdAt: r.created_at
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
handoffs(projectId, limit = 50) {
|
|
3375
|
+
return this.db.query("SELECT * FROM handoffs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map((r) => ({
|
|
3376
|
+
task: r.task,
|
|
3377
|
+
done: r.done,
|
|
3378
|
+
remaining: r.remaining,
|
|
3379
|
+
files: JSON.parse(r.files || "[]"),
|
|
3380
|
+
verify: r.verify ?? null,
|
|
3381
|
+
by: r.by ?? null,
|
|
3382
|
+
createdAt: r.created_at,
|
|
3383
|
+
sessionId: r.session_id ?? null
|
|
3384
|
+
}));
|
|
3385
|
+
}
|
|
3386
|
+
sessionContext(cwd) {
|
|
3387
|
+
if (!cwd || !existsSync4(cwd))
|
|
3388
|
+
return null;
|
|
3389
|
+
const toplevel = this.toplevel(cwd);
|
|
3390
|
+
const project = this.resolveProject(cwd);
|
|
3391
|
+
const lines = [];
|
|
3392
|
+
const held = this.heldClaimsWithWorktree().find((c) => isInside(cwd, c.worktree));
|
|
3393
|
+
if (held) {
|
|
3394
|
+
const left = Math.max(0, Math.round((new Date(held.expiresAt).getTime() - Date.now()) / 60000));
|
|
3395
|
+
lines.push(`[swarm] you hold ${held.task} (${left}m left, renews while you work) in ${held.worktree}`);
|
|
3396
|
+
const h = this.latestHandoff(held.projectId, held.task);
|
|
3397
|
+
if (h)
|
|
3398
|
+
lines.push(formatHandoff(h));
|
|
3399
|
+
const required = this.requiredGates(held.projectId);
|
|
3400
|
+
if (required.length) {
|
|
3401
|
+
const st = gateStatus(this.gateRuns(held.projectId, held.task), required);
|
|
3402
|
+
lines.push(`[swarm] gates on ${held.task}: ${st.map((g) => `${g.gate} ${g.verdict ?? "not run"}`).join(", ")} \u2014 record with swarm_gate_record (rubric required)`);
|
|
3403
|
+
}
|
|
3404
|
+
} else if (project) {
|
|
3405
|
+
const active = this.claimRows(project.id).filter((c) => isActive(c, Date.now()));
|
|
3406
|
+
if (active.length)
|
|
3407
|
+
lines.push(`[swarm] ${project.name}: held by others \u2014 ${active.map((c) => `${c.task} (${c.owner})`).join(", ")}. Claim a task (swarm_claim) to get your own worktree.`);
|
|
3408
|
+
}
|
|
3409
|
+
const res = this.resources(project?.id).filter((r) => !r.released);
|
|
3410
|
+
if (res.length)
|
|
3411
|
+
lines.push(`[swarm] resources held: ${res.map((r) => `${r.name}${r.port ? `:${r.port}` : ""} (${r.owner})`).join(", ")} \u2014 their ports are protected; don't kill them`);
|
|
3412
|
+
const modes = this.rulesFor(toplevel);
|
|
3413
|
+
const on = [
|
|
3414
|
+
"shared_tree",
|
|
3415
|
+
"destructive_git",
|
|
3416
|
+
"pattern_kill",
|
|
3417
|
+
"protected_ports",
|
|
3418
|
+
"no_foreign_worktree",
|
|
3419
|
+
"claim_required_to_write"
|
|
3420
|
+
].filter((k) => modes[k] !== "off").map((k) => `${k}=${modes[k]}`);
|
|
3421
|
+
if (on.length && (lines.length || on.some((x) => x.endsWith("=deny"))))
|
|
3422
|
+
lines.push(`[swarm] rules: ${on.join(" ")}`);
|
|
3423
|
+
return lines.length ? lines.join(`
|
|
3424
|
+
`) : null;
|
|
3425
|
+
}
|
|
3426
|
+
rowToGate(r) {
|
|
3427
|
+
return {
|
|
3428
|
+
id: r.id,
|
|
3429
|
+
projectId: r.project_id,
|
|
3430
|
+
task: r.task,
|
|
3431
|
+
gate: r.gate,
|
|
3432
|
+
verdict: r.verdict,
|
|
3433
|
+
rubric: r.rubric,
|
|
3434
|
+
evidence: r.evidence ?? null,
|
|
3435
|
+
sessionId: r.session_id ?? null,
|
|
3436
|
+
createdAt: r.created_at
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
gateRuns(projectId, task, limit = 200) {
|
|
3440
|
+
const rows = task ? this.db.query("SELECT * FROM gates WHERE project_id = ? AND task = ? ORDER BY created_at DESC, id DESC LIMIT ?").all(projectId, task, limit) : this.db.query("SELECT * FROM gates WHERE project_id = ? ORDER BY created_at DESC, id DESC LIMIT ?").all(projectId, limit);
|
|
3441
|
+
return rows.map((r) => this.rowToGate(r));
|
|
3442
|
+
}
|
|
3443
|
+
gateStatusFor(runs, required) {
|
|
3444
|
+
return gateStatus(runs, required);
|
|
3445
|
+
}
|
|
3446
|
+
requiredGates(projectId) {
|
|
3447
|
+
const p = this.project(projectId);
|
|
3448
|
+
return p ? loadConfig({ repoRoot: p.root, home: this.home }).gates.required : [];
|
|
3449
|
+
}
|
|
3450
|
+
recordGate(projectId, input) {
|
|
3451
|
+
if (!this.project(projectId))
|
|
3452
|
+
return { ok: false, reason: "unknown project" };
|
|
3453
|
+
const v = validateGateRun(input);
|
|
3454
|
+
if (!v.ok)
|
|
3455
|
+
return v;
|
|
3456
|
+
const createdAt = new Date().toISOString();
|
|
3457
|
+
const sessionId = this.knownSession(input.sessionId);
|
|
3458
|
+
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at)
|
|
3459
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt);
|
|
3460
|
+
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
3461
|
+
this.append({
|
|
3462
|
+
ts: createdAt,
|
|
3463
|
+
type: "gate.recorded",
|
|
3464
|
+
projectId,
|
|
3465
|
+
sessionId,
|
|
3466
|
+
payload: {
|
|
3467
|
+
task: run2.task,
|
|
3468
|
+
gate: run2.gate,
|
|
3469
|
+
verdict: run2.verdict,
|
|
3470
|
+
summary: `gate ${run2.gate} ${run2.verdict} on ${run2.task}`
|
|
3471
|
+
}
|
|
3472
|
+
});
|
|
3473
|
+
if (run2.verdict === "fail")
|
|
3474
|
+
this.append({
|
|
3475
|
+
ts: createdAt,
|
|
3476
|
+
type: "incident.opened",
|
|
3477
|
+
projectId,
|
|
3478
|
+
sessionId,
|
|
3479
|
+
payload: {
|
|
3480
|
+
rule: "gate_failed",
|
|
3481
|
+
action: "failed",
|
|
3482
|
+
command: `${run2.task} \xB7 ${run2.gate}`,
|
|
3483
|
+
reason: `${run2.rubric}${run2.evidence ? ` \u2014 ${run2.evidence.slice(0, 200)}` : ""}`
|
|
3484
|
+
}
|
|
3485
|
+
});
|
|
3486
|
+
this.touch();
|
|
3487
|
+
return { ok: true, run: run2 };
|
|
3488
|
+
}
|
|
2922
3489
|
taskCache = new Map;
|
|
2923
3490
|
tasks(projectId) {
|
|
2924
3491
|
const p = this.project(projectId);
|
|
2925
3492
|
if (!p)
|
|
2926
3493
|
return null;
|
|
2927
|
-
const source = loadConfig({ repoRoot: p.root }).tasks.source;
|
|
3494
|
+
const source = loadConfig({ repoRoot: p.root, home: this.home }).tasks.source;
|
|
2928
3495
|
if (!source)
|
|
2929
3496
|
return null;
|
|
2930
|
-
const path =
|
|
3497
|
+
const path = join6(p.root, source);
|
|
2931
3498
|
if (!existsSync4(path))
|
|
2932
|
-
return { source, tasks: [] };
|
|
3499
|
+
return { source, required: this.requiredGates(projectId), tasks: [] };
|
|
2933
3500
|
const mtime = statSync(path).mtimeMs;
|
|
2934
3501
|
let hit = this.taskCache.get(projectId);
|
|
2935
3502
|
if (!hit || hit.path !== path || hit.mtime !== mtime) {
|
|
@@ -2938,17 +3505,80 @@ class Store {
|
|
|
2938
3505
|
}
|
|
2939
3506
|
const now = Date.now();
|
|
2940
3507
|
const active = this.claimRows(projectId).filter((c) => isActive(c, now));
|
|
2941
|
-
|
|
3508
|
+
const required = this.requiredGates(projectId);
|
|
3509
|
+
const runs = this.gateRuns(projectId, undefined, 2000);
|
|
3510
|
+
const byTask = new Map;
|
|
3511
|
+
for (const r of runs)
|
|
3512
|
+
byTask.set(r.task, [...byTask.get(r.task) ?? [], r]);
|
|
3513
|
+
const board = taskBoard(hit.tasks, active).map((t) => {
|
|
3514
|
+
const tr = byTask.get(t.id) ?? [];
|
|
3515
|
+
return {
|
|
3516
|
+
...t,
|
|
3517
|
+
gates: gateStatus(tr, required).map((g) => ({
|
|
3518
|
+
gate: g.gate,
|
|
3519
|
+
verdict: g.verdict,
|
|
3520
|
+
fails: g.fails,
|
|
3521
|
+
runs: g.runs
|
|
3522
|
+
})),
|
|
3523
|
+
gated: gatesSatisfied(tr, required)
|
|
3524
|
+
};
|
|
3525
|
+
});
|
|
3526
|
+
return { source, required, tasks: board };
|
|
2942
3527
|
}
|
|
2943
3528
|
rulesFor(repoRoot) {
|
|
2944
3529
|
const key = repoRoot ?? "";
|
|
2945
3530
|
const hit = this.rulesCache.get(key);
|
|
2946
3531
|
if (hit && Date.now() - hit.at < 30000)
|
|
2947
3532
|
return hit.rules;
|
|
2948
|
-
const rules2 = loadConfig({ repoRoot }).rules;
|
|
3533
|
+
const rules2 = loadConfig({ repoRoot, home: this.home }).rules;
|
|
2949
3534
|
this.rulesCache.set(key, { at: Date.now(), rules: rules2 });
|
|
2950
3535
|
return rules2;
|
|
2951
3536
|
}
|
|
3537
|
+
evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
|
|
3538
|
+
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
3539
|
+
const cmd = tool === "Bash" ? input.command : undefined;
|
|
3540
|
+
const current = { id: sessionId, cwd, toplevel: this.toplevel(cwd) };
|
|
3541
|
+
const modes = this.rulesFor(current.toplevel);
|
|
3542
|
+
if (isWrite && (modes.no_foreign_worktree !== "off" || modes.claim_required_to_write !== "off")) {
|
|
3543
|
+
const target = absolutePath(input.file_path, cwd);
|
|
3544
|
+
const w = guardWrite(target, current, this.heldWorktrees(), modes, "file");
|
|
3545
|
+
if (w.action !== "allow") {
|
|
3546
|
+
if (recordIncident)
|
|
3547
|
+
this.openIncident(w, cwd, sessionId, `${tool} ${target}`);
|
|
3548
|
+
return { decision: w, display: `${tool} ${target}` };
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
if (cmd) {
|
|
3552
|
+
if (modes.no_foreign_worktree !== "off" || modes.claim_required_to_write !== "off") {
|
|
3553
|
+
const w = guardWrite(cwd, current, this.heldWorktrees(), modes, "bash");
|
|
3554
|
+
if (w.action !== "allow") {
|
|
3555
|
+
if (recordIncident)
|
|
3556
|
+
this.openIncident(w, cwd, sessionId, cmd);
|
|
3557
|
+
return { decision: w, display: cmd };
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
const d = guardBash(cmd, current, this.liveSessions(), Date.now(), {
|
|
3561
|
+
...modes,
|
|
3562
|
+
protected: { ports: [...new Set([...modes.protected.ports, ...this.heldPorts()])] }
|
|
3563
|
+
});
|
|
3564
|
+
if (d.action !== "allow" && recordIncident)
|
|
3565
|
+
this.openIncident(d, cwd, sessionId, cmd);
|
|
3566
|
+
return { decision: d, display: cmd };
|
|
3567
|
+
}
|
|
3568
|
+
return {
|
|
3569
|
+
decision: { action: "allow" },
|
|
3570
|
+
display: isWrite ? `${tool} ${input.file_path}` : tool
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
liveSessions() {
|
|
3574
|
+
const rows = this.db.query("SELECT id, cwd, last_seen_at, state FROM sessions WHERE state != 'ended' AND last_seen_at > ?").all(new Date(Date.now() - LIVE_WINDOW_MS - 1e4).toISOString());
|
|
3575
|
+
return rows.map((r) => ({
|
|
3576
|
+
id: r.id,
|
|
3577
|
+
toplevel: this.toplevel(r.cwd),
|
|
3578
|
+
lastSeenAt: r.last_seen_at,
|
|
3579
|
+
state: r.state
|
|
3580
|
+
}));
|
|
3581
|
+
}
|
|
2952
3582
|
guardHook(raw2) {
|
|
2953
3583
|
const tool = typeof raw2.tool_name === "string" ? raw2.tool_name : "";
|
|
2954
3584
|
const input = raw2.tool_input ?? {};
|
|
@@ -3005,7 +3635,7 @@ class Store {
|
|
|
3005
3635
|
loadPricing() {
|
|
3006
3636
|
this.prices = { ...PRICES };
|
|
3007
3637
|
for (const f of ["pricing.litellm.json", "pricing.json"]) {
|
|
3008
|
-
const p =
|
|
3638
|
+
const p = join6(this.home, f);
|
|
3009
3639
|
if (!existsSync4(p))
|
|
3010
3640
|
continue;
|
|
3011
3641
|
try {
|
|
@@ -3021,7 +3651,7 @@ class Store {
|
|
|
3021
3651
|
throw new Error(`pricing fetch ${r.status}`);
|
|
3022
3652
|
const j = await r.json();
|
|
3023
3653
|
const slim = Object.fromEntries(Object.entries(j).filter(([k, v]) => typeof v.input_cost_per_token === "number" && !k.includes("/")));
|
|
3024
|
-
writeFileSync2(
|
|
3654
|
+
writeFileSync2(join6(this.home, "pricing.litellm.json"), JSON.stringify(slim, null, 1));
|
|
3025
3655
|
this.loadPricing();
|
|
3026
3656
|
this.reprice();
|
|
3027
3657
|
}
|
|
@@ -3160,6 +3790,8 @@ class Store {
|
|
|
3160
3790
|
return n;
|
|
3161
3791
|
}
|
|
3162
3792
|
ingestHook(event, raw2) {
|
|
3793
|
+
if (typeof raw2.cwd === "string")
|
|
3794
|
+
this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
|
|
3163
3795
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
|
|
3164
3796
|
const project = existsSync4(cwd) ? this.resolveProject(cwd) : null;
|
|
3165
3797
|
const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
|
|
@@ -3173,8 +3805,24 @@ class Store {
|
|
|
3173
3805
|
}
|
|
3174
3806
|
return e;
|
|
3175
3807
|
}
|
|
3808
|
+
static LEDGER_EVENTS = new Set([
|
|
3809
|
+
"process.started",
|
|
3810
|
+
"process.exited",
|
|
3811
|
+
"resource.acquired",
|
|
3812
|
+
"resource.released",
|
|
3813
|
+
"resource.reaped",
|
|
3814
|
+
"claim.acquired",
|
|
3815
|
+
"claim.renewed",
|
|
3816
|
+
"claim.released",
|
|
3817
|
+
"claim.orphaned",
|
|
3818
|
+
"gate.recorded",
|
|
3819
|
+
"handoff.recorded",
|
|
3820
|
+
"incident.opened",
|
|
3821
|
+
"incident.acked",
|
|
3822
|
+
"run.result"
|
|
3823
|
+
]);
|
|
3176
3824
|
projectSession(e) {
|
|
3177
|
-
if (!e.sessionId)
|
|
3825
|
+
if (!e.sessionId || Store.LEDGER_EVENTS.has(e.type))
|
|
3178
3826
|
return;
|
|
3179
3827
|
const p = e.payload;
|
|
3180
3828
|
const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
|
|
@@ -3198,7 +3846,7 @@ class Store {
|
|
|
3198
3846
|
const size = statSync(path).size;
|
|
3199
3847
|
if (size <= offset)
|
|
3200
3848
|
return null;
|
|
3201
|
-
const fd =
|
|
3849
|
+
const fd = openSync2(path, "r");
|
|
3202
3850
|
const buf = Buffer.alloc(size - offset);
|
|
3203
3851
|
readSync(fd, buf, 0, buf.length, offset);
|
|
3204
3852
|
closeSync(fd);
|
|
@@ -3239,6 +3887,10 @@ class Store {
|
|
|
3239
3887
|
const lastText = [...d.turns].reverse().find((t) => t.text && !t.sidechain)?.text ?? null;
|
|
3240
3888
|
const lastModel = [...d.turns].reverse().find((t) => !t.sidechain)?.model ?? null;
|
|
3241
3889
|
this.db.query("UPDATE sessions SET title = COALESCE(?, title), model = COALESCE(?, model), version = COALESCE(?, version), last_text = COALESCE(?, last_text), branch = COALESCE(branch, ?), last_seen_at = MAX(COALESCE(last_seen_at, ''), ?) WHERE id = ?").run(d.title, agentId ? null : lastModel, d.version, agentId ? null : lastText, d.branch, new Date().toISOString(), sessionId);
|
|
3890
|
+
if (d.turns.length) {
|
|
3891
|
+
const cwdRow = this.db.query("SELECT cwd FROM sessions WHERE id = ?").get(sessionId);
|
|
3892
|
+
this.autoRenewFor(sessionId, cwdRow?.cwd);
|
|
3893
|
+
}
|
|
3242
3894
|
this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset").run(path, sessionId, agentId, r.next);
|
|
3243
3895
|
return d.turns.length;
|
|
3244
3896
|
}
|
|
@@ -3247,9 +3899,9 @@ class Store {
|
|
|
3247
3899
|
if (!s?.transcript_path || !existsSync4(s.transcript_path))
|
|
3248
3900
|
return 0;
|
|
3249
3901
|
let n = this.tailFile(s.transcript_path, sessionId, null);
|
|
3250
|
-
const subDir =
|
|
3902
|
+
const subDir = join6(dirname(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
|
|
3251
3903
|
for (const f of this.subagentFiles(subDir)) {
|
|
3252
|
-
n += this.tailFile(
|
|
3904
|
+
n += this.tailFile(join6(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
|
|
3253
3905
|
}
|
|
3254
3906
|
return n;
|
|
3255
3907
|
}
|
|
@@ -3281,7 +3933,7 @@ class Store {
|
|
|
3281
3933
|
return n;
|
|
3282
3934
|
}
|
|
3283
3935
|
codexRoot() {
|
|
3284
|
-
return process.env.SWARM_CODEX_DIR ??
|
|
3936
|
+
return process.env.SWARM_CODEX_DIR ?? join6(homedir3(), ".codex", "sessions");
|
|
3285
3937
|
}
|
|
3286
3938
|
codexRolloutFiles(sinceMs) {
|
|
3287
3939
|
const root = this.codexRoot();
|
|
@@ -3296,18 +3948,18 @@ class Store {
|
|
|
3296
3948
|
for (const y of ls(root)) {
|
|
3297
3949
|
if (!/^\d{4}$/.test(y))
|
|
3298
3950
|
continue;
|
|
3299
|
-
for (const m of ls(
|
|
3951
|
+
for (const m of ls(join6(root, y))) {
|
|
3300
3952
|
if (!/^\d\d$/.test(m))
|
|
3301
3953
|
continue;
|
|
3302
|
-
for (const day of ls(
|
|
3954
|
+
for (const day of ls(join6(root, y, m))) {
|
|
3303
3955
|
if (!/^\d\d$/.test(day))
|
|
3304
3956
|
continue;
|
|
3305
3957
|
if (Date.parse(`${y}-${m}-${day}T23:59:59Z`) < sinceMs)
|
|
3306
3958
|
continue;
|
|
3307
|
-
const dir =
|
|
3959
|
+
const dir = join6(root, y, m, day);
|
|
3308
3960
|
for (const f of ls(dir)) {
|
|
3309
3961
|
if (f.startsWith("rollout-") && f.endsWith(".jsonl"))
|
|
3310
|
-
out.push(
|
|
3962
|
+
out.push(join6(dir, f));
|
|
3311
3963
|
}
|
|
3312
3964
|
}
|
|
3313
3965
|
}
|
|
@@ -3324,7 +3976,7 @@ class Store {
|
|
|
3324
3976
|
return n;
|
|
3325
3977
|
}
|
|
3326
3978
|
grokRoot() {
|
|
3327
|
-
return process.env.SWARM_GROK_DIR ??
|
|
3979
|
+
return process.env.SWARM_GROK_DIR ?? join6(homedir3(), ".grok", "sessions");
|
|
3328
3980
|
}
|
|
3329
3981
|
grokSummary = new Map;
|
|
3330
3982
|
tailGrok(windowMs = 3 * 24 * 60 * 60000) {
|
|
@@ -3349,9 +4001,9 @@ class Store {
|
|
|
3349
4001
|
} catch {
|
|
3350
4002
|
cwd = enc;
|
|
3351
4003
|
}
|
|
3352
|
-
const cwdDir =
|
|
4004
|
+
const cwdDir = join6(root, enc);
|
|
3353
4005
|
for (const sid of ls(cwdDir)) {
|
|
3354
|
-
const path =
|
|
4006
|
+
const path = join6(cwdDir, sid, "updates.jsonl");
|
|
3355
4007
|
if (!existsSync4(path))
|
|
3356
4008
|
continue;
|
|
3357
4009
|
try {
|
|
@@ -3360,7 +4012,7 @@ class Store {
|
|
|
3360
4012
|
} catch {
|
|
3361
4013
|
continue;
|
|
3362
4014
|
}
|
|
3363
|
-
const sumPath =
|
|
4015
|
+
const sumPath = join6(cwdDir, sid, "summary.json");
|
|
3364
4016
|
let title;
|
|
3365
4017
|
let fresh = false;
|
|
3366
4018
|
try {
|
|
@@ -3447,7 +4099,7 @@ class Store {
|
|
|
3447
4099
|
worktreePath(projectId, task) {
|
|
3448
4100
|
const slug = (x) => x.replace(/[^a-zA-Z0-9._-]+/g, "-").toLowerCase();
|
|
3449
4101
|
const p = this.project(projectId);
|
|
3450
|
-
return
|
|
4102
|
+
return join6(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
|
|
3451
4103
|
}
|
|
3452
4104
|
claim(projectId, task, owner, baseRef = "HEAD") {
|
|
3453
4105
|
const p = this.project(projectId);
|
|
@@ -3461,7 +4113,7 @@ class Store {
|
|
|
3461
4113
|
const worktree = this.worktreePath(projectId, task);
|
|
3462
4114
|
if (existsSync4(worktree))
|
|
3463
4115
|
return { ok: false, error: `${worktree} already exists; release ${task} first` };
|
|
3464
|
-
|
|
4116
|
+
mkdirSync3(dirname(worktree), { recursive: true });
|
|
3465
4117
|
const created = worktreeAdd(p.root, worktree, branch, baseRef);
|
|
3466
4118
|
if (!created)
|
|
3467
4119
|
return { ok: false, error: `git worktree add failed for ${task}` };
|
|
@@ -3481,6 +4133,82 @@ class Store {
|
|
|
3481
4133
|
});
|
|
3482
4134
|
return { ok: true, task, owner, worktree: created, branch, expiresAt };
|
|
3483
4135
|
}
|
|
4136
|
+
autoRenewAt = new Map;
|
|
4137
|
+
autoRenewFor(sessionId, cwd) {
|
|
4138
|
+
if (!cwd)
|
|
4139
|
+
return;
|
|
4140
|
+
const key = sessionId ?? cwd;
|
|
4141
|
+
const last = this.autoRenewAt.get(key) ?? 0;
|
|
4142
|
+
const now = Date.now();
|
|
4143
|
+
if (now - last < 60000)
|
|
4144
|
+
return;
|
|
4145
|
+
this.autoRenewAt.set(key, now);
|
|
4146
|
+
for (const c of this.heldClaimsWithWorktree()) {
|
|
4147
|
+
if (!isInside(cwd, c.worktree))
|
|
4148
|
+
continue;
|
|
4149
|
+
if (!shouldAutoRenew({ state: "held", expiresAt: c.expiresAt }, now))
|
|
4150
|
+
continue;
|
|
4151
|
+
const expiresAt = nextExpiry(now);
|
|
4152
|
+
this.db.query("UPDATE claims SET expires_at = ? WHERE project_id = ? AND task = ? AND state = 'held'").run(expiresAt, c.projectId, c.task);
|
|
4153
|
+
this.append({
|
|
4154
|
+
ts: new Date(now).toISOString(),
|
|
4155
|
+
type: "claim.renewed",
|
|
4156
|
+
projectId: c.projectId,
|
|
4157
|
+
sessionId: this.knownSession(sessionId),
|
|
4158
|
+
payload: { task: c.task, expiresAt, auto: true, summary: `auto-renew ${c.task}` }
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
heldClaimsWithWorktree() {
|
|
4163
|
+
return this.db.query("SELECT project_id, task, worktree, expires_at FROM claims WHERE state = 'held' AND worktree != ''").all().map((r) => ({
|
|
4164
|
+
projectId: r.project_id,
|
|
4165
|
+
task: r.task,
|
|
4166
|
+
worktree: r.worktree,
|
|
4167
|
+
expiresAt: r.expires_at
|
|
4168
|
+
}));
|
|
4169
|
+
}
|
|
4170
|
+
sweepOrphans() {
|
|
4171
|
+
const now = Date.now();
|
|
4172
|
+
let n = 0;
|
|
4173
|
+
for (const p of this.projects()) {
|
|
4174
|
+
for (const c of this.claimRows(p.id)) {
|
|
4175
|
+
if (c.state !== "held" || isActive(c, now))
|
|
4176
|
+
continue;
|
|
4177
|
+
const exists = c.worktree ? existsSync4(c.worktree) : false;
|
|
4178
|
+
const work = exists ? heldWork(c.worktree) : null;
|
|
4179
|
+
if (reapAction(c, now, exists, work) !== "keep-orphaned")
|
|
4180
|
+
continue;
|
|
4181
|
+
this.db.query("UPDATE claims SET state = 'orphaned' WHERE project_id = ? AND task = ?").run(p.id, c.task);
|
|
4182
|
+
const ts = new Date(now).toISOString();
|
|
4183
|
+
this.append({
|
|
4184
|
+
ts,
|
|
4185
|
+
type: "claim.orphaned",
|
|
4186
|
+
projectId: p.id,
|
|
4187
|
+
sessionId: null,
|
|
4188
|
+
payload: {
|
|
4189
|
+
task: c.task,
|
|
4190
|
+
worktree: c.worktree,
|
|
4191
|
+
summary: `orphaned ${c.task} (holds work)`
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
this.append({
|
|
4195
|
+
ts,
|
|
4196
|
+
type: "incident.opened",
|
|
4197
|
+
projectId: p.id,
|
|
4198
|
+
sessionId: null,
|
|
4199
|
+
payload: {
|
|
4200
|
+
rule: "orphaned_claim",
|
|
4201
|
+
action: "orphaned",
|
|
4202
|
+
command: `${c.task} \u2192 ${c.worktree}`,
|
|
4203
|
+
reason: `The lease on "${c.task}" (held by ${c.owner}) expired while its worktree still holds ${work?.dirty ? "uncommitted" : "unpushed"} work. Nothing was removed: renew it, finish and push, or force-release to discard.`
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
4206
|
+
this.touch();
|
|
4207
|
+
n++;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
return n;
|
|
4211
|
+
}
|
|
3484
4212
|
renew(projectId, task) {
|
|
3485
4213
|
const row = this.db.query("SELECT state FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
|
|
3486
4214
|
if (!row)
|
|
@@ -4190,13 +4918,13 @@ function rowToEvent(r) {
|
|
|
4190
4918
|
}
|
|
4191
4919
|
|
|
4192
4920
|
// packages/daemon/src/app.ts
|
|
4193
|
-
var VERSION = "0.
|
|
4921
|
+
var VERSION = "0.5.0";
|
|
4194
4922
|
var WEB_DIR = (() => {
|
|
4195
4923
|
if (process.env.SWARM_WEB_DIR)
|
|
4196
4924
|
return process.env.SWARM_WEB_DIR;
|
|
4197
4925
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
4198
|
-
const dev =
|
|
4199
|
-
return existsSync5(
|
|
4926
|
+
const dev = join7(here, "../../web/public");
|
|
4927
|
+
return existsSync5(join7(dev, "index.html")) ? dev : join7(here, "../web");
|
|
4200
4928
|
})();
|
|
4201
4929
|
var REPLAY_TAIL = 200;
|
|
4202
4930
|
var wireCache = new WeakMap;
|
|
@@ -4211,6 +4939,7 @@ function wireJson(e) {
|
|
|
4211
4939
|
function createApp(store = new Store) {
|
|
4212
4940
|
const app = new Hono2;
|
|
4213
4941
|
const forge2 = new ForgeService(store);
|
|
4942
|
+
const runner = new Runner(store, store.home);
|
|
4214
4943
|
app.get("/v1/health", (c) => c.json({ ok: true, version: VERSION }));
|
|
4215
4944
|
app.get("/v1/projects", (c) => c.json(store.snapshot().projects));
|
|
4216
4945
|
app.post("/v1/projects", async (c) => {
|
|
@@ -4244,7 +4973,7 @@ function createApp(store = new Store) {
|
|
|
4244
4973
|
dir = homedir4();
|
|
4245
4974
|
}
|
|
4246
4975
|
try {
|
|
4247
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync5(
|
|
4976
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync5(join7(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
4248
4977
|
const parent = dirname2(dir);
|
|
4249
4978
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
4250
4979
|
} catch (e) {
|
|
@@ -4318,6 +5047,92 @@ function createApp(store = new Store) {
|
|
|
4318
5047
|
const r = await store.stopProcess(pid, c.req.query("project") || null);
|
|
4319
5048
|
return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
|
|
4320
5049
|
});
|
|
5050
|
+
app.get("/v1/runs", (c) => c.json(runner.list(c.req.query("project") || undefined)));
|
|
5051
|
+
app.post("/v1/runs", async (c) => {
|
|
5052
|
+
const b = await c.req.json().catch(() => ({}));
|
|
5053
|
+
if (!b.projectId || !b.task || !b.prompt)
|
|
5054
|
+
return c.json({ ok: false, error: "projectId, task and prompt required" }, 400);
|
|
5055
|
+
const r = await runner.start({
|
|
5056
|
+
projectId: b.projectId,
|
|
5057
|
+
task: b.task,
|
|
5058
|
+
prompt: b.prompt,
|
|
5059
|
+
owner: b.owner ?? "dashboard",
|
|
5060
|
+
model: b.model,
|
|
5061
|
+
permissionMode: b.permissionMode,
|
|
5062
|
+
allowedTools: b.allowedTools,
|
|
5063
|
+
maxTurns: b.maxTurns
|
|
5064
|
+
});
|
|
5065
|
+
return r.ok ? c.json(r, 201) : c.json({ ok: false, error: r.reason }, 409);
|
|
5066
|
+
});
|
|
5067
|
+
app.post("/v1/runs/:id/send", async (c) => {
|
|
5068
|
+
const b = await c.req.json().catch(() => ({}));
|
|
5069
|
+
if (!b.text?.trim())
|
|
5070
|
+
return c.json({ ok: false, error: "text required" }, 400);
|
|
5071
|
+
const r = runner.send(c.req.param("id"), b.text);
|
|
5072
|
+
return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
|
|
5073
|
+
});
|
|
5074
|
+
app.post("/v1/runs/:id/permissions/:reqId", async (c) => {
|
|
5075
|
+
const b = await c.req.json().catch(() => ({}));
|
|
5076
|
+
const r = runner.answerPermission(c.req.param("id"), c.req.param("reqId"), b.allow === true, b.message);
|
|
5077
|
+
return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
|
|
5078
|
+
});
|
|
5079
|
+
app.delete("/v1/runs/:id", async (c) => {
|
|
5080
|
+
const r = await runner.stop(c.req.param("id"));
|
|
5081
|
+
return r.ok ? c.json(r) : c.json({ ok: false, error: r.reason }, 404);
|
|
5082
|
+
});
|
|
5083
|
+
app.get("/v1/handoffs", (c) => {
|
|
5084
|
+
const project = c.req.query("project");
|
|
5085
|
+
if (!project)
|
|
5086
|
+
return c.json({ error: "project required" }, 400);
|
|
5087
|
+
const task = c.req.query("task");
|
|
5088
|
+
if (task) {
|
|
5089
|
+
const h = store.latestHandoff(project, task);
|
|
5090
|
+
return h ? c.json({ handoff: h, text: formatHandoff(h) }) : c.json({ handoff: null, text: null }, 404);
|
|
5091
|
+
}
|
|
5092
|
+
return c.json(store.handoffs(project));
|
|
5093
|
+
});
|
|
5094
|
+
app.post("/v1/handoffs", async (c) => {
|
|
5095
|
+
const b = await c.req.json().catch(() => ({}));
|
|
5096
|
+
if (!b.projectId || !b.task)
|
|
5097
|
+
return c.json({ ok: false, error: "projectId and task required" }, 400);
|
|
5098
|
+
const r = store.recordHandoff(b.projectId, {
|
|
5099
|
+
task: b.task,
|
|
5100
|
+
done: b.done ?? "",
|
|
5101
|
+
remaining: b.remaining ?? "",
|
|
5102
|
+
files: Array.isArray(b.files) ? b.files : [],
|
|
5103
|
+
verify: b.verify ?? null,
|
|
5104
|
+
by: b.by ?? null,
|
|
5105
|
+
sessionId: b.sessionId ?? null
|
|
5106
|
+
});
|
|
5107
|
+
return r.ok ? c.json(r, 201) : c.json({ ok: false, error: r.reason }, 400);
|
|
5108
|
+
});
|
|
5109
|
+
app.get("/v1/gates", (c) => {
|
|
5110
|
+
const project = c.req.query("project");
|
|
5111
|
+
if (!project)
|
|
5112
|
+
return c.json({ error: "project required" }, 400);
|
|
5113
|
+
const task = c.req.query("task") || undefined;
|
|
5114
|
+
const runs = store.gateRuns(project, task);
|
|
5115
|
+
const required = store.requiredGates(project);
|
|
5116
|
+
return c.json({
|
|
5117
|
+
required,
|
|
5118
|
+
runs,
|
|
5119
|
+
status: task ? store.gateStatusFor(runs, required) : undefined
|
|
5120
|
+
});
|
|
5121
|
+
});
|
|
5122
|
+
app.post("/v1/gates", async (c) => {
|
|
5123
|
+
const b = await c.req.json().catch(() => ({}));
|
|
5124
|
+
if (!b.projectId)
|
|
5125
|
+
return c.json({ ok: false, error: "projectId required" }, 400);
|
|
5126
|
+
const r = store.recordGate(b.projectId, {
|
|
5127
|
+
task: b.task ?? "",
|
|
5128
|
+
gate: b.gate ?? "",
|
|
5129
|
+
verdict: b.verdict,
|
|
5130
|
+
rubric: b.rubric,
|
|
5131
|
+
evidence: b.evidence,
|
|
5132
|
+
sessionId: b.sessionId ?? null
|
|
5133
|
+
});
|
|
5134
|
+
return r.ok ? c.json(r, 201) : c.json({ ok: false, error: r.reason }, 400);
|
|
5135
|
+
});
|
|
4321
5136
|
app.get("/v1/tasks", (c) => {
|
|
4322
5137
|
const project = c.req.query("project");
|
|
4323
5138
|
if (!project)
|
|
@@ -4376,6 +5191,14 @@ function createApp(store = new Store) {
|
|
|
4376
5191
|
const event = c.req.param("event");
|
|
4377
5192
|
const raw2 = await c.req.json().catch(() => ({}));
|
|
4378
5193
|
store.ingestHook(event, raw2);
|
|
5194
|
+
if (event === "SessionStart" && typeof raw2.cwd === "string") {
|
|
5195
|
+
const ctx = store.sessionContext(raw2.cwd);
|
|
5196
|
+
if (ctx)
|
|
5197
|
+
return c.json({
|
|
5198
|
+
additionalContext: ctx,
|
|
5199
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: ctx }
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
4379
5202
|
if (event === "PreToolUse" && process.env.SWARM_GUARD !== "off") {
|
|
4380
5203
|
const guard = store.guardHook(raw2);
|
|
4381
5204
|
if (guard) {
|
|
@@ -4416,23 +5239,23 @@ function createApp(store = new Store) {
|
|
|
4416
5239
|
});
|
|
4417
5240
|
});
|
|
4418
5241
|
});
|
|
4419
|
-
app.get("/", (c) => c.html(readFileSync4(
|
|
5242
|
+
app.get("/", (c) => c.html(readFileSync4(join7(WEB_DIR, "index.html"), "utf8")));
|
|
4420
5243
|
const MIME = { js: "text/javascript", css: "text/css" };
|
|
4421
5244
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
4422
5245
|
const f = c.req.param("file");
|
|
4423
|
-
const p =
|
|
5246
|
+
const p = join7(WEB_DIR, f);
|
|
4424
5247
|
if (!existsSync5(p))
|
|
4425
5248
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
4426
5249
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
4427
5250
|
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
4428
5251
|
});
|
|
4429
5252
|
});
|
|
4430
|
-
return { app, store, forge: forge2 };
|
|
5253
|
+
return { app, store, forge: forge2, runner };
|
|
4431
5254
|
}
|
|
4432
5255
|
|
|
4433
5256
|
// packages/daemon/src/bin.ts
|
|
4434
5257
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
4435
|
-
var { app, store } = createApp();
|
|
5258
|
+
var { app, store, runner } = createApp();
|
|
4436
5259
|
function serve() {
|
|
4437
5260
|
const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
|
|
4438
5261
|
try {
|
|
@@ -4461,6 +5284,8 @@ var tailer = setInterval(() => {
|
|
|
4461
5284
|
}
|
|
4462
5285
|
store.reapResources();
|
|
4463
5286
|
store.reapProcesses();
|
|
5287
|
+
if (tick % 12 === 0)
|
|
5288
|
+
store.sweepOrphans();
|
|
4464
5289
|
}, 5000);
|
|
4465
5290
|
store.refreshAllWorktrees();
|
|
4466
5291
|
var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);
|
|
@@ -4469,11 +5294,16 @@ var pruner = setInterval(() => store.prune(), 24 * 60 * 60000);
|
|
|
4469
5294
|
if (process.env.SWARM_OFFLINE !== "1")
|
|
4470
5295
|
store.refreshPricing().catch(() => {});
|
|
4471
5296
|
console.log(`swarmd ${VERSION} listening on http://127.0.0.1:${port}`);
|
|
4472
|
-
|
|
5297
|
+
var stopping = false;
|
|
5298
|
+
async function shutdown() {
|
|
5299
|
+
if (stopping)
|
|
5300
|
+
return;
|
|
5301
|
+
stopping = true;
|
|
4473
5302
|
clearInterval(tailer);
|
|
4474
5303
|
clearInterval(wtRefresh);
|
|
4475
5304
|
clearInterval(pruner);
|
|
4476
5305
|
clearDaemonInfo();
|
|
5306
|
+
await Promise.race([runner.stopAll(), Bun.sleep(6000)]);
|
|
4477
5307
|
server.stop(true);
|
|
4478
5308
|
process.exit(0);
|
|
4479
5309
|
}
|