@shipster/cli 0.5.0 → 0.5.1
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/cli.js +457 -212
- package/dist/event-worker.js +48 -16
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18509,7 +18509,7 @@ function readableFrom(current, legacy, overridden) {
|
|
|
18509
18509
|
if (overridden || existsSync(current) || !existsSync(legacy)) return current;
|
|
18510
18510
|
if (!warnedFor.has(legacy)) {
|
|
18511
18511
|
warnedFor.add(legacy);
|
|
18512
|
-
console.error(`
|
|
18512
|
+
console.error(`shipster: reading ${legacy}; the next write moves it to ${current}.`);
|
|
18513
18513
|
}
|
|
18514
18514
|
return legacy;
|
|
18515
18515
|
}
|
|
@@ -18523,7 +18523,7 @@ function retireLegacy(legacy, current, overridden) {
|
|
|
18523
18523
|
rmSync2(legacy);
|
|
18524
18524
|
} catch (err) {
|
|
18525
18525
|
if (err.code === "ENOENT") return;
|
|
18526
|
-
console.error(`
|
|
18526
|
+
console.error(`shipster: could not remove ${legacy} (${String(err)}); it may still hold a token, delete it by hand.`);
|
|
18527
18527
|
}
|
|
18528
18528
|
}
|
|
18529
18529
|
function configFileInUse() {
|
|
@@ -18579,7 +18579,7 @@ function writeConfig(config) {
|
|
|
18579
18579
|
function requireConfig() {
|
|
18580
18580
|
const config = readConfig();
|
|
18581
18581
|
if (!config) {
|
|
18582
|
-
console.error("Not initialized. Run `
|
|
18582
|
+
console.error("Not initialized. Run `shipster init` first.");
|
|
18583
18583
|
process.exit(1);
|
|
18584
18584
|
}
|
|
18585
18585
|
return config;
|
|
@@ -19069,7 +19069,7 @@ async function repoContextShowCommand() {
|
|
|
19069
19069
|
try {
|
|
19070
19070
|
const project = await client.getRepo(config.repoId);
|
|
19071
19071
|
if (!project.context) {
|
|
19072
|
-
console.log(source_default.dim("(no context set \u2014 use `
|
|
19072
|
+
console.log(source_default.dim("(no context set \u2014 use `shipster repo context edit` to add some)"));
|
|
19073
19073
|
return;
|
|
19074
19074
|
}
|
|
19075
19075
|
console.log(project.context);
|
|
@@ -19174,7 +19174,7 @@ async function threadsCommand() {
|
|
|
19174
19174
|
const threads = await client.listThreads(config.repoId);
|
|
19175
19175
|
spinner.stop();
|
|
19176
19176
|
if (threads.length === 0) {
|
|
19177
|
-
console.log(source_default.dim("No threads yet. Create one with `
|
|
19177
|
+
console.log(source_default.dim("No threads yet. Create one with `shipster thread new <title>`."));
|
|
19178
19178
|
return;
|
|
19179
19179
|
}
|
|
19180
19180
|
console.log();
|
|
@@ -19296,7 +19296,7 @@ async function inboxCommand(options) {
|
|
|
19296
19296
|
console.log(` ${source_default.blue("proposed")} ${source_default.dim(task2.id)}`);
|
|
19297
19297
|
console.log(` ${source_default.dim("suggests:")} ${suggested ?? "\u2014"} ${source_default.dim(date)}`);
|
|
19298
19298
|
console.log(` ${titlePreview}`);
|
|
19299
|
-
console.log(` ${source_default.dim(`
|
|
19299
|
+
console.log(` ${source_default.dim(`shipster task commit ${task2.id} --to <agent|@auto> (or --reject)`)}`);
|
|
19300
19300
|
console.log();
|
|
19301
19301
|
}
|
|
19302
19302
|
}
|
|
@@ -19309,7 +19309,7 @@ async function inboxCommand(options) {
|
|
|
19309
19309
|
console.log(` ${source_default.yellow("review")} ${source_default.dim(task2.id)}`);
|
|
19310
19310
|
console.log(` ${source_default.dim("from:")} ${task2.assignedTo ?? "?"} ${source_default.dim(date)}`);
|
|
19311
19311
|
console.log(` ${titlePreview}`);
|
|
19312
|
-
console.log(` ${source_default.dim(`
|
|
19312
|
+
console.log(` ${source_default.dim(`shipster task review ${task2.id} --decision approve|reject`)}`);
|
|
19313
19313
|
console.log();
|
|
19314
19314
|
}
|
|
19315
19315
|
}
|
|
@@ -19381,7 +19381,7 @@ async function statusCommand() {
|
|
|
19381
19381
|
}
|
|
19382
19382
|
if (unreadTotal > 0) {
|
|
19383
19383
|
console.log();
|
|
19384
|
-
console.log(source_default.yellow(` ${unreadTotal} unread message${unreadTotal === 1 ? "" : "s"}: run \`
|
|
19384
|
+
console.log(source_default.yellow(` ${unreadTotal} unread message${unreadTotal === 1 ? "" : "s"}: run \`shipster inbox\``));
|
|
19385
19385
|
}
|
|
19386
19386
|
} catch (err) {
|
|
19387
19387
|
spinner.fail(source_default.red("Failed to fetch status"));
|
|
@@ -19446,7 +19446,7 @@ async function startCommand() {
|
|
|
19446
19446
|
}
|
|
19447
19447
|
const totalUnread = session.unreadCount ?? session.unreadMessages.length;
|
|
19448
19448
|
if (totalUnread > 5) {
|
|
19449
|
-
console.log(source_default.dim(` \u2026and ${totalUnread - 5} more \u2014 run \`
|
|
19449
|
+
console.log(source_default.dim(` \u2026and ${totalUnread - 5} more \u2014 run \`shipster inbox\``));
|
|
19450
19450
|
}
|
|
19451
19451
|
}
|
|
19452
19452
|
console.log();
|
|
@@ -19835,7 +19835,7 @@ ${live.length} live, ${rows.length - live.length} revoked.`);
|
|
|
19835
19835
|
console.log(source_default.yellow("Cannot tell which of these you are holding: this config authenticates with a shared secret, not an agent token."));
|
|
19836
19836
|
}
|
|
19837
19837
|
if (live.length > 1) {
|
|
19838
|
-
console.log(source_default.yellow(`${live.length} live tokens: every one of them authenticates. '
|
|
19838
|
+
console.log(source_default.yellow(`${live.length} live tokens: every one of them authenticates. 'shipster token rotate' collapses them to one.`));
|
|
19839
19839
|
}
|
|
19840
19840
|
const dormant = live.filter((r) => !r.lastUsedAt && r.current !== true);
|
|
19841
19841
|
if (dormant.length) {
|
|
@@ -20028,7 +20028,7 @@ async function repoInviteCommand(opts) {
|
|
|
20028
20028
|
console.log(`
|
|
20029
20029
|
${source_default.bold("Send this to the new agent:")}
|
|
20030
20030
|
|
|
20031
|
-
${source_default.cyan(`
|
|
20031
|
+
${source_default.cyan(`shipster login --invite ${code}`)}
|
|
20032
20032
|
|
|
20033
20033
|
${source_default.dim("expires:")} ${invite.expiresAt}
|
|
20034
20034
|
${source_default.dim("invite id:")} ${invite.id}
|
|
@@ -20080,8 +20080,8 @@ async function loginCommand(opts) {
|
|
|
20080
20080
|
process.exit(1);
|
|
20081
20081
|
}
|
|
20082
20082
|
if (!opts.invite && !opts.token) {
|
|
20083
|
-
console.error(source_default.red("
|
|
20084
|
-
console.error(source_default.dim("Ask a repo member to run `
|
|
20083
|
+
console.error(source_default.red("shipster login requires --invite <code> or --token <token>"));
|
|
20084
|
+
console.error(source_default.dim("Ask a repo member to run `shipster repo invite` and share the code, or create an agent in the cloud dashboard."));
|
|
20085
20085
|
process.exit(1);
|
|
20086
20086
|
}
|
|
20087
20087
|
if (opts.workerType !== void 0) {
|
|
@@ -20174,7 +20174,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
20174
20174
|
failAfterRedeem(creds, "Not in a git repo. Clone first:", [
|
|
20175
20175
|
` git clone ${repoUrl}`,
|
|
20176
20176
|
` cd ${repoName}`,
|
|
20177
|
-
`
|
|
20177
|
+
` shipster login \u2026`
|
|
20178
20178
|
]);
|
|
20179
20179
|
}
|
|
20180
20180
|
const origin = getOriginUrl(gitRoot3);
|
|
@@ -20205,7 +20205,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
20205
20205
|
"Use a separate worktree for this agent:",
|
|
20206
20206
|
` git worktree add ../${repoName}-${slug}`,
|
|
20207
20207
|
` cd ../${repoName}-${slug}`,
|
|
20208
|
-
`
|
|
20208
|
+
` shipster login \u2026`
|
|
20209
20209
|
]
|
|
20210
20210
|
);
|
|
20211
20211
|
}
|
|
@@ -20258,7 +20258,7 @@ function logoutCommand(opts = {}) {
|
|
|
20258
20258
|
const released = releaseClaimForDir(target);
|
|
20259
20259
|
if (!released) {
|
|
20260
20260
|
console.error(`No agent claim for this directory (${target}).`);
|
|
20261
|
-
console.error("Log in with `
|
|
20261
|
+
console.error("Log in with `shipster login`, or pass --all to sweep claims whose working directories are gone.");
|
|
20262
20262
|
process.exit(2);
|
|
20263
20263
|
}
|
|
20264
20264
|
console.log(`Released claim for ${released.agentName} (${released.agentId}) on ${released.workingDir}.`);
|
|
@@ -20467,7 +20467,7 @@ async function requestDeviceGrant(api, proposed, rerun) {
|
|
|
20467
20467
|
if (started.status !== 201) {
|
|
20468
20468
|
const said = started.payload?.error?.message;
|
|
20469
20469
|
throw new Error(
|
|
20470
|
-
started.status === 429 ? "Too many join requests from your network just now. Wait a minute and try again." : said ?? `Could not reach
|
|
20470
|
+
started.status === 429 ? "Too many join requests from your network just now. Wait a minute and try again." : said ?? `Could not reach shipster at ${api} (HTTP ${started.status})`
|
|
20471
20471
|
);
|
|
20472
20472
|
}
|
|
20473
20473
|
const { data, deviceCode } = started.payload;
|
|
@@ -20534,7 +20534,7 @@ function wakeFiles(workerType, root) {
|
|
|
20534
20534
|
path: join8(root, ".cursor", "rules", "pitboss.mdc"),
|
|
20535
20535
|
write: (p) => {
|
|
20536
20536
|
if (existsSync5(p) && !readFileSync8(p, "utf-8").includes("description: Pitboss wake-ups")) {
|
|
20537
|
-
throw new Error(`${p} exists and was not written by
|
|
20537
|
+
throw new Error(`${p} exists and was not written by Shipster or Pitboss; leaving it alone`);
|
|
20538
20538
|
}
|
|
20539
20539
|
writeRepoFile(p, cursorRule(cli, root), root);
|
|
20540
20540
|
}
|
|
@@ -20712,7 +20712,7 @@ async function run(opts) {
|
|
|
20712
20712
|
console.log(source_default.yellow("\n Restart this session before using pitboss."));
|
|
20713
20713
|
console.log(source_default.dim(" A running MCP client keeps the tool schema it got at initialize."));
|
|
20714
20714
|
console.log(source_default.dim("\n To add another agent, on this machine or any other:"));
|
|
20715
|
-
console.log(source_default.dim(" give it its own worktree, run
|
|
20715
|
+
console.log(source_default.dim(" give it its own worktree, run shipster join there,"));
|
|
20716
20716
|
console.log(source_default.dim(` and pick ${repoName} on the approval screen.`));
|
|
20717
20717
|
console.log("");
|
|
20718
20718
|
}
|
|
@@ -20782,7 +20782,7 @@ function resolveAgentCreds({ dir, env: env3, cliConfig }) {
|
|
|
20782
20782
|
if (cliConfig) {
|
|
20783
20783
|
return validate2({ apiUrl: cliConfig.apiUrl, token: cliConfig.apiToken, agentId: cliConfig.agentId, repoId: cliConfig.repoId }, "cli-config");
|
|
20784
20784
|
}
|
|
20785
|
-
return { ok: false, reason: `no
|
|
20785
|
+
return { ok: false, reason: `no shipster agent configured for ${start}: run \`shipster join\` there` };
|
|
20786
20786
|
}
|
|
20787
20787
|
|
|
20788
20788
|
// src/commands/watch.ts
|
|
@@ -20978,7 +20978,7 @@ async function watchOnce(dir, since) {
|
|
|
20978
20978
|
}
|
|
20979
20979
|
return printAndExit(wakeLine(wake.first, wake.count, agentId), 0);
|
|
20980
20980
|
} catch (err) {
|
|
20981
|
-
console.error(`
|
|
20981
|
+
console.error(`shipster watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
|
|
20982
20982
|
await sleep2(backoffMs);
|
|
20983
20983
|
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
20984
20984
|
}
|
|
@@ -21040,7 +21040,7 @@ async function runStream(o) {
|
|
|
21040
21040
|
throw new Error("event stream closed");
|
|
21041
21041
|
} catch (err) {
|
|
21042
21042
|
if (o.signal?.aborted) break;
|
|
21043
|
-
o.warn(`
|
|
21043
|
+
o.warn(`shipster watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
|
|
21044
21044
|
await sleep2(backoffMs);
|
|
21045
21045
|
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
21046
21046
|
}
|
|
@@ -21104,7 +21104,7 @@ async function watchStatus(o) {
|
|
|
21104
21104
|
return { line: `cursor=${cursor} waiting=${waiting}${truncated ? "+" : ""}` };
|
|
21105
21105
|
}
|
|
21106
21106
|
function usageError(message) {
|
|
21107
|
-
console.error(`
|
|
21107
|
+
console.error(`shipster watch: ${message}`);
|
|
21108
21108
|
process.exit(2);
|
|
21109
21109
|
}
|
|
21110
21110
|
async function statusCommand2(dir) {
|
|
@@ -21123,7 +21123,7 @@ async function statusCommand2(dir) {
|
|
|
21123
21123
|
if ("refusal" in status2) return refuse2(status2.refusal);
|
|
21124
21124
|
console.log(status2.line);
|
|
21125
21125
|
} catch (err) {
|
|
21126
|
-
console.error(`
|
|
21126
|
+
console.error(`shipster watch --status: ${err.message}`);
|
|
21127
21127
|
process.exit(1);
|
|
21128
21128
|
}
|
|
21129
21129
|
}
|
|
@@ -21145,7 +21145,7 @@ async function watchCommand(options) {
|
|
|
21145
21145
|
const key = wakeKey(agentId, dir);
|
|
21146
21146
|
if (since) applySince(state, key, since);
|
|
21147
21147
|
const resuming = readCursor(state, key) !== null;
|
|
21148
|
-
console.log(source_default.bold("
|
|
21148
|
+
console.log(source_default.bold("shipster watch") + source_default.dim(` \u2014 ${agentId}`));
|
|
21149
21149
|
console.log(source_default.dim(
|
|
21150
21150
|
`Streaming live events you're subscribed to. Ctrl-C to stop.` + (kinds ? ` Filtering: ${[...kinds].join(", ")}` : "") + (resuming ? `
|
|
21151
21151
|
(Events since the last one printed are replayed first.)` : `
|
|
@@ -21611,16 +21611,16 @@ function isMutableSource(path3) {
|
|
|
21611
21611
|
return !NOT_SOURCE.some((part) => rooted.includes(part));
|
|
21612
21612
|
}
|
|
21613
21613
|
var pathspec = (paths) => paths && paths.length > 0 ? ["--", ...paths] : [];
|
|
21614
|
-
function changedLines({ base: base2, cwd, paths, exec:
|
|
21615
|
-
const diff =
|
|
21614
|
+
function changedLines({ base: base2, cwd, paths, exec: exec5 = defaultExec }) {
|
|
21615
|
+
const diff = exec5("git", ["diff", "-U0", "--no-color", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
|
|
21616
21616
|
const lines3 = /* @__PURE__ */ new Map();
|
|
21617
21617
|
for (const [path3, numbers] of parseUnifiedZero(diff)) {
|
|
21618
21618
|
if (isMutableSource(path3)) lines3.set(path3, numbers);
|
|
21619
21619
|
}
|
|
21620
21620
|
return lines3;
|
|
21621
21621
|
}
|
|
21622
|
-
function changedTestFiles({ base: base2, cwd, paths, exec:
|
|
21623
|
-
const out =
|
|
21622
|
+
function changedTestFiles({ base: base2, cwd, paths, exec: exec5 = defaultExec }) {
|
|
21623
|
+
const out = exec5("git", ["diff", "--name-only", "--diff-filter=AM", `${base2}...HEAD`, ...pathspec(paths)], { cwd });
|
|
21624
21624
|
return out.split("\n").filter((path3) => TEST.test(path3));
|
|
21625
21625
|
}
|
|
21626
21626
|
|
|
@@ -21976,8 +21976,8 @@ function importPattern(file) {
|
|
|
21976
21976
|
const base2 = basename2(file).replace(/\.[^.]+$/, "").replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
|
|
21977
21977
|
return `/${base2}(\\.[jt]sx?|\\.mjs)?['"]`;
|
|
21978
21978
|
}
|
|
21979
|
-
function directTestFiles(pkgDir, file,
|
|
21980
|
-
const out =
|
|
21979
|
+
function directTestFiles(pkgDir, file, exec5 = defaultGrep) {
|
|
21980
|
+
const out = exec5("git", ["grep", "-lE", importPattern(file), "--", "*.test.ts", "*.test.tsx", "*.test.js", "*.test.mjs"], pkgDir);
|
|
21981
21981
|
return out.split("\n").filter(Boolean).map((rel) => resolve5(pkgDir, rel));
|
|
21982
21982
|
}
|
|
21983
21983
|
function defaultGrep(cmd, args, cwd) {
|
|
@@ -22199,11 +22199,13 @@ async function executeMutants(input) {
|
|
|
22199
22199
|
};
|
|
22200
22200
|
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
22201
22201
|
try {
|
|
22202
|
+
const totalDeadline = input.totalBudgetMs === void 0 ? Infinity : now() + input.totalBudgetMs;
|
|
22202
22203
|
const files = [...new Set(input.candidates.map((c) => c.file))];
|
|
22203
22204
|
const baselines = /* @__PURE__ */ new Map();
|
|
22204
22205
|
const fileQueue = [...files];
|
|
22205
22206
|
const baselineWorker = async (workerIndex) => {
|
|
22206
22207
|
for (; ; ) {
|
|
22208
|
+
if (now() >= totalDeadline) return;
|
|
22207
22209
|
const file = fileQueue.shift();
|
|
22208
22210
|
if (file === void 0) return;
|
|
22209
22211
|
try {
|
|
@@ -22221,7 +22223,11 @@ async function executeMutants(input) {
|
|
|
22221
22223
|
const slowSeen = /* @__PURE__ */ new Set();
|
|
22222
22224
|
for (const candidate of input.candidates) {
|
|
22223
22225
|
const base2 = baselines.get(candidate.file);
|
|
22224
|
-
if (base2 ===
|
|
22226
|
+
if (base2 === void 0) {
|
|
22227
|
+
out.skippedDeadline += 1;
|
|
22228
|
+
continue;
|
|
22229
|
+
}
|
|
22230
|
+
if (base2 === null) {
|
|
22225
22231
|
out.errored += 1;
|
|
22226
22232
|
continue;
|
|
22227
22233
|
}
|
|
@@ -22249,7 +22255,7 @@ async function executeMutants(input) {
|
|
|
22249
22255
|
okMs.push(baselines.get(candidate.file).ms);
|
|
22250
22256
|
}
|
|
22251
22257
|
const baselineMs = okMs.length > 0 ? okMs.reduce((a, b) => a + b, 0) / okMs.length : 1;
|
|
22252
|
-
const deadline = now() + input.budgetMs;
|
|
22258
|
+
const deadline = Math.min(now() + input.budgetMs, totalDeadline);
|
|
22253
22259
|
const planned = plannedCount({ budgetMs: input.budgetMs, workers: input.workers, baselineMs });
|
|
22254
22260
|
const sampled = sampleMutants(runnable, planned, input.riskyPatterns);
|
|
22255
22261
|
out.skippedSampled = sampled.skipped.length;
|
|
@@ -22339,6 +22345,7 @@ async function runMutateDiff(options) {
|
|
|
22339
22345
|
session,
|
|
22340
22346
|
workers,
|
|
22341
22347
|
budgetMs: options.budgetMs,
|
|
22348
|
+
totalBudgetMs: options.totalBudgetMs,
|
|
22342
22349
|
baselineSkipMs: options.baselineSkipMs ?? DEFAULT_BASELINE_SKIP_MS,
|
|
22343
22350
|
riskyPatterns: config.riskyPatterns
|
|
22344
22351
|
});
|
|
@@ -22377,6 +22384,7 @@ async function mutateDiffCommand(opts) {
|
|
|
22377
22384
|
cwd: opts.cwd ?? process.cwd(),
|
|
22378
22385
|
base: base2,
|
|
22379
22386
|
budgetMs: positive(opts.budget, 120) * 1e3,
|
|
22387
|
+
totalBudgetMs: opts.totalBudget === void 0 ? void 0 : positive(opts.totalBudget, 0) * 1e3 || void 0,
|
|
22380
22388
|
timeoutMs: positive(opts.timeout, 60) * 1e3,
|
|
22381
22389
|
baselineTimeoutMs: positive(opts.baselineTimeout, 300) * 1e3,
|
|
22382
22390
|
paths: opts.paths ? opts.paths.split(",").map((p) => p.trim()).filter(Boolean) : void 0
|
|
@@ -22676,7 +22684,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
|
|
|
22676
22684
|
|
|
22677
22685
|
**Playbook rules:**
|
|
22678
22686
|
- Take the PR head sha from the PR itself (\`gh pr view <n> --json headRefOid\`), never from a handoff message or a task comment.
|
|
22679
|
-
- One branch per order, cut from the default branch: never stack on another order's unmerged branch
|
|
22687
|
+
- One branch per order, cut from the default branch: never stack on another order's unmerged branch.
|
|
22688
|
+
- A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
|
|
22680
22689
|
case "tester":
|
|
22681
22690
|
return `## Your role: Tester
|
|
22682
22691
|
|
|
@@ -22690,7 +22699,8 @@ Write, fix, and run tests on the branch in branchName metadata.
|
|
|
22690
22699
|
- If tests cannot pass due to an implementation bug: \`mcp__relai__add_task_comment\`
|
|
22691
22700
|
(type: "finding") with type "bug", severity "blocking", then
|
|
22692
22701
|
\`mcp__relai__assign_task\` back to the writer (previous assignee or list_agents).
|
|
22693
|
-
Do not complete
|
|
22702
|
+
Do not complete.
|
|
22703
|
+
- A test may delete only paths under a directory it created with mkdtemp in that run. Never remove, empty or clear /tmp, $TMPDIR or any other shared directory as a whole, and never point a cleanup or reset at the real one.`;
|
|
22694
22704
|
case "devops":
|
|
22695
22705
|
return `## Your role: DevOps
|
|
22696
22706
|
|
|
@@ -22706,6 +22716,7 @@ Handle CI/CD, infrastructure, build/deploy configuration.
|
|
|
22706
22716
|
throw new Error(`Unknown specialization "${spec}" \u2014 valid values: reviewer, architect, writer, tester, devops`);
|
|
22707
22717
|
}
|
|
22708
22718
|
}
|
|
22719
|
+
var PR_FROM_ORDER = "In this section `<prUrl>` is the `prUrl` that `mcp__relai__get_order` returns for `metadata.orderId`. Never use the task's own `metadata.prUrl`. If the `epicId` get_order returns is not this task's own `epicId`, if get_order has no `prUrl`, or the task's `metadata.prUrl` is set and differs from it, run no `gh` command: block the task with a question naming the orchestrator, and stop.";
|
|
22709
22720
|
var ORDER_RULES = `## Factory orders
|
|
22710
22721
|
|
|
22711
22722
|
An order's tasks carry
|
|
@@ -22774,7 +22785,7 @@ Find the default branch with \`git remote show origin\`.
|
|
|
22774
22785
|
is done. With one gate, block it on the build tasks alone.
|
|
22775
22786
|
- \`ci_passed\` gets no gate task: the merge task checks CI. Say so on the planning task's thread.
|
|
22776
22787
|
5. One merge task, blocked by every gate task (or every build task when there are no gates),
|
|
22777
|
-
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge"
|
|
22788
|
+
\`assignedTo: "@auto"\`, \`metadata: { orderId, orderRole: "merge" }\`. The server stamps \`prUrl\` on it from the order, so you may omit it.
|
|
22778
22789
|
If the order has \`pairBuilds\`, every build task needs \`verifyKind: "reviewer_agent"\` and a
|
|
22779
22790
|
\`verifyReviewerId\` on a different \`workerType\` from the writers (see \`mcp__relai__list_agents\`);
|
|
22780
22791
|
the API refuses a paired build task without one and routes it away from the reviewer's vendor.
|
|
@@ -22787,26 +22798,28 @@ Find the default branch with \`git remote show origin\`.
|
|
|
22787
22798
|
\`metadata.orderKind: "review"\`): the goal is one review posted on the existing pull request at the
|
|
22788
22799
|
order's \`prUrl\`. There is no order branch, no draft PR and no merge, so skip steps 2 to 6 above.
|
|
22789
22800
|
Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
22801
|
+
${PR_FROM_ORDER}
|
|
22790
22802
|
1. As the planner, read the PR with \`gh pr view <prUrl>\` and \`gh pr diff <prUrl>\`. Pick the
|
|
22791
22803
|
angles it needs: correctness, security, simplicity, tests and consistency with the codebase for a
|
|
22792
22804
|
real change; correctness and consistency alone for a small, low-risk one (docs, tests, under about
|
|
22793
22805
|
150 changed lines with no auth, input handling, secrets, money or migrations).
|
|
22794
22806
|
2. One task per angle, \`epicId\` = the order's \`epicId\`,
|
|
22795
|
-
\`metadata: { orderId, orderRole: "review", angle: "<angle>"
|
|
22807
|
+
\`metadata: { orderId, orderRole: "review", angle: "<angle>" }\`; the server stamps \`prUrl\`. Spread the angles
|
|
22796
22808
|
across vendors: assign each to a different agent from \`mcp__relai__list_agents\`, preferring a
|
|
22797
22809
|
different \`workerType\` from the agents already given one, and reuse agents only when there are
|
|
22798
22810
|
more angles than agents. Each description names the angle, the PR, what that angle hunts for, and
|
|
22799
22811
|
the reporting format below.
|
|
22800
22812
|
3. One lead task, \`blockedBy\` every angle task, assigned to yourself, with
|
|
22801
|
-
\`metadata: { orderId, orderRole: "review_lead"
|
|
22813
|
+
\`metadata: { orderId, orderRole: "review_lead" }\`.
|
|
22802
22814
|
4. When acceptance includes \`human_approval\`, add a gate task blocked by the lead
|
|
22803
22815
|
(\`metadata: { orderId, orderRole: "gate" }\`, \`verifyKind: "human_approval"\`) and a post task
|
|
22804
|
-
blocked by the gate (\`metadata: { orderId, orderRole: "post_review"
|
|
22816
|
+
blocked by the gate (\`metadata: { orderId, orderRole: "post_review" }\`).
|
|
22805
22817
|
5. If the order has \`relatedRepos\`, copy them into every angle and lead task's metadata and name them in
|
|
22806
22818
|
each description, so reviewers check the contracts this PR depends on in those repos.
|
|
22807
22819
|
6. Post the task list on the planning task's thread and complete it.
|
|
22808
22820
|
|
|
22809
22821
|
**orderRole "review"** (one angle of a review goal):
|
|
22822
|
+
- ${PR_FROM_ORDER} For the PR head, \`<n>\` is that PR's number.
|
|
22810
22823
|
- Read the diff and as much surrounding code as you need. To run anything, check out the PR head
|
|
22811
22824
|
detached: \`git fetch origin pull/<n>/head && git checkout --detach FETCH_HEAD\`. Never push, never
|
|
22812
22825
|
comment on the PR, never edit the repo's history.
|
|
@@ -22822,6 +22835,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
|
22822
22835
|
- Post the report with \`mcp__relai__add_task_comment\` on this task, then complete it.
|
|
22823
22836
|
|
|
22824
22837
|
**orderRole "review_lead"**:
|
|
22838
|
+
- ${PR_FROM_ORDER}
|
|
22825
22839
|
- Read every angle task's report with \`mcp__relai__get_task_comments\`. Merge duplicates, drop
|
|
22826
22840
|
findings you cannot reproduce or whose premise is wrong, and rank what is left. Keep an UNPROVEN mark
|
|
22827
22841
|
on any finding about a related repo nobody could read. Severity sets the
|
|
@@ -22834,7 +22848,7 @@ Nobody pushes to the PR's branch, approves it, or requests changes on it.
|
|
|
22834
22848
|
and put the review URL on the task's thread. With a gate, post the review as a comment on this task
|
|
22835
22849
|
for the owner to approve, and do not touch the PR.
|
|
22836
22850
|
|
|
22837
|
-
**orderRole "post_review"**:
|
|
22851
|
+
**orderRole "post_review"**: ${PR_FROM_ORDER} Once the gate is approved, post the lead's review comment from the
|
|
22838
22852
|
lead task's thread with \`gh pr review <prUrl> --comment --body-file <file>\`, put the review URL on
|
|
22839
22853
|
this task's thread, and complete.
|
|
22840
22854
|
|
|
@@ -22864,6 +22878,8 @@ this task's thread, and complete.
|
|
|
22864
22878
|
the rest; if it is not recorded, reject and name it so the builder records it.
|
|
22865
22879
|
|
|
22866
22880
|
**orderRole "merge"**:
|
|
22881
|
+
${PR_FROM_ORDER}
|
|
22882
|
+
|
|
22867
22883
|
**Playbook rules:**
|
|
22868
22884
|
- Report a merge you cannot clear under step 3 the same minute: post which check failed, at which sha, and who must act on the merge task's thread, then set the task blocked.
|
|
22869
22885
|
- Never merge on a stale head: re-read the PR head sha immediately before merging, pass it as \`--match-head-commit\`, and stop if it differs from the sha the gates approved, or the carried head after an Exit 0 conflict-only remerge.
|
|
@@ -22963,28 +22979,28 @@ var defaultExec2 = (file, args, opts) => new Promise((resolve19, reject) => {
|
|
|
22963
22979
|
function validBranch(name) {
|
|
22964
22980
|
return BRANCH_RE.test(name) && !name.startsWith("-") && !name.includes("..") && !name.endsWith("/") && !name.endsWith(".lock");
|
|
22965
22981
|
}
|
|
22966
|
-
function git2(
|
|
22967
|
-
return
|
|
22982
|
+
function git2(exec5, repoPath, args, maxBuffer = LESSONS_MAX_BUFFER) {
|
|
22983
|
+
return exec5("git", args, { cwd: repoPath, timeout: GIT_TIMEOUT_MS, maxBuffer });
|
|
22968
22984
|
}
|
|
22969
|
-
async function remoteBranchSha(
|
|
22985
|
+
async function remoteBranchSha(exec5, repoPath, branch) {
|
|
22970
22986
|
try {
|
|
22971
|
-
const out = (await git2(
|
|
22987
|
+
const out = (await git2(exec5, repoPath, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}^{commit}`])).trim();
|
|
22972
22988
|
return SHA_RE.test(out) ? out : null;
|
|
22973
22989
|
} catch {
|
|
22974
22990
|
return null;
|
|
22975
22991
|
}
|
|
22976
22992
|
}
|
|
22977
|
-
async function resolveDefaultBranch(repoPath,
|
|
22993
|
+
async function resolveDefaultBranch(repoPath, exec5) {
|
|
22978
22994
|
const candidates = [];
|
|
22979
22995
|
try {
|
|
22980
|
-
const out = (await git2(
|
|
22996
|
+
const out = (await git2(exec5, repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).trim();
|
|
22981
22997
|
if (out.startsWith("origin/")) candidates.push(out.slice("origin/".length));
|
|
22982
22998
|
} catch {
|
|
22983
22999
|
}
|
|
22984
23000
|
candidates.push("main", "master");
|
|
22985
23001
|
for (const branch of candidates) {
|
|
22986
23002
|
if (!validBranch(branch)) continue;
|
|
22987
|
-
const sha = await remoteBranchSha(
|
|
23003
|
+
const sha = await remoteBranchSha(exec5, repoPath, branch);
|
|
22988
23004
|
if (sha) return { branch, sha };
|
|
22989
23005
|
}
|
|
22990
23006
|
return null;
|
|
@@ -23000,10 +23016,10 @@ function entryFor(repoPath, sha) {
|
|
|
23000
23016
|
}
|
|
23001
23017
|
return entry;
|
|
23002
23018
|
}
|
|
23003
|
-
async function blobAt(
|
|
23019
|
+
async function blobAt(exec5, repoPath, ref, path3) {
|
|
23004
23020
|
let listing;
|
|
23005
23021
|
try {
|
|
23006
|
-
listing = await git2(
|
|
23022
|
+
listing = await git2(exec5, repoPath, ["ls-tree", "-z", ref, "--", path3]);
|
|
23007
23023
|
} catch {
|
|
23008
23024
|
return null;
|
|
23009
23025
|
}
|
|
@@ -23012,35 +23028,35 @@ async function blobAt(exec4, repoPath, ref, path3) {
|
|
|
23012
23028
|
const match = row.match(/^(\d{6}) (\w+) ([0-9a-f]{40})\t(.+)$/);
|
|
23013
23029
|
if (!match || match[4] !== path3 || match[2] !== "blob" || !BLOB_MODES.has(match[1])) return null;
|
|
23014
23030
|
try {
|
|
23015
|
-
return await git2(
|
|
23031
|
+
return await git2(exec5, repoPath, ["cat-file", "blob", match[3]]);
|
|
23016
23032
|
} catch {
|
|
23017
23033
|
return null;
|
|
23018
23034
|
}
|
|
23019
23035
|
}
|
|
23020
|
-
async function readLessons(repoPath,
|
|
23036
|
+
async function readLessons(repoPath, exec5, resolved) {
|
|
23021
23037
|
for (const path3 of LESSONS_PATHS) {
|
|
23022
|
-
const text = await blobAt(
|
|
23038
|
+
const text = await blobAt(exec5, repoPath, resolved.sha, path3);
|
|
23023
23039
|
if (text === void 0) continue;
|
|
23024
23040
|
if (text === null || text.includes("\0")) return null;
|
|
23025
23041
|
return { text, branch: resolved.branch, sha: resolved.sha };
|
|
23026
23042
|
}
|
|
23027
23043
|
return null;
|
|
23028
23044
|
}
|
|
23029
|
-
async function loadLessons(repoPath,
|
|
23030
|
-
const resolved = await resolveDefaultBranch(repoPath,
|
|
23045
|
+
async function loadLessons(repoPath, exec5) {
|
|
23046
|
+
const resolved = await resolveDefaultBranch(repoPath, exec5);
|
|
23031
23047
|
if (!resolved) return null;
|
|
23032
23048
|
const entry = entryFor(repoPath, resolved.sha);
|
|
23033
|
-
if (!entry.lessons) entry.lessons = readLessons(repoPath,
|
|
23049
|
+
if (!entry.lessons) entry.lessons = readLessons(repoPath, exec5, resolved);
|
|
23034
23050
|
return entry.lessons;
|
|
23035
23051
|
}
|
|
23036
|
-
async function loadTree(repoPath,
|
|
23037
|
-
const resolved = await resolveDefaultBranch(repoPath,
|
|
23052
|
+
async function loadTree(repoPath, exec5) {
|
|
23053
|
+
const resolved = await resolveDefaultBranch(repoPath, exec5);
|
|
23038
23054
|
if (!resolved) return null;
|
|
23039
23055
|
const entry = entryFor(repoPath, resolved.sha);
|
|
23040
23056
|
if (!entry.tree) {
|
|
23041
23057
|
entry.tree = (async () => {
|
|
23042
23058
|
try {
|
|
23043
|
-
const out = await git2(
|
|
23059
|
+
const out = await git2(exec5, repoPath, ["ls-tree", "-r", "-z", "--name-only", resolved.sha], TREE_MAX_BUFFER);
|
|
23044
23060
|
const names2 = out.split("\0").filter((n) => n !== "");
|
|
23045
23061
|
if (names2.length > MAX_TREE_ENTRIES) return null;
|
|
23046
23062
|
return new Set(names2);
|
|
@@ -23492,8 +23508,8 @@ async function lessonsBlock(config, deps = {}) {
|
|
|
23492
23508
|
try {
|
|
23493
23509
|
const budget = budgetFromEnv(deps.env ?? process.env);
|
|
23494
23510
|
if (budget === 0) return "";
|
|
23495
|
-
const
|
|
23496
|
-
const loaded = await loadLessons(config.repoPath,
|
|
23511
|
+
const exec5 = deps.exec ?? defaultExec2;
|
|
23512
|
+
const loaded = await loadLessons(config.repoPath, exec5);
|
|
23497
23513
|
if (!loaded) return "";
|
|
23498
23514
|
const { sections, warnings } = parseLessons(loaded.text);
|
|
23499
23515
|
const droppedCount = warnings.filter((w) => w === "dropped_instruction").length;
|
|
@@ -23501,7 +23517,7 @@ async function lessonsBlock(config, deps = {}) {
|
|
|
23501
23517
|
if (sections.length === 0) return warnings.length > 0 ? skip("unusable_file") : "";
|
|
23502
23518
|
let paths = [];
|
|
23503
23519
|
if (sections.some((s) => s.paths !== null)) {
|
|
23504
|
-
const tree = await loadTree(config.repoPath,
|
|
23520
|
+
const tree = await loadTree(config.repoPath, exec5);
|
|
23505
23521
|
if (tree) {
|
|
23506
23522
|
const tasks = await (deps.fetchTasks ?? listSessionTaskRows)(config);
|
|
23507
23523
|
paths = candidatePaths(tasks, tree);
|
|
@@ -23608,19 +23624,19 @@ async function fetchRepoRow(config, repoId, doFetch) {
|
|
|
23608
23624
|
return { reachable: false };
|
|
23609
23625
|
}
|
|
23610
23626
|
}
|
|
23611
|
-
async function originOf(root,
|
|
23627
|
+
async function originOf(root, exec5) {
|
|
23612
23628
|
try {
|
|
23613
|
-
const out = (await
|
|
23629
|
+
const out = (await exec5("git", ["remote", "get-url", "origin"], { cwd: root, timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 })).trim();
|
|
23614
23630
|
return out === "" ? null : out;
|
|
23615
23631
|
} catch {
|
|
23616
23632
|
return null;
|
|
23617
23633
|
}
|
|
23618
23634
|
}
|
|
23619
|
-
async function checkoutMatches(repoPath, repoUrl,
|
|
23635
|
+
async function checkoutMatches(repoPath, repoUrl, exec5) {
|
|
23620
23636
|
if (repoUrl === null) return null;
|
|
23621
23637
|
try {
|
|
23622
23638
|
const root = await realpath(repoPath);
|
|
23623
|
-
const origin = await originOf(root,
|
|
23639
|
+
const origin = await originOf(root, exec5);
|
|
23624
23640
|
if (origin === null) return null;
|
|
23625
23641
|
return normalizeRepoUrl(origin) === normalizeRepoUrl(repoUrl) ? root : null;
|
|
23626
23642
|
} catch {
|
|
@@ -23629,14 +23645,14 @@ async function checkoutMatches(repoPath, repoUrl, exec4) {
|
|
|
23629
23645
|
}
|
|
23630
23646
|
async function repoContextBlock(target, config, deps = {}) {
|
|
23631
23647
|
const log = deps.log ?? ((line) => console.log(line));
|
|
23632
|
-
const
|
|
23648
|
+
const exec5 = deps.exec ?? defaultExec2;
|
|
23633
23649
|
const doFetch = deps.fetch ?? ((...args) => fetch(...args));
|
|
23634
23650
|
const row = await fetchRepoRow(config, target.repoId, doFetch);
|
|
23635
23651
|
if (!row.reachable) {
|
|
23636
23652
|
log("[context] skipped: repo_unreachable");
|
|
23637
23653
|
return "";
|
|
23638
23654
|
}
|
|
23639
|
-
const root = await checkoutMatches(target.repoPath, row.repoUrl,
|
|
23655
|
+
const root = await checkoutMatches(target.repoPath, row.repoUrl, exec5);
|
|
23640
23656
|
if (root === null) {
|
|
23641
23657
|
log("[context] skipped: path_mismatch");
|
|
23642
23658
|
return "";
|
|
@@ -23651,7 +23667,7 @@ async function repoContextBlock(target, config, deps = {}) {
|
|
|
23651
23667
|
repoPath: root,
|
|
23652
23668
|
specialization: target.role
|
|
23653
23669
|
},
|
|
23654
|
-
{ exec:
|
|
23670
|
+
{ exec: exec5, env: deps.env, log, fetchTasks: deps.fetchTasks }
|
|
23655
23671
|
);
|
|
23656
23672
|
if (lessons !== "") parts.push(lessons);
|
|
23657
23673
|
const merged = await fetchRepoProfile(config, target.repoId, { fetch: doFetch });
|
|
@@ -24210,7 +24226,7 @@ function claudeArgs(opts) {
|
|
|
24210
24226
|
...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
|
|
24211
24227
|
];
|
|
24212
24228
|
}
|
|
24213
|
-
async function runClaudeSession(config) {
|
|
24229
|
+
async function runClaudeSession(config, opts = {}) {
|
|
24214
24230
|
const repoContextOn = readEnv("REPO_CONTEXT") === "1";
|
|
24215
24231
|
const lessonsPromise = repoContextOn ? Promise.resolve("") : lessonsBlock({ ...config, repoId: taskRepoId(config) });
|
|
24216
24232
|
const repoContextPromise = repoContextOn ? (readEnv("TASK_REPO_ID")?.trim() ? Promise.resolve(taskRepoId(config)) : contextRepoId(config)).then((repoId) => repoContextBlock({ repoId, repoPath: config.repoPath, role: config.specialization }, config)) : Promise.resolve("");
|
|
@@ -24235,7 +24251,8 @@ async function runClaudeSession(config) {
|
|
|
24235
24251
|
const proc = spawn3(config.claudeBin, args, {
|
|
24236
24252
|
cwd: config.repoPath,
|
|
24237
24253
|
env: { ...childEnv, PATH: process.env.PATH },
|
|
24238
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
24254
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24255
|
+
signal: opts.signal
|
|
24239
24256
|
});
|
|
24240
24257
|
console.log(`[claude-worker] Process PID: ${proc.pid}`);
|
|
24241
24258
|
Promise.all([lessonsPromise, repoContextPromise]).then(([lessons, repoContext2]) => {
|
|
@@ -24394,7 +24411,7 @@ function readCursorEvent(line) {
|
|
|
24394
24411
|
}
|
|
24395
24412
|
return {};
|
|
24396
24413
|
}
|
|
24397
|
-
async function runCursorSession(config) {
|
|
24414
|
+
async function runCursorSession(config, opts = {}) {
|
|
24398
24415
|
const model = pinnedModel();
|
|
24399
24416
|
recordLaunchedModel(model);
|
|
24400
24417
|
const args = cursorArgs(config.repoPath, buildPrompt(config, { lessons: await lessonsBlock({ ...config, repoId: taskRepoId(config) }) }), model);
|
|
@@ -24402,7 +24419,7 @@ async function runCursorSession(config) {
|
|
|
24402
24419
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24403
24420
|
const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
|
|
24404
24421
|
return new Promise((resolve19, reject) => {
|
|
24405
|
-
const proc = spawn4(bin(), args, { cwd: config.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
24422
|
+
const proc = spawn4(bin(), args, { cwd: config.repoPath, env: workerEnv("cursor", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
24406
24423
|
console.log(`[cursor-worker] Process PID: ${proc.pid}`);
|
|
24407
24424
|
const toolsUsed = [];
|
|
24408
24425
|
let buffer = "";
|
|
@@ -24514,7 +24531,7 @@ function readCopilotEvent(line) {
|
|
|
24514
24531
|
if (type === "session.warning" && POLICY_BLOCKS.some((re) => re.test(message))) return { error: message };
|
|
24515
24532
|
return {};
|
|
24516
24533
|
}
|
|
24517
|
-
async function runCopilotSession(config) {
|
|
24534
|
+
async function runCopilotSession(config, opts = {}) {
|
|
24518
24535
|
const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
|
|
24519
24536
|
const model = pinnedModel();
|
|
24520
24537
|
recordLaunchedModel(model);
|
|
@@ -24523,7 +24540,7 @@ async function runCopilotSession(config) {
|
|
|
24523
24540
|
const taskIdsPromise = listSessionTaskIds(config, "[copilot-worker]");
|
|
24524
24541
|
console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
|
|
24525
24542
|
return new Promise((resolve19, reject) => {
|
|
24526
|
-
const proc = spawn5(bin2(), args, { cwd: config.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
24543
|
+
const proc = spawn5(bin2(), args, { cwd: config.repoPath, env: workerEnv("copilot", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
24527
24544
|
console.log(`[copilot-worker] Process PID: ${proc.pid}`);
|
|
24528
24545
|
const toolsUsed = [];
|
|
24529
24546
|
let buffer = "";
|
|
@@ -24641,7 +24658,7 @@ function readCodexEvent(line) {
|
|
|
24641
24658
|
if (event.type === "error") return { error: String(event.message ?? "codex error") };
|
|
24642
24659
|
return {};
|
|
24643
24660
|
}
|
|
24644
|
-
async function runCodexSession(config) {
|
|
24661
|
+
async function runCodexSession(config, opts = {}) {
|
|
24645
24662
|
const profile = `pitboss-${config.agentId}`;
|
|
24646
24663
|
mkdirSync9(codexHome(), { recursive: true, mode: 448 });
|
|
24647
24664
|
const profilePath = join17(codexHome(), `${profile}.config.toml`);
|
|
@@ -24667,7 +24684,7 @@ async function runCodexSession(config) {
|
|
|
24667
24684
|
const taskIdsPromise = listSessionTaskIds(config, "[codex-worker]");
|
|
24668
24685
|
console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
|
|
24669
24686
|
return new Promise((resolve19, reject) => {
|
|
24670
|
-
const proc = spawn6(bin3(), args, { cwd: config.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"] });
|
|
24687
|
+
const proc = spawn6(bin3(), args, { cwd: config.repoPath, env: workerEnv("codex", process.env, grantFromEnv()), stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
24671
24688
|
console.log(`[codex-worker] Process PID: ${proc.pid}`);
|
|
24672
24689
|
const toolsUsed = [];
|
|
24673
24690
|
let buffer = "";
|
|
@@ -25117,6 +25134,10 @@ function seatKindOf(a, i, platform2) {
|
|
|
25117
25134
|
}
|
|
25118
25135
|
var ENV_REFUSED = (where) => `${where}: ownerPool entries take no env: account pools take no env grants, because every flagged repo's tasks would receive those variables`;
|
|
25119
25136
|
var OWNER_POOL_RUNTIMES = ["claude", "codex"];
|
|
25137
|
+
var OWNER_POOL_SPECIALIZATIONS = ["writer", "architect", "reviewer", "tester", "devops"];
|
|
25138
|
+
function ownerPoolSpecializationCheck(label, specialization, what) {
|
|
25139
|
+
if (!OWNER_POOL_SPECIALIZATIONS.includes(specialization)) throw new Error(`${label}: ownerPool ${what} mint ${OWNER_POOL_SPECIALIZATIONS.join(", ")} only; specialization "${specialization}" is not one of them`);
|
|
25140
|
+
}
|
|
25120
25141
|
function ownerPoolRuntimeCheck(label, runtime) {
|
|
25121
25142
|
if (!OWNER_POOL_RUNTIMES.includes(runtime)) throw new Error(`${label}: ownerPool entries run claude or codex only; the per-session reset cannot keep a ${runtime} seat's login apart from its config`);
|
|
25122
25143
|
}
|
|
@@ -25149,7 +25170,7 @@ function parsePool(a, i, specDir, platform2) {
|
|
|
25149
25170
|
const runtime = runtimeOf(a, i);
|
|
25150
25171
|
if (ownerPool) ownerPoolRuntimeCheck(`agents[${i}]`, runtime);
|
|
25151
25172
|
const specialization = specializationOf(a, i);
|
|
25152
|
-
if (ownerPool
|
|
25173
|
+
if (ownerPool) ownerPoolSpecializationCheck(`agents[${i}]`, specialization, "pools");
|
|
25153
25174
|
const max = a.max;
|
|
25154
25175
|
if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_POOL_SIZE) {
|
|
25155
25176
|
throw new Error(`agents[${i}].max must be an integer from 1 to ${MAX_POOL_SIZE}`);
|
|
@@ -25205,6 +25226,7 @@ function parseSpec(text, specDir, opts = {}) {
|
|
|
25205
25226
|
if (a.runAs === void 0) throw new Error(`${a.name}: ownerPool agents need runAs: without a seat user its home and config cannot be reset between sessions`);
|
|
25206
25227
|
}
|
|
25207
25228
|
const specialization = specializationOf(a, i);
|
|
25229
|
+
if (ownerPool) ownerPoolSpecializationCheck(a.name, specialization, "agents");
|
|
25208
25230
|
agents.push({ name: a.name, runtime, worktree: resolve7(specDir, a.worktree), specialization, ...ownerPool ? { ownerPool: true } : tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...seatKindOf(a, i, platform2) });
|
|
25209
25231
|
});
|
|
25210
25232
|
const entries = [
|
|
@@ -25335,8 +25357,11 @@ function effectiveBounds(spec, desired) {
|
|
|
25335
25357
|
const max = Math.min(desired.max, spec.max);
|
|
25336
25358
|
return { min: Math.min(desired.min, max), max, desiredMax: desired.max, capped: desired.max > spec.max };
|
|
25337
25359
|
}
|
|
25360
|
+
function busyCount(members) {
|
|
25361
|
+
return members.filter((m) => m.assigned + m.inProgress + (m.owedReviews ?? 0) > 0).length;
|
|
25362
|
+
}
|
|
25338
25363
|
function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
25339
|
-
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0;
|
|
25364
|
+
const isIdle = (m) => m.assigned === 0 && m.inProgress === 0 && (m.owedReviews ?? 0) === 0;
|
|
25340
25365
|
const running = members.filter((m) => m.running);
|
|
25341
25366
|
const stopped = members.filter((m) => !m.running);
|
|
25342
25367
|
let count = running.length;
|
|
@@ -25360,23 +25385,35 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
|
25360
25385
|
if (count >= min || count >= max) break;
|
|
25361
25386
|
take(m);
|
|
25362
25387
|
}
|
|
25363
|
-
if (start.length > 0) return { start, stop: [] };
|
|
25388
|
+
if (start.length > 0) return { start, stop: [], why: {} };
|
|
25364
25389
|
const stop = [];
|
|
25390
|
+
const why = {};
|
|
25365
25391
|
for (const m of running.filter(isIdle)) {
|
|
25366
25392
|
if (count <= max) break;
|
|
25367
25393
|
stop.push(m.name);
|
|
25394
|
+
why[m.name] = "over-cap";
|
|
25368
25395
|
count--;
|
|
25369
25396
|
}
|
|
25370
|
-
if (stop.length > 0) return { start: [], stop };
|
|
25397
|
+
if (stop.length > 0) return { start: [], stop, why };
|
|
25398
|
+
const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0).length;
|
|
25399
|
+
const freed = [];
|
|
25400
|
+
if (blockedOwners > 0 && count >= max) {
|
|
25401
|
+
for (const m of running.filter(isIdle)) {
|
|
25402
|
+
if (freed.length >= blockedOwners) break;
|
|
25403
|
+
freed.push(m.name);
|
|
25404
|
+
}
|
|
25405
|
+
if (freed.length > 0) return { start: [], stop: freed, why: Object.fromEntries(freed.map((n) => [n, "slot-for-owner"])) };
|
|
25406
|
+
}
|
|
25371
25407
|
let surplus = running.filter(isIdle).length - pending;
|
|
25372
25408
|
const eligible = running.filter((m) => isIdle(m) && m.idleMs >= idleStopMs).sort((a, b) => b.idleMs - a.idleMs);
|
|
25373
25409
|
for (const m of eligible) {
|
|
25374
25410
|
if (surplus <= 0 || count <= min) break;
|
|
25375
25411
|
stop.push(m.name);
|
|
25412
|
+
why[m.name] = "idle-timer";
|
|
25376
25413
|
count--;
|
|
25377
25414
|
surplus--;
|
|
25378
25415
|
}
|
|
25379
|
-
return { start, stop };
|
|
25416
|
+
return { start, stop, why };
|
|
25380
25417
|
}
|
|
25381
25418
|
function poolTakes(served, t) {
|
|
25382
25419
|
if (!served || served.length === 0) return true;
|
|
@@ -25385,12 +25422,12 @@ function poolTakes(served, t) {
|
|
|
25385
25422
|
if (served.includes(declared)) return true;
|
|
25386
25423
|
return served.some((d) => isDifficulty(d) && difficultyRung(d) === 2);
|
|
25387
25424
|
}
|
|
25388
|
-
function summarizeTasks(readyAll, heldAll, members, served) {
|
|
25425
|
+
function summarizeTasks(readyAll, heldAll, members, served, verifying = []) {
|
|
25389
25426
|
const ready = readyAll.filter((t) => !t.paused);
|
|
25390
25427
|
const held = heldAll.filter((t) => !(t.paused && t.status === "assigned"));
|
|
25391
25428
|
const nameOf = new Map(members.map((m) => [m.agentId, m.name]));
|
|
25392
25429
|
const counts = Object.fromEntries(
|
|
25393
|
-
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0 }])
|
|
25430
|
+
members.map((m) => [m.name, { assigned: 0, inProgress: 0, readyAssigned: 0, owedReviews: 0, verifyingOwn: 0 }])
|
|
25394
25431
|
);
|
|
25395
25432
|
const pending = ready.filter((t) => t.status === "pending" && t.autoAssign && poolTakes(served, t)).length;
|
|
25396
25433
|
let readyAssigned = 0;
|
|
@@ -25405,6 +25442,11 @@ function summarizeTasks(readyAll, heldAll, members, served) {
|
|
|
25405
25442
|
if (!name) continue;
|
|
25406
25443
|
if (t.status === "assigned") counts[name].assigned++;
|
|
25407
25444
|
if (t.status === "in_progress" || t.status === "pending_verification") counts[name].inProgress++;
|
|
25445
|
+
if (t.status === "pending_verification") counts[name].verifyingOwn++;
|
|
25446
|
+
}
|
|
25447
|
+
for (const t of verifying) {
|
|
25448
|
+
const name = t.status === "pending_verification" && t.verifyReviewerId ? nameOf.get(t.verifyReviewerId) : void 0;
|
|
25449
|
+
if (name) counts[name].owedReviews++;
|
|
25408
25450
|
}
|
|
25409
25451
|
return { pending, ready: pending + readyAssigned, members: counts };
|
|
25410
25452
|
}
|
|
@@ -25560,11 +25602,11 @@ async function abandonMinted(spec, agent, minted, reason) {
|
|
|
25560
25602
|
return;
|
|
25561
25603
|
}
|
|
25562
25604
|
console.log(source_default.red(
|
|
25563
|
-
` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id} (HTTP ${res.status}); remove it with
|
|
25605
|
+
` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id} (HTTP ${res.status}); remove it with shipster`
|
|
25564
25606
|
));
|
|
25565
25607
|
} catch {
|
|
25566
25608
|
console.log(source_default.red(
|
|
25567
|
-
` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id}; remove it with
|
|
25609
|
+
` \u2715 ${agent.name}: ${reason}; could not delete agent ${minted.id}; remove it with shipster`
|
|
25568
25610
|
));
|
|
25569
25611
|
}
|
|
25570
25612
|
}
|
|
@@ -25585,7 +25627,7 @@ function refuseNameClash(agent, existing, options) {
|
|
|
25585
25627
|
` \u2715 ${agent.name}: this repo already has an agent named ${agent.name} (id ${existing.id ?? "unknown"}, last seen ${existing.lastSeenAt ?? "never"}), and no credentials for it are in ${configDir()}`
|
|
25586
25628
|
));
|
|
25587
25629
|
console.log(source_default.yellow(
|
|
25588
|
-
` run
|
|
25630
|
+
` run shipster node up from the config directory that holds its credentials, or pass --adopt-new to register a new agent under the same name`
|
|
25589
25631
|
));
|
|
25590
25632
|
}
|
|
25591
25633
|
async function redeemInvite(spec, agent, code, have, specPath, options) {
|
|
@@ -25917,7 +25959,7 @@ async function onboardCommand(url, opts) {
|
|
|
25917
25959
|
// src/commands/node.ts
|
|
25918
25960
|
import { userInfo } from "node:os";
|
|
25919
25961
|
import { execFileSync as execFileSync15, spawn as spawnChild, spawnSync as spawnSyncChild } from "node:child_process";
|
|
25920
|
-
import { closeSync, existsSync as existsSync27, mkdirSync as mkdirSync25, openSync, readFileSync as readFileSync27, realpathSync as realpathSync13, rmSync as rmSync15 } from "node:fs";
|
|
25962
|
+
import { appendFileSync as appendFileSync3, closeSync, existsSync as existsSync27, mkdirSync as mkdirSync25, openSync, readFileSync as readFileSync27, realpathSync as realpathSync13, rmSync as rmSync15 } from "node:fs";
|
|
25921
25963
|
import { dirname as dirname24, resolve as resolve16 } from "node:path";
|
|
25922
25964
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
25923
25965
|
|
|
@@ -26911,7 +26953,7 @@ function resolveMcpServer(opts) {
|
|
|
26911
26953
|
if (exists(bundled)) return { command: execPath, args: [bundled] };
|
|
26912
26954
|
const dir = walkFor(opts.here, "mcp-server", exists);
|
|
26913
26955
|
if (dir) return checkoutMcp(dir, execPath);
|
|
26914
|
-
throw new Error("mcp-server not found next to this CLI. Run node from a
|
|
26956
|
+
throw new Error("mcp-server not found next to this CLI. Run node from a shipster checkout.");
|
|
26915
26957
|
}
|
|
26916
26958
|
function isEventWorkerCommand(command) {
|
|
26917
26959
|
return /\/tsx\/dist\/cli\.mjs \S*\/event-worker\/src\/index\.ts$/.test(command) || /(?:^|\s)\S*node(?:\.exe)? \S*\/event-worker\.js$/.test(command);
|
|
@@ -27734,9 +27776,9 @@ function prepareRoot(input, seats = false) {
|
|
|
27734
27776
|
}
|
|
27735
27777
|
async function enrollNode(input) {
|
|
27736
27778
|
const code = input.code || readEnv("NODE_ENROLL_CODE");
|
|
27737
|
-
if (!code) throw new EnrollError("No enrollment code. Pass it as an argument or set PITBOSS_NODE_ENROLL_CODE.", 2);
|
|
27779
|
+
if (!code) throw new EnrollError("No enrollment code. Pass it as an argument or set SHIPSTER_NODE_ENROLL_CODE (or PITBOSS_NODE_ENROLL_CODE).", 2);
|
|
27738
27780
|
const api = (input.api || readConfig()?.apiUrl)?.replace(/\/+$/, "");
|
|
27739
|
-
if (!api) throw new EnrollError("No API url. Pass --api <url>, or run `
|
|
27781
|
+
if (!api) throw new EnrollError("No API url. Pass --api <url>, or run `shipster login` first.", 2);
|
|
27740
27782
|
if (!input.force && readNodeCreds()) {
|
|
27741
27783
|
throw new EnrollError(`${nodeCredsPath()} already holds a node credential. To replace it, choose Re-enroll on the dashboard for a fresh code and pass --force.`);
|
|
27742
27784
|
}
|
|
@@ -27888,6 +27930,13 @@ function toSpec(desired, ctx) {
|
|
|
27888
27930
|
const name = assertName("agent name", a?.name, NAME2);
|
|
27889
27931
|
const who = `agent ${show(name)}`;
|
|
27890
27932
|
const runAs = seatOf(a, who, ctx.allow, users);
|
|
27933
|
+
if (repos.get(a.repo)?.kind === "account") {
|
|
27934
|
+
const { env: env3, tier } = a;
|
|
27935
|
+
const own = { ...common3({ ...a, env: void 0, tier: void 0 }, who, ctx.allow), env: env3, tier };
|
|
27936
|
+
const worktree2 = runAs ? seatDir(a, name) : dirIn(a, name, "agent").path;
|
|
27937
|
+
entries.push({ name, worktree: worktree2, ownerPool: true, ...own, ...runAs ? { runAs } : {} });
|
|
27938
|
+
continue;
|
|
27939
|
+
}
|
|
27891
27940
|
const worktree = runAs ? seatDir(a, name) : dirIn(a, name, "agent").path;
|
|
27892
27941
|
entries.push({ name, worktree, ...common3(a, who, ctx.allow), ...runAs ? { runAs } : {} });
|
|
27893
27942
|
}
|
|
@@ -27895,6 +27944,19 @@ function toSpec(desired, ctx) {
|
|
|
27895
27944
|
if (p?.ownerPool) throw new DesiredError("owner_pool_unsupported", `pool ${show(p?.pool)} is an owner-pool entry, which a node's desired state does not run`);
|
|
27896
27945
|
const pool = assertName("pool name", p?.pool, POOL_NAME2);
|
|
27897
27946
|
const seatUsers = poolSeatsOf(p, `pool ${show(pool)}`, ctx.allow, users);
|
|
27947
|
+
const home = repos.get(p.repo);
|
|
27948
|
+
if (home?.kind === "account") {
|
|
27949
|
+
const { env: env3, tier } = p;
|
|
27950
|
+
const own = { ...common3({ ...p, env: void 0, tier: void 0 }, `pool ${show(pool)}`, ctx.allow), env: env3, tier };
|
|
27951
|
+
let worktreeRoot = root;
|
|
27952
|
+
if (seatUsers) {
|
|
27953
|
+
for (let n = 1; n <= p.max; n++) seatDir(p, poolMemberName(pool, n));
|
|
27954
|
+
} else {
|
|
27955
|
+
worktreeRoot = dirIn(p, pool, "pool").path;
|
|
27956
|
+
}
|
|
27957
|
+
entries.push({ pool, ownerPool: true, worktreeRoot, ...own, ...seatUsers ? { runAs: { users: seatUsers } } : {}, difficulty: p.difficulty, min: p.min, max: p.max, burstMax: p.burstMax });
|
|
27958
|
+
continue;
|
|
27959
|
+
}
|
|
27898
27960
|
if (seatUsers) {
|
|
27899
27961
|
if (!repos.get(p.repo)) throw new DesiredError("repo_unavailable", `pool names repo ${show(p.repo)}, which this node cannot see`);
|
|
27900
27962
|
for (let n = 1; n <= p.max; n++) seatDir(p, poolMemberName(pool, n));
|
|
@@ -28203,6 +28265,10 @@ async function retireAgent(name, d) {
|
|
|
28203
28265
|
d.log(`${name}: its credential belongs to another server; it is left alone`);
|
|
28204
28266
|
return false;
|
|
28205
28267
|
}
|
|
28268
|
+
if (creds.desiredRoot !== d.root) {
|
|
28269
|
+
d.log(`${name}: its credential was not written by this node's root; it is left alone`);
|
|
28270
|
+
return false;
|
|
28271
|
+
}
|
|
28206
28272
|
let status2;
|
|
28207
28273
|
try {
|
|
28208
28274
|
status2 = (await d.deleteJson(`${d.api}/agents/${creds.agentId}`, creds.token)).status;
|
|
@@ -28285,7 +28351,7 @@ function createGrantIdentity(deps) {
|
|
|
28285
28351
|
const declinedUntil = /* @__PURE__ */ new Map();
|
|
28286
28352
|
const have = (e) => {
|
|
28287
28353
|
const c = deps.readCreds(e.agent.name);
|
|
28288
|
-
return c && c.api === deps.api && c.worktree === e.agent.worktree && c.repoId === e.repo.id ? c : null;
|
|
28354
|
+
return c && c.desiredRoot === deps.root && c.api === deps.api && c.worktree === e.agent.worktree && c.repoId === e.repo.id ? c : null;
|
|
28289
28355
|
};
|
|
28290
28356
|
async function redeem(group, repoId, invites) {
|
|
28291
28357
|
const { matched } = matchInvites(group.map((e) => e.agent), invites);
|
|
@@ -28497,6 +28563,7 @@ async function provisionEntries(entries, wanted, deps) {
|
|
|
28497
28563
|
for (const p of wanted.pools) for (const m of poolMembers(p)) poolOf2.set(m.name, p);
|
|
28498
28564
|
const pools = /* @__PURE__ */ new Map();
|
|
28499
28565
|
for (const { agent, repo: repo2 } of entries) {
|
|
28566
|
+
if (agent.ownerPool) continue;
|
|
28500
28567
|
const pool = poolOf2.get(agent.name);
|
|
28501
28568
|
if (!pool) {
|
|
28502
28569
|
await cloneInto(agent.worktree, repo2, deps);
|
|
@@ -28615,6 +28682,7 @@ async function provisionSeatClones(entries, deps) {
|
|
|
28615
28682
|
};
|
|
28616
28683
|
for (const { agent, repo: repo2 } of entries) {
|
|
28617
28684
|
if (!agent.runAs) continue;
|
|
28685
|
+
if (agent.ownerPool) continue;
|
|
28618
28686
|
const user = agent.runAs.user;
|
|
28619
28687
|
const dest = agent.worktree;
|
|
28620
28688
|
const uid = deps.seatUid(user);
|
|
@@ -28664,12 +28732,64 @@ async function provisionSeatClones(entries, deps) {
|
|
|
28664
28732
|
return refused;
|
|
28665
28733
|
}
|
|
28666
28734
|
|
|
28667
|
-
// src/node/owner-pool-
|
|
28735
|
+
// src/node/owner-pool-slot-guard.ts
|
|
28668
28736
|
import { execFile as execFile8 } from "node:child_process";
|
|
28669
|
-
import { chmodSync as chmodSync7, existsSync as existsSync22, lstatSync as lstatSync6, mkdirSync as mkdirSync21, readdirSync as readdirSync6, rmSync as rmSync12 } from "node:fs";
|
|
28670
|
-
import { dirname as dirname19, join as join34 } from "node:path";
|
|
28671
28737
|
import { promisify as promisify3 } from "node:util";
|
|
28672
28738
|
var exec3 = promisify3(execFile8);
|
|
28739
|
+
async function inspectCheckout(user, dest) {
|
|
28740
|
+
const runAs = realRunAsDeps();
|
|
28741
|
+
const git3 = async (...args) => {
|
|
28742
|
+
const full = ["-C", dest, ...args];
|
|
28743
|
+
if (!user) return (await exec3("git", full, { cwd: "/", timeout: 6e4 })).stdout;
|
|
28744
|
+
const info = runAs.resolveUser(user);
|
|
28745
|
+
const env3 = { PATH: process.env.PATH, HOME: info?.home ?? "/", USER: user, LOGNAME: user };
|
|
28746
|
+
const launch = planRunAsLaunch(user, "git", full, runAs);
|
|
28747
|
+
return (await exec3(launch.command, launch.args, { cwd: "/", env: env3, timeout: 6e4 })).stdout;
|
|
28748
|
+
};
|
|
28749
|
+
try {
|
|
28750
|
+
await git3("rev-parse", "--git-dir");
|
|
28751
|
+
} catch {
|
|
28752
|
+
return null;
|
|
28753
|
+
}
|
|
28754
|
+
let origin = null;
|
|
28755
|
+
try {
|
|
28756
|
+
origin = (await git3("config", "--get", "remote.origin.url")).trim() || null;
|
|
28757
|
+
} catch {
|
|
28758
|
+
origin = null;
|
|
28759
|
+
}
|
|
28760
|
+
if (origin === null) return { origin, dirty: false, unpushed: false, stashes: false };
|
|
28761
|
+
const dirty = (await git3("status", "--porcelain", "--untracked-files=all")).trim() !== "";
|
|
28762
|
+
const unpushed = (await git3("rev-list", "--branches", "--not", "--remotes", "-n", "1")).trim() !== "";
|
|
28763
|
+
const stashes = (await git3("stash", "list")).trim() !== "";
|
|
28764
|
+
return { origin, dirty, unpushed, stashes };
|
|
28765
|
+
}
|
|
28766
|
+
var realSlotGuardDeps = () => ({ inspect: inspectCheckout });
|
|
28767
|
+
async function refuseDirtySlots(agents, deps) {
|
|
28768
|
+
const refused = /* @__PURE__ */ new Map();
|
|
28769
|
+
for (const a of agents) {
|
|
28770
|
+
if (!a.ownerPool) continue;
|
|
28771
|
+
let facts;
|
|
28772
|
+
try {
|
|
28773
|
+
facts = await deps.inspect(a.runAs?.user, a.worktree);
|
|
28774
|
+
} catch (err) {
|
|
28775
|
+
const why = (err instanceof Error ? err.message : String(err)).split("\n")[0].slice(0, 160);
|
|
28776
|
+
refused.set(a.name, `owner_pool_slot_dirty: ${a.worktree} could not be inspected, so it is not reset: ${why}`);
|
|
28777
|
+
continue;
|
|
28778
|
+
}
|
|
28779
|
+
if (!facts || facts.origin === null) continue;
|
|
28780
|
+
const held = [facts.dirty && "uncommitted files", facts.unpushed && "unpushed commits", facts.stashes && "stashes"].filter(Boolean);
|
|
28781
|
+
if (held.length === 0) continue;
|
|
28782
|
+
refused.set(a.name, `owner_pool_slot_dirty: ${a.worktree} is a checkout holding ${held.join(", ")}, and an owner-pool slot is wiped before every session. Push or discard that work, or empty the directory, then the next generation adopts it.`);
|
|
28783
|
+
}
|
|
28784
|
+
return refused;
|
|
28785
|
+
}
|
|
28786
|
+
|
|
28787
|
+
// src/node/owner-pool-bases.ts
|
|
28788
|
+
import { execFile as execFile9 } from "node:child_process";
|
|
28789
|
+
import { chmodSync as chmodSync7, existsSync as existsSync22, lstatSync as lstatSync6, mkdirSync as mkdirSync21, readdirSync as readdirSync6, rmSync as rmSync12 } from "node:fs";
|
|
28790
|
+
import { dirname as dirname19, join as join34 } from "node:path";
|
|
28791
|
+
import { promisify as promisify4 } from "node:util";
|
|
28792
|
+
var exec4 = promisify4(execFile9);
|
|
28673
28793
|
var GROUP_OTHER_WRITE2 = 18;
|
|
28674
28794
|
var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
28675
28795
|
var OWN_TASK_STATUSES = "status=assigned,in_progress";
|
|
@@ -28809,13 +28929,13 @@ function realOwnerPoolBasesDeps(root, env3 = process.env, tokenHosts = [], log =
|
|
|
28809
28929
|
const runAs = realRunAsDeps();
|
|
28810
28930
|
const git3 = async (args, gitEnv) => {
|
|
28811
28931
|
try {
|
|
28812
|
-
await
|
|
28932
|
+
await exec4("git", args, { env: credentialFree ? credentialFreeEnv(gitEnv) : gitEnv, timeout: 10 * 6e4 });
|
|
28813
28933
|
} catch (err) {
|
|
28814
28934
|
throw new Error(err.stderr || (err instanceof Error ? err.message : String(err)));
|
|
28815
28935
|
}
|
|
28816
28936
|
};
|
|
28817
28937
|
const lockdown = async (dest) => {
|
|
28818
|
-
await
|
|
28938
|
+
await exec4("chmod", ["-R", "go-w", dest]);
|
|
28819
28939
|
chmodSync7(dest, 493);
|
|
28820
28940
|
chmodSync7(dirname19(dest), 493);
|
|
28821
28941
|
chmodSync7(root, 493);
|
|
@@ -29318,7 +29438,7 @@ function createCriticalNotifier(deps) {
|
|
|
29318
29438
|
`Disk critical on a node: ${describeDisk(d)}.`,
|
|
29319
29439
|
`Critical means under ${t.critGb} GB or ${t.critPct}% free.`,
|
|
29320
29440
|
`A janitor dry run would free ${freed}.`,
|
|
29321
|
-
`On the node, run:
|
|
29441
|
+
`On the node, run: shipster node janitor --dry-run`
|
|
29322
29442
|
].join("\n");
|
|
29323
29443
|
};
|
|
29324
29444
|
async function send(d, ep) {
|
|
@@ -29727,7 +29847,7 @@ async function runJanitor(opts) {
|
|
|
29727
29847
|
const empty = { removed: [], skipped: [], bytes: 0, capReached: false, errors: [] };
|
|
29728
29848
|
const base2 = { mode, dry: true, report: empty, skippedByReason: {}, removedBytes: 0, wouldRemoveBytes: 0, capReached: false, dailyCapReached: false, usedTodayBytes: 0, heldByFloor: 0, rebuildableOff: [], items: [], at };
|
|
29729
29849
|
if (mode === "live" && !settings.enabled) {
|
|
29730
|
-
return { ...base2, status: "refused", exitCode: 2, message: "this node has opted out of the janitor; run `
|
|
29850
|
+
return { ...base2, status: "refused", exitCode: 2, message: "this node has opted out of the janitor; run `shipster node janitor --enable` to turn deletion back on, or use --dry-run" };
|
|
29731
29851
|
}
|
|
29732
29852
|
const dry = mode === "dry" || !settings.enabled;
|
|
29733
29853
|
const runId = randomBytes11(6).toString("hex");
|
|
@@ -30178,9 +30298,9 @@ async function runDocker(client, handle, now, cfg) {
|
|
|
30178
30298
|
}
|
|
30179
30299
|
|
|
30180
30300
|
// src/node/janitor/docker-client.ts
|
|
30181
|
-
import { execFile as
|
|
30301
|
+
import { execFile as execFile10 } from "node:child_process";
|
|
30182
30302
|
var runDocker2 = (args) => new Promise((resolve19, reject) => {
|
|
30183
|
-
|
|
30303
|
+
execFile10("docker", args, { maxBuffer: 64 * 1024 * 1024, timeout: 6e4 }, (err, stdout, stderr) => {
|
|
30184
30304
|
if (!err) {
|
|
30185
30305
|
resolve19(stdout);
|
|
30186
30306
|
return;
|
|
@@ -30444,9 +30564,9 @@ function isBusy(path3, agents, now = Date.now(), walkLimit) {
|
|
|
30444
30564
|
}
|
|
30445
30565
|
|
|
30446
30566
|
// src/node/janitor/pr-state.ts
|
|
30447
|
-
import { execFile as
|
|
30448
|
-
import { promisify as
|
|
30449
|
-
var execFileP =
|
|
30567
|
+
import { execFile as execFile11 } from "node:child_process";
|
|
30568
|
+
import { promisify as promisify5 } from "node:util";
|
|
30569
|
+
var execFileP = promisify5(execFile11);
|
|
30450
30570
|
var realGh = async (cwd, args) => {
|
|
30451
30571
|
try {
|
|
30452
30572
|
const { stdout } = await execFileP("gh", args, {
|
|
@@ -30494,11 +30614,11 @@ function makePrStateOf(deps) {
|
|
|
30494
30614
|
}
|
|
30495
30615
|
|
|
30496
30616
|
// src/node/janitor/worktrees.ts
|
|
30497
|
-
import { execFile as
|
|
30617
|
+
import { execFile as execFile12 } from "node:child_process";
|
|
30498
30618
|
import { existsSync as existsSync24, readdirSync as readdirSync9, realpathSync as realpathSync11, statSync as statSync6 } from "node:fs";
|
|
30499
30619
|
import { dirname as dirname22, isAbsolute as isAbsolute4, join as join41, relative as relative4, sep as sep6 } from "node:path";
|
|
30500
|
-
import { promisify as
|
|
30501
|
-
var execFileP2 =
|
|
30620
|
+
import { promisify as promisify6 } from "node:util";
|
|
30621
|
+
var execFileP2 = promisify6(execFile12);
|
|
30502
30622
|
var RULE = "worktrees";
|
|
30503
30623
|
function decideWorktree(f) {
|
|
30504
30624
|
if (f.primary) return { action: "skip", reason: "primary" };
|
|
@@ -30976,7 +31096,7 @@ function logStoredOrphans(specNames, specRealpath) {
|
|
|
30976
31096
|
if (orphans.length > 0) {
|
|
30977
31097
|
console.log(source_default.yellow(`
|
|
30978
31098
|
stored credentials no longer in the spec: ${orphans.join(", ")}`));
|
|
30979
|
-
console.log(source_default.dim("
|
|
31099
|
+
console.log(source_default.dim(" shipster node prune <spec> to revoke them"));
|
|
30980
31100
|
}
|
|
30981
31101
|
if (unattributed.length > 0) {
|
|
30982
31102
|
console.log(source_default.yellow(` unattributed, not pruned: ${unattributed.join(", ")}`));
|
|
@@ -31487,7 +31607,18 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31487
31607
|
let lastSize;
|
|
31488
31608
|
const markedAt = /* @__PURE__ */ new Map();
|
|
31489
31609
|
const state = { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null, ...pool.ownerPool ? { ownerPool: true } : {} };
|
|
31490
|
-
const
|
|
31610
|
+
const poolLog = logPath(`${pool.pool}.pool`);
|
|
31611
|
+
const log = (line) => {
|
|
31612
|
+
const text = `${(/* @__PURE__ */ new Date()).toISOString()} pool ${pool.pool}: ${line}`;
|
|
31613
|
+
console.log(text);
|
|
31614
|
+
try {
|
|
31615
|
+
mkdirSync25(dirname24(poolLog), { recursive: true, mode: 448 });
|
|
31616
|
+
appendFileSync3(poolLog, `${text}
|
|
31617
|
+
`, { mode: 384 });
|
|
31618
|
+
} catch {
|
|
31619
|
+
return;
|
|
31620
|
+
}
|
|
31621
|
+
};
|
|
31491
31622
|
let busy = false;
|
|
31492
31623
|
const markStopped = async (member) => {
|
|
31493
31624
|
try {
|
|
@@ -31497,10 +31628,10 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31497
31628
|
log(`could not mark ${member.agent.name} unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
31498
31629
|
}
|
|
31499
31630
|
};
|
|
31500
|
-
const report = async (member, action, reason, ready) => {
|
|
31631
|
+
const report = async (member, action, reason, ready, size2) => {
|
|
31501
31632
|
const running = members.filter((m) => sup.isActive(m.agent.name)).length;
|
|
31502
31633
|
try {
|
|
31503
|
-
await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max });
|
|
31634
|
+
await reportScale(member.creds, { pool: pool.pool, action, reason, ready, running, max: pool.max, size: size2 });
|
|
31504
31635
|
} catch (err) {
|
|
31505
31636
|
log(`could not report ${action} ${member.agent.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31506
31637
|
}
|
|
@@ -31518,15 +31649,16 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31518
31649
|
fetchTasks(reader.creds, "ready=true", pool.ownerPool === true),
|
|
31519
31650
|
fetchTasks(reader.creds, "status=assigned,in_progress,pending_verification", pool.ownerPool === true)
|
|
31520
31651
|
]);
|
|
31652
|
+
const verifying = held.filter((t) => t.status === "pending_verification");
|
|
31521
31653
|
const { size: desired, capacityMode } = await fetchPoolSize(reader.creds, pool.pool);
|
|
31522
31654
|
if (pool.ownerPool && run4.bases) {
|
|
31523
31655
|
await run4.bases.sync(pool.pool, members.map((m) => ({ name: m.agent.name, runAs: m.agent.runAs, creds: m.creds })));
|
|
31524
31656
|
}
|
|
31525
|
-
const summary2 = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })), desired?.served);
|
|
31657
|
+
const summary2 = summarizeTasks(ready, held, members.map((m) => ({ name: m.agent.name, agentId: m.creds.agentId })), desired?.served, verifying);
|
|
31526
31658
|
const now = Date.now();
|
|
31527
31659
|
const views = members.map(({ agent }) => {
|
|
31528
31660
|
const counts = summary2.members[agent.name];
|
|
31529
|
-
if (counts.assigned + counts.inProgress > 0) lastBusy.set(agent.name, now);
|
|
31661
|
+
if (counts.assigned + counts.inProgress + counts.owedReviews > 0) lastBusy.set(agent.name, now);
|
|
31530
31662
|
return { name: agent.name, running: sup.isActive(agent.name), ...counts, idleMs: now - (lastBusy.get(agent.name) ?? now) };
|
|
31531
31663
|
});
|
|
31532
31664
|
const bounds = effectiveBounds(pool, desired);
|
|
@@ -31549,7 +31681,7 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31549
31681
|
max: bounds.max,
|
|
31550
31682
|
burstMax: pool.burstMax,
|
|
31551
31683
|
provisioned: pool.max,
|
|
31552
|
-
busy: views
|
|
31684
|
+
busy: busyCount(views),
|
|
31553
31685
|
idleWithNoReadyWork: summary2.ready === 0 && views.every((v) => v.assigned + v.inProgress === 0),
|
|
31554
31686
|
currentAccount,
|
|
31555
31687
|
snapshot: readCapacity(capacityDir(), now),
|
|
@@ -31572,6 +31704,10 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31572
31704
|
lastSize = { max: bounds.max, size: sizing.size };
|
|
31573
31705
|
Object.assign(state, { effectiveSize: sizing.size, burstMax: sizing.ceiling, capacityLevel: sizing.level, capacityUnused: sizing.unused });
|
|
31574
31706
|
const decision = reconcilePool({ pending: summary2.pending, members: views, min: Math.min(bounds.min, sizing.size), max: sizing.size, idleStopMs });
|
|
31707
|
+
if (decision.start.length > 0 || decision.stop.length > 0) {
|
|
31708
|
+
const view = (v) => `${v.name}{${v.running ? "up" : "down"} assigned ${v.assigned} inProgress ${v.inProgress} readyAssigned ${v.readyAssigned} owed ${v.owedReviews} idle ${Math.round(v.idleMs / 1e3)}s}`;
|
|
31709
|
+
log(`decision start [${decision.start.join(", ")}] stop [${decision.stop.join(", ")}] pending ${summary2.pending} ready ${summary2.ready} min ${Math.min(bounds.min, sizing.size)} max ${sizing.size}: ${views.map(view).join(" ")}`);
|
|
31710
|
+
}
|
|
31575
31711
|
for (const name of decision.start) {
|
|
31576
31712
|
const member = members.find((m) => m.agent.name === name);
|
|
31577
31713
|
try {
|
|
@@ -31588,14 +31724,17 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31588
31724
|
lastBusy.set(name, Date.now());
|
|
31589
31725
|
sup.startAgent(name);
|
|
31590
31726
|
log(`started ${name} (ready ${summary2.ready})`);
|
|
31591
|
-
await report(member, "started", `ready ${summary2.ready}`, summary2.ready);
|
|
31727
|
+
await report(member, "started", `ready ${summary2.ready}`, summary2.ready, sizing.size);
|
|
31592
31728
|
}
|
|
31729
|
+
const runningBefore = views.filter((v) => v.running).length;
|
|
31593
31730
|
for (const name of decision.stop) {
|
|
31594
31731
|
const member = members.find((m) => m.agent.name === name);
|
|
31595
31732
|
await sup.stopAgent(name);
|
|
31596
31733
|
await markStopped(member);
|
|
31597
|
-
|
|
31598
|
-
|
|
31734
|
+
const branch = decision.why[name];
|
|
31735
|
+
const reason = branch === "over-cap" ? `over cap: size ${sizing.size} below ${runningBefore} running (${sizing.reason})`.slice(0, 200) : branch === "slot-for-owner" ? "slot freed for a stopped owner with work" : `idle ${Math.round(idleStopMs / 1e3)}s`;
|
|
31736
|
+
log(`stopped ${name}: ${reason}`);
|
|
31737
|
+
await report(member, "stopped", reason, summary2.ready, sizing.size);
|
|
31599
31738
|
}
|
|
31600
31739
|
Object.assign(state, { ready: summary2.ready, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), error: void 0 });
|
|
31601
31740
|
} catch (err) {
|
|
@@ -31612,10 +31751,32 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31612
31751
|
return () => clearInterval(timer);
|
|
31613
31752
|
}
|
|
31614
31753
|
var MODEL_REFRESH_MS = 15e3;
|
|
31754
|
+
function desiredIdentity(api, root, nodeToken, log) {
|
|
31755
|
+
return createGrantIdentity({
|
|
31756
|
+
api,
|
|
31757
|
+
root,
|
|
31758
|
+
nodeToken,
|
|
31759
|
+
now: Date.now,
|
|
31760
|
+
log,
|
|
31761
|
+
readCreds: (n) => readCreds(n),
|
|
31762
|
+
writeCreds,
|
|
31763
|
+
workerType: workerTypeOf,
|
|
31764
|
+
post: (url, body, token2) => postJson(url, body, token2)
|
|
31765
|
+
});
|
|
31766
|
+
}
|
|
31767
|
+
function desiredReconcilerDeps(node2, log, io) {
|
|
31768
|
+
const root = node2.root;
|
|
31769
|
+
if (!root) throw new Error("the node record has no root");
|
|
31770
|
+
return {
|
|
31771
|
+
retire: (name) => retireAgent(name, { api: node2.api, root, readCreds: io.readCreds, removeCreds: io.removeCreds, deleteJson: io.deleteJson, log }),
|
|
31772
|
+
storedNames: () => desiredModeCredNames(io.listStoredAgentNames(), root, node2.api, io.readCreds),
|
|
31773
|
+
identity: desiredIdentity(node2.api, root, node2.token, log)
|
|
31774
|
+
};
|
|
31775
|
+
}
|
|
31615
31776
|
async function nodeUpDesired() {
|
|
31616
31777
|
try {
|
|
31617
31778
|
const node2 = readNodeCreds();
|
|
31618
|
-
if (!node2) throw new Error("no spec file given and this machine is not enrolled. Run `
|
|
31779
|
+
if (!node2) throw new Error("no spec file given and this machine is not enrolled. Run `shipster node enroll`, or pass a spec file.");
|
|
31619
31780
|
if (!node2.root) throw new Error(`${nodeCredsPath()} has no root. Re-enroll with a fresh code and --root <dir> (the directory worker clones live under).`);
|
|
31620
31781
|
const root = node2.root;
|
|
31621
31782
|
let previous = null;
|
|
@@ -31730,8 +31891,10 @@ async function nodeUpDesired() {
|
|
|
31730
31891
|
const provisionDeps = realProvisionDeps(root, process.env, node2.allow?.hosts);
|
|
31731
31892
|
const ownerBases = createOwnerPoolBases(realOwnerPoolBasesDeps(ownerPoolBasesRoot(configDir()), process.env, node2.allow?.hosts));
|
|
31732
31893
|
const seatCloneDeps = realSeatCloneDeps(root, process.env, node2.allow?.hosts);
|
|
31894
|
+
const slotGuardDeps = realSlotGuardDeps();
|
|
31733
31895
|
let seatRefusals = /* @__PURE__ */ new Map();
|
|
31734
31896
|
const seats = trackSeats();
|
|
31897
|
+
const deps = desiredReconcilerDeps(node2, log, { readCreds, removeCreds, deleteJson, listStoredAgentNames });
|
|
31735
31898
|
const reconciler = new Reconciler({
|
|
31736
31899
|
root,
|
|
31737
31900
|
api: node2.api,
|
|
@@ -31750,33 +31913,25 @@ async function nodeUpDesired() {
|
|
|
31750
31913
|
latest = wanted;
|
|
31751
31914
|
runner.expect(wanted);
|
|
31752
31915
|
seats.provision(wanted);
|
|
31753
|
-
await
|
|
31754
|
-
|
|
31916
|
+
const slotRefusals = await refuseDirtySlots(entries.map((e) => e.agent), slotGuardDeps);
|
|
31917
|
+
const open = entries.filter((e) => !slotRefusals.has(e.agent.name));
|
|
31918
|
+
await provisionEntries(open.filter((e) => !e.agent.runAs), wanted, provisionDeps);
|
|
31919
|
+
seatRefusals = new Map([...await provisionSeatClones(open.filter((e) => e.agent.runAs), seatCloneDeps), ...slotRefusals]);
|
|
31755
31920
|
},
|
|
31756
31921
|
retire(name) {
|
|
31757
31922
|
seats.retired(name);
|
|
31758
|
-
return
|
|
31923
|
+
return deps.retire(name);
|
|
31759
31924
|
},
|
|
31760
31925
|
pendingRetires,
|
|
31761
31926
|
savePendingRetires(names2) {
|
|
31762
31927
|
pendingRetires.splice(0, pendingRetires.length, ...names2);
|
|
31763
31928
|
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
31764
31929
|
},
|
|
31765
|
-
storedNames:
|
|
31766
|
-
identity:
|
|
31767
|
-
api: node2.api,
|
|
31768
|
-
root,
|
|
31769
|
-
nodeToken: node2.token,
|
|
31770
|
-
now: Date.now,
|
|
31771
|
-
log,
|
|
31772
|
-
readCreds: (n) => readCreds(n),
|
|
31773
|
-
writeCreds,
|
|
31774
|
-
workerType: workerTypeOf,
|
|
31775
|
-
post: (url, body, token2) => postJson(url, body, token2)
|
|
31776
|
-
}),
|
|
31930
|
+
storedNames: deps.storedNames,
|
|
31931
|
+
identity: deps.identity,
|
|
31777
31932
|
runner
|
|
31778
31933
|
});
|
|
31779
|
-
console.log(source_default.bold("\n
|
|
31934
|
+
console.log(source_default.bold("\n shipster node: desired state") + source_default.dim(` root ${root} state ${statePath2()}
|
|
31780
31935
|
`));
|
|
31781
31936
|
const keepAlive = setInterval(() => {
|
|
31782
31937
|
}, 6e4);
|
|
@@ -31915,7 +32070,7 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
31915
32070
|
const plainCount = runnable.filter((a) => !pooled.has(a.name)).length;
|
|
31916
32071
|
const poolNote = pools.length > 0 ? `, ${pools.length} pool(s)` : "";
|
|
31917
32072
|
console.log(source_default.bold(`
|
|
31918
|
-
|
|
32073
|
+
shipster node: ${plainCount} agent(s)${poolNote}`) + source_default.dim(` state ${statePath2()}
|
|
31919
32074
|
`));
|
|
31920
32075
|
const stopLoops = [];
|
|
31921
32076
|
const ownerBases = createOwnerPoolBases(ownerPoolBasesDepsFor(opts));
|
|
@@ -31992,7 +32147,7 @@ async function startRelayLogins(agents, needsLogin, onReady) {
|
|
|
31992
32147
|
}
|
|
31993
32148
|
if (!creds) {
|
|
31994
32149
|
for (const a of seats) needsLogin.add(a.name);
|
|
31995
|
-
console.log(source_default.yellow(` ! ${seats.length} relay seat(s) need a login but this machine has no node credential; run \`
|
|
32150
|
+
console.log(source_default.yellow(` ! ${seats.length} relay seat(s) need a login but this machine has no node credential; run \`shipster node enroll\``));
|
|
31996
32151
|
return null;
|
|
31997
32152
|
}
|
|
31998
32153
|
const manager = new LoginManager({
|
|
@@ -32066,7 +32221,7 @@ async function nodeEnrollCommand(code, opts) {
|
|
|
32066
32221
|
allowRunAs: commaList(opts.allowRunAs)
|
|
32067
32222
|
});
|
|
32068
32223
|
console.log(`Enrolled as "${name}" (${nodeId}). Credential saved to ${path3}.`);
|
|
32069
|
-
console.log("Start workers with:
|
|
32224
|
+
console.log("Start workers with: shipster node up <spec.json>, or shipster node up to follow the dashboard's desired state.");
|
|
32070
32225
|
} catch (err) {
|
|
32071
32226
|
console.error(source_default.red(`
|
|
32072
32227
|
${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -32108,7 +32263,7 @@ function nodeStatusCommand(opts) {
|
|
|
32108
32263
|
if (enrolled) console.log(nodeLine());
|
|
32109
32264
|
const diskColor = { ok: source_default.green, warn: source_default.yellow, critical: source_default.red };
|
|
32110
32265
|
for (const d of disk) console.log(` ${diskColor[d.level](describeDisk(d))}`);
|
|
32111
|
-
if (disk.some((d) => d.level === "critical")) console.log(source_default.red(" next:
|
|
32266
|
+
if (disk.some((d) => d.level === "critical")) console.log(source_default.red(" next: shipster node janitor --dry-run"));
|
|
32112
32267
|
const { invalid } = diskThresholds();
|
|
32113
32268
|
if (invalid.length > 0) console.log(source_default.yellow(` ! ignored ${invalid.join(", ")} (not a positive number within range); using the default`));
|
|
32114
32269
|
console.log(`${supervised ? source_default.green("supervisor up") : source_default.red("supervisor down")} pid ${state.supervisorPid} ${source_default.dim(state.specPath)}`);
|
|
@@ -32134,15 +32289,15 @@ import { dirname as dirname25, join as join45, resolve as resolve17 } from "node
|
|
|
32134
32289
|
import { createHash as createHash3 } from "node:crypto";
|
|
32135
32290
|
import { realpathSync as realpathSync14 } from "node:fs";
|
|
32136
32291
|
import { isAbsolute as isAbsolute5, join as join44 } from "node:path";
|
|
32137
|
-
var UNSUPPORTED = "
|
|
32138
|
-
function
|
|
32292
|
+
var UNSUPPORTED = "shipster node service supports Linux (systemd --user) and macOS (launchd) only";
|
|
32293
|
+
function build(configDir2, platform2, names2) {
|
|
32139
32294
|
if (platform2 !== "linux" && platform2 !== "darwin") throw new Error(UNSUPPORTED);
|
|
32140
32295
|
if (!isAbsolute5(configDir2)) throw new Error("configDir must be an absolute path");
|
|
32141
32296
|
const real3 = realpathSync14(configDir2);
|
|
32142
32297
|
const hash = createHash3("sha256").update(real3).digest("hex").slice(0, 10);
|
|
32143
32298
|
const logPath2 = join44(real3, "node", "logs", "service.log");
|
|
32144
32299
|
if (platform2 === "linux") {
|
|
32145
|
-
const label2 =
|
|
32300
|
+
const label2 = `${names2.systemd}${hash}`;
|
|
32146
32301
|
const unitFileName2 = `${label2}.service`;
|
|
32147
32302
|
return {
|
|
32148
32303
|
label: label2,
|
|
@@ -32151,7 +32306,7 @@ function serviceIdentity(configDir2, platform2) {
|
|
|
32151
32306
|
logPath: logPath2
|
|
32152
32307
|
};
|
|
32153
32308
|
}
|
|
32154
|
-
const label =
|
|
32309
|
+
const label = `${names2.launchd}${hash}`;
|
|
32155
32310
|
const unitFileName = `${label}.plist`;
|
|
32156
32311
|
return {
|
|
32157
32312
|
label,
|
|
@@ -32160,6 +32315,12 @@ function serviceIdentity(configDir2, platform2) {
|
|
|
32160
32315
|
logPath: logPath2
|
|
32161
32316
|
};
|
|
32162
32317
|
}
|
|
32318
|
+
function serviceIdentity(configDir2, platform2) {
|
|
32319
|
+
return build(configDir2, platform2, { systemd: "shipster-node-", launchd: "dev.shipster.node." });
|
|
32320
|
+
}
|
|
32321
|
+
function legacyServiceIdentity(configDir2, platform2) {
|
|
32322
|
+
return build(configDir2, platform2, { systemd: "pitboss-node-", launchd: "dev.pitboss.node." });
|
|
32323
|
+
}
|
|
32163
32324
|
|
|
32164
32325
|
// src/node/service/input.ts
|
|
32165
32326
|
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
@@ -32168,20 +32329,20 @@ var SECRET_KEY = /TOKEN|SECRET|KEY|PASSWORD/i;
|
|
|
32168
32329
|
var LINE_BREAK_OR_NUL = /[\0\r\n]/;
|
|
32169
32330
|
var SYSTEMD_EXPANSION = /[%$`]/;
|
|
32170
32331
|
var NPX_CACHE = ["/_npx/", "/.npm/_npx"];
|
|
32171
|
-
var NPX_MESSAGE = "run the installed
|
|
32332
|
+
var NPX_MESSAGE = "run the installed shipster, not npx: npm install -g @shipster/cli, then run shipster node service install";
|
|
32172
32333
|
function checkServiceInput(input, syntax) {
|
|
32173
32334
|
for (const key of Object.keys(input.env)) {
|
|
32174
32335
|
if (SECRET_KEY.test(key)) {
|
|
32175
32336
|
throw new Error(
|
|
32176
|
-
`
|
|
32337
|
+
`shipster node service: env ${JSON.stringify(key)} looks like a secret; it is never written into a service file`
|
|
32177
32338
|
);
|
|
32178
32339
|
}
|
|
32179
32340
|
if (!ALLOWED_ENV_KEYS.includes(key)) {
|
|
32180
|
-
throw new Error(`
|
|
32341
|
+
throw new Error(`shipster node service: env ${JSON.stringify(key)} is not an allowed env key`);
|
|
32181
32342
|
}
|
|
32182
32343
|
}
|
|
32183
|
-
if (!("PATH" in input.env)) throw new Error("
|
|
32184
|
-
if (!("HOME" in input.env)) throw new Error("
|
|
32344
|
+
if (!("PATH" in input.env)) throw new Error("shipster node service: env PATH is required");
|
|
32345
|
+
if (!("HOME" in input.env)) throw new Error("shipster node service: env HOME is required");
|
|
32185
32346
|
const texts = [
|
|
32186
32347
|
["nodeBin", input.nodeBin],
|
|
32187
32348
|
["cliEntry", input.cliEntry],
|
|
@@ -32192,11 +32353,11 @@ function checkServiceInput(input, syntax) {
|
|
|
32192
32353
|
];
|
|
32193
32354
|
for (const [name, value] of texts) {
|
|
32194
32355
|
if (LINE_BREAK_OR_NUL.test(value)) {
|
|
32195
|
-
throw new Error(`
|
|
32356
|
+
throw new Error(`shipster node service: ${name} contains a line break or NUL byte; refusing`);
|
|
32196
32357
|
}
|
|
32197
32358
|
}
|
|
32198
32359
|
for (const [name, value] of texts.slice(0, 4)) {
|
|
32199
|
-
if (!isAbsolute6(value)) throw new Error(`
|
|
32360
|
+
if (!isAbsolute6(value)) throw new Error(`shipster node service: ${name} must be an absolute path`);
|
|
32200
32361
|
}
|
|
32201
32362
|
for (const path3 of [input.nodeBin, input.cliEntry]) {
|
|
32202
32363
|
if (NPX_CACHE.some((marker) => path3.includes(marker))) throw new Error(NPX_MESSAGE);
|
|
@@ -32205,7 +32366,7 @@ function checkServiceInput(input, syntax) {
|
|
|
32205
32366
|
for (const [name, value] of texts) {
|
|
32206
32367
|
if (SYSTEMD_EXPANSION.test(value)) {
|
|
32207
32368
|
throw new Error(
|
|
32208
|
-
`
|
|
32369
|
+
`shipster node service: ${name} contains % $ or a backtick, which systemd would expand; refusing`
|
|
32209
32370
|
);
|
|
32210
32371
|
}
|
|
32211
32372
|
}
|
|
@@ -32218,7 +32379,7 @@ function xmlEscape(value) {
|
|
|
32218
32379
|
}
|
|
32219
32380
|
function renderLaunchdPlist(input) {
|
|
32220
32381
|
checkServiceInput(input, "launchd");
|
|
32221
|
-
if (!/^[A-Za-z0-9.-]+$/.test(input.label)) throw new Error("
|
|
32382
|
+
if (!/^[A-Za-z0-9.-]+$/.test(input.label)) throw new Error("shipster node service: label is not a plain reverse-DNS name");
|
|
32222
32383
|
const str = (value, indent) => `${indent}<string>${xmlEscape(value)}</string>`;
|
|
32223
32384
|
const environment = [["PITBOSS_CONFIG_DIR", input.configDir], ...Object.entries(input.env)];
|
|
32224
32385
|
const lines3 = [
|
|
@@ -32379,7 +32540,7 @@ function defaultServiceDeps() {
|
|
|
32379
32540
|
};
|
|
32380
32541
|
}
|
|
32381
32542
|
function realConfigDir(deps) {
|
|
32382
|
-
if (!existsSync28(deps.configDir)) throw new ServiceError(`config dir ${deps.configDir} does not exist; run
|
|
32543
|
+
if (!existsSync28(deps.configDir)) throw new ServiceError(`config dir ${deps.configDir} does not exist; run shipster node enroll first`);
|
|
32383
32544
|
return realpathSync15(deps.configDir);
|
|
32384
32545
|
}
|
|
32385
32546
|
function identityOf(deps, real3) {
|
|
@@ -32389,6 +32550,9 @@ function identityOf(deps, real3) {
|
|
|
32389
32550
|
throw new ServiceError(err instanceof Error ? err.message : String(err));
|
|
32390
32551
|
}
|
|
32391
32552
|
}
|
|
32553
|
+
function legacyIdentityOf(deps, real3) {
|
|
32554
|
+
return legacyServiceIdentity(real3, deps.platform);
|
|
32555
|
+
}
|
|
32392
32556
|
function nodeCredentialExists(deps) {
|
|
32393
32557
|
return existsSync28(join45(deps.configDir, "node", "node.json"));
|
|
32394
32558
|
}
|
|
@@ -32443,14 +32607,40 @@ function waitUntil(deps, probe2) {
|
|
|
32443
32607
|
}
|
|
32444
32608
|
return false;
|
|
32445
32609
|
}
|
|
32610
|
+
function isRunning(deps, identity) {
|
|
32611
|
+
if (deps.platform !== "linux") return launchdState(deps, identity.label)?.state === "running";
|
|
32612
|
+
const st = systemdState(deps, identity.unitFileName);
|
|
32613
|
+
return st.activeState === "active" && st.subState === "running";
|
|
32614
|
+
}
|
|
32615
|
+
function removeUnit(deps, identity) {
|
|
32616
|
+
const unitPath = identity.unitPath(deps.home);
|
|
32617
|
+
if (!existsSync28(unitPath)) return null;
|
|
32618
|
+
if (deps.platform === "linux") {
|
|
32619
|
+
const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
|
|
32620
|
+
if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
|
|
32621
|
+
unlinkSync5(unitPath);
|
|
32622
|
+
const reload = deps.exec("systemctl", ["--user", "daemon-reload"]);
|
|
32623
|
+
if (reload.status !== 0) deps.err("WARNING: systemctl --user daemon-reload failed");
|
|
32624
|
+
} else {
|
|
32625
|
+
if (launchdState(deps, identity.label) !== null) {
|
|
32626
|
+
const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
|
|
32627
|
+
if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
|
|
32628
|
+
}
|
|
32629
|
+
unlinkSync5(unitPath);
|
|
32630
|
+
}
|
|
32631
|
+
return unitPath;
|
|
32632
|
+
}
|
|
32446
32633
|
function reportNotRunning(deps, logPath2) {
|
|
32447
|
-
deps.err(`
|
|
32634
|
+
deps.err(`shipster node service: the node is not running after ${WAIT_ATTEMPTS} seconds. Last ${LOG_TAIL_LINES} lines of ${logPath2}:`);
|
|
32635
|
+
tailLog(deps, logPath2);
|
|
32636
|
+
return 1;
|
|
32637
|
+
}
|
|
32638
|
+
function tailLog(deps, logPath2) {
|
|
32448
32639
|
if (existsSync28(logPath2)) {
|
|
32449
32640
|
for (const line of readFileSync28(logPath2, "utf-8").replace(/\n$/, "").split("\n").slice(-LOG_TAIL_LINES)) deps.err(line);
|
|
32450
32641
|
} else {
|
|
32451
32642
|
deps.err("(no log yet)");
|
|
32452
32643
|
}
|
|
32453
|
-
return 1;
|
|
32454
32644
|
}
|
|
32455
32645
|
function startLinux(deps, identity, rewritten) {
|
|
32456
32646
|
const systemctl = (args) => deps.exec("systemctl", ["--user", ...args]);
|
|
@@ -32477,14 +32667,60 @@ function startDarwin(deps, identity, plist, rewritten) {
|
|
|
32477
32667
|
must(deps.exec("launchctl", ["enable", target]), "launchctl enable");
|
|
32478
32668
|
deps.out("a LaunchAgent starts when you log in, not at boot: the node does not come back after a reboot until you log in");
|
|
32479
32669
|
}
|
|
32670
|
+
var STABLE_POLLS = 3;
|
|
32671
|
+
function stopUnit(deps, identity) {
|
|
32672
|
+
if (deps.platform === "linux") {
|
|
32673
|
+
const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
|
|
32674
|
+
if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
|
|
32675
|
+
} else if (launchdState(deps, identity.label) !== null) {
|
|
32676
|
+
const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
|
|
32677
|
+
if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
|
|
32678
|
+
}
|
|
32679
|
+
}
|
|
32680
|
+
function restartUnit(deps, identity) {
|
|
32681
|
+
if (deps.platform === "linux") {
|
|
32682
|
+
const on2 = deps.exec("systemctl", ["--user", "enable", "--now", identity.unitFileName]);
|
|
32683
|
+
if (on2.status !== 0) deps.err(`WARNING: could not restart ${identity.unitFileName}: ${(on2.stderr || on2.stdout).trim()}`);
|
|
32684
|
+
return;
|
|
32685
|
+
}
|
|
32686
|
+
const target = `gui/${deps.uid}/${identity.label}`;
|
|
32687
|
+
const boot = deps.exec("launchctl", ["bootstrap", `gui/${deps.uid}`, identity.unitPath(deps.home)]);
|
|
32688
|
+
const on = deps.exec("launchctl", ["enable", target]);
|
|
32689
|
+
if (boot.status !== 0 || on.status !== 0) deps.err(`WARNING: could not restart ${identity.label}`);
|
|
32690
|
+
}
|
|
32691
|
+
function restartCount(deps, identity) {
|
|
32692
|
+
return deps.platform === "linux" ? systemdState(deps, identity.unitFileName).nRestarts : null;
|
|
32693
|
+
}
|
|
32694
|
+
function heartbeatSince(deps, since) {
|
|
32695
|
+
if (!nodeCredentialExists(deps)) return true;
|
|
32696
|
+
const hb = heartbeatOf(deps);
|
|
32697
|
+
return hb !== "unknown" && Date.parse(hb.at) >= since;
|
|
32698
|
+
}
|
|
32699
|
+
function waitStable(deps, identity, startedAt) {
|
|
32700
|
+
let baseline;
|
|
32701
|
+
let streak = 0;
|
|
32702
|
+
for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt++) {
|
|
32703
|
+
const count = restartCount(deps, identity);
|
|
32704
|
+
if (isRunning(deps, identity) && (baseline === void 0 || count === baseline)) {
|
|
32705
|
+
if (baseline === void 0) baseline = count;
|
|
32706
|
+
streak++;
|
|
32707
|
+
if (streak >= STABLE_POLLS && heartbeatSince(deps, startedAt)) return true;
|
|
32708
|
+
} else {
|
|
32709
|
+
baseline = void 0;
|
|
32710
|
+
streak = 0;
|
|
32711
|
+
}
|
|
32712
|
+
if (attempt < WAIT_ATTEMPTS - 1) deps.sleep(1e3);
|
|
32713
|
+
}
|
|
32714
|
+
return false;
|
|
32715
|
+
}
|
|
32480
32716
|
function installCore(opts, deps) {
|
|
32481
32717
|
const real3 = realConfigDir(deps);
|
|
32482
32718
|
const identity = identityOf(deps, real3);
|
|
32483
32719
|
if (!deps.cliEntry.startsWith("/") || !deps.cliEntry.endsWith(".js")) {
|
|
32484
|
-
throw new ServiceError("
|
|
32720
|
+
throw new ServiceError("shipster node service needs the built CLI (dist/cli.js); install @shipster/cli and run it from there");
|
|
32485
32721
|
}
|
|
32486
32722
|
if (opts.spec === void 0 && !nodeCredentialExists(deps)) {
|
|
32487
|
-
throw new ServiceError(`no node credential under ${deps.configDir}/node: run
|
|
32723
|
+
throw new ServiceError(`no node credential under ${deps.configDir}/node: run shipster node enroll first, or pass a spec file`);
|
|
32488
32724
|
}
|
|
32489
32725
|
const args = opts.spec === void 0 ? [] : [specArgument(opts.spec)];
|
|
32490
32726
|
const input = {
|
|
@@ -32498,18 +32734,40 @@ function installCore(opts, deps) {
|
|
|
32498
32734
|
const linux = deps.platform === "linux";
|
|
32499
32735
|
const text = linux ? renderSystemdUnit(input) : renderLaunchdPlist({ ...input, label: identity.label });
|
|
32500
32736
|
const unitPath = identity.unitPath(deps.home);
|
|
32737
|
+
const legacy = legacyIdentityOf(deps, real3);
|
|
32738
|
+
const legacyInstalled = existsSync28(legacy.unitPath(deps.home));
|
|
32501
32739
|
mkdirSync26(dirname25(identity.logPath), { recursive: true, mode: 448 });
|
|
32502
|
-
const
|
|
32503
|
-
|
|
32504
|
-
|
|
32505
|
-
|
|
32506
|
-
|
|
32507
|
-
|
|
32508
|
-
|
|
32509
|
-
|
|
32510
|
-
|
|
32511
|
-
|
|
32740
|
+
const newExisted = existsSync28(unitPath);
|
|
32741
|
+
if (legacyInstalled) stopUnit(deps, legacy);
|
|
32742
|
+
const startedAt = Date.now();
|
|
32743
|
+
let healthy = false;
|
|
32744
|
+
let rewritten = false;
|
|
32745
|
+
try {
|
|
32746
|
+
rewritten = placeUnit(unitPath, text, opts.force === true);
|
|
32747
|
+
deps.out(`${rewritten ? "wrote" : "unchanged"} ${unitPath}`);
|
|
32748
|
+
if (linux) startLinux(deps, identity, rewritten);
|
|
32749
|
+
else startDarwin(deps, identity, unitPath, rewritten);
|
|
32750
|
+
healthy = legacyInstalled ? waitStable(deps, identity, startedAt) : waitUntil(deps, () => isRunning(deps, identity));
|
|
32751
|
+
} catch (err) {
|
|
32752
|
+
if (!legacyInstalled) throw err;
|
|
32753
|
+
deps.err(err instanceof Error ? err.message : String(err));
|
|
32754
|
+
}
|
|
32755
|
+
if (!healthy) {
|
|
32756
|
+
if (!legacyInstalled) return reportNotRunning(deps, identity.logPath);
|
|
32757
|
+
if (newExisted) stopUnit(deps, identity);
|
|
32758
|
+
else removeUnit(deps, identity);
|
|
32759
|
+
restartUnit(deps, legacy);
|
|
32760
|
+
deps.err(
|
|
32761
|
+
`shipster node service: the new unit ${identity.unitFileName} did not become healthy within ${WAIT_ATTEMPTS} seconds; ${newExisted ? "stopped" : "removed"} it and restarted ${legacy.unitFileName}. Last lines of ${identity.logPath}:`
|
|
32762
|
+
);
|
|
32763
|
+
tailLog(deps, identity.logPath);
|
|
32764
|
+
return 1;
|
|
32765
|
+
}
|
|
32512
32766
|
deps.out(`running: ${identity.unitFileName}`);
|
|
32767
|
+
if (legacyInstalled) {
|
|
32768
|
+
const removed = removeUnit(deps, legacy);
|
|
32769
|
+
deps.out(`migrated from ${legacy.unitFileName}: removed ${removed}`);
|
|
32770
|
+
}
|
|
32513
32771
|
return 0;
|
|
32514
32772
|
}
|
|
32515
32773
|
function serviceInstall(opts, deps) {
|
|
@@ -32534,7 +32792,8 @@ function heartbeatOf(deps) {
|
|
|
32534
32792
|
}
|
|
32535
32793
|
function statusOf(deps) {
|
|
32536
32794
|
const real3 = realConfigDir(deps);
|
|
32537
|
-
const
|
|
32795
|
+
const current = identityOf(deps, real3);
|
|
32796
|
+
const identity = !existsSync28(current.unitPath(deps.home)) && existsSync28(legacyIdentityOf(deps, real3).unitPath(deps.home)) ? legacyIdentityOf(deps, real3) : current;
|
|
32538
32797
|
const unitPath = identity.unitPath(deps.home);
|
|
32539
32798
|
const base2 = { unit: identity.unitFileName, unitPath, logPath: identity.logPath, heartbeat: heartbeatOf(deps) };
|
|
32540
32799
|
if (!existsSync28(unitPath)) {
|
|
@@ -32599,26 +32858,12 @@ function serviceStatus(opts, deps) {
|
|
|
32599
32858
|
function serviceUninstall(deps) {
|
|
32600
32859
|
try {
|
|
32601
32860
|
const real3 = realConfigDir(deps);
|
|
32602
|
-
const
|
|
32603
|
-
|
|
32604
|
-
|
|
32605
|
-
deps.out(`not installed: ${unitPath} does not exist`);
|
|
32861
|
+
const removed = [identityOf(deps, real3), legacyIdentityOf(deps, real3)].map((identity) => removeUnit(deps, identity)).filter((path3) => path3 !== null);
|
|
32862
|
+
if (removed.length === 0) {
|
|
32863
|
+
deps.out(`not installed: ${identityOf(deps, real3).unitPath(deps.home)} does not exist`);
|
|
32606
32864
|
return 0;
|
|
32607
32865
|
}
|
|
32608
|
-
|
|
32609
|
-
const off = deps.exec("systemctl", ["--user", "disable", "--now", identity.unitFileName]);
|
|
32610
|
-
if (off.status !== 0) deps.err(`WARNING: systemctl --user disable --now ${identity.unitFileName}: ${(off.stderr || off.stdout).trim()}`);
|
|
32611
|
-
unlinkSync5(unitPath);
|
|
32612
|
-
const reload = deps.exec("systemctl", ["--user", "daemon-reload"]);
|
|
32613
|
-
if (reload.status !== 0) deps.err("WARNING: systemctl --user daemon-reload failed");
|
|
32614
|
-
} else {
|
|
32615
|
-
if (launchdState(deps, identity.label) !== null) {
|
|
32616
|
-
const off = deps.exec("launchctl", ["bootout", `gui/${deps.uid}/${identity.label}`]);
|
|
32617
|
-
if (off.status !== 0) deps.err(`WARNING: launchctl bootout failed: ${(off.stderr || off.stdout).trim()}`);
|
|
32618
|
-
}
|
|
32619
|
-
unlinkSync5(unitPath);
|
|
32620
|
-
}
|
|
32621
|
-
deps.out(`removed ${unitPath}`);
|
|
32866
|
+
for (const path3 of removed) deps.out(`removed ${path3}`);
|
|
32622
32867
|
deps.out("node.json, logs and the config dir are left alone");
|
|
32623
32868
|
return 0;
|
|
32624
32869
|
} catch (err) {
|
|
@@ -32660,7 +32905,7 @@ function resolveJanitorTarget(d) {
|
|
|
32660
32905
|
return { ok: true, mode: "spec", roots: plan2.roots, repos: plan2.repos, managed: plan2.managed, nodeId: d.identity()?.nodeId ?? "", optOut: plan2.optOut };
|
|
32661
32906
|
}
|
|
32662
32907
|
const node2 = d.identity();
|
|
32663
|
-
if (!node2) return { ok: false, message: "this machine has no enrolled node with a work root; run `
|
|
32908
|
+
if (!node2) return { ok: false, message: "this machine has no enrolled node with a work root; run `shipster node enroll` first" };
|
|
32664
32909
|
const listed = state?.managedWorktrees;
|
|
32665
32910
|
const managed = Array.isArray(listed) ? listed.filter((p) => typeof p === "string" && isAbsolute7(p)) : [];
|
|
32666
32911
|
return { ok: true, mode: "desired", roots: [node2.root], repos: [], managed, nodeId: node2.nodeId };
|
|
@@ -32712,7 +32957,7 @@ function print(r, capGb, dailyCapGb, healthyDays, roots) {
|
|
|
32712
32957
|
return;
|
|
32713
32958
|
}
|
|
32714
32959
|
console.log(source_default.bold(`
|
|
32715
|
-
|
|
32960
|
+
shipster node janitor: ${r.dry ? "dry run, nothing deleted" : "live"}`) + source_default.dim(` ${roots.join(", ")}`));
|
|
32716
32961
|
for (const i of r.items) console.log(` ${i.action === "removed" ? source_default.red("removed ") : source_default.yellow("would remove")} ${i.rule.padEnd(15)} ${size(i.bytes).padStart(9)} ${i.path}`);
|
|
32717
32962
|
for (const s of r.report.skipped.filter((x) => !x.rule.startsWith("docker"))) console.log(source_default.dim(` skipped ${s.rule.padEnd(15)} ${s.reason} ${s.path}${s.branch ? ` (${s.branch})` : ""}`));
|
|
32718
32963
|
for (const x of r.rebuildableOff) console.log(source_default.dim(` skipped rebuildable ${x.reason} ${x.path}`));
|
|
@@ -32856,14 +33101,14 @@ async function runDoctor(opts, deps) {
|
|
|
32856
33101
|
const top = base2.exec("git", ["rev-parse", "--show-toplevel"], { cwd: base2.cwd });
|
|
32857
33102
|
const root = top.stdout.trim();
|
|
32858
33103
|
if (!top.ok || !root) {
|
|
32859
|
-
deps.stderr(`
|
|
33104
|
+
deps.stderr(`shipster doctor: ${base2.cwd} is not a git repo`);
|
|
32860
33105
|
return 2;
|
|
32861
33106
|
}
|
|
32862
33107
|
const report = await runChecks(deps.checks, { ...base2, cwd: root });
|
|
32863
33108
|
deps.stdout(opts.json ? renderJson(report) : renderTable(report, { color: deps.color }));
|
|
32864
33109
|
return opts.strict && report.summary.fail > 0 ? 1 : 0;
|
|
32865
33110
|
} catch (err) {
|
|
32866
|
-
deps.stderr(`
|
|
33111
|
+
deps.stderr(`shipster doctor: ${err instanceof Error ? err.message : String(err)}`);
|
|
32867
33112
|
return 2;
|
|
32868
33113
|
}
|
|
32869
33114
|
}
|
|
@@ -33200,7 +33445,7 @@ function playbookCheck(id, name, judge) {
|
|
|
33200
33445
|
id,
|
|
33201
33446
|
name,
|
|
33202
33447
|
async run(env3) {
|
|
33203
|
-
if (env3.api === null) return warn(id, name, "not logged in to
|
|
33448
|
+
if (env3.api === null) return warn(id, name, "not logged in to shipster", "run shipster join");
|
|
33204
33449
|
const facts = await fetchFacts(env3.api, env3);
|
|
33205
33450
|
if (facts === "denied") return warn(id, name, "cannot read playbook facts for this repo", "check --repo and that this login can read the repo");
|
|
33206
33451
|
return judge(facts);
|
|
@@ -33243,7 +33488,7 @@ var autoMergeCheck = featureCheck("auto-merge", "Auto-merge is enabled", (f) =>
|
|
|
33243
33488
|
var conflictBotCheck = featureCheck("conflict-bot", "The conflict bot is enabled", (f) => f.conflictBot);
|
|
33244
33489
|
|
|
33245
33490
|
// src/doctor/checks/install-health.ts
|
|
33246
|
-
var JOIN = "run
|
|
33491
|
+
var JOIN = "run shipster join";
|
|
33247
33492
|
function result(id, name, status2, reason, fix) {
|
|
33248
33493
|
return { id, name, status: status2, reason, fix };
|
|
33249
33494
|
}
|
|
@@ -33261,7 +33506,7 @@ var TOKEN_ID = "token-valid";
|
|
|
33261
33506
|
var TOKEN_NAME = "The API accepts this login's token";
|
|
33262
33507
|
var tokenCheck = installCheck(TOKEN_ID, TOKEN_NAME, async (install) => {
|
|
33263
33508
|
if (!install.creds().ok) {
|
|
33264
|
-
return result(TOKEN_ID, TOKEN_NAME, "fail", "no usable
|
|
33509
|
+
return result(TOKEN_ID, TOKEN_NAME, "fail", "no usable shipster login for this folder", JOIN);
|
|
33265
33510
|
}
|
|
33266
33511
|
const code = await install.ping("agent");
|
|
33267
33512
|
if (code === 401 || code === 403) return result(TOKEN_ID, TOKEN_NAME, "fail", "token revoked or invalid", JOIN);
|
|
@@ -33276,11 +33521,11 @@ var tokenCheck = installCheck(TOKEN_ID, TOKEN_NAME, async (install) => {
|
|
|
33276
33521
|
);
|
|
33277
33522
|
});
|
|
33278
33523
|
var CONFIG_ID = "config-readable";
|
|
33279
|
-
var CONFIG_NAME = "The
|
|
33524
|
+
var CONFIG_NAME = "The shipster config is readable and its API is reachable";
|
|
33280
33525
|
var CONFIG_PROBLEM = {
|
|
33281
33526
|
missing: "does not exist",
|
|
33282
33527
|
unreadable: "cannot be read",
|
|
33283
|
-
invalid: "is not a valid
|
|
33528
|
+
invalid: "is not a valid shipster config"
|
|
33284
33529
|
};
|
|
33285
33530
|
var configCheck = installCheck(CONFIG_ID, CONFIG_NAME, async (install) => {
|
|
33286
33531
|
const found = install.creds();
|
|
@@ -33290,12 +33535,12 @@ var configCheck = installCheck(CONFIG_ID, CONFIG_NAME, async (install) => {
|
|
|
33290
33535
|
const { state, file } = install.config();
|
|
33291
33536
|
if (state !== "ok") return result(CONFIG_ID, CONFIG_NAME, "fail", `config file ${file} ${CONFIG_PROBLEM[state]}`, JOIN);
|
|
33292
33537
|
const code = await install.ping("livez");
|
|
33293
|
-
if (code === null) return result(CONFIG_ID, CONFIG_NAME, "fail", "the configured API is unreachable", "check the API url or your network, or run
|
|
33294
|
-
if (code !== 200) return result(CONFIG_ID, CONFIG_NAME, "fail", `the configured API answered ${code} on /livez`, "check the API url or run
|
|
33538
|
+
if (code === null) return result(CONFIG_ID, CONFIG_NAME, "fail", "the configured API is unreachable", "check the API url or your network, or run shipster join");
|
|
33539
|
+
if (code !== 200) return result(CONFIG_ID, CONFIG_NAME, "fail", `the configured API answered ${code} on /livez`, "check the API url or run shipster join");
|
|
33295
33540
|
return result(CONFIG_ID, CONFIG_NAME, "ok", `config file ${file} is readable and the API answers`, "");
|
|
33296
33541
|
});
|
|
33297
33542
|
var MCP_ID = "mcp-server";
|
|
33298
|
-
var MCP_NAME = "The
|
|
33543
|
+
var MCP_NAME = "The shipster MCP server starts and answers a read-only call";
|
|
33299
33544
|
var mcpCheck = installCheck(MCP_ID, MCP_NAME, async (install) => {
|
|
33300
33545
|
const r = await install.mcp();
|
|
33301
33546
|
if (r.ok) return result(MCP_ID, MCP_NAME, "ok", "list_agents answered", "");
|
|
@@ -33304,13 +33549,13 @@ var mcpCheck = installCheck(MCP_ID, MCP_NAME, async (install) => {
|
|
|
33304
33549
|
const apiDown = code === null || code >= 500 && code < 600 || code === 429;
|
|
33305
33550
|
if (apiDown) return result(MCP_ID, MCP_NAME, "warn", `${r.reason}; the API did not answer cleanly`, "check the API and re-run");
|
|
33306
33551
|
}
|
|
33307
|
-
return result(MCP_ID, MCP_NAME, "fail", r.reason, "upgrade @
|
|
33552
|
+
return result(MCP_ID, MCP_NAME, "fail", r.reason, "upgrade @shipster/cli or run shipster join");
|
|
33308
33553
|
});
|
|
33309
33554
|
var WATCH_ID = "watcher-config";
|
|
33310
33555
|
var WATCH_NAME = "The watch config for this folder is valid";
|
|
33311
33556
|
var watcherCheck = installCheck(WATCH_ID, WATCH_NAME, (install) => {
|
|
33312
33557
|
const found = install.creds();
|
|
33313
|
-
if (found.ok) return result(WATCH_ID, WATCH_NAME, "ok", `
|
|
33558
|
+
if (found.ok) return result(WATCH_ID, WATCH_NAME, "ok", `shipster watch resolves its config from ${found.source}`, "");
|
|
33314
33559
|
return result(WATCH_ID, WATCH_NAME, "fail", `${REFUSED_MARKER} ${found.reason}`, JOIN);
|
|
33315
33560
|
});
|
|
33316
33561
|
|
|
@@ -33338,7 +33583,7 @@ function promptRulesCheck(load2) {
|
|
|
33338
33583
|
const mergeMissing = missingRules(p.merge, MERGE_RULES);
|
|
33339
33584
|
if (mergeMissing.length) problems.push(`missing ${mergeMissing.join(", ")} in ${role} merge section`);
|
|
33340
33585
|
}
|
|
33341
|
-
if (problems.length) return { ...base2, status: "fail", reason: problems.join("; "), fix: "upgrade @
|
|
33586
|
+
if (problems.length) return { ...base2, status: "fail", reason: problems.join("; "), fix: "upgrade @shipster/cli" };
|
|
33342
33587
|
return { ...base2, status: "ok", reason: "all six playbook rules are in the shipped prompts", fix: "" };
|
|
33343
33588
|
}
|
|
33344
33589
|
};
|
|
@@ -33577,7 +33822,7 @@ function makeInstallProbe(cwd, repoOverride) {
|
|
|
33577
33822
|
},
|
|
33578
33823
|
async mcp() {
|
|
33579
33824
|
const found = creds();
|
|
33580
|
-
if (!found.ok) return { ok: false, reason: "no usable
|
|
33825
|
+
if (!found.ok) return { ok: false, reason: "no usable shipster login for this folder" };
|
|
33581
33826
|
const repoId = repoOverride || found.repoId || "";
|
|
33582
33827
|
if (!repoId) return { ok: false, reason: "no repo id to start the MCP server with" };
|
|
33583
33828
|
const entry = readEnv("DOCTOR_MCP_ENTRY") || fileURLToPath4(new URL("mcp.js", import.meta.url));
|
|
@@ -33604,7 +33849,7 @@ function parseGithubSlug(url) {
|
|
|
33604
33849
|
}
|
|
33605
33850
|
function makeRealEnv(opts) {
|
|
33606
33851
|
const cwd = resolve18(opts.dir ?? process.cwd());
|
|
33607
|
-
const
|
|
33852
|
+
const exec5 = (cmd, args, o) => {
|
|
33608
33853
|
const r = spawnSync4(cmd, args, { cwd: o?.cwd ?? cwd, encoding: "utf8", timeout: o?.timeoutMs ?? 1e4 });
|
|
33609
33854
|
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "", code: r.status };
|
|
33610
33855
|
};
|
|
@@ -33619,7 +33864,7 @@ function makeRealEnv(opts) {
|
|
|
33619
33864
|
return res.json();
|
|
33620
33865
|
}
|
|
33621
33866
|
} : null;
|
|
33622
|
-
const remote =
|
|
33867
|
+
const remote = exec5("git", ["remote", "get-url", "origin"]);
|
|
33623
33868
|
return {
|
|
33624
33869
|
cwd,
|
|
33625
33870
|
readFile(path3) {
|
|
@@ -33629,7 +33874,7 @@ function makeRealEnv(opts) {
|
|
|
33629
33874
|
return null;
|
|
33630
33875
|
}
|
|
33631
33876
|
},
|
|
33632
|
-
exec:
|
|
33877
|
+
exec: exec5,
|
|
33633
33878
|
api,
|
|
33634
33879
|
now: () => Date.now(),
|
|
33635
33880
|
repoSlug: remote.ok ? parseGithubSlug(remote.stdout) : null,
|
|
@@ -33651,14 +33896,14 @@ async function doctorCommand(opts) {
|
|
|
33651
33896
|
|
|
33652
33897
|
// src/index.ts
|
|
33653
33898
|
var program2 = new Command();
|
|
33654
|
-
program2.name("
|
|
33899
|
+
program2.name("shipster").description("Coordinate your coding agents from the terminal").version(CLI_VERSION).option("--no-input", "Never prompt; require values via flags").addHelpText("after", "\nFormerly pitboss: the pitboss command still works. See docs/renamed-from-pitboss.md.\n").hook("preAction", (thisCommand) => {
|
|
33655
33900
|
if (thisCommand.opts().input === false) setEnv("NO_INPUT", "1");
|
|
33656
33901
|
});
|
|
33657
33902
|
program2.command("mcp", { hidden: true }).description("Run the MCP server on stdio (used by agent config, not by hand)").allowUnknownOption().action(mcpCommand);
|
|
33658
|
-
program2.command("join").description("Connect this machine's agents: opens a code you approve in the browser").option("--api <url>", "API base URL of your
|
|
33659
|
-
program2.command("init").description("Alias for `join`, kept so existing docs keep working").option("--api <url>", "
|
|
33903
|
+
program2.command("join").description("Connect this machine's agents: opens a code you approve in the browser").option("--api <url>", "API base URL of your Shipster instance (defaults to the hosted one)").action(joinCommand);
|
|
33904
|
+
program2.command("init").description("Alias for `join`, kept so existing docs keep working").option("--api <url>", "API base URL of your Shipster instance (defaults to the hosted one)").action(joinCommand);
|
|
33660
33905
|
program2.command("status").description("Show your agent info, online agents, task summary, and unread count").action(statusCommand);
|
|
33661
|
-
program2.command("doctor").description("Check this install and repo for setup problems; exits 0 unless --strict and a check fails").option("--json", "Print the stable JSON report").option("--strict", "Exit 1 when any check fails (warnings never change the code)").option("--dir <path>", "Repo folder to check (defaults to the current directory)").option("--repo <id>", "
|
|
33906
|
+
program2.command("doctor").description("Check this install and repo for setup problems; exits 0 unless --strict and a check fails").option("--json", "Print the stable JSON report").option("--strict", "Exit 1 when any check fails (warnings never change the code)").option("--dir <path>", "Repo folder to check (defaults to the current directory)").option("--repo <id>", "Shipster repo id to use for API checks (defaults to this folder's config)").action(doctorCommand);
|
|
33662
33907
|
program2.command("start").description("Show your session orientation: project context, your open tasks, unread messages, open threads").action(startCommand);
|
|
33663
33908
|
program2.command("watch").description("Stream live events you're subscribed to (new tasks, messages, reviews) until Ctrl-C").option("--kinds <list>", "Only show these event kinds (comma-separated, e.g. task.created,message.posted)").option("--once", "Block until the next event for this folder's agent, print it as JSON, and exit").option("--since <evt_id|now>", "Start after this event (an evt_ id), or skip the backlog (now). Overwrites the saved position").option("--status", "Print the saved position and how many events are waiting, then exit. Opens the event stream briefly, so the API marks the agent as listening").option("--dir <path>", "Folder whose agent config --once uses (defaults to the current directory)").action(watchCommand);
|
|
33664
33909
|
var hook = program2.command("hook", { hidden: true }).description("Agent runtime hooks that keep the watcher running (installed by join)");
|
|
@@ -33684,7 +33929,7 @@ program2.command("inbox").description("Show unread messages").option("-r, --read
|
|
|
33684
33929
|
program2.command("repos").description("List all repos on this server").action(reposListCommand);
|
|
33685
33930
|
var agentsCmd = program2.command("agents").description("List agents in the current repo").action(agentsListCommand);
|
|
33686
33931
|
agentsCmd.command("report").description("Read-only report of duplicate and stale agent rows; changes nothing").option("--repo <id>", "Limit to one repo (default: every repo you own)").option("--json", "Print the rows as JSON").action(agentsReportCommand);
|
|
33687
|
-
program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `
|
|
33932
|
+
program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `shipster repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", `Agent runtime: ${WORKER_TYPES.join(", ")} (default: claude)`).option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
|
|
33688
33933
|
program2.command("logout").description("Release this directory's local agent claim (does not revoke the server token)").option("--all", "Sweep claims whose working directories no longer exist").action(logoutCommand);
|
|
33689
33934
|
var repo = program2.command("repo").description("Repo operations");
|
|
33690
33935
|
repo.command("show [id]").description("Show a repo's details (defaults to the current repo)").action(repoShowCommand);
|
|
@@ -33694,23 +33939,23 @@ repoContext.command("edit").description("Open the pinned context in $EDITOR").ac
|
|
|
33694
33939
|
repo.command("invite").description("Create an invite code for another agent to join this repo").option("-n, --name <name>", "Suggested agent name (the new agent can override)").option("-s, --specialization <s>", "Suggested specialization").option("--role <role>", "Role this invite grants: orchestrator | worker (default worker)").option("--ttl <seconds>", "Expiry in seconds (default: 7 days)").action(repoInviteCommand);
|
|
33695
33940
|
var pr = program2.command("pr").description("Pull request helpers");
|
|
33696
33941
|
pr.command("remerge-check <sha>").description("Classify a merge commit's remerge diff: exit 0 conflict-only, 1 behaviour change, 2 error").option("--json", "Print the classification as JSON").action(remergeCheckCommand);
|
|
33697
|
-
program2.command("mutate-diff").description("Mutate the lines this branch changed and run the related tests; survivors are gaps the tests do not cover. Always exits 0").option("--base <ref>", "Compare against this ref (default origin/main)").option("--json", "Print the report as JSON instead of a table").option("--markdown", "Print the report as GitHub markdown").option("--markdown-out <file>", "Also write the GitHub markdown report to this file").option("--budget <sec>", "Stop launching mutants after this many seconds (default 120)").option("--timeout <sec>", "Kill a single mutant run after this many seconds (default 60)").option("--baseline-timeout <sec>", "Kill an unmutated baseline run after this many seconds (default 300); a baseline over 60 s skips its file").option("--paths <list>", "Comma-separated git pathspecs that limit the diff").option("--out <file>", "Also write the JSON report to this file").action(mutateDiffCommand);
|
|
33942
|
+
program2.command("mutate-diff").description("Mutate the lines this branch changed and run the related tests; survivors are gaps the tests do not cover. Always exits 0").option("--base <ref>", "Compare against this ref (default origin/main)").option("--json", "Print the report as JSON instead of a table").option("--markdown", "Print the report as GitHub markdown").option("--markdown-out <file>", "Also write the GitHub markdown report to this file").option("--budget <sec>", "Stop launching mutants after this many seconds (default 120)").option("--total-budget <sec>", "Start no baseline or mutant after this many seconds, so a report is always written (default: no cap)").option("--timeout <sec>", "Kill a single mutant run after this many seconds (default 60)").option("--baseline-timeout <sec>", "Kill an unmutated baseline run after this many seconds (default 300); a baseline over 60 s skips its file").option("--paths <list>", "Comma-separated git pathspecs that limit the diff").option("--out <file>", "Also write the JSON report to this file").action(mutateDiffCommand);
|
|
33698
33943
|
var token = program2.command("token").description("Manage your agent's API tokens");
|
|
33699
33944
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
33700
33945
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
33701
33946
|
token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
|
|
33702
|
-
program2.command("onboard <github-url>").description("Set a GitHub repo up for
|
|
33947
|
+
program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
|
|
33703
33948
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
33704
33949
|
node.command("up [spec]").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them). With no spec file, follows the desired state the dashboard holds for this enrolled node").option(
|
|
33705
33950
|
"--invite <name=code>",
|
|
33706
|
-
"Redeem a pre-minted worker invite for that spec agent (repeatable). Prefer
|
|
33951
|
+
"Redeem a pre-minted worker invite for that spec agent (repeatable). Prefer SHIPSTER_NODE_INVITE_<NAME>: a flag puts the code on argv, which ps and shell history can see",
|
|
33707
33952
|
(value, acc) => {
|
|
33708
33953
|
acc.push(value);
|
|
33709
33954
|
return acc;
|
|
33710
33955
|
},
|
|
33711
33956
|
[]
|
|
33712
33957
|
).option("--adopt-new", "Register a new agent even when one with the same name already exists in the repo. Without it a name clash refuses that agent, since its credentials live in another config dir").option("--unsafe-shared-user", "Local development only: run owner-pool seats that have no runAs as this node's own user, which can read anything that user can, including ~/.ssh (SSH keys cannot be detected). Logs a warning on every start, and the node refuses to start while it holds GH_TOKEN or GITHUB_TOKEN, while `gh auth token` succeeds, while a git credential.helper is configured, or while ~/.git-credentials exists. Also PITBOSS_UNSAFE_SHARED_USER=1").action(nodeUpCommand);
|
|
33713
|
-
node.command("enroll [code]").description("Turn a dashboard enrollment code into this machine's node credential. Prefer
|
|
33958
|
+
node.command("enroll [code]").description("Turn a dashboard enrollment code into this machine's node credential. Prefer SHIPSTER_NODE_ENROLL_CODE: an argument is visible to ps and shell history").option("--api <url>", "API url (default: the one `shipster login` saved)").option("--force", "Replace an existing node credential").option("--root <dir>", "Directory every desired worker clone lives under (default <config>/node/work); created 0700, never / or your home directory").option("--allow-env <names>", "Comma-separated environment variable NAMES a dashboard-added worker may be granted (default: none)").option("--allow-host <hosts>", "Comma-separated hosts, besides github.com, this machine's GH_TOKEN may be sent to when cloning (default: none)").option("--allow-run-as <users>", "Comma-separated system users a dashboard-added worker may run as, Linux only (default: none, so workers run as the node user)").action(nodeEnrollCommand);
|
|
33714
33959
|
node.command("status").description("Show the agents the last `node up` on this machine is supervising").option("--json", "Print machine-readable output").action(nodeStatusCommand);
|
|
33715
33960
|
node.command("check <spec>").description("Verify the runAs file layout (owners, modes, users, capabilities) with stat only. Exit 0 ok, 1 violation, 2 spec unreadable").option("--root <dir>", "Work root: must be traversable and not listable by workers").option("--caps-from <file>", "`systemctl show pitboss-node` output to check the unit's capability sets (read from /proc/self/status inside the unit)").action(nodeCheckCommand);
|
|
33716
33961
|
node.command("_prepare <seat>", { hidden: true }).description("Internal: write a runAs seat's MCP config as that seat's user, from one JSON document on stdin").action(nodePrepareCommand);
|