cubefarm 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -91
- package/bin/cubefarm.js +2 -1
- package/dist/assets/ToyWorld-CrlIe2hV.js +2 -0
- package/dist/assets/index-CazA79UU.css +1 -0
- package/dist/assets/index-DDIwWVME.js +4316 -0
- package/dist/assets/rapier-B1_J71Lw.js +1 -0
- package/dist/index.html +2 -2
- package/dist-server/index.js +674 -135
- package/dist-server/index.js.map +4 -4
- package/package.json +78 -79
- package/dist/assets/ToyWorld-Bj5kLnpv.js +0 -2
- package/dist/assets/index-8tdBZH9c.css +0 -1
- package/dist/assets/index-uo74me0U.js +0 -4316
- package/dist/assets/rapier-DO_XuNdK.js +0 -1
package/dist-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path8 from "node:path";
|
|
4
|
+
import fs6 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
|
|
@@ -154,6 +154,25 @@ async function createIssue(fullName, title, body, labels = []) {
|
|
|
154
154
|
if (!match) throw new Error(`Could not read the new issue number from gh output: ${out}`);
|
|
155
155
|
return Number(match[1]);
|
|
156
156
|
}
|
|
157
|
+
async function issueState(fullName, number) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = await ghJson(["issue", "view", String(number), "-R", fullName, "--json", "state"]);
|
|
160
|
+
return raw.state === "OPEN" ? "OPEN" : "CLOSED";
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function editIssue(fullName, number, edit) {
|
|
166
|
+
const args = ["issue", "edit", String(number), "-R", fullName];
|
|
167
|
+
if (edit.body !== void 0) args.push("--body-file", "-");
|
|
168
|
+
for (const l of edit.addLabels ?? []) {
|
|
169
|
+
await ensureLabel(fullName, l);
|
|
170
|
+
args.push("--add-label", l);
|
|
171
|
+
}
|
|
172
|
+
for (const l of edit.removeLabels ?? []) args.push("--remove-label", l);
|
|
173
|
+
if (args.length === 5) return;
|
|
174
|
+
await gh(args, edit.body !== void 0 ? { input: edit.body || " " } : void 0);
|
|
175
|
+
}
|
|
157
176
|
async function mergePull(fullName, number, method, headSha) {
|
|
158
177
|
const pr = await ghJson(["pr", "view", String(number), "-R", fullName, "--json", "headRefName,isCrossRepository"]);
|
|
159
178
|
await gh(["pr", "merge", String(number), "-R", fullName, `--${method}`, ...headSha ? ["--match-head-commit", headSha] : []], { timeoutMs: 6e4 });
|
|
@@ -282,37 +301,40 @@ async function syncMain(fullName, defaultBranch, opts) {
|
|
|
282
301
|
const result = await withRepoLock(fullName, async () => {
|
|
283
302
|
if (!await exists(path2.join(dir, ".git"))) return null;
|
|
284
303
|
const g = (args, timeoutMs) => git(args, { cwd: dir, timeoutMs });
|
|
304
|
+
let behind = 0;
|
|
285
305
|
try {
|
|
286
306
|
await g(["fetch", "origin", "--prune"], 18e4);
|
|
287
307
|
const target = `origin/${defaultBranch}`;
|
|
288
|
-
|
|
308
|
+
behind = Number(await g(["rev-list", "--count", `HEAD..${target}`]));
|
|
289
309
|
const commits = `${behind} commit${behind === 1 ? "" : "s"}`;
|
|
290
310
|
const branch = await g(["symbolic-ref", "--short", "-q", "HEAD"]).catch(() => "");
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
295
|
-
if (
|
|
311
|
+
const stays = (status) => ({ status, behind, updatable: false });
|
|
312
|
+
if (branch !== defaultBranch) return stays(`on ${branch ? `branch ${branch}` : "a detached HEAD"}${behind ? ` (${defaultBranch} is ${commits} ahead)` : ""}`);
|
|
313
|
+
if (behind === 0) return stays("in sync");
|
|
314
|
+
if (Number(await g(["rev-list", "--count", `${target}..HEAD`])) > 0) return stays(`diverged: local commits, and ${commits} to pull`);
|
|
315
|
+
if (!opts.touch) return { status: `update ready (${commits})`, behind, updatable: true };
|
|
316
|
+
if (await g(["status", "--porcelain", "--untracked-files=no"])) return { status: `${behind} behind: local changes`, behind, updatable: true };
|
|
296
317
|
const before = await g(["rev-parse", "HEAD"]);
|
|
297
318
|
try {
|
|
298
319
|
await g(["merge", "--ff-only", target], 12e4);
|
|
299
320
|
} catch {
|
|
300
|
-
return { status: `${behind} behind: local files are in the way
|
|
321
|
+
return { status: `${behind} behind: local files are in the way`, behind, updatable: true };
|
|
301
322
|
}
|
|
302
323
|
const changed = (await g(["diff", "--name-only", before, "HEAD"])).split(/\r?\n/);
|
|
303
324
|
const install = changed.some((f) => f === "package.json" || f === "package-lock.json") && await exists(path2.join(dir, "package.json"));
|
|
304
|
-
return { status: `updated to ${await g(["rev-parse", "--short", "HEAD"])}`, install };
|
|
325
|
+
return { status: `updated to ${await g(["rev-parse", "--short", "HEAD"])}`, behind: 0, updatable: false, install };
|
|
305
326
|
} catch (err) {
|
|
306
|
-
return { status: `sync failed: ${err.message.split(/\r?\n/)[0].slice(0, 160)}
|
|
327
|
+
return { status: `sync failed: ${err.message.split(/\r?\n/)[0].slice(0, 160)}`, behind, updatable: false };
|
|
307
328
|
}
|
|
308
329
|
});
|
|
309
|
-
if (!result?.install) return result
|
|
330
|
+
if (!result?.install) return result;
|
|
331
|
+
const { install: _install, ...sync } = result;
|
|
310
332
|
try {
|
|
311
333
|
await npmInstall(dir);
|
|
312
|
-
return `${
|
|
334
|
+
return { ...sync, status: `${sync.status} \xB7 dependencies installed` };
|
|
313
335
|
} catch (err) {
|
|
314
336
|
console.warn(`npm install in ${dir} failed:`, err.message);
|
|
315
|
-
return `${
|
|
337
|
+
return { ...sync, status: `${sync.status} \xB7 npm install failed` };
|
|
316
338
|
}
|
|
317
339
|
}
|
|
318
340
|
function githubFromConfig(config) {
|
|
@@ -632,6 +654,8 @@ function describeOfficeTool(action, input) {
|
|
|
632
654
|
return `\u{1F44B} propose_let_go ${String(input.agent_id ?? "")}`;
|
|
633
655
|
case "file_issue":
|
|
634
656
|
return `\u{1F4DD} file_issue "${clip(String(input.title ?? ""), 70)}"${floor}${input.specialty ? ` \xB7 ${input.specialty}` : ""}`;
|
|
657
|
+
case "route_issue":
|
|
658
|
+
return `\u{1F500} route_issue #${String(input.number ?? "?")}${floor}${input.specialty !== void 0 ? ` \xB7 ${input.specialty || "no specialty"}` : ""}${Array.isArray(input.depends_on) ? ` \xB7 depends on ${input.depends_on.map((n) => `#${n}`).join(", ") || "nothing"}` : ""}`;
|
|
635
659
|
}
|
|
636
660
|
return `\u{1F3E2} ${action}(${clip(JSON.stringify(input), 120)})`;
|
|
637
661
|
}
|
|
@@ -764,6 +788,9 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
764
788
|
cb.log([{ kind: "error", text: `\u26A0 Subscription usage ${info.status === "rejected" ? "limit reached" : "warning"} (${info.rateLimitType ?? "limit"}) \xB7 resets ${when}` }]);
|
|
765
789
|
const overage = info.isUsingOverage || info.overageStatus === "allowed" || info.overageStatus === "allowed_warning";
|
|
766
790
|
if (info.status === "rejected" && !overage) cb.limited?.(info.resetsAt ? info.resetsAt * 1e3 : null);
|
|
791
|
+
if (info.status === "allowed_warning" && !overage) {
|
|
792
|
+
cb.usageWarning?.({ resetsAt: info.resetsAt ? info.resetsAt * 1e3 : null, rateLimitType: info.rateLimitType ?? null, utilization: info.utilization ?? null });
|
|
793
|
+
}
|
|
767
794
|
}
|
|
768
795
|
break;
|
|
769
796
|
}
|
|
@@ -1051,6 +1078,71 @@ var realPreviews = {
|
|
|
1051
1078
|
hasDefault: async (fullName) => fs3.access(path4.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
1052
1079
|
};
|
|
1053
1080
|
|
|
1081
|
+
// server/officeUpdate.ts
|
|
1082
|
+
import fs4 from "node:fs/promises";
|
|
1083
|
+
import path5 from "node:path";
|
|
1084
|
+
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
1085
|
+
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
1086
|
+
function underLauncher(proc = process) {
|
|
1087
|
+
return typeof proc.send === "function" && proc.env.SWARM_LAUNCHER === "1";
|
|
1088
|
+
}
|
|
1089
|
+
function drainDecision(s) {
|
|
1090
|
+
const idle = (state) => ({ state, drainingSince: null, send: false, timedOut: false });
|
|
1091
|
+
if (s.sent) return { state: "updating", drainingSince: s.drainingSince, send: false, timedOut: false };
|
|
1092
|
+
if (s.behind <= 0) return idle("none");
|
|
1093
|
+
if (!s.launcher) return idle(s.failed ? "failed" : "available");
|
|
1094
|
+
const postponed = s.postponedUntil !== null && s.now < s.postponedUntil && s.behind <= s.postponedBehind;
|
|
1095
|
+
const failedHold = s.failed !== null && s.behind <= (s.failedBehind ?? Infinity);
|
|
1096
|
+
const due = s.requested || s.autoUpdate && !postponed && !failedHold;
|
|
1097
|
+
if (!due) return idle(s.failed ? "failed" : s.autoUpdate && postponed ? "waiting" : "available");
|
|
1098
|
+
const since = s.drainingSince ?? s.now;
|
|
1099
|
+
const timedOut = s.running > 0 && s.now - since >= DRAIN_TIMEOUT_MS;
|
|
1100
|
+
return { state: "draining", drainingSince: since, send: s.running === 0 || timedOut, timedOut };
|
|
1101
|
+
}
|
|
1102
|
+
function parseLastUpdate(raw) {
|
|
1103
|
+
try {
|
|
1104
|
+
const u = JSON.parse(raw);
|
|
1105
|
+
if (!u || typeof u.from !== "string" || typeof u.ok !== "boolean") return null;
|
|
1106
|
+
return {
|
|
1107
|
+
from: u.from,
|
|
1108
|
+
to: typeof u.to === "string" ? u.to : "",
|
|
1109
|
+
ok: u.ok,
|
|
1110
|
+
error: typeof u.error === "string" ? u.error : void 0,
|
|
1111
|
+
installed: !!u.installed,
|
|
1112
|
+
built: !!u.built,
|
|
1113
|
+
at: Number(u.at) || Date.now()
|
|
1114
|
+
};
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function takeLastUpdate(home) {
|
|
1120
|
+
const file = path5.join(home, "last-update.json");
|
|
1121
|
+
const raw = await fs4.readFile(file, "utf8").catch(() => null);
|
|
1122
|
+
if (raw === null) return null;
|
|
1123
|
+
await fs4.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
1124
|
+
return parseLastUpdate(raw);
|
|
1125
|
+
}
|
|
1126
|
+
function lastUpdateMessage(u, commits) {
|
|
1127
|
+
if (!u.ok) return `\u26A0\uFE0F Update failed and was rolled back: ${u.error || "unknown error"}`;
|
|
1128
|
+
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
1129
|
+
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
1130
|
+
}
|
|
1131
|
+
var OFFICE_DIR = path5.resolve(import.meta.dirname, "..");
|
|
1132
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path5.resolve(a).toLowerCase() === path5.resolve(b).toLowerCase() : path5.resolve(a) === path5.resolve(b);
|
|
1133
|
+
var realOffice = {
|
|
1134
|
+
launcher: underLauncher(),
|
|
1135
|
+
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
1136
|
+
isOwnFolder: (dir) => isSamePath(dir, OFFICE_DIR),
|
|
1137
|
+
commitsBetween: (from, to) => git(["rev-list", "--count", `${from}..${to}`], { cwd: OFFICE_DIR }).then(Number).catch(() => null),
|
|
1138
|
+
takeLastUpdate: () => takeLastUpdate(HOME_DIR),
|
|
1139
|
+
async update(from) {
|
|
1140
|
+
if (!underLauncher()) throw new Error("The office was not started by the launcher");
|
|
1141
|
+
await new Promise((resolve, reject) => process.send({ type: "office:update", from }, void 0, {}, (err) => err ? reject(err) : resolve()));
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1054
1146
|
// server/backend.ts
|
|
1055
1147
|
var realBackend = {
|
|
1056
1148
|
demo: false,
|
|
@@ -1060,6 +1152,8 @@ var realBackend = {
|
|
|
1060
1152
|
listIssues,
|
|
1061
1153
|
listPulls,
|
|
1062
1154
|
createIssue,
|
|
1155
|
+
issueState,
|
|
1156
|
+
editIssue,
|
|
1063
1157
|
mergePull,
|
|
1064
1158
|
updateBranch,
|
|
1065
1159
|
closePull,
|
|
@@ -1081,13 +1175,14 @@ var realBackend = {
|
|
|
1081
1175
|
removeDesk,
|
|
1082
1176
|
releaseDesk,
|
|
1083
1177
|
startSession,
|
|
1084
|
-
previews: realPreviews
|
|
1178
|
+
previews: realPreviews,
|
|
1179
|
+
office: realOffice
|
|
1085
1180
|
};
|
|
1086
1181
|
|
|
1087
1182
|
// server/demo.ts
|
|
1088
1183
|
import crypto2 from "node:crypto";
|
|
1089
1184
|
import http from "node:http";
|
|
1090
|
-
import
|
|
1185
|
+
import path6 from "node:path";
|
|
1091
1186
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1092
1187
|
function issue(n, title, body, fullName, labels = []) {
|
|
1093
1188
|
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
@@ -1095,6 +1190,7 @@ function issue(n, title, body, fullName, labels = []) {
|
|
|
1095
1190
|
var repos = /* @__PURE__ */ new Map();
|
|
1096
1191
|
var bareRepos = /* @__PURE__ */ new Set();
|
|
1097
1192
|
var mergedSinceSync = /* @__PURE__ */ new Map();
|
|
1193
|
+
var closedIssues = /* @__PURE__ */ new Set();
|
|
1098
1194
|
var fakeSha = () => crypto2.randomBytes(20).toString("hex");
|
|
1099
1195
|
function runChecks(pr, fail = Math.random() < 0.2) {
|
|
1100
1196
|
Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
|
|
@@ -1139,7 +1235,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
1139
1235
|
</svg>`;
|
|
1140
1236
|
}
|
|
1141
1237
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
1142
|
-
const branch =
|
|
1238
|
+
const branch = path6.basename(opts.cwd);
|
|
1143
1239
|
const hue = issueNumber * 67 % 360;
|
|
1144
1240
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
1145
1241
|
const port = 5200 + issueNumber % 50;
|
|
@@ -1277,7 +1373,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
1277
1373
|
number: n,
|
|
1278
1374
|
title,
|
|
1279
1375
|
url,
|
|
1280
|
-
headRefName: `swarm/issue-${number}-${
|
|
1376
|
+
headRefName: `swarm/issue-${number}-${path6.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
1281
1377
|
state: "OPEN",
|
|
1282
1378
|
isDraft: false,
|
|
1283
1379
|
mergeable: "MERGEABLE",
|
|
@@ -1329,6 +1425,16 @@ function fakeSession(opts, cb, fullName) {
|
|
|
1329
1425
|
}
|
|
1330
1426
|
};
|
|
1331
1427
|
}
|
|
1428
|
+
var USAGE_WARNING_AT = 4;
|
|
1429
|
+
var USAGE_WARNING_MS = 5 * 6e4;
|
|
1430
|
+
var sessionsStarted = 0;
|
|
1431
|
+
function fakeUsageWarning(cb) {
|
|
1432
|
+
setTimeout(() => {
|
|
1433
|
+
const resetsAt = Date.now() + USAGE_WARNING_MS;
|
|
1434
|
+
cb.log([{ kind: "error", text: `\u26A0 Subscription usage warning (five_hour) \xB7 resets ${new Date(resetsAt).toLocaleTimeString()} (demo)` }]);
|
|
1435
|
+
cb.usageWarning?.({ resetsAt, rateLimitType: "five_hour", utilization: 0.82 });
|
|
1436
|
+
}, 3e3);
|
|
1437
|
+
}
|
|
1332
1438
|
function createDemoBackend() {
|
|
1333
1439
|
const deskRepo = /* @__PURE__ */ new Map();
|
|
1334
1440
|
const folders = /* @__PURE__ */ new Map();
|
|
@@ -1379,6 +1485,16 @@ function createDemoBackend() {
|
|
|
1379
1485
|
r.issues.push(issue(n, title, body, fullName, labels));
|
|
1380
1486
|
return n;
|
|
1381
1487
|
},
|
|
1488
|
+
issueState: async (fullName, number) => {
|
|
1489
|
+
const r = repos.get(fullName);
|
|
1490
|
+
return r?.issues.some((i) => i.number === number) ? "OPEN" : closedIssues.has(`${fullName}#${number}`) ? "CLOSED" : null;
|
|
1491
|
+
},
|
|
1492
|
+
editIssue: async (fullName, number, edit) => {
|
|
1493
|
+
const i = repos.get(fullName)?.issues.find((x) => x.number === number);
|
|
1494
|
+
if (!i) throw new Error(`Unknown issue #${number}`);
|
|
1495
|
+
if (edit.body !== void 0) i.body = edit.body;
|
|
1496
|
+
i.labels = [...i.labels.filter((l) => !edit.removeLabels?.includes(l)), ...(edit.addLabels ?? []).filter((l) => !i.labels.includes(l))];
|
|
1497
|
+
},
|
|
1382
1498
|
mergePull: async (fullName, number, _method, headSha) => {
|
|
1383
1499
|
const r = repos.get(fullName);
|
|
1384
1500
|
const pr = r?.pulls.find((p) => p.number === number);
|
|
@@ -1387,6 +1503,7 @@ function createDemoBackend() {
|
|
|
1387
1503
|
mergedSinceSync.set(fullName, (mergedSinceSync.get(fullName) ?? 0) + 1);
|
|
1388
1504
|
pr.state = "MERGED";
|
|
1389
1505
|
pr.mergedAt = now();
|
|
1506
|
+
for (const n of pr.closesIssues) closedIssues.add(`${fullName}#${n}`);
|
|
1390
1507
|
r.issues = r.issues.filter((i) => !pr.closesIssues.includes(i.number));
|
|
1391
1508
|
},
|
|
1392
1509
|
updateBranch: async (fullName, number) => {
|
|
@@ -1428,10 +1545,10 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
1428
1545
|
ensureClone: async () => new Promise((r) => setTimeout(r, 400)),
|
|
1429
1546
|
syncMain: async (fullName, _branch, { touch }) => {
|
|
1430
1547
|
const behind = mergedSinceSync.get(fullName) ?? 0;
|
|
1431
|
-
if (behind === 0) return "in sync";
|
|
1432
|
-
if (!touch) return `update ready (${behind} commit${behind === 1 ? "" : "s"})
|
|
1548
|
+
if (behind === 0) return { status: "in sync", behind: 0, updatable: false };
|
|
1549
|
+
if (!touch) return { status: `update ready (${behind} commit${behind === 1 ? "" : "s"})`, behind, updatable: true };
|
|
1433
1550
|
mergedSinceSync.set(fullName, 0);
|
|
1434
|
-
return `updated to ${fakeSha().slice(0, 7)}
|
|
1551
|
+
return { status: `updated to ${fakeSha().slice(0, 7)}`, behind: 0, updatable: false };
|
|
1435
1552
|
},
|
|
1436
1553
|
mainDir: (fullName) => `/demo/${fullName}/main`,
|
|
1437
1554
|
deskDir: (fullName, slug) => `/demo/${fullName}/desks/${slug}`,
|
|
@@ -1443,10 +1560,32 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
1443
1560
|
},
|
|
1444
1561
|
removeDesk: async () => void 0,
|
|
1445
1562
|
releaseDesk: async () => void 0,
|
|
1446
|
-
startSession: (opts, cb) =>
|
|
1447
|
-
|
|
1563
|
+
startSession: (opts, cb) => {
|
|
1564
|
+
if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
|
|
1565
|
+
return opts.role === "ceo" ? ceoSession(opts, cb) : fakeSession(opts, cb, deskRepo.get(opts.cwd) ?? [...repos.keys()][0]);
|
|
1566
|
+
},
|
|
1567
|
+
previews: demoPreviews,
|
|
1568
|
+
office: demoOffice
|
|
1448
1569
|
};
|
|
1449
1570
|
}
|
|
1571
|
+
var OFFICE_REPO = "demo-co/pixel-todo";
|
|
1572
|
+
var DEMO_HEAD = `0ff1ce5${"0".repeat(33)}`;
|
|
1573
|
+
var lastFakeUpdate = { to: "", commits: 0 };
|
|
1574
|
+
var demoOffice = {
|
|
1575
|
+
launcher: underLauncher() || process.env.SWARM_DEMO_LAUNCHER === "1",
|
|
1576
|
+
head: async () => DEMO_HEAD,
|
|
1577
|
+
isOwnFolder: (dir) => dir === `/demo/${OFFICE_REPO}/main`,
|
|
1578
|
+
// Only its own fake updates have a count; one the real launcher did (u) gets no count rather than a wrong one.
|
|
1579
|
+
commitsBetween: async (_from, to) => to === lastFakeUpdate.to ? lastFakeUpdate.commits : null,
|
|
1580
|
+
takeLastUpdate: () => takeLastUpdate(HOME_DIR),
|
|
1581
|
+
async update(from) {
|
|
1582
|
+
await new Promise((r) => setTimeout(r, 4e3));
|
|
1583
|
+
lastFakeUpdate.commits = mergedSinceSync.get(OFFICE_REPO) ?? 0;
|
|
1584
|
+
mergedSinceSync.set(OFFICE_REPO, 0);
|
|
1585
|
+
lastFakeUpdate.to = fakeSha();
|
|
1586
|
+
return { from, to: lastFakeUpdate.to, ok: true, installed: true, built: true, at: Date.now() };
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1450
1589
|
function placeholderPage(title, hue) {
|
|
1451
1590
|
const safe = title.replace(/[<>&"]/g, "");
|
|
1452
1591
|
return `<!doctype html>
|
|
@@ -1716,6 +1855,13 @@ Acceptance criteria:
|
|
|
1716
1855
|
return `Floor ${f.floor} is overstaffed: ${devs} developers for ${f.backlog.length} open issues. I suggest letting ${idle[idle.length - 1].name} go; it's on your phone.`;
|
|
1717
1856
|
}
|
|
1718
1857
|
}
|
|
1858
|
+
for (const f of s.floors) {
|
|
1859
|
+
const specialist = f.team.find((a) => a.role === "dev" && a.specialty);
|
|
1860
|
+
const unrouted = f.backlog.find((i) => !i.specialty && !i.inProgress);
|
|
1861
|
+
if (!specialist || !unrouted) continue;
|
|
1862
|
+
const out = await use("route_issue", { floor: f.floor, number: unrouted.number, specialty: specialist.specialty });
|
|
1863
|
+
if (!out.startsWith("Refused")) return `Floor ${f.floor}: #${unrouted.number} had no specialty, so I routed it to ${specialist.specialty}, ${specialist.name}'s lane.`;
|
|
1864
|
+
}
|
|
1719
1865
|
const issues = s.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
1720
1866
|
const prs = s.floors.reduce((n, f) => n + f.pullRequests.length, 0);
|
|
1721
1867
|
return `All ${s.floors.length} floors look healthy: ${issues} open issues and ${prs} pull requests in flight. No changes needed.`;
|
|
@@ -1794,13 +1940,80 @@ Acceptance criteria:
|
|
|
1794
1940
|
}
|
|
1795
1941
|
|
|
1796
1942
|
// server/swarm.ts
|
|
1797
|
-
import
|
|
1798
|
-
import
|
|
1943
|
+
import fs5 from "node:fs/promises";
|
|
1944
|
+
import path7 from "node:path";
|
|
1799
1945
|
import crypto3 from "node:crypto";
|
|
1800
1946
|
|
|
1801
1947
|
// server/ceo.ts
|
|
1802
1948
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
1803
1949
|
import { z } from "zod";
|
|
1950
|
+
|
|
1951
|
+
// shared/issues.ts
|
|
1952
|
+
function issueSpecialty(labels) {
|
|
1953
|
+
const l = labels.find((x) => /^swarm:/i.test(x) && !/^swarm:skip$/i.test(x));
|
|
1954
|
+
return l ? l.slice(6).toLowerCase() : "";
|
|
1955
|
+
}
|
|
1956
|
+
function blockers(body, open) {
|
|
1957
|
+
const out = /* @__PURE__ */ new Set();
|
|
1958
|
+
for (const m of (body ?? "").matchAll(/\b(?:depends\s+on|blocked\s+by)\s*:?\s*((?:#\d+(?:\s*(?:,|and|&)\s*)?)+)/gi)) {
|
|
1959
|
+
for (const n of m[1].matchAll(/#(\d+)/g)) if (open.has(Number(n[1]))) out.add(Number(n[1]));
|
|
1960
|
+
}
|
|
1961
|
+
return [...out];
|
|
1962
|
+
}
|
|
1963
|
+
var DEPENDENCY = /\b(?:depends\s+on|blocked\s+by)\s*:?\s*(?:#\d+(?:\s*(?:,|and|&)\s*)?)+/gi;
|
|
1964
|
+
var STATEMENT = new RegExp(`\\s*${DEPENDENCY.source}[.;,]?`, "gi");
|
|
1965
|
+
function setDependsOn(body, deps) {
|
|
1966
|
+
const lines = [];
|
|
1967
|
+
let dropped = false;
|
|
1968
|
+
for (const line of (body ?? "").split(/\r?\n/)) {
|
|
1969
|
+
const rest2 = line.match(DEPENDENCY) ? line.replace(STATEMENT, "").trimEnd() : line;
|
|
1970
|
+
if (rest2 !== line && !/[\p{L}\p{N}]/u.test(rest2)) {
|
|
1971
|
+
dropped = true;
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
if (dropped && !rest2.trim() && !lines[lines.length - 1]?.trim()) continue;
|
|
1975
|
+
dropped = false;
|
|
1976
|
+
lines.push(rest2);
|
|
1977
|
+
}
|
|
1978
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
1979
|
+
const rest = lines.join("\n");
|
|
1980
|
+
if (deps.length === 0) return rest;
|
|
1981
|
+
return `Depends on ${deps.map((n) => `#${n}`).join(", ")}${rest ? `
|
|
1982
|
+
|
|
1983
|
+
${rest}` : ""}`;
|
|
1984
|
+
}
|
|
1985
|
+
function holdUps(issues) {
|
|
1986
|
+
const open = new Set(issues.map((i) => i.number));
|
|
1987
|
+
const waiters = /* @__PURE__ */ new Map();
|
|
1988
|
+
for (const i of issues) for (const b of blockers(i.body, open)) waiters.set(b, [...waiters.get(b) ?? [], i.number]);
|
|
1989
|
+
const chains = /* @__PURE__ */ new Map();
|
|
1990
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1991
|
+
const chain = (n) => {
|
|
1992
|
+
const known = chains.get(n);
|
|
1993
|
+
if (known !== void 0) return known;
|
|
1994
|
+
if (visiting.has(n)) return 0;
|
|
1995
|
+
visiting.add(n);
|
|
1996
|
+
const longest = Math.max(0, ...(waiters.get(n) ?? []).map((w) => chain(w) + 1));
|
|
1997
|
+
visiting.delete(n);
|
|
1998
|
+
chains.set(n, longest);
|
|
1999
|
+
return longest;
|
|
2000
|
+
};
|
|
2001
|
+
const out = /* @__PURE__ */ new Map();
|
|
2002
|
+
for (const i of issues) {
|
|
2003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2004
|
+
const stack = [...waiters.get(i.number) ?? []];
|
|
2005
|
+
while (stack.length) {
|
|
2006
|
+
const n = stack.pop();
|
|
2007
|
+
if (n === i.number || seen.has(n)) continue;
|
|
2008
|
+
seen.add(n);
|
|
2009
|
+
stack.push(...waiters.get(n) ?? []);
|
|
2010
|
+
}
|
|
2011
|
+
out.set(i.number, { chain: chain(i.number), waiting: seen.size });
|
|
2012
|
+
}
|
|
2013
|
+
return out;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// server/ceo.ts
|
|
1804
2017
|
var EFFORT = z.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
1805
2018
|
function createOfficeTools(h) {
|
|
1806
2019
|
const run2 = async (fn) => {
|
|
@@ -1880,6 +2093,17 @@ function createOfficeTools(h) {
|
|
|
1880
2093
|
specialty: z.string().max(24).optional()
|
|
1881
2094
|
},
|
|
1882
2095
|
(a) => run2(() => h.fileIssue(a))
|
|
2096
|
+
),
|
|
2097
|
+
tool(
|
|
2098
|
+
"route_issue",
|
|
2099
|
+
'Fix the routing of an open issue instead of filing a duplicate: change its specialty, rewrite its "Depends on #N" line, or both. The rest of the body stays as it is.',
|
|
2100
|
+
{
|
|
2101
|
+
floor: z.number().int(),
|
|
2102
|
+
number: z.number().int().positive().describe("The issue number"),
|
|
2103
|
+
specialty: z.string().max(24).optional().describe('Sets swarm:<specialty> and removes any other; "" for none. Someone on the floor, or a pending proposal, must have it.'),
|
|
2104
|
+
depends_on: z.array(z.number().int().positive()).max(10).optional().describe("Issues it waits for; [] for none. Not for an issue in progress.")
|
|
2105
|
+
},
|
|
2106
|
+
(a) => run2(() => h.routeIssue(a))
|
|
1883
2107
|
)
|
|
1884
2108
|
];
|
|
1885
2109
|
const server2 = createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs });
|
|
@@ -1917,7 +2141,8 @@ function ceoSystemPrompt(o) {
|
|
|
1917
2141
|
'- Titles are specific ("Three.js graphics engineer", not "Developer"). A specialty is a short lowercase slug ("graphics", "gameplay", "frontend", "backend", "content", "a11y", "devops"). Only route an issue to a specialty that someone on the floor has, or that you are proposing to hire.',
|
|
1918
2142
|
"- Before proposing a hire, check the floor and the pending proposals for someone who already covers it. If the manager declined a similar proposal (recentDecisions), do not propose it again unless something has changed, and say what.",
|
|
1919
2143
|
`- ${o.hiring === "auto" ? "Hiring is on auto: proposals within the team cap are approved immediately, so be deliberate." : "The manager approves every hire, so explain each reason in a sentence or two they can decide on."}`,
|
|
1920
|
-
`- Issues: plan for parallel work. What keeps a floor busy is the number of issues that can start right now (capacity.issuesReadyToStart in company_status); aim for at least one per developer. Write "Depends on #N" only when an issue truly cannot start until #N's code is merged, because it waits until #N is closed. Keep dependency chains to two steps at most, keep foundation issues small, and split big pieces into parts that can be built side by side. The office starts the issues that hold up others first. Do not duplicate open issues. File at most 12 issues per job.`,
|
|
2144
|
+
`- Issues: plan for parallel work. What keeps a floor busy is the number of issues that can start right now (capacity.issuesReadyToStart in company_status); aim for at least one per developer. Write "Depends on #N" only when an issue truly cannot start until #N's code is merged, because it waits until #N is closed. Keep dependency chains to two steps at most, keep foundation issues small, and split big pieces into parts that can be built side by side. The office starts the issues that hold up others first. Do not duplicate open issues: fix an existing issue's specialty or dependencies with route_issue. File at most 12 issues per job, or per message from the manager.`,
|
|
2145
|
+
"- When company.usage in company_status says pacing or paused, Claude's usage is running low and the office is finishing open work first: file only what is needed next, not a whole milestone.",
|
|
1921
2146
|
"- Your final message goes straight to the manager's phone. Keep it short and plain: what you found, what you proposed, what you filed, and any question you need answered. No headings, no tables."
|
|
1922
2147
|
].join("\n");
|
|
1923
2148
|
}
|
|
@@ -1951,7 +2176,7 @@ ${floor.backlog === 0 ? "The backlog is empty: plan the first milestone as issue
|
|
|
1951
2176
|
return [
|
|
1952
2177
|
"Periodic review of the company. For every floor, look at:",
|
|
1953
2178
|
"- floors without a profile or QA brief: study them and write one",
|
|
1954
|
-
"- backlog against the team (capacity): fewer issues ready to start than free developers, long dependency chains, a specialty with a long queue",
|
|
2179
|
+
"- backlog against the team (capacity): fewer issues ready to start than free developers, long dependency chains, a specialty with a long queue (fix those with route_issue)",
|
|
1955
2180
|
"- pull requests stuck in QA or marked as needing a human",
|
|
1956
2181
|
"- floors with a brief and an empty backlog: plan the next milestone",
|
|
1957
2182
|
"Propose hires or let-gos only when clearly justified. If nothing needs doing, reply with one short sentence saying so."
|
|
@@ -1974,6 +2199,81 @@ function jobLabel(job, floor) {
|
|
|
1974
2199
|
return "Replying to you";
|
|
1975
2200
|
}
|
|
1976
2201
|
}
|
|
2202
|
+
var IssueCap = class {
|
|
2203
|
+
// floors that got issues, to refresh when the job ends
|
|
2204
|
+
constructor(max) {
|
|
2205
|
+
this.max = max;
|
|
2206
|
+
}
|
|
2207
|
+
max;
|
|
2208
|
+
filed = 0;
|
|
2209
|
+
// since the job started or the manager's last message
|
|
2210
|
+
total = 0;
|
|
2211
|
+
// in the whole job
|
|
2212
|
+
repos = /* @__PURE__ */ new Set();
|
|
2213
|
+
check() {
|
|
2214
|
+
if (this.filed >= this.max) throw new Error(`You already filed ${this.max} issues in this job. That's plenty for one milestone. The manager's next message allows more.`);
|
|
2215
|
+
}
|
|
2216
|
+
record(repoId2) {
|
|
2217
|
+
this.filed++;
|
|
2218
|
+
this.total++;
|
|
2219
|
+
this.repos.add(repoId2);
|
|
2220
|
+
}
|
|
2221
|
+
managerMessage() {
|
|
2222
|
+
this.filed = 0;
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
function waitsDepth(n, deps, seen = /* @__PURE__ */ new Set()) {
|
|
2226
|
+
if (seen.has(n)) return 0;
|
|
2227
|
+
seen.add(n);
|
|
2228
|
+
const depth = Math.max(0, ...(deps.get(n) ?? []).map((d) => waitsDepth(d, deps, seen) + 1));
|
|
2229
|
+
seen.delete(n);
|
|
2230
|
+
return depth;
|
|
2231
|
+
}
|
|
2232
|
+
function planRoute(r) {
|
|
2233
|
+
const issue2 = r.issues.find((i) => i.number === r.number);
|
|
2234
|
+
if (!issue2) throw new Error(r.closed(r.number) ? `#${r.number} is closed.` : `There is no open issue #${r.number} on floor ${r.floor}.`);
|
|
2235
|
+
if (r.specialty === void 0 && r.dependsOn === void 0) throw new Error("Nothing to change: pass specialty, depends_on or both.");
|
|
2236
|
+
const plan = { addLabels: [], removeLabels: [], body: null, summary: "" };
|
|
2237
|
+
const done = [];
|
|
2238
|
+
if (r.specialty !== void 0) {
|
|
2239
|
+
const slug = specialtySlug(r.specialty);
|
|
2240
|
+
if (r.specialty.trim() && !slug) throw new Error(`"${r.specialty}" is not a specialty. Use a short lowercase slug, or "" for none.`);
|
|
2241
|
+
if (slug && !r.specialties.includes(slug)) {
|
|
2242
|
+
const have = [...new Set(r.specialties)].join(", ") || "none";
|
|
2243
|
+
throw new Error(`Nobody on floor ${r.floor} has the specialty "${slug}", and no pending proposal does. Specialties there: ${have}.`);
|
|
2244
|
+
}
|
|
2245
|
+
const label = slug ? specialtyLabel(slug) : null;
|
|
2246
|
+
plan.removeLabels = issue2.labels.filter((l) => /^swarm:/i.test(l) && !/^swarm:skip$/i.test(l) && l !== label);
|
|
2247
|
+
if (label && !issue2.labels.includes(label)) plan.addLabels = [label];
|
|
2248
|
+
done.push(slug ? `routed to ${slug}` : "no specialty");
|
|
2249
|
+
}
|
|
2250
|
+
if (r.dependsOn !== void 0) {
|
|
2251
|
+
if (r.inProgress) throw new Error(`#${r.number} is already in progress, so its dependencies can't change. Changing its specialty is fine.`);
|
|
2252
|
+
const deps = [...new Set(r.dependsOn.map(Number))];
|
|
2253
|
+
const open = new Set(r.issues.map((i) => i.number));
|
|
2254
|
+
for (const d of deps) {
|
|
2255
|
+
if (d === r.number) throw new Error(`#${r.number} can't depend on itself.`);
|
|
2256
|
+
if (!open.has(d)) throw new Error(r.closed(d) ? `#${d} is closed, so there's nothing to wait for.` : `There is no open issue #${d} on floor ${r.floor}.`);
|
|
2257
|
+
}
|
|
2258
|
+
const body = setDependsOn(issue2.body, deps);
|
|
2259
|
+
const after = r.issues.map((i) => i.number === r.number ? { ...i, body } : i);
|
|
2260
|
+
const waits = new Map(after.map((i) => [i.number, blockers(i.body, open)]));
|
|
2261
|
+
const loop = deps.find((d) => reaches(d, r.number, waits));
|
|
2262
|
+
if (loop !== void 0) throw new Error(`#${loop} already waits for #${r.number}, directly or through other issues, so that would be a cycle.`);
|
|
2263
|
+
const depth = waitsDepth(r.number, waits) + (holdUps(after).get(r.number)?.chain ?? 0);
|
|
2264
|
+
if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: split the work so more of it can start side by side.`);
|
|
2265
|
+
if (body !== issue2.body) plan.body = body;
|
|
2266
|
+
done.push(deps.length ? `depends on ${deps.map((d) => `#${d}`).join(", ")}` : "no dependencies");
|
|
2267
|
+
}
|
|
2268
|
+
plan.summary = `#${r.number} on floor ${r.floor}: ${done.join(", ")}.`;
|
|
2269
|
+
return plan;
|
|
2270
|
+
}
|
|
2271
|
+
function reaches(from, to, waits, seen = /* @__PURE__ */ new Set()) {
|
|
2272
|
+
if (from === to) return true;
|
|
2273
|
+
if (seen.has(from)) return false;
|
|
2274
|
+
seen.add(from);
|
|
2275
|
+
return (waits.get(from) ?? []).some((n) => reaches(n, to, waits, seen));
|
|
2276
|
+
}
|
|
1977
2277
|
function specialtySlug(s) {
|
|
1978
2278
|
const slug = String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
1979
2279
|
return slug === "skip" ? "" : slug;
|
|
@@ -1989,6 +2289,38 @@ var HttpError = class extends Error {
|
|
|
1989
2289
|
status;
|
|
1990
2290
|
};
|
|
1991
2291
|
|
|
2292
|
+
// server/mergeGate.ts
|
|
2293
|
+
var MAX_MERGE_FIXES = 3;
|
|
2294
|
+
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
2295
|
+
var MERGE_RETRY_MS = 10 * 6e4;
|
|
2296
|
+
function mergeStep(pr, rec, now2, opts) {
|
|
2297
|
+
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
2298
|
+
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
2299
|
+
const set = {};
|
|
2300
|
+
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
2301
|
+
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
2302
|
+
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
2303
|
+
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
2304
|
+
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
2305
|
+
}
|
|
2306
|
+
if (pr.checks === "failing") {
|
|
2307
|
+
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
2308
|
+
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
2309
|
+
}
|
|
2310
|
+
if (pr.checks === "pending") {
|
|
2311
|
+
const since = rec.pendingSince ?? now2;
|
|
2312
|
+
if (rec.pendingSince == null) set.pendingSince = since;
|
|
2313
|
+
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
2314
|
+
if (alert) set.alerted = true;
|
|
2315
|
+
return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
|
|
2316
|
+
}
|
|
2317
|
+
set.pendingSince = null;
|
|
2318
|
+
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
2319
|
+
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
2320
|
+
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
2321
|
+
return { do: "merge", set };
|
|
2322
|
+
}
|
|
2323
|
+
|
|
1992
2324
|
// server/previews.ts
|
|
1993
2325
|
var PREVIEW_BASE_PORT = 6300;
|
|
1994
2326
|
var LOG_KEEP = 200;
|
|
@@ -2189,47 +2521,41 @@ var Previews = class {
|
|
|
2189
2521
|
}
|
|
2190
2522
|
};
|
|
2191
2523
|
|
|
2192
|
-
//
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2524
|
+
// server/pacing.ts
|
|
2525
|
+
var PACING_MS = 60 * 6e4;
|
|
2526
|
+
var DEFAULT_PACING_SESSIONS = 3;
|
|
2527
|
+
function mayStart(kind, c) {
|
|
2528
|
+
if (c.now < c.pausedUntil) return false;
|
|
2529
|
+
if (kind === "issue" && c.now < c.pacingUntil) return c.running < c.pacingSessions;
|
|
2530
|
+
return true;
|
|
2196
2531
|
}
|
|
2197
|
-
function
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
}
|
|
2202
|
-
return [...out];
|
|
2532
|
+
function usageView(c) {
|
|
2533
|
+
if (c.now < c.pausedUntil) return { state: "paused", until: c.pausedUntil };
|
|
2534
|
+
if (c.now < c.pacingUntil) return { state: "pacing", until: c.pacingUntil };
|
|
2535
|
+
return { state: "normal", until: null };
|
|
2203
2536
|
}
|
|
2204
|
-
function
|
|
2205
|
-
const
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
if (n === i.number || seen.has(n)) continue;
|
|
2227
|
-
seen.add(n);
|
|
2228
|
-
stack.push(...waiters.get(n) ?? []);
|
|
2229
|
-
}
|
|
2230
|
-
out.set(i.number, { chain: chain(i.number), waiting: seen.size });
|
|
2231
|
-
}
|
|
2232
|
-
return out;
|
|
2537
|
+
function clock(at, now2) {
|
|
2538
|
+
const d = new Date(at);
|
|
2539
|
+
const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2540
|
+
return d.toDateString() === new Date(now2).toDateString() ? hm : `${d.toLocaleDateString("en-US", { weekday: "short" })} ${hm}`;
|
|
2541
|
+
}
|
|
2542
|
+
function usageLabel(u, now2) {
|
|
2543
|
+
return u.state === "normal" || u.until === null ? "normal" : `${u.state} until ${clock(u.until, now2)}`;
|
|
2544
|
+
}
|
|
2545
|
+
function clampPacingSessions(v) {
|
|
2546
|
+
const n = Math.round(Number(v));
|
|
2547
|
+
return Number.isFinite(n) && n > 0 ? Math.min(32, n) : DEFAULT_PACING_SESSIONS;
|
|
2548
|
+
}
|
|
2549
|
+
var LIMIT_NAMES = {
|
|
2550
|
+
five_hour: "5-hour limit",
|
|
2551
|
+
seven_day: "weekly limit",
|
|
2552
|
+
seven_day_opus: "weekly Opus limit",
|
|
2553
|
+
seven_day_sonnet: "weekly Sonnet limit"
|
|
2554
|
+
};
|
|
2555
|
+
function pacingMessage(w, until, sessions, now2) {
|
|
2556
|
+
const pct = w.utilization === null ? null : Math.round(w.utilization <= 1 ? w.utilization * 100 : w.utilization);
|
|
2557
|
+
const what = [w.rateLimitType ? LIMIT_NAMES[w.rateLimitType] ?? w.rateLimitType.replace(/_/g, " ") : null, pct === null ? null : `${pct}%`].filter(Boolean).join(", ");
|
|
2558
|
+
return `\u{1F422} Claude's usage is getting high${what ? ` (${what})` : ""}. Until ${clock(until, now2)} the office finishes open work first and starts at most ${sessions} session${sessions === 1 ? "" : "s"} at a time.`;
|
|
2233
2559
|
}
|
|
2234
2560
|
|
|
2235
2561
|
// shared/types.ts
|
|
@@ -2279,8 +2605,8 @@ var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
2279
2605
|
var CEO_MODEL = "claude-opus-5-5";
|
|
2280
2606
|
var CEO_EFFORT = "xhigh";
|
|
2281
2607
|
var CEO_NAME = "Morgan";
|
|
2282
|
-
var CEO_DIR =
|
|
2283
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
2608
|
+
var CEO_DIR = path7.join(HOME_DIR, "ceo");
|
|
2609
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path7.resolve(import.meta.dirname, ".."));
|
|
2284
2610
|
var MAX_PENDING_REQUESTS = 8;
|
|
2285
2611
|
var MAX_ISSUES_PER_JOB = 12;
|
|
2286
2612
|
var KEEP_MESSAGES = 200;
|
|
@@ -2292,20 +2618,15 @@ var FREE = ["idle", "done"];
|
|
|
2292
2618
|
var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
2293
2619
|
var MAX_ISSUE_FAILURES = 2;
|
|
2294
2620
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
2295
|
-
var MAX_MERGE_FIXES = 3;
|
|
2296
|
-
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
2297
|
-
var MERGE_RETRY_MS = 10 * 6e4;
|
|
2298
|
-
var OFFICE_DIR = path6.resolve(import.meta.dirname, "..");
|
|
2299
|
-
var isSamePath = (a, b) => process.platform === "win32" ? path6.resolve(a).toLowerCase() === path6.resolve(b).toLowerCase() : path6.resolve(a) === path6.resolve(b);
|
|
2300
2621
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
2301
|
-
var SCREENS_DIR =
|
|
2622
|
+
var SCREENS_DIR = path7.join(HOME_DIR, "screens");
|
|
2302
2623
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
2303
|
-
var screenFile = (agentId, mime) =>
|
|
2624
|
+
var screenFile = (agentId, mime) => path7.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
2304
2625
|
async function loadScreen(agentId) {
|
|
2305
2626
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
2306
|
-
const file =
|
|
2627
|
+
const file = path7.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
2307
2628
|
try {
|
|
2308
|
-
const [data, stat] = await Promise.all([
|
|
2629
|
+
const [data, stat] = await Promise.all([fs5.readFile(file), fs5.stat(file)]);
|
|
2309
2630
|
return { data, mime, at: stat.mtimeMs };
|
|
2310
2631
|
} catch {
|
|
2311
2632
|
}
|
|
@@ -2313,7 +2634,7 @@ async function loadScreen(agentId) {
|
|
|
2313
2634
|
return null;
|
|
2314
2635
|
}
|
|
2315
2636
|
async function removeScreens(agentId) {
|
|
2316
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
2637
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs5.rm(path7.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
2317
2638
|
}
|
|
2318
2639
|
var QA_SCHEMA = {
|
|
2319
2640
|
type: "object",
|
|
@@ -2405,7 +2726,9 @@ var Swarm = class {
|
|
|
2405
2726
|
companyName: "",
|
|
2406
2727
|
projectsDir: DEFAULT_PROJECTS_DIR,
|
|
2407
2728
|
setupDone: false,
|
|
2408
|
-
tutorialStep: 0
|
|
2729
|
+
tutorialStep: 0,
|
|
2730
|
+
autoUpdate: true,
|
|
2731
|
+
pacingSessions: DEFAULT_PACING_SESSIONS
|
|
2409
2732
|
},
|
|
2410
2733
|
repos: [],
|
|
2411
2734
|
agents: [],
|
|
@@ -2427,10 +2750,11 @@ var Swarm = class {
|
|
|
2427
2750
|
updateJob: (a) => this.updateJob(a),
|
|
2428
2751
|
proposeHire: (a) => this.proposeHire(a),
|
|
2429
2752
|
proposeLetGo: (a) => this.proposeLetGo(a),
|
|
2430
|
-
fileIssue: (a) => this.fileIssue(a)
|
|
2753
|
+
fileIssue: (a) => this.fileIssue(a),
|
|
2754
|
+
routeIssue: (a) => this.routeIssue(a)
|
|
2431
2755
|
});
|
|
2432
2756
|
}
|
|
2433
|
-
ceoIssues =
|
|
2757
|
+
ceoIssues = new IssueCap(MAX_ISSUES_PER_JOB);
|
|
2434
2758
|
// issues filed during the current CEO job
|
|
2435
2759
|
messageSeq = 1;
|
|
2436
2760
|
agentRt = /* @__PURE__ */ new Map();
|
|
@@ -2442,10 +2766,25 @@ var Swarm = class {
|
|
|
2442
2766
|
flushTimer = null;
|
|
2443
2767
|
logSeq = 1;
|
|
2444
2768
|
previews;
|
|
2769
|
+
officeHead = null;
|
|
2770
|
+
// the commit the office runs (null: not a git checkout, so no self-update)
|
|
2771
|
+
officeUpdate = {
|
|
2772
|
+
behind: 0,
|
|
2773
|
+
requested: false,
|
|
2774
|
+
postponedUntil: null,
|
|
2775
|
+
postponedBehind: 0,
|
|
2776
|
+
failed: null,
|
|
2777
|
+
failedBehind: null,
|
|
2778
|
+
drainingSince: null,
|
|
2779
|
+
sent: false,
|
|
2780
|
+
handedOver: false
|
|
2781
|
+
// sessions cut off by the hand-over are the restarted office's to recover
|
|
2782
|
+
};
|
|
2783
|
+
lastOfficeView = "";
|
|
2445
2784
|
// ---------- lifecycle ----------
|
|
2446
2785
|
async init() {
|
|
2447
2786
|
try {
|
|
2448
|
-
const raw = await
|
|
2787
|
+
const raw = await fs5.readFile(STATE_FILE, "utf8");
|
|
2449
2788
|
const loaded = JSON.parse(raw);
|
|
2450
2789
|
this.state = {
|
|
2451
2790
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -2540,6 +2879,9 @@ var Swarm = class {
|
|
|
2540
2879
|
]);
|
|
2541
2880
|
for (const r of this.state.repos) void this.previews.refreshDefault(r);
|
|
2542
2881
|
this.recover(interrupted);
|
|
2882
|
+
this.officeHead = await this.backend.office.head();
|
|
2883
|
+
const updated = await this.backend.office.takeLastUpdate().catch(() => null);
|
|
2884
|
+
if (updated) await this.reportUpdate(updated);
|
|
2543
2885
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
|
|
2544
2886
|
setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
2545
2887
|
this.save();
|
|
@@ -2660,7 +3002,10 @@ var Swarm = class {
|
|
|
2660
3002
|
requests: this.state.requests,
|
|
2661
3003
|
ceo: this.ceoInfo(),
|
|
2662
3004
|
messages: this.state.messages.slice(-100),
|
|
2663
|
-
phoneReadAt: this.state.phoneReadAt
|
|
3005
|
+
phoneReadAt: this.state.phoneReadAt,
|
|
3006
|
+
usage: this.usageNow(),
|
|
3007
|
+
officeCommit: this.officeHead?.slice(0, 7) ?? null,
|
|
3008
|
+
officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
|
|
2664
3009
|
};
|
|
2665
3010
|
}
|
|
2666
3011
|
screenshot(agentId) {
|
|
@@ -2719,14 +3064,17 @@ var Swarm = class {
|
|
|
2719
3064
|
// ---------- persistence ----------
|
|
2720
3065
|
save() {
|
|
2721
3066
|
if (this.saveTimer) return;
|
|
2722
|
-
this.saveTimer = setTimeout(
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3067
|
+
this.saveTimer = setTimeout(() => void this.writeState().catch((err) => console.warn("could not save the state", err)), 1500);
|
|
3068
|
+
}
|
|
3069
|
+
/** Write the state file now, e.g. before the office stops or hands itself to the launcher. */
|
|
3070
|
+
async writeState() {
|
|
3071
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
3072
|
+
this.saveTimer = null;
|
|
3073
|
+
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
3074
|
+
await fs5.mkdir(path7.dirname(STATE_FILE), { recursive: true });
|
|
3075
|
+
const tmp = `${STATE_FILE}.tmp`;
|
|
3076
|
+
await fs5.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
3077
|
+
await fs5.rename(tmp, STATE_FILE);
|
|
2730
3078
|
}
|
|
2731
3079
|
// ---------- lookups ----------
|
|
2732
3080
|
repo(id) {
|
|
@@ -2808,7 +3156,7 @@ var Swarm = class {
|
|
|
2808
3156
|
const name = fullName.split("/")[1];
|
|
2809
3157
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
2810
3158
|
if (taken) throw new HttpError(409, `${taken.path} already exists and isn't a clone of ${fullName}. Connect that folder instead, or rename it first.`);
|
|
2811
|
-
return
|
|
3159
|
+
return path7.join(root, name);
|
|
2812
3160
|
}
|
|
2813
3161
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
2814
3162
|
async createRepo(name, opts) {
|
|
@@ -2820,11 +3168,11 @@ var Swarm = class {
|
|
|
2820
3168
|
}
|
|
2821
3169
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
2822
3170
|
async listProjectFolders(dir) {
|
|
2823
|
-
const root = dir?.trim() ?
|
|
3171
|
+
const root = dir?.trim() ? path7.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
2824
3172
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
2825
3173
|
throw new HttpError(400, err.message);
|
|
2826
3174
|
});
|
|
2827
|
-
const same = (a, b) =>
|
|
3175
|
+
const same = (a, b) => path7.resolve(a).toLowerCase() === path7.resolve(b).toLowerCase();
|
|
2828
3176
|
const floorOf = (f) => this.state.repos.find((r) => r.localPath && same(r.localPath, f.path) || f.github && r.id.toLowerCase() === f.github.toLowerCase())?.floor ?? null;
|
|
2829
3177
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
2830
3178
|
}
|
|
@@ -2921,46 +3269,38 @@ var Swarm = class {
|
|
|
2921
3269
|
}
|
|
2922
3270
|
/** One step toward merging a QA-passed PR. Returns true when it merged. */
|
|
2923
3271
|
async advanceMerge(repo, rec, pr) {
|
|
2924
|
-
|
|
2925
|
-
if (
|
|
3272
|
+
let step = mergeStep(pr, rec, Date.now(), { base: repo.defaultBranch });
|
|
3273
|
+
if (step.do === "details") {
|
|
2926
3274
|
const d = await this.backend.prDetails(repo.fullName, pr.number);
|
|
2927
3275
|
pr = { ...pr, headSha: d.headSha, mergeable: d.mergeable, mergeState: d.mergeState };
|
|
3276
|
+
step = mergeStep(pr, rec, Date.now(), { base: repo.defaultBranch, detailed: true });
|
|
2928
3277
|
}
|
|
2929
|
-
rec
|
|
2930
|
-
if (
|
|
3278
|
+
Object.assign(rec, step.set);
|
|
3279
|
+
if (step.do === "requeue") {
|
|
2931
3280
|
this.setQa(rec, { status: "queued", round: rec.round + 1, retests: rec.retests + 1, mergeNote: null, pendingSince: null });
|
|
2932
3281
|
setTimeout(() => this.schedule(), 200);
|
|
2933
3282
|
return false;
|
|
2934
3283
|
}
|
|
2935
|
-
if (
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
if (pr.checks === "failing") {
|
|
2939
|
-
return this.sendBack(repo, rec, "checks", pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n"));
|
|
2940
|
-
}
|
|
2941
|
-
if (pr.checks === "pending") {
|
|
2942
|
-
rec.pendingSince ??= Date.now();
|
|
2943
|
-
if (Date.now() - rec.pendingSince > CHECKS_ALERT_MS && !rec.alerted) {
|
|
2944
|
-
rec.alerted = true;
|
|
3284
|
+
if (step.do === "send-back") return this.sendBack(repo, rec, step.reason, step.instructions, step.needsHuman);
|
|
3285
|
+
if (step.do === "wait") {
|
|
3286
|
+
if (step.alert) {
|
|
2945
3287
|
this.postMessage("office", `\u23F3 PR #${pr.number} on ${repo.fullName} passed QA, but its checks (${pr.pendingChecks.join(", ")}) have been running for over ${CHECKS_ALERT_MS / 6e4} minutes. It merges as soon as they finish.`);
|
|
2946
3288
|
}
|
|
2947
|
-
return
|
|
3289
|
+
return step.note === void 0 ? false : this.mergeNote(rec, step.note);
|
|
2948
3290
|
}
|
|
2949
|
-
|
|
2950
|
-
if (pr.mergeable === "UNKNOWN") return this.mergeNote(rec, "GitHub is checking it can merge");
|
|
2951
|
-
if (pr.mergeState === "BEHIND") {
|
|
3291
|
+
if (step.do === "update-branch") {
|
|
2952
3292
|
this.mergeNote(rec, `updating the branch with ${repo.defaultBranch}`);
|
|
2953
3293
|
await this.backend.updateBranch(repo.fullName, pr.number);
|
|
2954
3294
|
const details = await this.backend.prDetails(repo.fullName, pr.number);
|
|
2955
3295
|
this.setQa(rec, { passedSha: details.headSha });
|
|
2956
3296
|
return false;
|
|
2957
3297
|
}
|
|
2958
|
-
if (
|
|
3298
|
+
if (step.do !== "merge") return false;
|
|
2959
3299
|
this.mergeNote(rec, "merging\u2026");
|
|
2960
3300
|
let error = "";
|
|
2961
3301
|
for (const method of ["squash", "merge", "rebase"]) {
|
|
2962
3302
|
try {
|
|
2963
|
-
await this.backend.mergePull(repo.fullName, pr.number, method, rec.passedSha);
|
|
3303
|
+
await this.backend.mergePull(repo.fullName, pr.number, method, rec.passedSha ?? pr.headSha);
|
|
2964
3304
|
error = "";
|
|
2965
3305
|
break;
|
|
2966
3306
|
} catch (err) {
|
|
@@ -2989,8 +3329,8 @@ var Swarm = class {
|
|
|
2989
3329
|
return false;
|
|
2990
3330
|
}
|
|
2991
3331
|
/** QA passed, but the PR can't merge as it is: a developer fixes it, and QA re-tests if the code changed. */
|
|
2992
|
-
sendBack(repo, rec, reason, fixInstructions) {
|
|
2993
|
-
if (
|
|
3332
|
+
sendBack(repo, rec, reason, fixInstructions, needsHuman) {
|
|
3333
|
+
if (needsHuman) {
|
|
2994
3334
|
this.setQa(rec, { status: "needs-human", mergeNote: null });
|
|
2995
3335
|
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${reason === "conflict" ? `conflicts with ${repo.defaultBranch}` : "fails its checks"} after ${MAX_MERGE_FIXES} fixes, so it needs you.`);
|
|
2996
3336
|
return false;
|
|
@@ -3003,14 +3343,16 @@ var Swarm = class {
|
|
|
3003
3343
|
async syncFolder(repo) {
|
|
3004
3344
|
const rt = this.repoRt.get(repo.id);
|
|
3005
3345
|
if (!rt || rt.cloneStatus !== "ready") return rt?.folderSync ?? null;
|
|
3006
|
-
const own =
|
|
3007
|
-
const
|
|
3346
|
+
const own = this.backend.office.isOwnFolder(this.backend.mainDir(repo.fullName));
|
|
3347
|
+
const sync = await this.backend.syncMain(repo.fullName, repo.defaultBranch, { touch: !own }).catch((err) => ({ status: `sync failed: ${oneLine(err)}`, behind: 0, updatable: false }));
|
|
3348
|
+
const status = sync?.status ?? null;
|
|
3349
|
+
if (own && sync) this.setOfficeBehind(sync.updatable ? sync.behind : 0);
|
|
3008
3350
|
if (!this.repoRt.has(repo.id)) return null;
|
|
3009
3351
|
const before = rt.folderSync;
|
|
3010
3352
|
rt.folderSync = status;
|
|
3011
3353
|
this.emitRepo(repo);
|
|
3012
3354
|
if (status && status !== before) {
|
|
3013
|
-
const name = repo.localPath ?
|
|
3355
|
+
const name = repo.localPath ? path7.basename(repo.localPath) : repo.fullName;
|
|
3014
3356
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
3015
3357
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
3016
3358
|
}
|
|
@@ -3030,8 +3372,9 @@ var Swarm = class {
|
|
|
3030
3372
|
return this.previews.stop(this.repo(id));
|
|
3031
3373
|
}
|
|
3032
3374
|
/** Server shutdown: stop every floor's app so nothing is left holding a preview port. */
|
|
3033
|
-
shutdown() {
|
|
3034
|
-
|
|
3375
|
+
async shutdown() {
|
|
3376
|
+
await this.writeState().catch((err) => console.warn("could not save the state", err));
|
|
3377
|
+
await this.previews.stopAll(this.state.repos);
|
|
3035
3378
|
}
|
|
3036
3379
|
async cloneRepo(id) {
|
|
3037
3380
|
const repo = this.state.repos.find((r) => r.id === id);
|
|
@@ -3419,9 +3762,10 @@ Note from the manager: ${note}` : ""
|
|
|
3419
3762
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
3420
3763
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
3421
3764
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
3422
|
-
void removeScreens(a.id).then(() =>
|
|
3765
|
+
void removeScreens(a.id).then(() => fs5.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs5.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
3423
3766
|
},
|
|
3424
3767
|
limited: (at) => this.pauseForLimit(at),
|
|
3768
|
+
usageWarning: (info) => this.paceForWarning(info),
|
|
3425
3769
|
finished: (result) => void this.onFinished(a, repo, result)
|
|
3426
3770
|
},
|
|
3427
3771
|
repo.defaultBranch
|
|
@@ -3430,6 +3774,7 @@ Note from the manager: ${note}` : ""
|
|
|
3430
3774
|
async onFinished(a, repo, result) {
|
|
3431
3775
|
const rt = this.agentRt.get(a.id);
|
|
3432
3776
|
if (!rt || !this.state.agents.includes(a)) return;
|
|
3777
|
+
if (this.officeUpdate.handedOver) return;
|
|
3433
3778
|
rt.session = null;
|
|
3434
3779
|
rt.currentTool = null;
|
|
3435
3780
|
a.endedAt = Date.now();
|
|
@@ -3842,9 +4187,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3842
4187
|
if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
3843
4188
|
if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
|
|
3844
4189
|
if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
|
|
3845
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir =
|
|
4190
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path7.resolve(patch.projectsDir.trim());
|
|
3846
4191
|
if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
|
|
3847
4192
|
if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
4193
|
+
if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
|
|
4194
|
+
if (patch.pacingSessions !== void 0) s.pacingSessions = clampPacingSessions(patch.pacingSessions);
|
|
3848
4195
|
this.save();
|
|
3849
4196
|
this.broadcast({ type: "settings", settings: s });
|
|
3850
4197
|
this.emitCeo();
|
|
@@ -3879,6 +4226,9 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3879
4226
|
// `${repoId}#${issue}`: its developer was asked once to finish the missing PR
|
|
3880
4227
|
pausedUntil = 0;
|
|
3881
4228
|
// Claude's usage limit was hit: nothing new starts before this
|
|
4229
|
+
pacingUntil = 0;
|
|
4230
|
+
// Claude warned about usage: new issues are paced until this
|
|
4231
|
+
lastUsage = "";
|
|
3882
4232
|
/** Backlog issues that can start now, most urgent first: the ones holding up the longest chain of other issues, then the oldest. */
|
|
3883
4233
|
readyIssues(repo) {
|
|
3884
4234
|
const issues = this.repoRt.get(repo.id).issues;
|
|
@@ -3938,7 +4288,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3938
4288
|
* free developer is least needed elsewhere, so nobody sits idle while there is work that can start.
|
|
3939
4289
|
*/
|
|
3940
4290
|
startIssueWork(repo) {
|
|
3941
|
-
if (!repo.autoAssign) return false;
|
|
4291
|
+
if (!repo.autoAssign || !this.mayStart("issue")) return false;
|
|
3942
4292
|
const free = this.available(repo, "dev");
|
|
3943
4293
|
const ready = free.length ? this.readyIssues(repo) : [];
|
|
3944
4294
|
if (ready.length === 0) return false;
|
|
@@ -3958,6 +4308,39 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3958
4308
|
const fresh = Date.now() >= this.pausedUntil;
|
|
3959
4309
|
this.pausedUntil = until;
|
|
3960
4310
|
if (fresh) this.postMessage("office", `\u23F8 Claude's usage limit was reached. The office starts no new work until ${new Date(until).toLocaleTimeString()}; sessions already running carry on.`);
|
|
4311
|
+
this.emitUsage();
|
|
4312
|
+
}
|
|
4313
|
+
/** Claude warned that usage is getting high: pace new issues until the window resets, rather than run into the limit. */
|
|
4314
|
+
paceForWarning(info) {
|
|
4315
|
+
const now2 = Date.now();
|
|
4316
|
+
const until = info.resetsAt && info.resetsAt > now2 ? info.resetsAt : now2 + PACING_MS;
|
|
4317
|
+
if (until <= this.pacingUntil) return;
|
|
4318
|
+
const fresh = now2 >= this.pacingUntil;
|
|
4319
|
+
this.pacingUntil = until;
|
|
4320
|
+
if (fresh) this.postMessage("office", pacingMessage(info, until, this.state.settings.pacingSessions, now2));
|
|
4321
|
+
this.emitUsage();
|
|
4322
|
+
}
|
|
4323
|
+
/** May work of this kind start now, as far as Claude's usage goes? */
|
|
4324
|
+
mayStart(kind) {
|
|
4325
|
+
return mayStart(kind, { now: Date.now(), pausedUntil: this.pausedUntil, pacingUntil: this.pacingUntil, running: this.running(), pacingSessions: this.state.settings.pacingSessions });
|
|
4326
|
+
}
|
|
4327
|
+
usageNow() {
|
|
4328
|
+
return usageView({ now: Date.now(), pausedUntil: this.pausedUntil, pacingUntil: this.pacingUntil });
|
|
4329
|
+
}
|
|
4330
|
+
emitUsage() {
|
|
4331
|
+
const usage = this.usageNow();
|
|
4332
|
+
const key = JSON.stringify(usage);
|
|
4333
|
+
if (key === this.lastUsage) return;
|
|
4334
|
+
this.lastUsage = key;
|
|
4335
|
+
this.broadcast({ type: "usage", usage });
|
|
4336
|
+
}
|
|
4337
|
+
/** Pacing and pauses run out on their own: say so when pacing ends. */
|
|
4338
|
+
tickUsage() {
|
|
4339
|
+
if (this.pacingUntil && Date.now() >= this.pacingUntil) {
|
|
4340
|
+
this.pacingUntil = 0;
|
|
4341
|
+
this.postMessage("office", "\u2705 Claude's usage is back to normal. The office starts new work at full speed again.");
|
|
4342
|
+
}
|
|
4343
|
+
this.emitUsage();
|
|
3961
4344
|
}
|
|
3962
4345
|
/** A failed session releases its issue for someone else; an issue that keeps failing waits for the manager. */
|
|
3963
4346
|
issueFailed(repo, n) {
|
|
@@ -3975,6 +4358,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3975
4358
|
* so no repo can hog the slots.
|
|
3976
4359
|
*/
|
|
3977
4360
|
schedule() {
|
|
4361
|
+
this.tickUsage();
|
|
4362
|
+
if (this.officeUpdateTick()) return;
|
|
3978
4363
|
if (this.limited()) return;
|
|
3979
4364
|
this.maybeHeartbeat();
|
|
3980
4365
|
this.startCeoWork();
|
|
@@ -3996,6 +4381,126 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
3996
4381
|
}
|
|
3997
4382
|
}
|
|
3998
4383
|
}
|
|
4384
|
+
// ---------- the office's own update ----------
|
|
4385
|
+
drainInput() {
|
|
4386
|
+
const u = this.officeUpdate;
|
|
4387
|
+
return {
|
|
4388
|
+
now: Date.now(),
|
|
4389
|
+
behind: this.officeHead ? u.behind : 0,
|
|
4390
|
+
launcher: this.backend.office.launcher,
|
|
4391
|
+
autoUpdate: this.state.settings.autoUpdate !== false,
|
|
4392
|
+
requested: u.requested,
|
|
4393
|
+
postponedUntil: u.postponedUntil,
|
|
4394
|
+
postponedBehind: u.postponedBehind,
|
|
4395
|
+
failed: u.failed,
|
|
4396
|
+
failedBehind: u.failedBehind,
|
|
4397
|
+
drainingSince: u.drainingSince,
|
|
4398
|
+
sent: u.sent,
|
|
4399
|
+
running: this.running()
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
officeUpdateView() {
|
|
4403
|
+
const u = this.officeUpdate;
|
|
4404
|
+
const input = this.drainInput();
|
|
4405
|
+
const d = drainDecision(input);
|
|
4406
|
+
const until = u.postponedUntil ? new Date(u.postponedUntil).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
4407
|
+
const detail = d.state === "failed" ? u.failed : d.state === "waiting" ? `Postponed until ${until}, or until a newer commit lands.` : null;
|
|
4408
|
+
return { state: d.state, behind: input.behind, launcher: input.launcher, drainingSince: u.drainingSince ?? d.drainingSince, running: input.running, detail };
|
|
4409
|
+
}
|
|
4410
|
+
emitOfficeUpdate() {
|
|
4411
|
+
if (!this.officeHead) return;
|
|
4412
|
+
const view = this.officeUpdateView();
|
|
4413
|
+
const key = JSON.stringify(view);
|
|
4414
|
+
if (key === this.lastOfficeView) return;
|
|
4415
|
+
this.lastOfficeView = key;
|
|
4416
|
+
this.broadcast({ type: "officeUpdate", officeUpdate: view });
|
|
4417
|
+
}
|
|
4418
|
+
/** The own-folder check found the office this many commits behind GitHub. */
|
|
4419
|
+
setOfficeBehind(behind) {
|
|
4420
|
+
const u = this.officeUpdate;
|
|
4421
|
+
if (u.failed && u.failedBehind === null) u.failedBehind = behind;
|
|
4422
|
+
u.behind = behind;
|
|
4423
|
+
if (behind === 0) Object.assign(u, { requested: false, failed: null, failedBehind: null });
|
|
4424
|
+
this.officeUpdateTick();
|
|
4425
|
+
}
|
|
4426
|
+
/**
|
|
4427
|
+
* Take the office's own update one step (see drainDecision): drain, and once nothing is running, or the drain timed
|
|
4428
|
+
* out, hand it to the launcher. True while nothing new may start.
|
|
4429
|
+
*/
|
|
4430
|
+
officeUpdateTick() {
|
|
4431
|
+
if (!this.officeHead) return false;
|
|
4432
|
+
const u = this.officeUpdate;
|
|
4433
|
+
const d = drainDecision(this.drainInput());
|
|
4434
|
+
if (d.state === "draining" && u.drainingSince === null) {
|
|
4435
|
+
const running = this.running();
|
|
4436
|
+
this.postMessage(
|
|
4437
|
+
"office",
|
|
4438
|
+
`\u2B06\uFE0F Updating the office (${u.behind} new commit${u.behind === 1 ? "" : "s"}). Nothing new starts${running ? ` while ${running} running session${running === 1 ? "" : "s"} finish` : ""}; then it installs and restarts.`
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
u.drainingSince = d.drainingSince;
|
|
4442
|
+
if (d.send) void this.handOver(d.timedOut);
|
|
4443
|
+
this.emitOfficeUpdate();
|
|
4444
|
+
return d.state === "draining" || d.state === "updating";
|
|
4445
|
+
}
|
|
4446
|
+
/** Drained: hand the update to the launcher. Sessions still running after the timeout stop the way a restart stops them. */
|
|
4447
|
+
async handOver(timedOut) {
|
|
4448
|
+
const u = this.officeUpdate;
|
|
4449
|
+
const from = this.officeHead;
|
|
4450
|
+
u.sent = true;
|
|
4451
|
+
const busy = this.state.agents.filter((a) => BUSY.includes(a.status));
|
|
4452
|
+
if (timedOut) this.postMessage("office", `\u23F1 ${busy.length} session${busy.length === 1 ? " was" : "s were"} still running after 20 minutes. Stopped; the work goes back to the queue after the update.`);
|
|
4453
|
+
this.emitOfficeUpdate();
|
|
4454
|
+
u.handedOver = true;
|
|
4455
|
+
await this.writeState().catch((err) => console.warn("could not save the state", err));
|
|
4456
|
+
for (const a of busy) this.agentRt.get(a.id)?.session?.stop();
|
|
4457
|
+
try {
|
|
4458
|
+
const result = await this.backend.office.update(from);
|
|
4459
|
+
if (result) await this.afterUpdate(result);
|
|
4460
|
+
} catch (err) {
|
|
4461
|
+
await this.afterUpdate({ from, to: from, ok: false, error: `the launcher didn't take the update: ${oneLine(err)}`, installed: false, built: false, at: Date.now() });
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
/** An update that didn't restart the office (the demo, or a failed hand-over): recover like a restart would, then report it. */
|
|
4465
|
+
async afterUpdate(result) {
|
|
4466
|
+
const u = this.officeUpdate;
|
|
4467
|
+
Object.assign(u, { sent: false, handedOver: false, requested: false, drainingSince: null });
|
|
4468
|
+
const interrupted = this.state.agents.filter((a) => BUSY.includes(a.status));
|
|
4469
|
+
for (const a of interrupted) {
|
|
4470
|
+
Object.assign(a, { status: "stopped", lastError: "The office updated itself while this agent was working." });
|
|
4471
|
+
const rt = this.agentRt.get(a.id);
|
|
4472
|
+
if (rt) Object.assign(rt, { session: null, currentTool: null });
|
|
4473
|
+
}
|
|
4474
|
+
this.ensureCeo(interrupted);
|
|
4475
|
+
this.recover(interrupted);
|
|
4476
|
+
interrupted.forEach((a) => this.emitAgent(a));
|
|
4477
|
+
await this.reportUpdate(result);
|
|
4478
|
+
if (!result.ok) u.failedBehind = u.behind;
|
|
4479
|
+
const own = this.state.repos.find((r) => this.backend.office.isOwnFolder(this.backend.mainDir(r.fullName)));
|
|
4480
|
+
if (own) await this.syncFolder(own);
|
|
4481
|
+
this.emitCeo();
|
|
4482
|
+
this.save();
|
|
4483
|
+
setTimeout(() => this.schedule(), 300);
|
|
4484
|
+
}
|
|
4485
|
+
/** One phone message about the last update (the launcher's last-update.json, or the demo's fake). */
|
|
4486
|
+
async reportUpdate(result) {
|
|
4487
|
+
const commits = result.ok && result.to ? await this.backend.office.commitsBetween(result.from, result.to) : null;
|
|
4488
|
+
this.postMessage("office", lastUpdateMessage(result, commits));
|
|
4489
|
+
Object.assign(this.officeUpdate, { failed: result.ok ? null : result.error || "unknown error", failedBehind: null });
|
|
4490
|
+
}
|
|
4491
|
+
/** The manager's Update now (drain now, even with automatic updates off) or Later (not for 2 hours, or until a newer commit). */
|
|
4492
|
+
updateOffice(action) {
|
|
4493
|
+
if (action !== "now" && action !== "later") throw new HttpError(400, 'action must be "now" or "later"');
|
|
4494
|
+
const u = this.officeUpdate;
|
|
4495
|
+
if (!this.officeHead || !this.backend.office.launcher) throw new HttpError(409, "The office can only update itself when its launcher started it (npm run dev or npm start).");
|
|
4496
|
+
if (u.sent) throw new HttpError(409, "The update is already under way.");
|
|
4497
|
+
if (u.behind <= 0) throw new HttpError(409, "The office is up to date: there is nothing to update.");
|
|
4498
|
+
if (action === "now") Object.assign(u, { requested: true, postponedUntil: null });
|
|
4499
|
+
else Object.assign(u, { requested: false, postponedUntil: Date.now() + POSTPONE_MS, postponedBehind: u.behind });
|
|
4500
|
+
this.officeUpdateTick();
|
|
4501
|
+
setTimeout(() => this.schedule(), 200);
|
|
4502
|
+
return this.officeUpdateView();
|
|
4503
|
+
}
|
|
3999
4504
|
// ---------- the CEO ----------
|
|
4000
4505
|
ceo() {
|
|
4001
4506
|
return this.state.agents.find((a) => a.id === CEO_ID);
|
|
@@ -4122,11 +4627,11 @@ ${job.text}` } : job;
|
|
|
4122
4627
|
this.state.ceo.lastReviewAt = Date.now();
|
|
4123
4628
|
this.state.ceo.lastFingerprint = this.fingerprint();
|
|
4124
4629
|
}
|
|
4125
|
-
this.ceoIssues =
|
|
4630
|
+
this.ceoIssues = new IssueCap(MAX_ISSUES_PER_JOB);
|
|
4126
4631
|
this.emitAgent(a);
|
|
4127
4632
|
this.emitCeo();
|
|
4128
4633
|
this.save();
|
|
4129
|
-
await
|
|
4634
|
+
await fs5.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
4130
4635
|
if (a.status !== "working") {
|
|
4131
4636
|
this.state.ceo.job = null;
|
|
4132
4637
|
this.emitCeo();
|
|
@@ -4141,7 +4646,7 @@ ${job.text}` } : job;
|
|
|
4141
4646
|
name: a.name,
|
|
4142
4647
|
company: s.companyName,
|
|
4143
4648
|
manager: s.managerName,
|
|
4144
|
-
notesFile:
|
|
4649
|
+
notesFile: path7.join(CEO_DIR, "NOTES.md"),
|
|
4145
4650
|
sessionLimit: s.sessionLimit,
|
|
4146
4651
|
teamCap: s.teamCap,
|
|
4147
4652
|
hiring: s.hiring
|
|
@@ -4170,6 +4675,7 @@ ${job.text}` } : job;
|
|
|
4170
4675
|
screenshot: () => void 0,
|
|
4171
4676
|
turn: (text) => this.postMessage("ceo", text),
|
|
4172
4677
|
limited: (at) => this.pauseForLimit(at),
|
|
4678
|
+
usageWarning: (info) => this.paceForWarning(info),
|
|
4173
4679
|
finished: (result) => this.onCeoFinished(a, result)
|
|
4174
4680
|
},
|
|
4175
4681
|
""
|
|
@@ -4177,7 +4683,7 @@ ${job.text}` } : job;
|
|
|
4177
4683
|
}
|
|
4178
4684
|
onCeoFinished(a, result) {
|
|
4179
4685
|
const rt = this.agentRt.get(a.id);
|
|
4180
|
-
if (!rt) return;
|
|
4686
|
+
if (!rt || this.officeUpdate.handedOver) return;
|
|
4181
4687
|
rt.session = null;
|
|
4182
4688
|
rt.currentTool = null;
|
|
4183
4689
|
a.endedAt = Date.now();
|
|
@@ -4194,7 +4700,7 @@ ${job.text}` } : job;
|
|
|
4194
4700
|
this.postMessage("office", `\u26A0\uFE0F ${a.name} hit a problem while ${what}: ${a.lastError.slice(0, 240)}`);
|
|
4195
4701
|
} else {
|
|
4196
4702
|
a.status = "done";
|
|
4197
|
-
const filed = this.ceoIssues.
|
|
4703
|
+
const filed = this.ceoIssues.total ? ` \xB7 ${this.ceoIssues.total} issue${this.ceoIssues.total === 1 ? "" : "s"} filed` : "";
|
|
4198
4704
|
this.appendLog(a, [{ kind: "done", text: `\u2714 Done in ${this.minutes(a)}m \xB7 ${a.turns} turns${filed}` }]);
|
|
4199
4705
|
}
|
|
4200
4706
|
for (const id of this.ceoIssues.repos) void this.syncRepo(id);
|
|
@@ -4212,6 +4718,7 @@ ${job.text}` } : job;
|
|
|
4212
4718
|
const rt = this.agentRt.get(a.id);
|
|
4213
4719
|
if (rt.session) {
|
|
4214
4720
|
this.appendLog(a, [{ kind: "manager", text: `\u25B6 Manager: ${t}` }]);
|
|
4721
|
+
this.ceoIssues.managerMessage();
|
|
4215
4722
|
rt.session.send(`Message from the manager (they read your reply on their phone, so keep it short):
|
|
4216
4723
|
${t}`);
|
|
4217
4724
|
return;
|
|
@@ -4434,6 +4941,7 @@ ${t}`);
|
|
|
4434
4941
|
teamCap: s.teamCap,
|
|
4435
4942
|
sessionLimit: s.sessionLimit || "none",
|
|
4436
4943
|
sessionsRunning: this.running(),
|
|
4944
|
+
usage: usageLabel(this.usageNow(), Date.now()),
|
|
4437
4945
|
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa }
|
|
4438
4946
|
},
|
|
4439
4947
|
floors,
|
|
@@ -4577,7 +5085,7 @@ ${t}`);
|
|
|
4577
5085
|
}
|
|
4578
5086
|
async fileIssue(x) {
|
|
4579
5087
|
const repo = this.floorRepo(x.floor);
|
|
4580
|
-
|
|
5088
|
+
this.ceoIssues.check();
|
|
4581
5089
|
const title = String(x.title ?? "").trim().slice(0, 120);
|
|
4582
5090
|
if (!title) throw new Error("An issue needs a title.");
|
|
4583
5091
|
const slug = specialtySlug(x.specialty);
|
|
@@ -4586,10 +5094,35 @@ ${t}`);
|
|
|
4586
5094
|
---
|
|
4587
5095
|
_Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
4588
5096
|
const n = await this.backend.createIssue(repo.fullName, title, body, slug ? [specialtyLabel(slug)] : []);
|
|
4589
|
-
this.ceoIssues.
|
|
4590
|
-
this.ceoIssues.repos.add(repo.id);
|
|
5097
|
+
this.ceoIssues.record(repo.id);
|
|
4591
5098
|
return `Filed #${n} on floor ${repo.floor}: ${title}${slug ? ` (routed to ${slug})` : ""}.`;
|
|
4592
5099
|
}
|
|
5100
|
+
/** Change an open issue's specialty and/or dependencies (see planRoute for what is refused). */
|
|
5101
|
+
async routeIssue(x) {
|
|
5102
|
+
const repo = this.floorRepo(x.floor);
|
|
5103
|
+
const issues = this.repoRt.get(repo.id)?.issues ?? [];
|
|
5104
|
+
const asked = [Number(x.number), ...(x.depends_on ?? []).map(Number)].filter((n) => !issues.some((i) => i.number === n));
|
|
5105
|
+
const states = new Map(await Promise.all([...new Set(asked)].map(async (n) => [n, await this.backend.issueState(repo.fullName, n).catch(() => null)])));
|
|
5106
|
+
const specialties = [
|
|
5107
|
+
...this.state.agents.filter((a) => a.repoId === repo.id && a.specialty).map((a) => a.specialty.toLowerCase()),
|
|
5108
|
+
...this.state.requests.filter((r) => r.status === "pending" && r.kind === "hire" && r.repoId === repo.id && r.specialty).map((r) => r.specialty)
|
|
5109
|
+
];
|
|
5110
|
+
const plan = planRoute({
|
|
5111
|
+
floor: repo.floor,
|
|
5112
|
+
number: Number(x.number),
|
|
5113
|
+
specialty: x.specialty,
|
|
5114
|
+
dependsOn: x.depends_on,
|
|
5115
|
+
issues,
|
|
5116
|
+
closed: (n) => states.get(n) === "CLOSED",
|
|
5117
|
+
inProgress: this.issueTaken(repo, Number(x.number)),
|
|
5118
|
+
specialties
|
|
5119
|
+
});
|
|
5120
|
+
if (plan.body !== null || plan.addLabels.length || plan.removeLabels.length) {
|
|
5121
|
+
await this.backend.editIssue(repo.fullName, Number(x.number), { body: plan.body ?? void 0, addLabels: plan.addLabels, removeLabels: plan.removeLabels });
|
|
5122
|
+
await this.syncRepo(repo.id);
|
|
5123
|
+
}
|
|
5124
|
+
return plan.summary;
|
|
5125
|
+
}
|
|
4593
5126
|
};
|
|
4594
5127
|
|
|
4595
5128
|
// server/index.ts
|
|
@@ -4706,6 +5239,7 @@ app.get("/api/agents/:id/screen", (req, res) => {
|
|
|
4706
5239
|
res.end(shot.data);
|
|
4707
5240
|
});
|
|
4708
5241
|
app.patch("/api/settings", route((req) => swarm.updateSettings(req.body ?? {})));
|
|
5242
|
+
app.post("/api/office/update", route((req) => swarm.updateOffice(req.body?.action)));
|
|
4709
5243
|
app.post("/api/ceo/message", route((req) => swarm.messageCeo(str(req.body.text))));
|
|
4710
5244
|
app.post("/api/ceo/review", route(() => swarm.requestReview()));
|
|
4711
5245
|
app.post("/api/phone/read", route((req) => swarm.markPhoneRead(Number(req.body?.at) || Date.now())));
|
|
@@ -4714,10 +5248,10 @@ app.post(
|
|
|
4714
5248
|
route((req) => swarm.approveRequest(String(req.params.id), { name: str(req.body?.name) || void 0, model: typeof req.body?.model === "string" ? req.body.model : void 0, effort: typeof req.body?.effort === "string" ? req.body.effort : void 0 }))
|
|
4715
5249
|
);
|
|
4716
5250
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
4717
|
-
var dist =
|
|
4718
|
-
if (
|
|
5251
|
+
var dist = path8.resolve(import.meta.dirname, "../dist");
|
|
5252
|
+
if (fs6.existsSync(dist)) {
|
|
4719
5253
|
app.use(express.static(dist));
|
|
4720
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
5254
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path8.join(dist, "index.html")));
|
|
4721
5255
|
}
|
|
4722
5256
|
app.use((err, _req, res, _next) => {
|
|
4723
5257
|
const parserStatus = err?.status;
|
|
@@ -4750,4 +5284,9 @@ var shutdown = (signal) => {
|
|
|
4750
5284
|
};
|
|
4751
5285
|
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
4752
5286
|
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
5287
|
+
if (underLauncher()) {
|
|
5288
|
+
process.on("message", (msg) => {
|
|
5289
|
+
if (msg?.type === "office:shutdown") shutdown("update");
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
4753
5292
|
//# sourceMappingURL=index.js.map
|