@ra3orblade/swarm 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/swarm-hook.js +9 -5
- package/dist/swarm-mcp.js +994 -1376
- package/dist/swarm.js +47 -47
- package/dist/swarmd.js +467 -418
- package/package.json +1 -1
- package/web/dashboard.css +1 -1
- package/web/dashboard.js +22 -17
- package/web/index.html +2 -3
- package/web/menus.js +7 -7
- package/web/release-notes.js +1 -1
package/dist/swarmd.js
CHANGED
|
@@ -968,8 +968,8 @@ function sinceToIso(since, now = Date.now()) {
|
|
|
968
968
|
const m = /^(\d+)([dhm])$/.exec(since.trim());
|
|
969
969
|
if (m) {
|
|
970
970
|
const n = Number(m[1]);
|
|
971
|
-
const
|
|
972
|
-
return new Date(now - n *
|
|
971
|
+
const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
|
|
972
|
+
return new Date(now - n * ms).toISOString();
|
|
973
973
|
}
|
|
974
974
|
const t = Date.parse(since);
|
|
975
975
|
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
@@ -1421,22 +1421,22 @@ function contextReport(results, reads, turns, opts = {}) {
|
|
|
1421
1421
|
const sessions = [];
|
|
1422
1422
|
for (const sessionId of ids) {
|
|
1423
1423
|
const worstAll = rereadWaste(readsBySession.get(sessionId) ?? []);
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1424
|
+
const wastedChars = worstAll.reduce((a, w) => a + w.wastedChars, 0);
|
|
1425
|
+
const toolChars = charsBySession.get(sessionId) ?? 0;
|
|
1426
1426
|
const tk = tokensBySession.get(sessionId);
|
|
1427
1427
|
const cacheable = (tk?.cacheRead ?? 0) + (tk?.input ?? 0);
|
|
1428
1428
|
sessions.push({
|
|
1429
1429
|
sessionId,
|
|
1430
|
-
toolChars
|
|
1431
|
-
toolTokens: estTokens(
|
|
1430
|
+
toolChars,
|
|
1431
|
+
toolTokens: estTokens(toolChars),
|
|
1432
1432
|
thinking: tk?.thinking ?? 0,
|
|
1433
1433
|
cacheRead: tk?.cacheRead ?? 0,
|
|
1434
1434
|
input: tk?.input ?? 0,
|
|
1435
1435
|
cacheHit: cacheable ? (tk?.cacheRead ?? 0) / cacheable : 0,
|
|
1436
1436
|
reads: (readsBySession.get(sessionId) ?? []).length,
|
|
1437
1437
|
rereadFiles: worstAll.length,
|
|
1438
|
-
wastedChars
|
|
1439
|
-
wasteShare:
|
|
1438
|
+
wastedChars,
|
|
1439
|
+
wasteShare: toolChars ? wastedChars / toolChars : 0,
|
|
1440
1440
|
worst: worstAll.slice(0, o.worstLimit)
|
|
1441
1441
|
});
|
|
1442
1442
|
}
|
|
@@ -1678,22 +1678,26 @@ function otherLiveInSameTree(current, sessions, now, withinMs = LIVE_WINDOW_MS)
|
|
|
1678
1678
|
}
|
|
1679
1679
|
return null;
|
|
1680
1680
|
}
|
|
1681
|
+
var GIT_GLOBAL_OPTS = "(?:\\s+(?:-C\\s+\\S+|-c\\s+\\S+|--git-dir(?:=|\\s+)\\S+|--work-tree(?:=|\\s+)\\S+|--namespace(?:=|\\s+)\\S+|--no-pager|--paginate|--bare|--literal-pathspecs|--exec-path(?:=\\S+)?))*";
|
|
1682
|
+
function gitVerb(verb, tail) {
|
|
1683
|
+
return new RegExp(`\\bgit${GIT_GLOBAL_OPTS}\\s+${verb}${tail}`);
|
|
1684
|
+
}
|
|
1681
1685
|
function isBroadStage(cmd) {
|
|
1682
1686
|
const c = cmd.trim();
|
|
1683
|
-
if (
|
|
1687
|
+
if (gitVerb("add", "\\s+(-A\\b|--all\\b|\\.(\\s|$))").test(c))
|
|
1684
1688
|
return true;
|
|
1685
|
-
if (
|
|
1689
|
+
if (gitVerb("commit", "\\b[^|&;]*\\s-[a-zA-Z]*a").test(c))
|
|
1686
1690
|
return true;
|
|
1687
|
-
if (
|
|
1691
|
+
if (gitVerb("add", "\\s*$").test(c))
|
|
1688
1692
|
return true;
|
|
1689
1693
|
return false;
|
|
1690
1694
|
}
|
|
1691
1695
|
function isDestructiveGit(cmd) {
|
|
1692
1696
|
const c = cmd.trim();
|
|
1693
|
-
return
|
|
1697
|
+
return gitVerb("reset", "\\s+[^|&;]*--hard\\b").test(c) || gitVerb("checkout", "\\s+(--\\s+)?\\.(\\s|$)").test(c) || gitVerb("checkout", "\\s+-f\\b").test(c) || gitVerb("restore", "\\s+(--\\s+)?\\.(\\s|$)").test(c) || gitVerb("clean", "\\s+[^|&;]*-[a-zA-Z]*f").test(c) || gitVerb("stash", "\\s+(drop|clear)\\b").test(c) || gitVerb("branch", "\\s+[^|&;]*-[a-zA-Z]*D").test(c);
|
|
1694
1698
|
}
|
|
1695
1699
|
function isPatternKill(cmd) {
|
|
1696
|
-
return /\bpkill\s
|
|
1700
|
+
return /\bpkill\s+(-[a-zA-Z]*f\b|--full\b)/.test(cmd) || /\bkillall\b/.test(cmd) || /\bpgrep\s+(-[a-zA-Z]*f\b|--full\b)[^|]*\|\s*[^|]*\bkill\b/.test(cmd);
|
|
1697
1701
|
}
|
|
1698
1702
|
function killedPorts(cmd) {
|
|
1699
1703
|
const ports = new Set;
|
|
@@ -1858,20 +1862,20 @@ function dryRunRules(calls, modes, ctx) {
|
|
|
1858
1862
|
if (d.action === "allow")
|
|
1859
1863
|
continue;
|
|
1860
1864
|
byRule[d.rule][d.action]++;
|
|
1861
|
-
const
|
|
1865
|
+
const norm = normalizeDisplay(display);
|
|
1862
1866
|
if (hits.length < maxHits)
|
|
1863
1867
|
hits.push({
|
|
1864
1868
|
ts: c.ts,
|
|
1865
1869
|
sessionId: c.sessionId,
|
|
1866
1870
|
rule: d.rule,
|
|
1867
1871
|
action: d.action,
|
|
1868
|
-
display:
|
|
1872
|
+
display: norm,
|
|
1869
1873
|
completed: c.completed
|
|
1870
1874
|
});
|
|
1871
|
-
const key = `${d.rule} ${
|
|
1875
|
+
const key = `${d.rule} ${norm}`;
|
|
1872
1876
|
const g = groups.get(key) ?? {
|
|
1873
1877
|
rule: d.rule,
|
|
1874
|
-
display:
|
|
1878
|
+
display: norm,
|
|
1875
1879
|
fires: 0,
|
|
1876
1880
|
completedRatio: 0,
|
|
1877
1881
|
sessions: 0,
|
|
@@ -2362,7 +2366,7 @@ var HYGIENE_DEFAULTS = {
|
|
|
2362
2366
|
hungryRssKb: 1024 * 1024,
|
|
2363
2367
|
heavyKb: 2 * 1024 * 1024
|
|
2364
2368
|
};
|
|
2365
|
-
var days2 = (
|
|
2369
|
+
var days2 = (ms) => ms / 86400000;
|
|
2366
2370
|
function classifyProcess(p, opts = {}) {
|
|
2367
2371
|
const o = { ...HYGIENE_DEFAULTS, ...opts };
|
|
2368
2372
|
if (!p.alive)
|
|
@@ -3026,11 +3030,11 @@ function parseTo(to) {
|
|
|
3026
3030
|
return { kind: "session", id: t };
|
|
3027
3031
|
return { kind: "task", task: t };
|
|
3028
3032
|
}
|
|
3029
|
-
function formatMessages(
|
|
3030
|
-
if (!
|
|
3033
|
+
function formatMessages(ms) {
|
|
3034
|
+
if (!ms.length)
|
|
3031
3035
|
return null;
|
|
3032
|
-
const lines =
|
|
3033
|
-
return `[swarm] While you were working, message${
|
|
3036
|
+
const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
|
|
3037
|
+
return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
|
|
3034
3038
|
${lines.join(`
|
|
3035
3039
|
`)}
|
|
3036
3040
|
Reply with swarm_send if a reply is expected.`;
|
|
@@ -3718,8 +3722,8 @@ function commandSignature(command) {
|
|
|
3718
3722
|
const more = tokens.length > 2;
|
|
3719
3723
|
return `${name}${second ? ` ${second}` : ""}${more ? " \u2026" : ""}`;
|
|
3720
3724
|
}
|
|
3721
|
-
function ruleEffect(incidents, changes = [], now = Date.now(),
|
|
3722
|
-
const since = now -
|
|
3725
|
+
function ruleEffect(incidents, changes = [], now = Date.now(), days = 30) {
|
|
3726
|
+
const since = now - days * DAY;
|
|
3723
3727
|
const rows = incidents.filter((i) => i.rule && Date.parse(i.at) >= since);
|
|
3724
3728
|
const byRule = new Map;
|
|
3725
3729
|
for (const i of rows)
|
|
@@ -3993,12 +3997,12 @@ function parseMarkdownTasks(text) {
|
|
|
3993
3997
|
const cells = splitRow(line);
|
|
3994
3998
|
if (!cols) {
|
|
3995
3999
|
const lc = cells.map((c) => c.toLowerCase());
|
|
3996
|
-
const
|
|
4000
|
+
const id = lc.indexOf("id");
|
|
3997
4001
|
const task = lc.findIndex((c) => c === "task" || c === "title");
|
|
3998
|
-
if (
|
|
4002
|
+
if (id < 0 || task < 0 || !isSeparator(lines[i + 1] ?? ""))
|
|
3999
4003
|
continue;
|
|
4000
4004
|
cols = {
|
|
4001
|
-
id
|
|
4005
|
+
id,
|
|
4002
4006
|
task,
|
|
4003
4007
|
depends: lc.findIndex((c) => c.startsWith("depend")),
|
|
4004
4008
|
status: lc.indexOf("status")
|
|
@@ -4315,10 +4319,10 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
4315
4319
|
|
|
4316
4320
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
4317
4321
|
var compose = (middleware, onError, onNotFound) => {
|
|
4318
|
-
return (
|
|
4322
|
+
return (context, next) => {
|
|
4319
4323
|
let index = -1;
|
|
4320
|
-
return
|
|
4321
|
-
async function
|
|
4324
|
+
return dispatch(0);
|
|
4325
|
+
async function dispatch(i) {
|
|
4322
4326
|
if (i <= index) {
|
|
4323
4327
|
throw new Error("next() called multiple times");
|
|
4324
4328
|
}
|
|
@@ -4328,31 +4332,31 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
4328
4332
|
let handler;
|
|
4329
4333
|
if (middleware[i]) {
|
|
4330
4334
|
handler = middleware[i][0][0];
|
|
4331
|
-
|
|
4335
|
+
context.req.routeIndex = i;
|
|
4332
4336
|
} else {
|
|
4333
4337
|
handler = i === middleware.length && next || undefined;
|
|
4334
4338
|
}
|
|
4335
4339
|
if (handler) {
|
|
4336
4340
|
try {
|
|
4337
|
-
res = await handler(
|
|
4341
|
+
res = await handler(context, () => dispatch(i + 1));
|
|
4338
4342
|
} catch (err) {
|
|
4339
4343
|
if (err instanceof Error && onError) {
|
|
4340
|
-
|
|
4341
|
-
res = await onError(err,
|
|
4344
|
+
context.error = err;
|
|
4345
|
+
res = await onError(err, context);
|
|
4342
4346
|
isError = true;
|
|
4343
4347
|
} else {
|
|
4344
4348
|
throw err;
|
|
4345
4349
|
}
|
|
4346
4350
|
}
|
|
4347
4351
|
} else {
|
|
4348
|
-
if (
|
|
4349
|
-
res = await onNotFound(
|
|
4352
|
+
if (context.finalized === false && onNotFound) {
|
|
4353
|
+
res = await onNotFound(context);
|
|
4350
4354
|
}
|
|
4351
4355
|
}
|
|
4352
|
-
if (res && (
|
|
4353
|
-
|
|
4356
|
+
if (res && (context.finalized === false || isError)) {
|
|
4357
|
+
context.res = res;
|
|
4354
4358
|
}
|
|
4355
|
-
return
|
|
4359
|
+
return context;
|
|
4356
4360
|
}
|
|
4357
4361
|
};
|
|
4358
4362
|
};
|
|
@@ -4773,7 +4777,7 @@ var raw = (value, callbacks) => {
|
|
|
4773
4777
|
escapedString.callbacks = callbacks;
|
|
4774
4778
|
return escapedString;
|
|
4775
4779
|
};
|
|
4776
|
-
var resolveCallback = async (str, phase, preserveCallbacks,
|
|
4780
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
|
|
4777
4781
|
if (typeof str === "object" && !(str instanceof String)) {
|
|
4778
4782
|
if (!(str instanceof Promise)) {
|
|
4779
4783
|
str = str.toString();
|
|
@@ -4791,7 +4795,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context2, buffer) =>
|
|
|
4791
4795
|
} else {
|
|
4792
4796
|
buffer = [str];
|
|
4793
4797
|
}
|
|
4794
|
-
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context
|
|
4798
|
+
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))).then(() => buffer[0]));
|
|
4795
4799
|
if (preserveCallbacks) {
|
|
4796
4800
|
return raw(await resStr, callbacks);
|
|
4797
4801
|
} else {
|
|
@@ -5190,11 +5194,11 @@ var Hono = class _Hono {
|
|
|
5190
5194
|
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
|
|
5191
5195
|
return (async () => {
|
|
5192
5196
|
try {
|
|
5193
|
-
const
|
|
5194
|
-
if (!
|
|
5197
|
+
const context = await composed(c);
|
|
5198
|
+
if (!context.finalized) {
|
|
5195
5199
|
throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
|
|
5196
5200
|
}
|
|
5197
|
-
return
|
|
5201
|
+
return context.res;
|
|
5198
5202
|
} catch (err) {
|
|
5199
5203
|
return this.#handleError(err, c);
|
|
5200
5204
|
}
|
|
@@ -5267,7 +5271,7 @@ var Node = class _Node {
|
|
|
5267
5271
|
#index;
|
|
5268
5272
|
#varIndex;
|
|
5269
5273
|
#children = /* @__PURE__ */ Object.create(null);
|
|
5270
|
-
insert(tokens, index, paramMap,
|
|
5274
|
+
insert(tokens, index, paramMap, context, isStatic) {
|
|
5271
5275
|
let node = this;
|
|
5272
5276
|
for (let i = 0, len = tokens.length;i < len; i++) {
|
|
5273
5277
|
const token = tokens[i];
|
|
@@ -5300,7 +5304,7 @@ var Node = class _Node {
|
|
|
5300
5304
|
nextNode = node.#children[regexpStr] = new _Node;
|
|
5301
5305
|
}
|
|
5302
5306
|
if (name !== "") {
|
|
5303
|
-
nextNode.#varIndex ??=
|
|
5307
|
+
nextNode.#varIndex ??= context.varIndex++;
|
|
5304
5308
|
paramMap.push([name, nextNode.#varIndex]);
|
|
5305
5309
|
}
|
|
5306
5310
|
} else {
|
|
@@ -5613,7 +5617,7 @@ var SmartRouter = class {
|
|
|
5613
5617
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/trie-router/node.js
|
|
5614
5618
|
var emptyParams = /* @__PURE__ */ Object.create(null);
|
|
5615
5619
|
var order = 0;
|
|
5616
|
-
var Node2 = class
|
|
5620
|
+
var Node2 = class _Node {
|
|
5617
5621
|
#methods = [];
|
|
5618
5622
|
#children = /* @__PURE__ */ Object.create(null);
|
|
5619
5623
|
#patterns = [];
|
|
@@ -5629,7 +5633,7 @@ var Node2 = class _Node2 {
|
|
|
5629
5633
|
const pattern = getPattern(p, nextP) || (nextP === undefined && p && p.indexOf("*") === p.length - 1 ? p : null);
|
|
5630
5634
|
const isParam = Array.isArray(pattern);
|
|
5631
5635
|
const key = isParam ? pattern[0] : pattern || p;
|
|
5632
|
-
const child = curNode.#children[key] ||= new
|
|
5636
|
+
const child = curNode.#children[key] ||= new _Node;
|
|
5633
5637
|
if (pattern && !child.#pattern) {
|
|
5634
5638
|
child.#pattern = pattern;
|
|
5635
5639
|
curNode.#patterns.push(child);
|
|
@@ -5825,8 +5829,8 @@ var StreamingApi = class {
|
|
|
5825
5829
|
`);
|
|
5826
5830
|
return this;
|
|
5827
5831
|
}
|
|
5828
|
-
sleep(
|
|
5829
|
-
return new Promise((res) => setTimeout(res,
|
|
5832
|
+
sleep(ms) {
|
|
5833
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
5830
5834
|
}
|
|
5831
5835
|
async close() {
|
|
5832
5836
|
this.closed = true;
|
|
@@ -5937,11 +5941,11 @@ class Dispatcher {
|
|
|
5937
5941
|
forge;
|
|
5938
5942
|
entries = new Map;
|
|
5939
5943
|
opts = new Map;
|
|
5940
|
-
constructor(store, runner,
|
|
5944
|
+
constructor(store, runner, forge) {
|
|
5941
5945
|
this.store = store;
|
|
5942
5946
|
this.runner = runner;
|
|
5943
|
-
this.forge =
|
|
5944
|
-
runner.onEnd((
|
|
5947
|
+
this.forge = forge;
|
|
5948
|
+
runner.onEnd((run) => void this.onRunEnd(run));
|
|
5945
5949
|
}
|
|
5946
5950
|
project(projectId) {
|
|
5947
5951
|
let m = this.entries.get(projectId);
|
|
@@ -6028,10 +6032,10 @@ class Dispatcher {
|
|
|
6028
6032
|
const e = m.get(t.id);
|
|
6029
6033
|
const opts = this.opts.get(projectId) ?? { owner: "dispatch" };
|
|
6030
6034
|
const cfg = this.store.config(projectId);
|
|
6031
|
-
const
|
|
6035
|
+
const gates = cfg.gates;
|
|
6032
6036
|
const prompt = taskPrompt(t, {
|
|
6033
|
-
requiredGates:
|
|
6034
|
-
executableGates:
|
|
6037
|
+
requiredGates: gates.required,
|
|
6038
|
+
executableGates: gates.required.filter((g) => gates.defs[g]),
|
|
6035
6039
|
openPr: cfg.dispatch.require_pr
|
|
6036
6040
|
});
|
|
6037
6041
|
const r = await this.runner.start({
|
|
@@ -6098,76 +6102,76 @@ class Dispatcher {
|
|
|
6098
6102
|
await this.startOne(projectId, { id: e.task, title: e.title });
|
|
6099
6103
|
}
|
|
6100
6104
|
}
|
|
6101
|
-
async onRunEnd(
|
|
6102
|
-
const m = this.entries.get(
|
|
6103
|
-
const e = m?.get(
|
|
6104
|
-
if (!e || e.runId !==
|
|
6105
|
+
async onRunEnd(run) {
|
|
6106
|
+
const m = this.entries.get(run.projectId);
|
|
6107
|
+
const e = m?.get(run.task);
|
|
6108
|
+
if (!e || e.runId !== run.id)
|
|
6105
6109
|
return;
|
|
6106
|
-
const cfg = this.store.config(
|
|
6110
|
+
const cfg = this.store.config(run.projectId);
|
|
6107
6111
|
const required = cfg.gates.required;
|
|
6108
|
-
let runs = this.store.gateRuns(
|
|
6112
|
+
let runs = this.store.gateRuns(run.projectId, run.task);
|
|
6109
6113
|
const status = this.store.gateStatusFor(runs, required);
|
|
6110
6114
|
const missing = required.filter((g) => cfg.gates.defs[g] && status.find((s) => s.gate === g)?.verdict !== "pass");
|
|
6111
|
-
if (missing.length && !
|
|
6112
|
-
await this.store.runGates(
|
|
6113
|
-
sessionId:
|
|
6115
|
+
if (missing.length && !run.stopped) {
|
|
6116
|
+
await this.store.runGates(run.projectId, run.task, missing, {
|
|
6117
|
+
sessionId: run.sessionId,
|
|
6114
6118
|
owner: "dispatch"
|
|
6115
6119
|
});
|
|
6116
|
-
runs = this.store.gateRuns(
|
|
6120
|
+
runs = this.store.gateRuns(run.projectId, run.task);
|
|
6117
6121
|
}
|
|
6118
6122
|
const satisfied = gatesSatisfied(runs, required);
|
|
6119
6123
|
await this.forge.refresh(0).catch(() => {});
|
|
6120
|
-
const branch = `task/${
|
|
6121
|
-
const pr = this.forge.prs().find((p) => p.projectId ===
|
|
6124
|
+
const branch = `task/${run.task}`;
|
|
6125
|
+
const pr = this.forge.prs().find((p) => p.projectId === run.projectId && p.branch === branch);
|
|
6122
6126
|
const outcome = dispatchOutcome({
|
|
6123
|
-
exitCode:
|
|
6124
|
-
isError:
|
|
6127
|
+
exitCode: run.exitCode,
|
|
6128
|
+
isError: run.result?.isError ?? false,
|
|
6125
6129
|
gatesSatisfied: satisfied,
|
|
6126
6130
|
prOpen: Boolean(pr),
|
|
6127
6131
|
requirePr: cfg.dispatch.require_pr,
|
|
6128
|
-
stopped:
|
|
6132
|
+
stopped: run.stopped ?? false
|
|
6129
6133
|
});
|
|
6130
6134
|
const verdicts = this.store.gateStatusFor(runs, required).map((s) => `${s.gate} ${s.verdict ?? "\u2014"}`).join(", ");
|
|
6131
6135
|
const detail = [
|
|
6132
|
-
`exit ${
|
|
6136
|
+
`exit ${run.exitCode}${run.result?.isError ? " (error)" : ""}`,
|
|
6133
6137
|
required.length ? `gates: ${verdicts}` : null,
|
|
6134
6138
|
pr ? `PR ${pr.url}` : cfg.dispatch.require_pr ? "no PR" : null
|
|
6135
6139
|
].filter(Boolean).join(" \xB7 ");
|
|
6136
6140
|
e.state = "finished";
|
|
6137
|
-
e.endedAt =
|
|
6141
|
+
e.endedAt = run.endedAt;
|
|
6138
6142
|
e.outcome = outcome;
|
|
6139
6143
|
e.detail = detail;
|
|
6140
|
-
e.costUsd =
|
|
6141
|
-
const ts =
|
|
6144
|
+
e.costUsd = run.result?.costUsd ?? null;
|
|
6145
|
+
const ts = run.endedAt ?? new Date().toISOString();
|
|
6142
6146
|
this.store.append({
|
|
6143
6147
|
ts,
|
|
6144
6148
|
type: "dispatch.finished",
|
|
6145
|
-
projectId:
|
|
6146
|
-
sessionId:
|
|
6149
|
+
projectId: run.projectId,
|
|
6150
|
+
sessionId: run.sessionId,
|
|
6147
6151
|
payload: {
|
|
6148
|
-
task:
|
|
6149
|
-
runId:
|
|
6152
|
+
task: run.task,
|
|
6153
|
+
runId: run.id,
|
|
6150
6154
|
outcome,
|
|
6151
6155
|
detail,
|
|
6152
6156
|
costUsd: e.costUsd,
|
|
6153
|
-
summary: `dispatch ${
|
|
6157
|
+
summary: `dispatch ${run.task}: ${outcome} \u2014 ${detail}`
|
|
6154
6158
|
}
|
|
6155
6159
|
});
|
|
6156
6160
|
if (outcome !== "done" && outcome !== "stopped")
|
|
6157
6161
|
this.store.append({
|
|
6158
6162
|
ts,
|
|
6159
6163
|
type: "incident.opened",
|
|
6160
|
-
projectId:
|
|
6161
|
-
sessionId:
|
|
6164
|
+
projectId: run.projectId,
|
|
6165
|
+
sessionId: run.sessionId,
|
|
6162
6166
|
payload: {
|
|
6163
6167
|
rule: "dispatch_failed",
|
|
6164
6168
|
action: outcome,
|
|
6165
|
-
command:
|
|
6166
|
-
reason: `dispatched run on ${
|
|
6169
|
+
command: run.task,
|
|
6170
|
+
reason: `dispatched run on ${run.task} ended ${outcome}: ${detail}. The worktree and claim are kept; resume it from the session page or release it.`
|
|
6167
6171
|
}
|
|
6168
6172
|
});
|
|
6169
6173
|
this.store.touch();
|
|
6170
|
-
await this.fill(
|
|
6174
|
+
await this.fill(run.projectId);
|
|
6171
6175
|
}
|
|
6172
6176
|
clear(projectId, task) {
|
|
6173
6177
|
const m = this.project(projectId);
|
|
@@ -6223,7 +6227,7 @@ class ForgeService {
|
|
|
6223
6227
|
return all.sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
|
|
6224
6228
|
}
|
|
6225
6229
|
async refresh(maxAgeMs = 120000) {
|
|
6226
|
-
const projects = this.store.
|
|
6230
|
+
const projects = this.store.liveProjects();
|
|
6227
6231
|
await Promise.all(projects.map(async (p) => {
|
|
6228
6232
|
const hit = this.cache.get(p.id);
|
|
6229
6233
|
if (hit && Date.now() - hit.at < maxAgeMs)
|
|
@@ -6246,18 +6250,24 @@ class ForgeService {
|
|
|
6246
6250
|
mergedCached(projectId, root) {
|
|
6247
6251
|
const hit = this.outcomeCache.get(projectId);
|
|
6248
6252
|
const fresh = !!hit && Date.now() - hit.at < 600000;
|
|
6249
|
-
if (!fresh
|
|
6250
|
-
|
|
6251
|
-
this.outcomeInflight.set(projectId, run2);
|
|
6252
|
-
}
|
|
6253
|
+
if (!fresh)
|
|
6254
|
+
this.merged(projectId, root).catch(() => {});
|
|
6253
6255
|
return { merged: hit?.merged ?? [], reverted: hit?.reverted ?? [], fresh };
|
|
6254
6256
|
}
|
|
6255
6257
|
async merged(projectId, root) {
|
|
6256
6258
|
const hit = this.outcomeCache.get(projectId);
|
|
6257
6259
|
if (hit && Date.now() - hit.at < 600000)
|
|
6258
6260
|
return hit;
|
|
6261
|
+
const inflight = this.outcomeInflight.get(projectId);
|
|
6262
|
+
if (inflight)
|
|
6263
|
+
return inflight;
|
|
6264
|
+
const run = this.fetchMerged(projectId, root).finally(() => this.outcomeInflight.delete(projectId));
|
|
6265
|
+
this.outcomeInflight.set(projectId, run);
|
|
6266
|
+
return run;
|
|
6267
|
+
}
|
|
6268
|
+
async fetchMerged(projectId, root) {
|
|
6259
6269
|
let merged = [];
|
|
6260
|
-
const remote = this.remote(root);
|
|
6270
|
+
const remote = await this.remote(root);
|
|
6261
6271
|
if (remote?.forge === "github") {
|
|
6262
6272
|
const out = await this.run([
|
|
6263
6273
|
"gh",
|
|
@@ -6293,38 +6303,44 @@ class ForgeService {
|
|
|
6293
6303
|
mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
|
|
6294
6304
|
}));
|
|
6295
6305
|
}
|
|
6296
|
-
const log =
|
|
6297
|
-
|
|
6298
|
-
"-C",
|
|
6299
|
-
root,
|
|
6300
|
-
"log",
|
|
6301
|
-
"--grep",
|
|
6302
|
-
"This reverts commit",
|
|
6303
|
-
"--format=%B",
|
|
6304
|
-
"-n",
|
|
6305
|
-
"300"
|
|
6306
|
-
]);
|
|
6307
|
-
const reverted = log.exitCode === 0 ? [...parseReverts(new TextDecoder().decode(log.stdout))] : [];
|
|
6306
|
+
const log = await this.run(["git", "log", "--grep", "This reverts commit", "--format=%B", "-n", "300"], root);
|
|
6307
|
+
const reverted = log ? [...parseReverts(log)] : [];
|
|
6308
6308
|
const entry = { at: Date.now(), merged, reverted };
|
|
6309
6309
|
this.outcomeCache.set(projectId, entry);
|
|
6310
6310
|
return entry;
|
|
6311
6311
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6312
|
+
remoteCache = new Map;
|
|
6313
|
+
async remote(root) {
|
|
6314
|
+
const hit = this.remoteCache.get(root);
|
|
6315
|
+
if (hit && Date.now() - hit.at < (hit.v ? 600000 : 60000))
|
|
6316
|
+
return hit.v;
|
|
6317
|
+
const out = await this.run(["git", "remote", "get-url", "origin"], root);
|
|
6318
|
+
const v = out ? parseRemote(out.trim()) : null;
|
|
6319
|
+
this.remoteCache.set(root, { at: Date.now(), v });
|
|
6320
|
+
return v;
|
|
6317
6321
|
}
|
|
6318
|
-
async run(cmd, cwd) {
|
|
6322
|
+
async run(cmd, cwd, timeoutMs = 20000) {
|
|
6319
6323
|
const bin = findBin(cmd[0]);
|
|
6320
6324
|
if (!bin)
|
|
6321
6325
|
return null;
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6326
|
+
let proc;
|
|
6327
|
+
try {
|
|
6328
|
+
proc = Bun.spawn([bin, ...cmd.slice(1)], { cwd, stdout: "pipe", stderr: "ignore" });
|
|
6329
|
+
} catch {
|
|
6330
|
+
return null;
|
|
6331
|
+
}
|
|
6332
|
+
const killer = setTimeout(() => proc.kill(), timeoutMs);
|
|
6333
|
+
try {
|
|
6334
|
+
const out = await new Response(proc.stdout).text();
|
|
6335
|
+
return await proc.exited === 0 ? out : null;
|
|
6336
|
+
} catch {
|
|
6337
|
+
return null;
|
|
6338
|
+
} finally {
|
|
6339
|
+
clearTimeout(killer);
|
|
6340
|
+
}
|
|
6325
6341
|
}
|
|
6326
6342
|
async poll(projectId, root) {
|
|
6327
|
-
const remote = this.remote(root);
|
|
6343
|
+
const remote = await this.remote(root);
|
|
6328
6344
|
if (!remote)
|
|
6329
6345
|
return [];
|
|
6330
6346
|
let prs = [];
|
|
@@ -6339,35 +6355,35 @@ class ForgeService {
|
|
|
6339
6355
|
}
|
|
6340
6356
|
return prs.map((pr) => ({ ...pr, projectId, projectRoot: root }));
|
|
6341
6357
|
}
|
|
6342
|
-
async openPR(projectId,
|
|
6358
|
+
async openPR(projectId, worktree, draft) {
|
|
6343
6359
|
const p = this.store.projects().find((x) => x.id === projectId);
|
|
6344
6360
|
if (!p)
|
|
6345
6361
|
return { ok: false, error: "unknown project" };
|
|
6346
|
-
if (
|
|
6362
|
+
if (worktree.main)
|
|
6347
6363
|
return { ok: false, error: "that is the main checkout \u2014 open the PR from a task worktree" };
|
|
6348
|
-
if (!
|
|
6364
|
+
if (!worktree.branch)
|
|
6349
6365
|
return { ok: false, error: "detached HEAD \u2014 check out a branch first" };
|
|
6350
|
-
if (
|
|
6366
|
+
if (worktree.dirty > 0)
|
|
6351
6367
|
return {
|
|
6352
6368
|
ok: false,
|
|
6353
|
-
error: `${
|
|
6369
|
+
error: `${worktree.path} has uncommitted changes \u2014 commit them first (Swarm never commits for you)`
|
|
6354
6370
|
};
|
|
6355
|
-
const remote = this.remote(p.root);
|
|
6371
|
+
const remote = await this.remote(p.root);
|
|
6356
6372
|
if (!remote)
|
|
6357
6373
|
return { ok: false, error: "no GitHub/GitLab remote on origin" };
|
|
6358
6374
|
const cli = remote.forge === "github" ? "gh" : "glab";
|
|
6359
6375
|
const bin = findBin(cli);
|
|
6360
6376
|
if (!bin)
|
|
6361
6377
|
return { ok: false, error: `${cli} is not installed` };
|
|
6362
|
-
const sh = async (
|
|
6363
|
-
const proc = Bun.spawn(
|
|
6378
|
+
const sh = async (cmd, cwd) => {
|
|
6379
|
+
const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
6364
6380
|
const out = await new Response(proc.stdout).text() + await new Response(proc.stderr).text();
|
|
6365
6381
|
return { ok: await proc.exited === 0, out: out.trim() };
|
|
6366
6382
|
};
|
|
6367
|
-
const push = await sh(["git", "push", "-u", "origin",
|
|
6383
|
+
const push = await sh(["git", "push", "-u", "origin", worktree.branch], worktree.path);
|
|
6368
6384
|
if (!push.ok)
|
|
6369
6385
|
return { ok: false, error: `git push failed: ${push.out.slice(0, 400)}` };
|
|
6370
|
-
const existing = this.prs().find((x) => x.projectId === projectId && x.branch ===
|
|
6386
|
+
const existing = this.prs().find((x) => x.projectId === projectId && x.branch === worktree.branch);
|
|
6371
6387
|
if (existing)
|
|
6372
6388
|
return { ok: true, url: existing.url, number: existing.number };
|
|
6373
6389
|
const cmd = remote.forge === "github" ? [
|
|
@@ -6375,7 +6391,7 @@ class ForgeService {
|
|
|
6375
6391
|
"pr",
|
|
6376
6392
|
"create",
|
|
6377
6393
|
"--head",
|
|
6378
|
-
|
|
6394
|
+
worktree.branch,
|
|
6379
6395
|
"--title",
|
|
6380
6396
|
draft.title,
|
|
6381
6397
|
"--body",
|
|
@@ -6386,7 +6402,7 @@ class ForgeService {
|
|
|
6386
6402
|
"mr",
|
|
6387
6403
|
"create",
|
|
6388
6404
|
"--source-branch",
|
|
6389
|
-
|
|
6405
|
+
worktree.branch,
|
|
6390
6406
|
"--title",
|
|
6391
6407
|
draft.title,
|
|
6392
6408
|
"--description",
|
|
@@ -6394,7 +6410,7 @@ class ForgeService {
|
|
|
6394
6410
|
"--yes",
|
|
6395
6411
|
...draft.isDraft ? ["--draft"] : []
|
|
6396
6412
|
];
|
|
6397
|
-
const r = await sh(cmd,
|
|
6413
|
+
const r = await sh(cmd, worktree.path);
|
|
6398
6414
|
if (!r.ok)
|
|
6399
6415
|
return { ok: false, error: `${cli} failed: ${r.out.slice(0, 400)}` };
|
|
6400
6416
|
const url = r.out.match(/https?:\/\/\S+/)?.[0] ?? r.out;
|
|
@@ -6406,7 +6422,7 @@ class ForgeService {
|
|
|
6406
6422
|
const p = this.store.projects().find((x) => x.id === projectId);
|
|
6407
6423
|
if (!p)
|
|
6408
6424
|
return { ok: false, output: "unknown project" };
|
|
6409
|
-
const remote = this.remote(p.root);
|
|
6425
|
+
const remote = await this.remote(p.root);
|
|
6410
6426
|
if (!remote)
|
|
6411
6427
|
return { ok: false, output: "no forge remote" };
|
|
6412
6428
|
const cmd = remote.forge === "github" ? ["gh", "pr", "merge", String(number), "--squash"] : ["glab", "mr", "merge", String(number), "--squash", "--yes"];
|
|
@@ -6676,9 +6692,9 @@ class Runner {
|
|
|
6676
6692
|
return [...this.live.values()].map((x) => x.run).filter((r) => !projectId || r.projectId === projectId);
|
|
6677
6693
|
}
|
|
6678
6694
|
get(idOrTask) {
|
|
6679
|
-
for (const { run
|
|
6680
|
-
if (
|
|
6681
|
-
return
|
|
6695
|
+
for (const { run } of this.live.values())
|
|
6696
|
+
if (run.id === idOrTask || run.sessionId === idOrTask || run.task === idOrTask)
|
|
6697
|
+
return run;
|
|
6682
6698
|
return null;
|
|
6683
6699
|
}
|
|
6684
6700
|
async start(input) {
|
|
@@ -6706,14 +6722,14 @@ class Runner {
|
|
|
6706
6722
|
reason: `a run on ${input.task} is already live \u2014 stop it or send it input`
|
|
6707
6723
|
};
|
|
6708
6724
|
const held = this.store.claims(input.projectId).find((c) => c.task === input.task && c.state === "held" && c.owner === input.owner);
|
|
6709
|
-
let
|
|
6710
|
-
if (!
|
|
6725
|
+
let worktree = held?.worktree ?? "";
|
|
6726
|
+
if (!worktree) {
|
|
6711
6727
|
const c = this.store.claim(input.projectId, input.task, input.owner);
|
|
6712
6728
|
if (!c.ok)
|
|
6713
6729
|
return { ok: false, reason: c.error };
|
|
6714
|
-
|
|
6730
|
+
worktree = c.worktree;
|
|
6715
6731
|
}
|
|
6716
|
-
await this.store.awaitBootstrap(
|
|
6732
|
+
await this.store.awaitBootstrap(worktree);
|
|
6717
6733
|
const sessionId = crypto.randomUUID();
|
|
6718
6734
|
const id = sessionId.slice(0, 8);
|
|
6719
6735
|
const logDir = join6(this.home, "logs", project.id);
|
|
@@ -6750,20 +6766,20 @@ class Runner {
|
|
|
6750
6766
|
args.push("--disallowedTools", ...profile.disallowedTools);
|
|
6751
6767
|
if (input.maxTurns)
|
|
6752
6768
|
args.push("--max-turns", String(input.maxTurns));
|
|
6753
|
-
this.store.preregisterSpawnedSession(sessionId, project.id,
|
|
6769
|
+
this.store.preregisterSpawnedSession(sessionId, project.id, worktree, input.task);
|
|
6754
6770
|
const proc = Bun.spawn(args, {
|
|
6755
|
-
cwd:
|
|
6771
|
+
cwd: worktree,
|
|
6756
6772
|
env: { ...process.env, SWARM_RUN_ID: id, SWARM_OWNER: input.owner },
|
|
6757
6773
|
stdin: "pipe",
|
|
6758
6774
|
stdout: "pipe",
|
|
6759
6775
|
stderr: logFd
|
|
6760
6776
|
});
|
|
6761
|
-
const
|
|
6777
|
+
const run = {
|
|
6762
6778
|
id,
|
|
6763
6779
|
sessionId,
|
|
6764
6780
|
projectId: project.id,
|
|
6765
6781
|
task: input.task,
|
|
6766
|
-
worktree
|
|
6782
|
+
worktree,
|
|
6767
6783
|
pid: proc.pid,
|
|
6768
6784
|
owner: input.owner,
|
|
6769
6785
|
model: input.model ?? null,
|
|
@@ -6777,20 +6793,20 @@ class Runner {
|
|
|
6777
6793
|
result: null,
|
|
6778
6794
|
pending: []
|
|
6779
6795
|
};
|
|
6780
|
-
this.live.set(id, { run
|
|
6796
|
+
this.live.set(id, { run, proc });
|
|
6781
6797
|
this.store.registerProcess({
|
|
6782
6798
|
pid: proc.pid,
|
|
6783
6799
|
projectId: project.id,
|
|
6784
6800
|
sessionId,
|
|
6785
6801
|
kind: "proc",
|
|
6786
6802
|
name: `run:${input.task}`,
|
|
6787
|
-
cwd:
|
|
6803
|
+
cwd: worktree,
|
|
6788
6804
|
cmd: `claude -p (run ${id})`,
|
|
6789
6805
|
owner: input.owner,
|
|
6790
6806
|
log
|
|
6791
6807
|
});
|
|
6792
6808
|
this.store.append({
|
|
6793
|
-
ts:
|
|
6809
|
+
ts: run.startedAt,
|
|
6794
6810
|
type: "session.started",
|
|
6795
6811
|
projectId: project.id,
|
|
6796
6812
|
sessionId,
|
|
@@ -6798,7 +6814,7 @@ class Runner {
|
|
|
6798
6814
|
});
|
|
6799
6815
|
this.pump(id, proc);
|
|
6800
6816
|
this.send(id, input.prompt);
|
|
6801
|
-
return { ok: true, run
|
|
6817
|
+
return { ok: true, run };
|
|
6802
6818
|
}
|
|
6803
6819
|
async pump(id, proc) {
|
|
6804
6820
|
const entry = this.live.get(id);
|
|
@@ -6857,7 +6873,7 @@ class Runner {
|
|
|
6857
6873
|
}
|
|
6858
6874
|
}
|
|
6859
6875
|
}
|
|
6860
|
-
onLine(
|
|
6876
|
+
onLine(run, line) {
|
|
6861
6877
|
if (!line.startsWith("{"))
|
|
6862
6878
|
return;
|
|
6863
6879
|
let j;
|
|
@@ -6867,41 +6883,41 @@ class Runner {
|
|
|
6867
6883
|
return;
|
|
6868
6884
|
}
|
|
6869
6885
|
if (j.type === "control_request" && j.request?.subtype === "can_use_tool") {
|
|
6870
|
-
this.onPermissionRequest(
|
|
6886
|
+
this.onPermissionRequest(run, j.request_id, j.request.tool_name ?? "", j.request.input ?? {});
|
|
6871
6887
|
return;
|
|
6872
6888
|
}
|
|
6873
6889
|
if (j.type !== "result")
|
|
6874
6890
|
return;
|
|
6875
|
-
|
|
6891
|
+
run.result = {
|
|
6876
6892
|
costUsd: Number(j.total_cost_usd ?? 0),
|
|
6877
6893
|
turns: Number(j.num_turns ?? 0),
|
|
6878
6894
|
isError: Boolean(j.is_error),
|
|
6879
6895
|
at: new Date().toISOString()
|
|
6880
6896
|
};
|
|
6881
6897
|
this.store.append({
|
|
6882
|
-
ts:
|
|
6898
|
+
ts: run.result.at,
|
|
6883
6899
|
type: "run.result",
|
|
6884
|
-
projectId:
|
|
6885
|
-
sessionId:
|
|
6900
|
+
projectId: run.projectId,
|
|
6901
|
+
sessionId: run.sessionId,
|
|
6886
6902
|
payload: {
|
|
6887
|
-
runId:
|
|
6888
|
-
task:
|
|
6889
|
-
...
|
|
6890
|
-
summary: `turn done \xB7 $${
|
|
6903
|
+
runId: run.id,
|
|
6904
|
+
task: run.task,
|
|
6905
|
+
...run.result,
|
|
6906
|
+
summary: `turn done \xB7 $${run.result.costUsd.toFixed(2)} \xB7 ${run.result.turns} turns${run.result.isError ? " \xB7 error" : ""}`
|
|
6891
6907
|
}
|
|
6892
6908
|
});
|
|
6893
6909
|
}
|
|
6894
|
-
onPermissionRequest(
|
|
6895
|
-
const { decision, display } = this.store.evaluateTool(tool, input,
|
|
6910
|
+
onPermissionRequest(run, requestId, tool, input) {
|
|
6911
|
+
const { decision, display } = this.store.evaluateTool(tool, input, run.sessionId, run.worktree, true);
|
|
6896
6912
|
if (decision.action === "deny") {
|
|
6897
|
-
this.answerPermission(
|
|
6913
|
+
this.answerPermission(run.id, requestId, false, `[swarm] ${decision.reason}`);
|
|
6898
6914
|
return;
|
|
6899
6915
|
}
|
|
6900
6916
|
if (decision.action === "allow") {
|
|
6901
|
-
this.answerPermission(
|
|
6917
|
+
this.answerPermission(run.id, requestId, true);
|
|
6902
6918
|
return;
|
|
6903
6919
|
}
|
|
6904
|
-
|
|
6920
|
+
run.pending.push({
|
|
6905
6921
|
requestId,
|
|
6906
6922
|
tool,
|
|
6907
6923
|
input,
|
|
@@ -6912,10 +6928,10 @@ class Runner {
|
|
|
6912
6928
|
this.store.append({
|
|
6913
6929
|
ts: new Date().toISOString(),
|
|
6914
6930
|
type: "permission.requested",
|
|
6915
|
-
projectId:
|
|
6916
|
-
sessionId:
|
|
6931
|
+
projectId: run.projectId,
|
|
6932
|
+
sessionId: run.sessionId,
|
|
6917
6933
|
payload: {
|
|
6918
|
-
runId:
|
|
6934
|
+
runId: run.id,
|
|
6919
6935
|
requestId,
|
|
6920
6936
|
tool,
|
|
6921
6937
|
display: display.slice(0, 300),
|
|
@@ -6975,17 +6991,17 @@ class Runner {
|
|
|
6975
6991
|
return { ok: true };
|
|
6976
6992
|
}
|
|
6977
6993
|
async stop(id) {
|
|
6978
|
-
const
|
|
6979
|
-
if (!
|
|
6994
|
+
const run = this.get(id);
|
|
6995
|
+
if (!run)
|
|
6980
6996
|
return { ok: false, reason: "no live run" };
|
|
6981
|
-
const entry = this.live.get(
|
|
6982
|
-
|
|
6997
|
+
const entry = this.live.get(run.id);
|
|
6998
|
+
run.stopped = true;
|
|
6983
6999
|
try {
|
|
6984
7000
|
const stdin = entry?.proc.stdin;
|
|
6985
7001
|
if (stdin && typeof stdin !== "number")
|
|
6986
7002
|
stdin.end();
|
|
6987
7003
|
} catch {}
|
|
6988
|
-
return this.store.stopProcess(
|
|
7004
|
+
return this.store.stopProcess(run.pid, run.projectId, 5000);
|
|
6989
7005
|
}
|
|
6990
7006
|
async stopAll() {
|
|
6991
7007
|
await Promise.all([...this.live.keys()].map((id) => this.stop(id)));
|
|
@@ -7084,8 +7100,8 @@ class TaskSources {
|
|
|
7084
7100
|
const prev = this.cache.get(projectId);
|
|
7085
7101
|
let entry;
|
|
7086
7102
|
try {
|
|
7087
|
-
const
|
|
7088
|
-
entry = { at: Date.now(), tasks
|
|
7103
|
+
const tasks = kind === "github" ? await this.github(root, opts.labels) : await this.linear(opts.team);
|
|
7104
|
+
entry = { at: Date.now(), tasks, error: null };
|
|
7089
7105
|
} catch (e) {
|
|
7090
7106
|
entry = { at: Date.now(), tasks: prev?.tasks ?? [], error: e.message };
|
|
7091
7107
|
} finally {
|
|
@@ -7122,14 +7138,14 @@ class TaskSources {
|
|
|
7122
7138
|
`)[0] ?? code}`);
|
|
7123
7139
|
return normalizeGithubIssues(JSON.parse(out));
|
|
7124
7140
|
}
|
|
7125
|
-
async linear(
|
|
7141
|
+
async linear(team) {
|
|
7126
7142
|
const key = this.env.LINEAR_API_KEY;
|
|
7127
7143
|
if (!key)
|
|
7128
7144
|
throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
|
|
7129
7145
|
const r = await fetch("https://api.linear.app/graphql", {
|
|
7130
7146
|
method: "POST",
|
|
7131
7147
|
headers: { "content-type": "application/json", authorization: key },
|
|
7132
|
-
body: JSON.stringify({ query: linearIssuesQuery(
|
|
7148
|
+
body: JSON.stringify({ query: linearIssuesQuery(team) })
|
|
7133
7149
|
});
|
|
7134
7150
|
if (!r.ok)
|
|
7135
7151
|
throw new Error(`Linear API ${r.status}`);
|
|
@@ -7267,14 +7283,14 @@ class Store {
|
|
|
7267
7283
|
this.db.transaction(() => {
|
|
7268
7284
|
for (const r of rows) {
|
|
7269
7285
|
let payload;
|
|
7270
|
-
let
|
|
7286
|
+
let raw;
|
|
7271
7287
|
try {
|
|
7272
7288
|
payload = JSON.parse(r.payload);
|
|
7273
|
-
|
|
7289
|
+
raw = r.raw ? JSON.parse(r.raw) : undefined;
|
|
7274
7290
|
} catch {
|
|
7275
7291
|
continue;
|
|
7276
7292
|
}
|
|
7277
|
-
const slim = slimForStorage({ payload, raw
|
|
7293
|
+
const slim = slimForStorage({ payload, raw });
|
|
7278
7294
|
upd.run(JSON.stringify(slim.payload ?? null), slim.raw === undefined ? null : JSON.stringify(slim.raw), r.seq);
|
|
7279
7295
|
}
|
|
7280
7296
|
this.setMeta("events_slim", "1");
|
|
@@ -7477,7 +7493,7 @@ class Store {
|
|
|
7477
7493
|
if (!held)
|
|
7478
7494
|
return null;
|
|
7479
7495
|
const manual = this.db.query("SELECT id, by FROM handoffs WHERE project_id = ? AND task = ? AND session_id = ?").all(held.projectId, held.task, sessionId);
|
|
7480
|
-
if (manual.some((
|
|
7496
|
+
if (manual.some((h) => !isAutoHandoff(h)))
|
|
7481
7497
|
return null;
|
|
7482
7498
|
const row = this.db.query("SELECT last_text FROM sessions WHERE id = ?").get(sessionId);
|
|
7483
7499
|
const h = deriveHandoff(held.task, this.sessionEvents(sessionId, 2000), { lastText: row?.last_text ?? null, sessionId });
|
|
@@ -7753,10 +7769,10 @@ class Store {
|
|
|
7753
7769
|
return out.length ? out.join(`
|
|
7754
7770
|
`) : null;
|
|
7755
7771
|
}
|
|
7756
|
-
wfInsert(projectId, task, workflow, steps,
|
|
7772
|
+
wfInsert(projectId, task, workflow, steps, actor) {
|
|
7757
7773
|
const now = new Date().toISOString();
|
|
7758
7774
|
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)
|
|
7759
|
-
VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now,
|
|
7775
|
+
VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor.kind, actor.id);
|
|
7760
7776
|
this.touch();
|
|
7761
7777
|
return Number(r.lastInsertRowid);
|
|
7762
7778
|
}
|
|
@@ -7877,10 +7893,10 @@ class Store {
|
|
|
7877
7893
|
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
7878
7894
|
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
7879
7895
|
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
7880
|
-
const
|
|
7881
|
-
if (
|
|
7882
|
-
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${
|
|
7883
|
-
return
|
|
7896
|
+
const ms = rows.map(rowToMessage);
|
|
7897
|
+
if (ms.length && !opts.peek)
|
|
7898
|
+
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));
|
|
7899
|
+
return ms;
|
|
7884
7900
|
}
|
|
7885
7901
|
markMessageDelivered(id, sessionId) {
|
|
7886
7902
|
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);
|
|
@@ -7975,13 +7991,13 @@ class Store {
|
|
|
7975
7991
|
reason: `gate ${gate} has no command \u2014 add [gates.${gate}] cmd = "\u2026" to .swarm.toml, or record it with swarm gate record`
|
|
7976
7992
|
};
|
|
7977
7993
|
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
7978
|
-
const
|
|
7979
|
-
if (!
|
|
7994
|
+
const worktree = claim?.worktree;
|
|
7995
|
+
if (!worktree || !existsSync6(worktree))
|
|
7980
7996
|
return {
|
|
7981
7997
|
ok: false,
|
|
7982
7998
|
reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
|
|
7983
7999
|
};
|
|
7984
|
-
const cwd = def.cwd ? join8(
|
|
8000
|
+
const cwd = def.cwd ? join8(worktree, def.cwd) : worktree;
|
|
7985
8001
|
if (!existsSync6(cwd))
|
|
7986
8002
|
return { ok: false, reason: `gate cwd ${cwd} does not exist` };
|
|
7987
8003
|
const key = `${projectId}:${task}:${gate}`;
|
|
@@ -7992,7 +8008,7 @@ class Store {
|
|
|
7992
8008
|
mkdirSync4(logDir, { recursive: true });
|
|
7993
8009
|
const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
|
|
7994
8010
|
if (def.builtin === "review")
|
|
7995
|
-
return this.runReviewGate(projectId, task, gate, def, { worktree
|
|
8011
|
+
return this.runReviewGate(projectId, task, gate, def, { worktree, cwd, key, log }, opts);
|
|
7996
8012
|
writeFileSync2(log, `$ ${def.cmd}
|
|
7997
8013
|
# cwd ${cwd} \xB7 ${new Date().toISOString()}
|
|
7998
8014
|
`);
|
|
@@ -8006,7 +8022,7 @@ class Store {
|
|
|
8006
8022
|
stderr: fd,
|
|
8007
8023
|
env: {
|
|
8008
8024
|
...process.env,
|
|
8009
|
-
SWARM_WORKTREE:
|
|
8025
|
+
SWARM_WORKTREE: worktree,
|
|
8010
8026
|
SWARM_TASK: task,
|
|
8011
8027
|
SWARM_GATE: gate,
|
|
8012
8028
|
CI: process.env.CI ?? "1"
|
|
@@ -8014,7 +8030,7 @@ class Store {
|
|
|
8014
8030
|
});
|
|
8015
8031
|
} catch (e) {
|
|
8016
8032
|
closeSync(fd);
|
|
8017
|
-
const
|
|
8033
|
+
const run = this.recordGate(projectId, {
|
|
8018
8034
|
...executedGateInput(task, gate, def.cmd, {
|
|
8019
8035
|
exitCode: null,
|
|
8020
8036
|
durationMs: 0,
|
|
@@ -8022,7 +8038,7 @@ class Store {
|
|
|
8022
8038
|
}),
|
|
8023
8039
|
sessionId: opts.sessionId ?? null
|
|
8024
8040
|
});
|
|
8025
|
-
return { ok: true, pid: 0, log, done: Promise.resolve(
|
|
8041
|
+
return { ok: true, pid: 0, log, done: Promise.resolve(run.ok ? run.run : null) };
|
|
8026
8042
|
}
|
|
8027
8043
|
const started = Date.now();
|
|
8028
8044
|
const reg = this.registerProcess({
|
|
@@ -8061,10 +8077,10 @@ class Store {
|
|
|
8061
8077
|
durationMs: Date.now() - started,
|
|
8062
8078
|
output
|
|
8063
8079
|
});
|
|
8064
|
-
const
|
|
8080
|
+
const run = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
|
|
8065
8081
|
if (reg.ok)
|
|
8066
8082
|
this.processes(projectId);
|
|
8067
|
-
return
|
|
8083
|
+
return run.ok ? run.run : null;
|
|
8068
8084
|
}).finally(() => {
|
|
8069
8085
|
this.gateJobs.delete(key);
|
|
8070
8086
|
this.touch();
|
|
@@ -8081,15 +8097,15 @@ class Store {
|
|
|
8081
8097
|
return { ok: false, reason: "unknown project" };
|
|
8082
8098
|
const started = Date.now();
|
|
8083
8099
|
const record = (input) => {
|
|
8084
|
-
const
|
|
8085
|
-
return
|
|
8100
|
+
const run = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
|
|
8101
|
+
return run.ok ? run.run : null;
|
|
8086
8102
|
};
|
|
8087
8103
|
const done = (async () => {
|
|
8088
8104
|
let diffText = "";
|
|
8089
|
-
let
|
|
8105
|
+
let stat = "";
|
|
8090
8106
|
try {
|
|
8091
8107
|
const diff = await worktreeDiff(p.root, where.worktree);
|
|
8092
|
-
|
|
8108
|
+
stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
8093
8109
|
`);
|
|
8094
8110
|
diffText = await worktreePatch(where.worktree, diff.base);
|
|
8095
8111
|
} catch (e) {
|
|
@@ -8111,7 +8127,7 @@ class Store {
|
|
|
8111
8127
|
task,
|
|
8112
8128
|
title: taskRow?.title ?? null,
|
|
8113
8129
|
branch: w?.branch ?? null,
|
|
8114
|
-
stat
|
|
8130
|
+
stat,
|
|
8115
8131
|
patch: diffText
|
|
8116
8132
|
});
|
|
8117
8133
|
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
@@ -8200,9 +8216,9 @@ ${err}
|
|
|
8200
8216
|
this.gateJobs.set(where.key, done);
|
|
8201
8217
|
return { ok: true, pid: 0, log: where.log, done };
|
|
8202
8218
|
}
|
|
8203
|
-
async runGates(projectId, task,
|
|
8219
|
+
async runGates(projectId, task, gates, opts = {}) {
|
|
8204
8220
|
const cfg = this.gateDefs(projectId);
|
|
8205
|
-
const names =
|
|
8221
|
+
const names = gates?.length ? gates : (cfg?.required ?? []).filter((g) => cfg?.defs[g]);
|
|
8206
8222
|
const key = `${projectId}:${task}`;
|
|
8207
8223
|
const batch = (async () => {
|
|
8208
8224
|
const started = [];
|
|
@@ -8215,9 +8231,9 @@ ${err}
|
|
|
8215
8231
|
continue;
|
|
8216
8232
|
}
|
|
8217
8233
|
started.push(g);
|
|
8218
|
-
const
|
|
8219
|
-
if (
|
|
8220
|
-
runs.push(
|
|
8234
|
+
const run = await r.done;
|
|
8235
|
+
if (run)
|
|
8236
|
+
runs.push(run);
|
|
8221
8237
|
}
|
|
8222
8238
|
return { started, skipped, runs };
|
|
8223
8239
|
})();
|
|
@@ -8271,21 +8287,21 @@ ${err}
|
|
|
8271
8287
|
const sessionId = this.knownSession(input.sessionId);
|
|
8272
8288
|
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, duration_ms, created_at, actor_kind, actor_id)
|
|
8273
8289
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, typeof input.durationMs === "number" ? Math.max(0, Math.round(input.durationMs)) : null, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
|
|
8274
|
-
const
|
|
8275
|
-
this.remember(gateDoc(projectId,
|
|
8290
|
+
const run = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
8291
|
+
this.remember(gateDoc(projectId, run.id, run, sessionId));
|
|
8276
8292
|
this.append({
|
|
8277
8293
|
ts: createdAt,
|
|
8278
8294
|
type: "gate.recorded",
|
|
8279
8295
|
projectId,
|
|
8280
8296
|
sessionId,
|
|
8281
8297
|
payload: {
|
|
8282
|
-
task:
|
|
8283
|
-
gate:
|
|
8284
|
-
verdict:
|
|
8285
|
-
summary: `gate ${
|
|
8298
|
+
task: run.task,
|
|
8299
|
+
gate: run.gate,
|
|
8300
|
+
verdict: run.verdict,
|
|
8301
|
+
summary: `gate ${run.gate} ${run.verdict} on ${run.task}`
|
|
8286
8302
|
}
|
|
8287
8303
|
});
|
|
8288
|
-
if (
|
|
8304
|
+
if (run.verdict === "fail")
|
|
8289
8305
|
this.append({
|
|
8290
8306
|
ts: createdAt,
|
|
8291
8307
|
type: "incident.opened",
|
|
@@ -8294,12 +8310,12 @@ ${err}
|
|
|
8294
8310
|
payload: {
|
|
8295
8311
|
rule: "gate_failed",
|
|
8296
8312
|
action: "failed",
|
|
8297
|
-
command: `${
|
|
8298
|
-
reason: `${
|
|
8313
|
+
command: `${run.task} \xB7 ${run.gate}`,
|
|
8314
|
+
reason: `${run.rubric}${run.evidence ? ` \u2014 ${run.evidence.slice(0, 200)}` : ""}`
|
|
8299
8315
|
}
|
|
8300
8316
|
});
|
|
8301
8317
|
this.touch();
|
|
8302
|
-
return { ok: true, run
|
|
8318
|
+
return { ok: true, run };
|
|
8303
8319
|
}
|
|
8304
8320
|
taskCache = new Map;
|
|
8305
8321
|
taskSources = new TaskSources;
|
|
@@ -8383,8 +8399,8 @@ ${err}
|
|
|
8383
8399
|
const row = this.db.query("SELECT id FROM projects WHERE root = ?").get(root);
|
|
8384
8400
|
return row?.id ?? null;
|
|
8385
8401
|
}
|
|
8386
|
-
noteRuleChange(key,
|
|
8387
|
-
const sig = JSON.stringify(Object.entries(
|
|
8402
|
+
noteRuleChange(key, rules) {
|
|
8403
|
+
const sig = JSON.stringify(Object.entries(rules).sort());
|
|
8388
8404
|
const metaKey = `rules.sig:${key}`;
|
|
8389
8405
|
const prev = this.db.query("SELECT value FROM meta WHERE key = ?").get(metaKey)?.value;
|
|
8390
8406
|
if (prev === sig)
|
|
@@ -8393,7 +8409,7 @@ ${err}
|
|
|
8393
8409
|
if (prev === undefined)
|
|
8394
8410
|
return;
|
|
8395
8411
|
const before = new Map(JSON.parse(prev));
|
|
8396
|
-
const after = new Map(Object.entries(
|
|
8412
|
+
const after = new Map(Object.entries(rules));
|
|
8397
8413
|
const added = [...after.keys()].filter((r) => !before.has(r)).sort();
|
|
8398
8414
|
const removed = [...before.keys()].filter((r) => !after.has(r)).sort();
|
|
8399
8415
|
const retuned = [...after.entries()].filter(([r, mode]) => before.has(r) && before.get(r) !== mode).map(([r, mode]) => `${r}=${mode}`).sort();
|
|
@@ -8533,11 +8549,11 @@ ${err}
|
|
|
8533
8549
|
state: r.state
|
|
8534
8550
|
}));
|
|
8535
8551
|
}
|
|
8536
|
-
guardHook(
|
|
8537
|
-
const tool = typeof
|
|
8538
|
-
const input =
|
|
8539
|
-
const id = typeof
|
|
8540
|
-
const cwd = typeof
|
|
8552
|
+
guardHook(raw) {
|
|
8553
|
+
const tool = typeof raw.tool_name === "string" ? raw.tool_name : "";
|
|
8554
|
+
const input = raw.tool_input ?? {};
|
|
8555
|
+
const id = typeof raw.session_id === "string" ? raw.session_id : "";
|
|
8556
|
+
const cwd = typeof raw.cwd === "string" ? raw.cwd : "";
|
|
8541
8557
|
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
8542
8558
|
const cmd = tool === "Bash" ? input.command : undefined;
|
|
8543
8559
|
if (!isWrite && !cmd)
|
|
@@ -8682,6 +8698,9 @@ ${err}
|
|
|
8682
8698
|
createdAt: r.created_at
|
|
8683
8699
|
}));
|
|
8684
8700
|
}
|
|
8701
|
+
liveProjects() {
|
|
8702
|
+
return this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
|
|
8703
|
+
}
|
|
8685
8704
|
project(id) {
|
|
8686
8705
|
const r = this.db.query("SELECT * FROM projects WHERE id = ?").get(id);
|
|
8687
8706
|
if (!r)
|
|
@@ -8714,9 +8733,9 @@ ${err}
|
|
|
8714
8733
|
if (!explicit) {
|
|
8715
8734
|
const hit = this.cwdProject.get(path);
|
|
8716
8735
|
if (hit && Date.now() - hit.t < 60000) {
|
|
8717
|
-
const
|
|
8718
|
-
if (
|
|
8719
|
-
return
|
|
8736
|
+
const p = this.project(hit.id);
|
|
8737
|
+
if (p)
|
|
8738
|
+
return p;
|
|
8720
8739
|
}
|
|
8721
8740
|
}
|
|
8722
8741
|
const p = this.resolveProjectUncached(path, explicit, name);
|
|
@@ -8810,25 +8829,25 @@ ${err}
|
|
|
8810
8829
|
e = { ...e, payload: redactValue(e.payload, res), raw: redactValue(e.raw, res) };
|
|
8811
8830
|
const slim = slimForStorage(e);
|
|
8812
8831
|
const p = e.payload ?? {};
|
|
8813
|
-
const
|
|
8814
|
-
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),
|
|
8815
|
-
const stored = { ...e, actor
|
|
8832
|
+
const actor = e.actor ?? this.actorFor(typeof p.owner === "string" ? p.owner : typeof p.by === "string" ? p.by : null, e.sessionId);
|
|
8833
|
+
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), actor.kind, actor.id);
|
|
8834
|
+
const stored = { ...e, actor, seq: Number(r.lastInsertRowid) };
|
|
8816
8835
|
if (stored.type === "incident.opened")
|
|
8817
8836
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
8818
8837
|
this.projectSession(stored);
|
|
8819
8838
|
if (stored.type === "incident.opened") {
|
|
8820
8839
|
const webhook = this.policyFor(null).config.notify.webhook;
|
|
8821
8840
|
if (webhook) {
|
|
8822
|
-
const
|
|
8841
|
+
const p = stored.payload ?? {};
|
|
8823
8842
|
const project = this.project(stored.projectId)?.name ?? stored.projectId;
|
|
8824
8843
|
fetch(webhook, {
|
|
8825
8844
|
method: "POST",
|
|
8826
8845
|
headers: { "content-type": "application/json" },
|
|
8827
8846
|
body: JSON.stringify({
|
|
8828
|
-
text: `Swarm incident \xB7 ${
|
|
8829
|
-
${
|
|
8830
|
-
${
|
|
8831
|
-
rule:
|
|
8847
|
+
text: `Swarm incident \xB7 ${p.rule ?? "?"} \xB7 ${project}
|
|
8848
|
+
${p.command ?? ""}
|
|
8849
|
+
${p.reason ?? ""}`.trim(),
|
|
8850
|
+
rule: p.rule,
|
|
8832
8851
|
project,
|
|
8833
8852
|
sessionId: stored.sessionId,
|
|
8834
8853
|
ts: stored.ts
|
|
@@ -8837,15 +8856,15 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8837
8856
|
}).catch(() => {});
|
|
8838
8857
|
}
|
|
8839
8858
|
}
|
|
8840
|
-
const
|
|
8841
|
-
if (
|
|
8859
|
+
const team = this.policyFor(null).config.team;
|
|
8860
|
+
if (team.url && team.forward.includes("ledger") && isAuditType(stored.type)) {
|
|
8842
8861
|
this.db.query("INSERT INTO outbox (kind, payload, created_at) VALUES ('event', ?, ?)").run(JSON.stringify({
|
|
8843
8862
|
seq: stored.seq,
|
|
8844
8863
|
ts: stored.ts,
|
|
8845
8864
|
type: stored.type,
|
|
8846
8865
|
projectId: stored.projectId,
|
|
8847
8866
|
sessionId: stored.sessionId,
|
|
8848
|
-
actor
|
|
8867
|
+
actor,
|
|
8849
8868
|
payload: slim.payload ?? null
|
|
8850
8869
|
}), stored.ts);
|
|
8851
8870
|
}
|
|
@@ -8898,9 +8917,9 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8898
8917
|
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);
|
|
8899
8918
|
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
8900
8919
|
}
|
|
8901
|
-
prune(
|
|
8920
|
+
prune(days) {
|
|
8902
8921
|
const cfg = this.policyFor(null).config;
|
|
8903
|
-
const chatter =
|
|
8922
|
+
const chatter = days ?? cfg.events.retain_days;
|
|
8904
8923
|
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
8905
8924
|
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
8906
8925
|
if (cfg.audit.retain_days > 0) {
|
|
@@ -8913,12 +8932,12 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8913
8932
|
this.touch();
|
|
8914
8933
|
return n;
|
|
8915
8934
|
}
|
|
8916
|
-
ingestHook(event,
|
|
8917
|
-
if (typeof
|
|
8918
|
-
this.autoRenewFor(typeof
|
|
8919
|
-
const cwd = typeof
|
|
8935
|
+
ingestHook(event, raw) {
|
|
8936
|
+
if (typeof raw.cwd === "string")
|
|
8937
|
+
this.autoRenewFor(typeof raw.session_id === "string" ? raw.session_id : null, raw.cwd);
|
|
8938
|
+
const cwd = typeof raw.cwd === "string" ? raw.cwd : process.cwd();
|
|
8920
8939
|
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
8921
|
-
const e = this.append(normalizeHook(event,
|
|
8940
|
+
const e = this.append(normalizeHook(event, raw, project?.id ?? "p_unknown"));
|
|
8922
8941
|
if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
|
|
8923
8942
|
if (existsSync6(cwd)) {
|
|
8924
8943
|
this.autoHandoff(e.sessionId, cwd);
|
|
@@ -8926,8 +8945,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8926
8945
|
}
|
|
8927
8946
|
this.rememberSession(e.sessionId);
|
|
8928
8947
|
}
|
|
8929
|
-
if (e.sessionId && typeof
|
|
8930
|
-
this.db.query("UPDATE sessions SET transcript_path = ? WHERE id = ? AND transcript_path IS NULL").run(
|
|
8948
|
+
if (e.sessionId && typeof raw.transcript_path === "string") {
|
|
8949
|
+
this.db.query("UPDATE sessions SET transcript_path = ? WHERE id = ? AND transcript_path IS NULL").run(raw.transcript_path, e.sessionId);
|
|
8931
8950
|
const last = this.lastTail.get(e.sessionId) ?? 0;
|
|
8932
8951
|
if (Date.now() - last > 2000) {
|
|
8933
8952
|
this.lastTail.set(e.sessionId, Date.now());
|
|
@@ -9468,11 +9487,11 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9468
9487
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
9469
9488
|
}
|
|
9470
9489
|
const branch = `task/${task}`;
|
|
9471
|
-
const
|
|
9472
|
-
if (existsSync6(
|
|
9473
|
-
return { ok: false, error: `${
|
|
9474
|
-
mkdirSync4(dirname3(
|
|
9475
|
-
const created = worktreeAdd(p.root,
|
|
9490
|
+
const worktree = this.worktreePath(projectId, task);
|
|
9491
|
+
if (existsSync6(worktree))
|
|
9492
|
+
return { ok: false, error: `${worktree} already exists; release ${task} first` };
|
|
9493
|
+
mkdirSync4(dirname3(worktree), { recursive: true });
|
|
9494
|
+
const created = worktreeAdd(p.root, worktree, branch, baseRef);
|
|
9476
9495
|
if (!created)
|
|
9477
9496
|
return { ok: false, error: `git worktree add failed for ${task}` };
|
|
9478
9497
|
this.invalidateWorktrees(projectId);
|
|
@@ -9495,13 +9514,13 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9495
9514
|
return { ok: true, task, owner, worktree: created, branch, expiresAt, bootstrap };
|
|
9496
9515
|
}
|
|
9497
9516
|
bootstraps = new Map;
|
|
9498
|
-
bootstrapWorktree(projectId, task, repoRoot,
|
|
9499
|
-
const plan = planBootstrap(loadConfig({ repoRoot, home: this.home }), repoRoot,
|
|
9517
|
+
bootstrapWorktree(projectId, task, repoRoot, worktree) {
|
|
9518
|
+
const plan = planBootstrap(loadConfig({ repoRoot, home: this.home }), repoRoot, worktree);
|
|
9500
9519
|
if (!needsBootstrap(plan))
|
|
9501
9520
|
return null;
|
|
9502
|
-
const job = runBootstrap(plan, { worktree
|
|
9521
|
+
const job = runBootstrap(plan, { worktree, home: this.home, projectId, task });
|
|
9503
9522
|
const done = job.done.then((o) => {
|
|
9504
|
-
this.bootstraps.delete(
|
|
9523
|
+
this.bootstraps.delete(worktree);
|
|
9505
9524
|
const ts = new Date().toISOString();
|
|
9506
9525
|
const ok = !o.setup || o.setup.exitCode === 0;
|
|
9507
9526
|
this.append({
|
|
@@ -9511,7 +9530,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9511
9530
|
sessionId: null,
|
|
9512
9531
|
payload: {
|
|
9513
9532
|
task,
|
|
9514
|
-
worktree
|
|
9533
|
+
worktree,
|
|
9515
9534
|
ok,
|
|
9516
9535
|
log: job.log,
|
|
9517
9536
|
...o,
|
|
@@ -9534,11 +9553,11 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9534
9553
|
this.touch();
|
|
9535
9554
|
return o;
|
|
9536
9555
|
});
|
|
9537
|
-
this.bootstraps.set(
|
|
9556
|
+
this.bootstraps.set(worktree, done);
|
|
9538
9557
|
return job.log;
|
|
9539
9558
|
}
|
|
9540
|
-
awaitBootstrap(
|
|
9541
|
-
return this.bootstraps.get(
|
|
9559
|
+
awaitBootstrap(worktree) {
|
|
9560
|
+
return this.bootstraps.get(worktree) ?? Promise.resolve();
|
|
9542
9561
|
}
|
|
9543
9562
|
autoRenewAt = new Map;
|
|
9544
9563
|
autoRenewFor(sessionId, cwd) {
|
|
@@ -9574,8 +9593,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9574
9593
|
expiresAt: r.expires_at
|
|
9575
9594
|
}));
|
|
9576
9595
|
}
|
|
9577
|
-
waiting(projectId,
|
|
9578
|
-
const since = new Date(Date.now() -
|
|
9596
|
+
waiting(projectId, days = 7) {
|
|
9597
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
9579
9598
|
const pArgs = projectId ? [projectId] : [];
|
|
9580
9599
|
const paired = this.db.query(`SELECT type, session_id, project_id, ts,
|
|
9581
9600
|
COALESCE(json_extract(payload,'$.requestId'), json_extract(payload,'$.id')) AS key,
|
|
@@ -9643,8 +9662,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9643
9662
|
}))
|
|
9644
9663
|
};
|
|
9645
9664
|
}
|
|
9646
|
-
gateHealth(projectId,
|
|
9647
|
-
const since = new Date(Date.now() -
|
|
9665
|
+
gateHealth(projectId, days = 30) {
|
|
9666
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
9648
9667
|
const rows = this.db.query(`SELECT project_id, task, gate, verdict, duration_ms, created_at FROM gates
|
|
9649
9668
|
WHERE created_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
9650
9669
|
return gateHealth(rows.filter((r) => r.verdict === "pass" || r.verdict === "fail").map((r) => ({
|
|
@@ -9784,8 +9803,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9784
9803
|
this.refreshDisk(trees.map((t) => t.path), diskTtlMs);
|
|
9785
9804
|
return hygieneReport(procs, trees);
|
|
9786
9805
|
}
|
|
9787
|
-
lineage(projectId,
|
|
9788
|
-
const since = new Date(Date.now() -
|
|
9806
|
+
lineage(projectId, days = 14, expanded = []) {
|
|
9807
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
9789
9808
|
const pArgs = projectId ? [projectId] : [];
|
|
9790
9809
|
const rows = this.db.query(`SELECT id, project_id, title, agent, kind, state, parent_id, started_at, ended_at
|
|
9791
9810
|
FROM sessions WHERE last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
@@ -9856,8 +9875,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9856
9875
|
edges.push(...handoffEdges(holds));
|
|
9857
9876
|
return lineageGraph(sessions, edges, { expanded });
|
|
9858
9877
|
}
|
|
9859
|
-
mcpHealth(projectId,
|
|
9860
|
-
const since = new Date(Date.now() -
|
|
9878
|
+
mcpHealth(projectId, days = 7) {
|
|
9879
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
9861
9880
|
const rows = this.db.query(`SELECT session_id, seq, ts, type,
|
|
9862
9881
|
json_extract(payload,'$.tool') AS tool,
|
|
9863
9882
|
json_extract(payload,'$.toolResponse') AS response
|
|
@@ -9897,8 +9916,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9897
9916
|
abandon(sessionId, tool, at);
|
|
9898
9917
|
return mcpHealth(calls);
|
|
9899
9918
|
}
|
|
9900
|
-
context(projectId,
|
|
9901
|
-
const since = new Date(Date.now() -
|
|
9919
|
+
context(projectId, days = 7) {
|
|
9920
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
9902
9921
|
const pArgs = projectId ? [projectId] : [];
|
|
9903
9922
|
const where = projectId ? " AND project_id = ?" : "";
|
|
9904
9923
|
const results = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
@@ -9945,10 +9964,10 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9945
9964
|
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9946
9965
|
const out = [];
|
|
9947
9966
|
for (const pid of projects) {
|
|
9948
|
-
const
|
|
9967
|
+
const tasks = [
|
|
9949
9968
|
...new Set(this.claims(pid).map((c) => splitArmTask(c.task)).filter((x) => x.arm).map((x) => x.task))
|
|
9950
9969
|
];
|
|
9951
|
-
for (const t of
|
|
9970
|
+
for (const t of tasks.sort())
|
|
9952
9971
|
out.push({ ...this.abTrial(pid, t), projectId: pid });
|
|
9953
9972
|
}
|
|
9954
9973
|
const rank = { undecided: 0, "all-failed": 1, winner: 2 };
|
|
@@ -10006,8 +10025,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10006
10025
|
const sid = sess?.id ?? c.sessionId;
|
|
10007
10026
|
const g = gateRuns.filter((x) => x.task === c.task);
|
|
10008
10027
|
const latest = new Map;
|
|
10009
|
-
for (const
|
|
10010
|
-
latest.set(
|
|
10028
|
+
for (const run of [...g].sort((a, b) => a.createdAt < b.createdAt ? -1 : 1))
|
|
10029
|
+
latest.set(run.gate, run);
|
|
10011
10030
|
const verdicts = [...latest.values()];
|
|
10012
10031
|
const diff = c.worktree ? this.diffCache.get(c.worktree)?.v : null;
|
|
10013
10032
|
arms.push({
|
|
@@ -10057,8 +10076,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10057
10076
|
})
|
|
10058
10077
|
};
|
|
10059
10078
|
}
|
|
10060
|
-
transitions(projectId,
|
|
10061
|
-
const since = new Date(Date.now() -
|
|
10079
|
+
transitions(projectId, days = 7, minWeight = 1) {
|
|
10080
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
10062
10081
|
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool
|
|
10063
10082
|
FROM events
|
|
10064
10083
|
WHERE type = 'tool.requested' AND ts >= ?
|
|
@@ -10066,14 +10085,14 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10066
10085
|
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
10067
10086
|
return transitionGraph(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "" })), { minWeight });
|
|
10068
10087
|
}
|
|
10069
|
-
resourceHolding(projectId,
|
|
10070
|
-
const since = new Date(Date.now() -
|
|
10088
|
+
resourceHolding(projectId, days = 3) {
|
|
10089
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
10071
10090
|
const p = projectId ? " AND c.project_id = ?" : "";
|
|
10072
10091
|
const args = projectId ? [projectId] : [];
|
|
10073
10092
|
const ended = new Map(this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all().map((r) => [r.id, r.ended_at]));
|
|
10074
10093
|
const claims = this.db.query(`SELECT c.task AS name, c.owner, c.project_id, c.expires_at, c.actor_id AS session_id
|
|
10075
10094
|
FROM claims c WHERE c.state = 'held' AND c.released_at IS NULL${p}`).all(...args);
|
|
10076
|
-
const
|
|
10095
|
+
const resources = this.db.query(`SELECT c.name, c.owner, c.project_id, c.expires_at, c.session_id, c.port
|
|
10077
10096
|
FROM resources c WHERE c.released = 0${p}`).all(...args);
|
|
10078
10097
|
const procs = this.db.query(`SELECT c.name, c.owner, c.project_id, c.session_id, c.port
|
|
10079
10098
|
FROM processes c WHERE c.ended_at IS NULL${p}`).all(...args);
|
|
@@ -10090,7 +10109,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10090
10109
|
expiresAt: r.expires_at,
|
|
10091
10110
|
projectId: r.project_id
|
|
10092
10111
|
})),
|
|
10093
|
-
...
|
|
10112
|
+
...resources.map((r) => ({
|
|
10094
10113
|
kind: r.port ? "port" : "lease",
|
|
10095
10114
|
name: r.port ? String(r.port) : r.name,
|
|
10096
10115
|
owner: r.owner ?? "unknown",
|
|
@@ -10119,8 +10138,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10119
10138
|
}));
|
|
10120
10139
|
return resourceGraph(held, wanted);
|
|
10121
10140
|
}
|
|
10122
|
-
fileHeat(projectId,
|
|
10123
|
-
const since = new Date(Date.now() -
|
|
10141
|
+
fileHeat(projectId, days = 14) {
|
|
10142
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
10124
10143
|
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
10125
10144
|
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10126
10145
|
FROM events
|
|
@@ -10128,8 +10147,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10128
10147
|
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10129
10148
|
return fileHeat(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "", path: r.path ?? "" })));
|
|
10130
10149
|
}
|
|
10131
|
-
security(projectId,
|
|
10132
|
-
const since = new Date(Date.now() -
|
|
10150
|
+
security(projectId, days = 14) {
|
|
10151
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
10133
10152
|
const rows = this.db.query(`SELECT session_id, ts, json_extract(payload,'$.tool') AS tool,
|
|
10134
10153
|
COALESCE(json_extract(payload,'$.toolInput.command'),
|
|
10135
10154
|
json_extract(payload,'$.toolInput.url'), '') AS command,
|
|
@@ -10144,8 +10163,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10144
10163
|
at: r.ts
|
|
10145
10164
|
})));
|
|
10146
10165
|
}
|
|
10147
|
-
ruleEffect(projectId,
|
|
10148
|
-
const since = new Date(Date.now() -
|
|
10166
|
+
ruleEffect(projectId, days = 30) {
|
|
10167
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
10149
10168
|
const rows = this.db.query(`SELECT e.seq, e.ts,
|
|
10150
10169
|
json_extract(e.payload,'$.rule') AS rule,
|
|
10151
10170
|
COALESCE(json_extract(e.payload,'$.command'), '') AS command,
|
|
@@ -10159,7 +10178,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10159
10178
|
command: r.command ?? "",
|
|
10160
10179
|
at: r.ts,
|
|
10161
10180
|
acked: Boolean(r.acked)
|
|
10162
|
-
})), changes.map((c) => ({ at: c.ts, added: JSON.parse(c.added) })), Date.now(),
|
|
10181
|
+
})), changes.map((c) => ({ at: c.ts, added: JSON.parse(c.added) })), Date.now(), days);
|
|
10163
10182
|
}
|
|
10164
10183
|
reclaimBuild(path, { dryRun = false } = {}) {
|
|
10165
10184
|
const report = this.hygiene();
|
|
@@ -10206,6 +10225,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10206
10225
|
checkStalls() {
|
|
10207
10226
|
const live = this.db.query("SELECT id, project_id FROM sessions WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?").all(new Date(Date.now() - IDLE_MS).toISOString());
|
|
10208
10227
|
const liveIds = new Set(live.map((s) => s.id));
|
|
10228
|
+
const before = JSON.stringify([...this.stalls].map(([id, v]) => [id, v.kind, v.reason]));
|
|
10209
10229
|
for (const id of [...this.stalls.keys()])
|
|
10210
10230
|
if (!liveIds.has(id))
|
|
10211
10231
|
this.stalls.delete(id);
|
|
@@ -10224,15 +10244,15 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10224
10244
|
ts: ""
|
|
10225
10245
|
};
|
|
10226
10246
|
});
|
|
10227
|
-
const
|
|
10228
|
-
if (!
|
|
10247
|
+
const stall = detectStall(calls);
|
|
10248
|
+
if (!stall) {
|
|
10229
10249
|
this.stalls.delete(s.id);
|
|
10230
10250
|
continue;
|
|
10231
10251
|
}
|
|
10232
10252
|
flagged++;
|
|
10233
10253
|
const prev = this.stalls.get(s.id);
|
|
10234
|
-
this.stalls.set(s.id,
|
|
10235
|
-
if (prev?.kind ===
|
|
10254
|
+
this.stalls.set(s.id, stall);
|
|
10255
|
+
if (prev?.kind === stall.kind)
|
|
10236
10256
|
continue;
|
|
10237
10257
|
this.append({
|
|
10238
10258
|
ts: new Date().toISOString(),
|
|
@@ -10240,13 +10260,16 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10240
10260
|
projectId: s.project_id,
|
|
10241
10261
|
sessionId: s.id,
|
|
10242
10262
|
payload: {
|
|
10243
|
-
kind:
|
|
10244
|
-
reason:
|
|
10245
|
-
summary: `session looks stuck \u2014 ${
|
|
10263
|
+
kind: stall.kind,
|
|
10264
|
+
reason: stall.reason,
|
|
10265
|
+
summary: `session looks stuck \u2014 ${stall.reason}`
|
|
10246
10266
|
}
|
|
10247
10267
|
});
|
|
10248
10268
|
this.touch();
|
|
10249
10269
|
}
|
|
10270
|
+
const after = JSON.stringify([...this.stalls].map(([id, v]) => [id, v.kind, v.reason]));
|
|
10271
|
+
if (after !== before)
|
|
10272
|
+
this.touch();
|
|
10250
10273
|
return flagged;
|
|
10251
10274
|
}
|
|
10252
10275
|
sweepOrphans() {
|
|
@@ -10311,18 +10334,18 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10311
10334
|
const row = this.db.query("SELECT * FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
|
|
10312
10335
|
if (!row)
|
|
10313
10336
|
return { ok: false, error: `no claim on ${task}` };
|
|
10314
|
-
const
|
|
10315
|
-
if (
|
|
10316
|
-
const work = heldWork(
|
|
10337
|
+
const worktree = row.worktree ?? "";
|
|
10338
|
+
if (worktree && existsSync6(worktree)) {
|
|
10339
|
+
const work = heldWork(worktree);
|
|
10317
10340
|
const can = canRelease(work, force);
|
|
10318
10341
|
if (!can.ok)
|
|
10319
10342
|
return {
|
|
10320
10343
|
ok: false,
|
|
10321
|
-
error: releaseRefusalMessage(can,
|
|
10344
|
+
error: releaseRefusalMessage(can, worktree),
|
|
10322
10345
|
refused: can.reason
|
|
10323
10346
|
};
|
|
10324
|
-
if (p && !worktreeRemove(p.root,
|
|
10325
|
-
return { ok: false, error: `git worktree remove failed for ${
|
|
10347
|
+
if (p && !worktreeRemove(p.root, worktree, force))
|
|
10348
|
+
return { ok: false, error: `git worktree remove failed for ${worktree}` };
|
|
10326
10349
|
this.invalidateWorktrees(projectId);
|
|
10327
10350
|
}
|
|
10328
10351
|
const releasedAt = new Date().toISOString();
|
|
@@ -10431,12 +10454,12 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10431
10454
|
const p = this.project(projectId);
|
|
10432
10455
|
if (!p)
|
|
10433
10456
|
return Promise.resolve([]);
|
|
10434
|
-
const
|
|
10457
|
+
const run = listWorktreesAsync(p.root).then((v) => {
|
|
10435
10458
|
this.wtCache.set(projectId, { v, t: Date.now() });
|
|
10436
10459
|
return v;
|
|
10437
10460
|
}).finally(() => this.wtInflight.delete(projectId));
|
|
10438
|
-
this.wtInflight.set(projectId,
|
|
10439
|
-
return
|
|
10461
|
+
this.wtInflight.set(projectId, run);
|
|
10462
|
+
return run;
|
|
10440
10463
|
}
|
|
10441
10464
|
createWorktree(projectId, name, baseRef = "HEAD", branch) {
|
|
10442
10465
|
const p = this.project(projectId);
|
|
@@ -10548,7 +10571,7 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10548
10571
|
const taskRow = this.tasks(projectId)?.tasks.find((t) => t.id === task) ?? null;
|
|
10549
10572
|
const handoff = this.latestHandoff(projectId, task);
|
|
10550
10573
|
const required = this.requiredGates(projectId);
|
|
10551
|
-
const
|
|
10574
|
+
const gates = required.length ? this.gateStatusFor(this.gateRuns(projectId, task), required).map((g) => ({
|
|
10552
10575
|
gate: g.gate,
|
|
10553
10576
|
verdict: g.verdict
|
|
10554
10577
|
})) : [];
|
|
@@ -10557,19 +10580,19 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10557
10580
|
task,
|
|
10558
10581
|
title: taskRow?.title ?? null,
|
|
10559
10582
|
handoff,
|
|
10560
|
-
gates
|
|
10583
|
+
gates,
|
|
10561
10584
|
files: diff.files,
|
|
10562
10585
|
commits: diff.commits
|
|
10563
10586
|
});
|
|
10564
10587
|
return { ok: true, task, worktree: w, ...d, diff };
|
|
10565
10588
|
}
|
|
10566
|
-
recordPrOpened(projectId, task,
|
|
10589
|
+
recordPrOpened(projectId, task, worktree, url) {
|
|
10567
10590
|
this.append({
|
|
10568
10591
|
ts: new Date().toISOString(),
|
|
10569
10592
|
type: "pr.opened",
|
|
10570
10593
|
projectId,
|
|
10571
10594
|
sessionId: null,
|
|
10572
|
-
payload: { task, worktree
|
|
10595
|
+
payload: { task, worktree, url, summary: `PR opened for ${task}: ${url}` }
|
|
10573
10596
|
});
|
|
10574
10597
|
this.touch();
|
|
10575
10598
|
}
|
|
@@ -10580,22 +10603,31 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10580
10603
|
this.wtCache.clear();
|
|
10581
10604
|
}
|
|
10582
10605
|
async refreshAllWorktrees() {
|
|
10583
|
-
await Promise.all(this.
|
|
10606
|
+
await Promise.all(this.liveProjects().map((p) => this.refreshWorktrees(p.id)));
|
|
10584
10607
|
}
|
|
10585
10608
|
sessions() {
|
|
10609
|
+
return this.memoised("sessions", 2000, () => this.computeSessions());
|
|
10610
|
+
}
|
|
10611
|
+
computeSessions() {
|
|
10612
|
+
const page = this.db.query("SELECT id FROM sessions ORDER BY last_seen_at DESC LIMIT 200").all();
|
|
10613
|
+
if (!page.length)
|
|
10614
|
+
return [];
|
|
10615
|
+
const ids = page.map((r) => r.id);
|
|
10616
|
+
const holes = ids.map(() => "?").join(",");
|
|
10586
10617
|
const rows = this.db.query(`SELECT s.*, COUNT(t.id) AS turns, COALESCE(SUM(t.input),0) AS input, COALESCE(SUM(t.output),0) AS output,
|
|
10587
10618
|
COALESCE(SUM(t.cache_write),0) AS cache_write, COALESCE(SUM(t.cache_read),0) AS cache_read, COALESCE(SUM(t.thinking),0) AS thinking,
|
|
10588
10619
|
SUM(t.cost_usd) AS cost_usd, MAX(t.cost_usd IS NULL AND t.id IS NOT NULL) AS unpriced,
|
|
10589
10620
|
(SELECT model FROM turns lt WHERE lt.session_id = s.id AND lt.agent_id IS NULL AND lt.sidechain = 0 ORDER BY lt.ts DESC LIMIT 1) AS live_model,
|
|
10590
10621
|
(SELECT COUNT(DISTINCT model) FROM turns lm WHERE lm.session_id = s.id AND lm.agent_id IS NULL AND lm.sidechain = 0) AS model_count
|
|
10591
10622
|
FROM sessions s LEFT JOIN turns t ON t.session_id = s.id
|
|
10592
|
-
|
|
10623
|
+
WHERE s.id IN (${holes})
|
|
10624
|
+
GROUP BY s.id ORDER BY s.last_seen_at DESC`).all(...ids);
|
|
10593
10625
|
const idleBefore = Date.now() - IDLE_MS;
|
|
10594
10626
|
const sparkRows = this.db.query(`SELECT session_id, output, cost_usd FROM (
|
|
10595
10627
|
SELECT t.session_id, t.output, t.cost_usd, t.ts,
|
|
10596
10628
|
ROW_NUMBER() OVER (PARTITION BY t.session_id ORDER BY t.ts DESC) AS rn
|
|
10597
|
-
FROM turns t WHERE t.agent_id IS NULL AND t.sidechain = 0
|
|
10598
|
-
) WHERE rn <= 24 ORDER BY ts`).all();
|
|
10629
|
+
FROM turns t WHERE t.agent_id IS NULL AND t.sidechain = 0 AND t.session_id IN (${holes})
|
|
10630
|
+
) WHERE rn <= 24 ORDER BY ts`).all(...ids);
|
|
10599
10631
|
const sparks = new Map;
|
|
10600
10632
|
for (const x of sparkRows) {
|
|
10601
10633
|
const a = sparks.get(x.session_id) ?? [];
|
|
@@ -10869,10 +10901,10 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
10869
10901
|
for (const [k, v] of Object.entries(JSON.parse(r.tc || "{}")))
|
|
10870
10902
|
tools[k] = (tools[k] ?? 0) + v;
|
|
10871
10903
|
}
|
|
10872
|
-
const sessionRow = (
|
|
10904
|
+
const sessionRow = (order) => this.db.query(`SELECT s.id, s.title, s.project_id AS projectId, s.started_at AS startedAt, s.last_seen_at AS lastSeenAt,
|
|
10873
10905
|
COUNT(t.id) AS turns, SUM(t.cost_usd) AS cost, SUM(t.output) AS output, s.tool_calls AS toolCalls
|
|
10874
10906
|
FROM sessions s JOIN turns t ON t.session_id = s.id WHERE ${scope} AND s.kind != 'subagent'
|
|
10875
|
-
GROUP BY s.id ORDER BY ${
|
|
10907
|
+
GROUP BY s.id ORDER BY ${order} DESC LIMIT 1`).get(arg);
|
|
10876
10908
|
const biggestTurn = this.db.query(`SELECT t.session_id AS sessionId, s.title, t.ts, t.output, t.thinking, t.cost_usd AS cost, t.model
|
|
10877
10909
|
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE ${scope} ORDER BY t.output DESC LIMIT 1`).get(arg);
|
|
10878
10910
|
const busiestDay = daily.slice().sort((a, b) => (b.cost ?? 0) - (a.cost ?? 0) || b.turns - a.turns)[0] ?? null;
|
|
@@ -11012,8 +11044,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11012
11044
|
const rows = this.db.query("SELECT * FROM resources WHERE released = 0").all();
|
|
11013
11045
|
const now = Date.now();
|
|
11014
11046
|
let n = 0;
|
|
11015
|
-
for (const
|
|
11016
|
-
if (this.reapIfDead(this.rowToResource(
|
|
11047
|
+
for (const raw of rows)
|
|
11048
|
+
if (this.reapIfDead(this.rowToResource(raw), now))
|
|
11017
11049
|
n++;
|
|
11018
11050
|
return n;
|
|
11019
11051
|
}
|
|
@@ -11154,11 +11186,11 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11154
11186
|
return { ok: true, process: p };
|
|
11155
11187
|
}
|
|
11156
11188
|
async stopProcess(pid, projectId, graceMs = 3000) {
|
|
11157
|
-
const
|
|
11158
|
-
if (!
|
|
11189
|
+
const raw = this.db.query(`SELECT rowid, * FROM processes WHERE pid = ? AND ended_at IS NULL${projectId ? " AND project_id = ?" : ""}`).get(...projectId ? [pid, projectId] : [pid]);
|
|
11190
|
+
if (!raw)
|
|
11159
11191
|
return { ok: false, reason: "not a registered process" };
|
|
11160
|
-
const p = this.rowToProcess(
|
|
11161
|
-
const rowid =
|
|
11192
|
+
const p = this.rowToProcess(raw);
|
|
11193
|
+
const rowid = raw.rowid;
|
|
11162
11194
|
if (!this.processIsOurs(p)) {
|
|
11163
11195
|
this.endProcess(rowid, p, "exited");
|
|
11164
11196
|
return { ok: true, reason: "already gone" };
|
|
@@ -11185,8 +11217,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11185
11217
|
}
|
|
11186
11218
|
acquireResource(input) {
|
|
11187
11219
|
const key = input.projectId ?? "";
|
|
11188
|
-
const
|
|
11189
|
-
let existing =
|
|
11220
|
+
const raw = this.db.query("SELECT * FROM resources WHERE name = ? AND project_id = ?").get(input.name, key);
|
|
11221
|
+
let existing = raw ? this.rowToResource(raw) : null;
|
|
11190
11222
|
if (existing && this.reapIfDead(existing))
|
|
11191
11223
|
existing = null;
|
|
11192
11224
|
const d = canAcquire(existing, { owner: input.owner }, Date.now(), Store.pidAlive);
|
|
@@ -11228,10 +11260,10 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11228
11260
|
return { ok: true, resource };
|
|
11229
11261
|
}
|
|
11230
11262
|
releaseResource(name, projectId, owner, force = false) {
|
|
11231
|
-
const
|
|
11232
|
-
if (!
|
|
11263
|
+
const raw = this.db.query("SELECT * FROM resources WHERE name = ? AND project_id = ? AND released = 0").get(name, projectId ?? "");
|
|
11264
|
+
if (!raw)
|
|
11233
11265
|
return { ok: false, reason: "not held" };
|
|
11234
|
-
const r = this.rowToResource(
|
|
11266
|
+
const r = this.rowToResource(raw);
|
|
11235
11267
|
if (!force) {
|
|
11236
11268
|
if (!owner)
|
|
11237
11269
|
return { ok: false, reason: `held by ${r.owner}; pass owner or force` };
|
|
@@ -11285,14 +11317,14 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11285
11317
|
const from = localDayIso(-13);
|
|
11286
11318
|
const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
|
|
11287
11319
|
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
11288
|
-
const
|
|
11320
|
+
const days = [];
|
|
11289
11321
|
for (let i = 13;i >= 0; i--)
|
|
11290
|
-
|
|
11322
|
+
days.push(localDayIso(-i).slice(0, 10));
|
|
11291
11323
|
const out = {};
|
|
11292
11324
|
for (const r of rows) {
|
|
11293
11325
|
out[r.pid] ??= new Array(14).fill(0);
|
|
11294
11326
|
const arr = out[r.pid];
|
|
11295
|
-
const i =
|
|
11327
|
+
const i = days.indexOf(r.day);
|
|
11296
11328
|
if (i >= 0)
|
|
11297
11329
|
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
11298
11330
|
}
|
|
@@ -11300,13 +11332,13 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
11300
11332
|
}
|
|
11301
11333
|
snapshot() {
|
|
11302
11334
|
const worktrees = {};
|
|
11303
|
-
const projects = this.
|
|
11335
|
+
const projects = this.liveProjects();
|
|
11304
11336
|
for (const p of projects)
|
|
11305
11337
|
worktrees[p.id] = this.worktrees(p.id);
|
|
11306
11338
|
return {
|
|
11307
11339
|
projects,
|
|
11308
11340
|
worktrees,
|
|
11309
|
-
sessions: this.
|
|
11341
|
+
sessions: this.sessions(),
|
|
11310
11342
|
spend: this.memoised("spend", 30000, () => this.spend()),
|
|
11311
11343
|
spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
|
|
11312
11344
|
claims: this.claims(),
|
|
@@ -11351,18 +11383,18 @@ function slimForStorage(e) {
|
|
|
11351
11383
|
toolResponse: clip(p.toolResponse, TOOL_RESPONSE_MAX)
|
|
11352
11384
|
};
|
|
11353
11385
|
}
|
|
11354
|
-
let
|
|
11355
|
-
if (
|
|
11356
|
-
const r =
|
|
11386
|
+
let raw = e.raw;
|
|
11387
|
+
if (raw && typeof raw === "object") {
|
|
11388
|
+
const r = raw;
|
|
11357
11389
|
if (RAW_TOOL_KEYS.some((k) => (k in r))) {
|
|
11358
11390
|
const rest = {};
|
|
11359
11391
|
for (const k of Object.keys(r))
|
|
11360
11392
|
if (!RAW_TOOL_KEYS.includes(k))
|
|
11361
11393
|
rest[k] = r[k];
|
|
11362
|
-
|
|
11394
|
+
raw = rest;
|
|
11363
11395
|
}
|
|
11364
11396
|
}
|
|
11365
|
-
return { payload, raw
|
|
11397
|
+
return { payload, raw };
|
|
11366
11398
|
}
|
|
11367
11399
|
function toWire(e) {
|
|
11368
11400
|
const { raw: _raw, ...rest } = e;
|
|
@@ -11475,12 +11507,12 @@ class TeamForwarder {
|
|
|
11475
11507
|
return this.store.clusterKeyFor(projectId);
|
|
11476
11508
|
}
|
|
11477
11509
|
status() {
|
|
11478
|
-
const
|
|
11510
|
+
const team = this.store.policyFor(null).config.team;
|
|
11479
11511
|
const box = this.store.outboxStatus();
|
|
11480
11512
|
return {
|
|
11481
|
-
configured:
|
|
11482
|
-
url:
|
|
11483
|
-
forward:
|
|
11513
|
+
configured: team.url != null,
|
|
11514
|
+
url: team.url,
|
|
11515
|
+
forward: team.forward,
|
|
11484
11516
|
pending: box.pending,
|
|
11485
11517
|
oldest: box.oldest,
|
|
11486
11518
|
lastAckAt: this.store.metaValue("team_last_ack") ?? null,
|
|
@@ -11490,14 +11522,14 @@ class TeamForwarder {
|
|
|
11490
11522
|
};
|
|
11491
11523
|
}
|
|
11492
11524
|
async tick(now = Date.now()) {
|
|
11493
|
-
const
|
|
11494
|
-
if (!
|
|
11525
|
+
const team = this.store.policyFor(null).config.team;
|
|
11526
|
+
if (!team.url)
|
|
11495
11527
|
return 0;
|
|
11496
|
-
if (now - this.lastTry <
|
|
11528
|
+
if (now - this.lastTry < team.interval * 1000 + this.backoffMs)
|
|
11497
11529
|
return 0;
|
|
11498
11530
|
this.lastTry = now;
|
|
11499
11531
|
const records = [];
|
|
11500
|
-
const events =
|
|
11532
|
+
const events = team.forward.includes("ledger") ? this.store.outboxPending() : [];
|
|
11501
11533
|
for (const e of events) {
|
|
11502
11534
|
const body = JSON.parse(e.payload);
|
|
11503
11535
|
if (typeof body.projectId === "string")
|
|
@@ -11505,7 +11537,7 @@ class TeamForwarder {
|
|
|
11505
11537
|
records.push({ seq: e.seq, kind: e.kind, body });
|
|
11506
11538
|
}
|
|
11507
11539
|
let spendRows = 0;
|
|
11508
|
-
if (
|
|
11540
|
+
if (team.forward.includes("cost") && now - this.lastSpend > SPEND_EVERY_MS) {
|
|
11509
11541
|
const day = new Date(now).toISOString().slice(0, 10);
|
|
11510
11542
|
for (const r of this.store.spendRollup(day)) {
|
|
11511
11543
|
records.push({
|
|
@@ -11612,30 +11644,30 @@ class TeamForwarder {
|
|
|
11612
11644
|
});
|
|
11613
11645
|
if (!res.ok)
|
|
11614
11646
|
return;
|
|
11615
|
-
const { policy
|
|
11616
|
-
if (!
|
|
11647
|
+
const { policy } = await res.json();
|
|
11648
|
+
if (!policy)
|
|
11617
11649
|
return;
|
|
11618
11650
|
let pinned = this.store.metaValue("team_policy_pubkey");
|
|
11619
11651
|
if (!pinned) {
|
|
11620
|
-
pinned =
|
|
11652
|
+
pinned = policy.publicKey;
|
|
11621
11653
|
this.store.setMetaValue("team_policy_pubkey", pinned);
|
|
11622
11654
|
}
|
|
11623
|
-
if (!verifyPolicySignature(
|
|
11655
|
+
if (!verifyPolicySignature(policy.toml, policy.signature, pinned)) {
|
|
11624
11656
|
this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
|
|
11625
11657
|
return;
|
|
11626
11658
|
}
|
|
11627
11659
|
const file = join9(this.store.home, "policy.toml");
|
|
11628
11660
|
const prev = this.store.metaValue("team_policy_sig");
|
|
11629
|
-
if (prev ===
|
|
11661
|
+
if (prev === policy.signature)
|
|
11630
11662
|
return;
|
|
11631
|
-
writeFileSync3(file,
|
|
11663
|
+
writeFileSync3(file, policy.toml, { mode: 384 });
|
|
11632
11664
|
writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
|
|
11633
|
-
signature:
|
|
11665
|
+
signature: policy.signature,
|
|
11634
11666
|
publicKey: pinned,
|
|
11635
11667
|
fetchedAt: new Date(now).toISOString(),
|
|
11636
11668
|
url: base
|
|
11637
11669
|
}), { mode: 384 });
|
|
11638
|
-
this.store.setMetaValue("team_policy_sig",
|
|
11670
|
+
this.store.setMetaValue("team_policy_sig", policy.signature);
|
|
11639
11671
|
} catch {}
|
|
11640
11672
|
}
|
|
11641
11673
|
}
|
|
@@ -11646,12 +11678,12 @@ class WorkflowEngine {
|
|
|
11646
11678
|
runner;
|
|
11647
11679
|
forge;
|
|
11648
11680
|
active = new Map;
|
|
11649
|
-
constructor(store, runner,
|
|
11681
|
+
constructor(store, runner, forge) {
|
|
11650
11682
|
this.store = store;
|
|
11651
11683
|
this.runner = runner;
|
|
11652
|
-
this.forge =
|
|
11684
|
+
this.forge = forge;
|
|
11653
11685
|
store.wfSweepOrphans();
|
|
11654
|
-
runner.onEnd((
|
|
11686
|
+
runner.onEnd((run) => void this.onRunEnd(run));
|
|
11655
11687
|
}
|
|
11656
11688
|
start(projectId, task, workflow, opts = {}) {
|
|
11657
11689
|
const def = this.store.config(projectId).workflows[workflow];
|
|
@@ -11729,11 +11761,11 @@ class WorkflowEngine {
|
|
|
11729
11761
|
}
|
|
11730
11762
|
if (s.kind === "gate") {
|
|
11731
11763
|
const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
|
|
11732
|
-
const
|
|
11733
|
-
if (!
|
|
11764
|
+
const run = r.runs.find((x) => x.gate === s.gate);
|
|
11765
|
+
if (!run)
|
|
11734
11766
|
return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
|
|
11735
|
-
if (
|
|
11736
|
-
return this.fail(w, `gate ${s.gate} failed \u2014 ${
|
|
11767
|
+
if (run.verdict !== "pass")
|
|
11768
|
+
return this.fail(w, `gate ${s.gate} failed \u2014 ${run.rubric}`);
|
|
11737
11769
|
w.step++;
|
|
11738
11770
|
continue;
|
|
11739
11771
|
}
|
|
@@ -11753,15 +11785,15 @@ class WorkflowEngine {
|
|
|
11753
11785
|
}
|
|
11754
11786
|
this.finish(w, "done", null);
|
|
11755
11787
|
}
|
|
11756
|
-
async onRunEnd(
|
|
11757
|
-
const w = this.active.get(`${
|
|
11758
|
-
if (!w || w.runId !==
|
|
11788
|
+
async onRunEnd(run) {
|
|
11789
|
+
const w = this.active.get(`${run.projectId}:${run.task}`);
|
|
11790
|
+
if (!w || w.runId !== run.id)
|
|
11759
11791
|
return;
|
|
11760
11792
|
w.runId = null;
|
|
11761
|
-
if (
|
|
11793
|
+
if (run.stopped)
|
|
11762
11794
|
return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
|
|
11763
|
-
if (
|
|
11764
|
-
return this.fail(w, `${this.label(w)} exited ${
|
|
11795
|
+
if (run.exitCode !== 0 || run.result?.isError)
|
|
11796
|
+
return this.fail(w, `${this.label(w)} exited ${run.exitCode}${run.result?.isError ? " (error)" : ""} \u2014 log: ${run.log}`);
|
|
11765
11797
|
w.step++;
|
|
11766
11798
|
this.advance(w);
|
|
11767
11799
|
}
|
|
@@ -11831,7 +11863,7 @@ class WorkflowEngine {
|
|
|
11831
11863
|
}
|
|
11832
11864
|
|
|
11833
11865
|
// packages/daemon/src/app.ts
|
|
11834
|
-
var VERSION = "0.13.
|
|
11866
|
+
var VERSION = "0.13.2";
|
|
11835
11867
|
var WEB_DIR = (() => {
|
|
11836
11868
|
if (process.env.SWARM_WEB_DIR)
|
|
11837
11869
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -11849,8 +11881,8 @@ function wireJson(e) {
|
|
|
11849
11881
|
}
|
|
11850
11882
|
return s;
|
|
11851
11883
|
}
|
|
11852
|
-
function hookRepoRoot(store,
|
|
11853
|
-
const cwd = typeof
|
|
11884
|
+
function hookRepoRoot(store, raw) {
|
|
11885
|
+
const cwd = typeof raw.cwd === "string" ? raw.cwd : "";
|
|
11854
11886
|
return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
|
|
11855
11887
|
}
|
|
11856
11888
|
function claudeSettings() {
|
|
@@ -11878,17 +11910,24 @@ function diskVersion() {
|
|
|
11878
11910
|
return null;
|
|
11879
11911
|
}
|
|
11880
11912
|
}
|
|
11881
|
-
function
|
|
11913
|
+
function expandHome(p, home = homedir4()) {
|
|
11914
|
+
if (p === "~")
|
|
11915
|
+
return home;
|
|
11916
|
+
if (p.startsWith("~/"))
|
|
11917
|
+
return join10(home, p.slice(2));
|
|
11918
|
+
return p;
|
|
11919
|
+
}
|
|
11920
|
+
function createApp(store = new Store, hooks = {}) {
|
|
11882
11921
|
const app = new Hono2;
|
|
11883
|
-
const
|
|
11922
|
+
const forge = new ForgeService(store);
|
|
11884
11923
|
const runner = new Runner(store, store.home);
|
|
11885
|
-
const dispatcher = new Dispatcher(store, runner,
|
|
11886
|
-
const
|
|
11887
|
-
const
|
|
11924
|
+
const dispatcher = new Dispatcher(store, runner, forge);
|
|
11925
|
+
const workflows = new WorkflowEngine(store, runner, forge);
|
|
11926
|
+
const team = new TeamForwarder(store, VERSION);
|
|
11888
11927
|
store.onBudgetStop((projectId) => {
|
|
11889
11928
|
dispatcher.clear(projectId);
|
|
11890
|
-
for (const
|
|
11891
|
-
runner.stop(
|
|
11929
|
+
for (const run of runner.list(projectId))
|
|
11930
|
+
runner.stop(run.id);
|
|
11892
11931
|
});
|
|
11893
11932
|
app.use("/v1/*", async (c, next) => {
|
|
11894
11933
|
if (c.req.path === "/v1/health")
|
|
@@ -11913,13 +11952,13 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11913
11952
|
schema: store.schemaVersion(),
|
|
11914
11953
|
auth: store.policyFor(null).config.daemon.auth
|
|
11915
11954
|
}));
|
|
11916
|
-
app.get("/v1/projects", (c) => c.json(store.
|
|
11955
|
+
app.get("/v1/projects", (c) => c.json(store.liveProjects()));
|
|
11917
11956
|
app.post("/v1/projects", async (c) => {
|
|
11918
11957
|
const { path, name } = await c.req.json();
|
|
11919
11958
|
if (!path)
|
|
11920
11959
|
return c.json({ error: "path required" }, 400);
|
|
11921
11960
|
try {
|
|
11922
|
-
return c.json(store.resolveProject(path, true, name), 201);
|
|
11961
|
+
return c.json(store.resolveProject(expandHome(path), true, name), 201);
|
|
11923
11962
|
} catch (e) {
|
|
11924
11963
|
return c.json({ error: e.message }, 400);
|
|
11925
11964
|
}
|
|
@@ -11939,10 +11978,12 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11939
11978
|
});
|
|
11940
11979
|
app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
|
|
11941
11980
|
app.get("/v1/fs/ls", (c) => {
|
|
11942
|
-
const q = c.req.query("path");
|
|
11981
|
+
const q = expandHome(c.req.query("path") ?? "");
|
|
11982
|
+
if (q && !existsSync7(q))
|
|
11983
|
+
return c.json({ error: "no such folder", path: q }, 404);
|
|
11943
11984
|
let dir;
|
|
11944
11985
|
try {
|
|
11945
|
-
dir = realpathSync3(q
|
|
11986
|
+
dir = realpathSync3(q || homedir4());
|
|
11946
11987
|
} catch {
|
|
11947
11988
|
dir = homedir4();
|
|
11948
11989
|
}
|
|
@@ -11954,7 +11995,14 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11954
11995
|
return c.json({ error: e.message, path: dir }, 400);
|
|
11955
11996
|
}
|
|
11956
11997
|
});
|
|
11957
|
-
app.get("/v1/state", (c) =>
|
|
11998
|
+
app.get("/v1/state", (c) => {
|
|
11999
|
+
const body = JSON.stringify(store.snapshot());
|
|
12000
|
+
const etag = `W/"${Bun.hash(body).toString(36)}"`;
|
|
12001
|
+
const head = { etag, "cache-control": "no-cache" };
|
|
12002
|
+
if (c.req.header("if-none-match") === etag)
|
|
12003
|
+
return c.body(null, 304, head);
|
|
12004
|
+
return c.body(body, 200, { ...head, "content-type": "application/json" });
|
|
12005
|
+
});
|
|
11958
12006
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
11959
12007
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11960
12008
|
app.get("/v1/graphs/lineage", (c) => c.json(store.lineage(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)), c.req.queries("expand") ?? [])));
|
|
@@ -11981,7 +12029,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11981
12029
|
})));
|
|
11982
12030
|
const outcomesFor = async (project, opts = {}) => {
|
|
11983
12031
|
const blocking = opts.blocking !== false;
|
|
11984
|
-
const sessions = store.
|
|
12032
|
+
const sessions = store.sessions().filter((s) => !project || s.projectId === project).map((s) => ({
|
|
11985
12033
|
id: s.id,
|
|
11986
12034
|
branch: s.branch,
|
|
11987
12035
|
model: s.model,
|
|
@@ -11992,8 +12040,9 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11992
12040
|
const prs = [];
|
|
11993
12041
|
const reverted = new Set;
|
|
11994
12042
|
let stale = false;
|
|
11995
|
-
|
|
11996
|
-
|
|
12043
|
+
const scope = store.liveProjects().filter((x) => !project || x.id === project);
|
|
12044
|
+
const outcomes = await Promise.all(scope.map(async (p) => blocking ? { ...await forge.merged(p.id, p.root), fresh: true } : forge.mergedCached(p.id, p.root)));
|
|
12045
|
+
for (const o of outcomes) {
|
|
11997
12046
|
if (!o.fresh)
|
|
11998
12047
|
stale = true;
|
|
11999
12048
|
for (const m of o.merged)
|
|
@@ -12001,7 +12050,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12001
12050
|
for (const sha of o.reverted)
|
|
12002
12051
|
reverted.add(sha);
|
|
12003
12052
|
}
|
|
12004
|
-
for (const pr of
|
|
12053
|
+
for (const pr of forge.prs())
|
|
12005
12054
|
if (!project || pr.projectId === project)
|
|
12006
12055
|
prs.push({
|
|
12007
12056
|
branch: pr.branch,
|
|
@@ -12076,10 +12125,10 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12076
12125
|
const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0);
|
|
12077
12126
|
const { branches, stale } = await outcomesFor(project, { blocking: false });
|
|
12078
12127
|
const projects = store.projects().filter((p) => !project || p.id === project);
|
|
12079
|
-
const
|
|
12128
|
+
const tasks = [];
|
|
12080
12129
|
for (const p of projects)
|
|
12081
12130
|
for (const t of store.tasks(p.id)?.tasks ?? [])
|
|
12082
|
-
|
|
12131
|
+
tasks.push({ id: t.id, title: t.title, status: t.statusText, url: null });
|
|
12083
12132
|
const claims = store.claims(project).map((cl) => ({
|
|
12084
12133
|
task: cl.task,
|
|
12085
12134
|
sessionId: cl.sessionId,
|
|
@@ -12089,14 +12138,14 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12089
12138
|
acquiredAt: cl.acquiredAt,
|
|
12090
12139
|
state: cl.state
|
|
12091
12140
|
}));
|
|
12092
|
-
const sessions = store.
|
|
12141
|
+
const sessions = store.sessions().filter((s) => !project || s.projectId === project).map((s) => ({
|
|
12093
12142
|
id: s.id,
|
|
12094
12143
|
title: s.title,
|
|
12095
12144
|
agent: s.agent,
|
|
12096
12145
|
branch: s.branch,
|
|
12097
12146
|
costUsd: s.costUsd
|
|
12098
12147
|
}));
|
|
12099
|
-
const full = provenance(
|
|
12148
|
+
const full = provenance(tasks, claims, sessions, branches);
|
|
12100
12149
|
return c.json({
|
|
12101
12150
|
...full,
|
|
12102
12151
|
chains: full.chains.slice(offset, offset + limit),
|
|
@@ -12127,7 +12176,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12127
12176
|
return c.json({ error: e.message }, 500);
|
|
12128
12177
|
}
|
|
12129
12178
|
});
|
|
12130
|
-
app.get("/v1/team", (c) => c.json(
|
|
12179
|
+
app.get("/v1/team", (c) => c.json(team.status()));
|
|
12131
12180
|
app.post("/v1/team/credentials", async (c) => {
|
|
12132
12181
|
const b = await c.req.json().catch(() => ({}));
|
|
12133
12182
|
if (typeof b.token !== "string" || !b.token)
|
|
@@ -12142,8 +12191,8 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12142
12191
|
const p = id ? store.project(id) : null;
|
|
12143
12192
|
if (id && !p)
|
|
12144
12193
|
return c.json({ error: "unknown project" }, 404);
|
|
12145
|
-
const { provenance
|
|
12146
|
-
return c.json({ ...
|
|
12194
|
+
const { provenance, overridden, policy } = store.policyFor(p?.root ?? null);
|
|
12195
|
+
return c.json({ ...policy, provenance, overridden });
|
|
12147
12196
|
});
|
|
12148
12197
|
app.get("/v1/audit", (c) => {
|
|
12149
12198
|
const since = sinceToIso(c.req.query("since"));
|
|
@@ -12163,9 +12212,9 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12163
12212
|
return c.body(formatAudit(rows, format), 200, { "content-type": ct });
|
|
12164
12213
|
});
|
|
12165
12214
|
app.post("/v1/daemon/restart", (c) => {
|
|
12166
|
-
if (!
|
|
12215
|
+
if (!hooks.restart)
|
|
12167
12216
|
return c.json({ error: "not restartable in this environment" }, 501);
|
|
12168
|
-
setTimeout(() =>
|
|
12217
|
+
setTimeout(() => hooks.restart?.(), 50);
|
|
12169
12218
|
return c.json({ ok: true, restarting: true });
|
|
12170
12219
|
});
|
|
12171
12220
|
app.get("/v1/rules/dryrun", (c) => {
|
|
@@ -12197,12 +12246,12 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12197
12246
|
const r = store.acquireResource({ ...b, name: b.name, owner: b.owner });
|
|
12198
12247
|
return r.ok ? c.json(r, 201) : c.json({ ok: false, error: r.reason }, 409);
|
|
12199
12248
|
});
|
|
12200
|
-
app.get("/v1/prs", (c) => c.json(
|
|
12249
|
+
app.get("/v1/prs", (c) => c.json(forge.prs()));
|
|
12201
12250
|
app.post("/v1/prs/merge", async (c) => {
|
|
12202
12251
|
const b = await c.req.json().catch(() => ({}));
|
|
12203
12252
|
if (!b.projectId || !b.number)
|
|
12204
12253
|
return c.json({ error: "projectId and number are required" }, 400);
|
|
12205
|
-
const r = await
|
|
12254
|
+
const r = await forge.merge(b.projectId, b.number);
|
|
12206
12255
|
return r.ok ? c.json(r) : c.json({ error: r.output || "merge failed" }, 409);
|
|
12207
12256
|
});
|
|
12208
12257
|
app.delete("/v1/resources/:name", (c) => {
|
|
@@ -12292,9 +12341,9 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12292
12341
|
const b = await c.req.json().catch(() => ({}));
|
|
12293
12342
|
const r = store.answer(Number(c.req.param("id")), b.text, b.by ?? null);
|
|
12294
12343
|
if (r.ok && r.question.sessionId) {
|
|
12295
|
-
const
|
|
12296
|
-
if (
|
|
12297
|
-
const sent = runner.send(
|
|
12344
|
+
const run = runner.get(r.question.sessionId);
|
|
12345
|
+
if (run && run.sessionId === r.question.sessionId) {
|
|
12346
|
+
const sent = runner.send(run.id, `[swarm] answer from ${b.by ?? "a human"} to your question "${r.question.text.slice(0, 200)}": ${r.question.answer}`);
|
|
12298
12347
|
if (sent.ok)
|
|
12299
12348
|
store.inbox(r.question.sessionId);
|
|
12300
12349
|
}
|
|
@@ -12306,13 +12355,13 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12306
12355
|
const project = c.req.query("project");
|
|
12307
12356
|
if (!project)
|
|
12308
12357
|
return c.json({ error: "project required" }, 400);
|
|
12309
|
-
return c.json({ defs: store.config(project).workflows, runs:
|
|
12358
|
+
return c.json({ defs: store.config(project).workflows, runs: workflows.status(project) });
|
|
12310
12359
|
});
|
|
12311
12360
|
app.post("/v1/workflows", async (c) => {
|
|
12312
12361
|
const b = await c.req.json().catch(() => ({}));
|
|
12313
12362
|
if (!b.projectId || !b.task || !b.workflow)
|
|
12314
12363
|
return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
|
|
12315
|
-
const r =
|
|
12364
|
+
const r = workflows.start(b.projectId, b.task, b.workflow, {
|
|
12316
12365
|
...b.owner ? { owner: b.owner } : {},
|
|
12317
12366
|
sessionId: b.sessionId ?? null
|
|
12318
12367
|
});
|
|
@@ -12322,7 +12371,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12322
12371
|
const b = await c.req.json().catch(() => ({}));
|
|
12323
12372
|
if (!b.projectId || !b.task)
|
|
12324
12373
|
return c.json({ ok: false, error: "projectId and task required" }, 400);
|
|
12325
|
-
const r =
|
|
12374
|
+
const r = workflows.stop(b.projectId, b.task);
|
|
12326
12375
|
return c.json(r, r.ok ? 200 : 404);
|
|
12327
12376
|
});
|
|
12328
12377
|
app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
|
|
@@ -12346,11 +12395,11 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12346
12395
|
if (!r.ok)
|
|
12347
12396
|
return c.json(r, 400);
|
|
12348
12397
|
const m = r.message;
|
|
12349
|
-
const
|
|
12350
|
-
if (
|
|
12351
|
-
const sent = runner.send(
|
|
12398
|
+
const run = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
|
|
12399
|
+
if (run && !run.endedAt) {
|
|
12400
|
+
const sent = runner.send(run.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
|
|
12352
12401
|
if (sent.ok)
|
|
12353
|
-
store.markMessageDelivered(m.id,
|
|
12402
|
+
store.markMessageDelivered(m.id, run.sessionId);
|
|
12354
12403
|
}
|
|
12355
12404
|
return c.json({ ok: true, message: store.message(m.id) }, 201);
|
|
12356
12405
|
});
|
|
@@ -12558,7 +12607,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12558
12607
|
const d = await store.prDraftFor(b.projectId, ref);
|
|
12559
12608
|
if (!d.ok)
|
|
12560
12609
|
return c.json(d, 404);
|
|
12561
|
-
const r = await
|
|
12610
|
+
const r = await forge.openPR(b.projectId, d.worktree, {
|
|
12562
12611
|
title: b.title?.trim() || d.title,
|
|
12563
12612
|
body: b.body ?? d.body,
|
|
12564
12613
|
isDraft: b.draft ?? false
|
|
@@ -12629,21 +12678,21 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12629
12678
|
app.post("/v1/sessions/:id/tail", (c) => c.json({ turns: store.tailSession(c.req.param("id")) }));
|
|
12630
12679
|
app.post("/v1/hook/:event", async (c) => {
|
|
12631
12680
|
const event = c.req.param("event");
|
|
12632
|
-
const
|
|
12633
|
-
store.ingestHook(event,
|
|
12634
|
-
if (event === "SessionStart" && typeof
|
|
12635
|
-
store.checkPolicy(
|
|
12636
|
-
const ctx = store.sessionContext(
|
|
12681
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
12682
|
+
store.ingestHook(event, raw);
|
|
12683
|
+
if (event === "SessionStart" && typeof raw.cwd === "string") {
|
|
12684
|
+
store.checkPolicy(raw.cwd, typeof raw.session_id === "string" ? raw.session_id : null);
|
|
12685
|
+
const ctx = store.sessionContext(raw.cwd);
|
|
12637
12686
|
if (ctx)
|
|
12638
12687
|
return c.json({
|
|
12639
12688
|
additionalContext: ctx,
|
|
12640
12689
|
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: ctx }
|
|
12641
12690
|
});
|
|
12642
12691
|
}
|
|
12643
|
-
const sid = typeof
|
|
12692
|
+
const sid = typeof raw.session_id === "string" ? raw.session_id : null;
|
|
12644
12693
|
const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
|
|
12645
|
-
if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store,
|
|
12646
|
-
const guard = store.guardHook(
|
|
12694
|
+
if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw))) {
|
|
12695
|
+
const guard = store.guardHook(raw);
|
|
12647
12696
|
if (guard) {
|
|
12648
12697
|
return c.json({
|
|
12649
12698
|
hookSpecificOutput: {
|
|
@@ -12668,22 +12717,22 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12668
12717
|
});
|
|
12669
12718
|
app.get("/v1/events", (c) => {
|
|
12670
12719
|
const full = c.req.query("full") === "1";
|
|
12671
|
-
const
|
|
12672
|
-
const since =
|
|
12673
|
-
return streamSSE(c, async (
|
|
12720
|
+
const raw = Number(c.req.query("since") ?? 0);
|
|
12721
|
+
const since = raw > 0 ? raw : Math.max(0, store.seq() - REPLAY_TAIL);
|
|
12722
|
+
return streamSSE(c, async (stream) => {
|
|
12674
12723
|
for (const e of store.since(since, 5000, full)) {
|
|
12675
|
-
await
|
|
12724
|
+
await stream.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
|
|
12676
12725
|
}
|
|
12677
|
-
await
|
|
12678
|
-
await new Promise((
|
|
12726
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
12727
|
+
await new Promise((resolve) => {
|
|
12679
12728
|
const off = store.subscribe((e) => {
|
|
12680
|
-
|
|
12729
|
+
stream.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
|
|
12681
12730
|
});
|
|
12682
|
-
const beat = setInterval(() => void
|
|
12683
|
-
|
|
12731
|
+
const beat = setInterval(() => void stream.writeSSE({ event: "ping", data: "" }), 15000);
|
|
12732
|
+
stream.onAbort(() => {
|
|
12684
12733
|
clearInterval(beat);
|
|
12685
12734
|
off();
|
|
12686
|
-
|
|
12735
|
+
resolve();
|
|
12687
12736
|
});
|
|
12688
12737
|
});
|
|
12689
12738
|
});
|
|
@@ -12714,7 +12763,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
12714
12763
|
etag
|
|
12715
12764
|
});
|
|
12716
12765
|
});
|
|
12717
|
-
return { app, store, forge
|
|
12766
|
+
return { app, store, forge, runner, dispatcher, workflows, team };
|
|
12718
12767
|
}
|
|
12719
12768
|
|
|
12720
12769
|
// packages/daemon/src/demo.ts
|
|
@@ -12751,8 +12800,8 @@ function seedDemo(store) {
|
|
|
12751
12800
|
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));
|
|
12752
12801
|
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
12753
12802
|
VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
|
|
12754
|
-
const gate = (task,
|
|
12755
|
-
VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task,
|
|
12803
|
+
const gate = (task, gate, 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)
|
|
12804
|
+
VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate, verdict, rubric, sid, iso(ago));
|
|
12756
12805
|
gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
|
|
12757
12806
|
gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
|
|
12758
12807
|
gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
|