@shipster/cli 0.5.2 → 0.5.3
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 +213 -111
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18414,6 +18414,26 @@ function parseDesired(input) {
|
|
|
18414
18414
|
}
|
|
18415
18415
|
}
|
|
18416
18416
|
|
|
18417
|
+
// ../../shared/types/src/tier-plan.ts
|
|
18418
|
+
function ladder(rungs) {
|
|
18419
|
+
if (rungs === 3) return DIFFICULTIES.map((d) => ({ tier: tierForDifficulty(d), difficulty: [d] }));
|
|
18420
|
+
return [{ tier: "cheap", difficulty: ["small", "standard"] }, { tier: "sensitive", difficulty: ["large"] }];
|
|
18421
|
+
}
|
|
18422
|
+
function planWriterTiers(models) {
|
|
18423
|
+
if (models.some((m) => m.trim() === "")) throw new Error("writer models: an empty model name");
|
|
18424
|
+
if (new Set(models).size !== models.length) throw new Error("writer models: a repeated model");
|
|
18425
|
+
if (models.length > 3) throw new Error("writer models: at most three, one per difficulty");
|
|
18426
|
+
if (models.length < 2) return { pools: [{ tier: "all", ...models[0] ? { model: models[0] } : {} }], poolOrder: null };
|
|
18427
|
+
const pools = ladder(models.length).map((rung, i) => ({ tier: rung.tier, model: models[i], difficulty: rung.difficulty }));
|
|
18428
|
+
return { pools, poolOrder: pools.map((p) => p.tier) };
|
|
18429
|
+
}
|
|
18430
|
+
function planReviewerSeat(writer, available) {
|
|
18431
|
+
const other = available.find((r) => r !== writer.runtime);
|
|
18432
|
+
if (other !== void 0) return { runtime: other, separation: "runtime" };
|
|
18433
|
+
const top = writer.models.at(-1);
|
|
18434
|
+
return { runtime: writer.runtime, ...top ? { model: top } : {}, separation: writer.models.length > 1 ? "model" : "none" };
|
|
18435
|
+
}
|
|
18436
|
+
|
|
18417
18437
|
// ../../shared/types/src/secret-patterns.ts
|
|
18418
18438
|
var SECRET_PATTERNS = [
|
|
18419
18439
|
/\bsk-/,
|
|
@@ -18473,6 +18493,14 @@ function isDifficulty(value) {
|
|
|
18473
18493
|
function difficultyRung(d) {
|
|
18474
18494
|
return DIFFICULTIES.indexOf(d);
|
|
18475
18495
|
}
|
|
18496
|
+
var TIER_OF_DIFFICULTY = {
|
|
18497
|
+
small: "cheap",
|
|
18498
|
+
standard: "standard",
|
|
18499
|
+
large: "sensitive"
|
|
18500
|
+
};
|
|
18501
|
+
function tierForDifficulty(d) {
|
|
18502
|
+
return TIER_OF_DIFFICULTY[d];
|
|
18503
|
+
}
|
|
18476
18504
|
var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
|
|
18477
18505
|
var DEFAULT_REVIEW_SILENT_MS = 40 * 60 * 1e3;
|
|
18478
18506
|
var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
|
|
@@ -24883,95 +24911,12 @@ var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
|
|
|
24883
24911
|
var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
|
|
24884
24912
|
|
|
24885
24913
|
// src/node/onboard.ts
|
|
24886
|
-
import { join as
|
|
24887
|
-
var MAX_POOL = 30;
|
|
24888
|
-
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
24889
|
-
function runtimeBinary(runtime, env3) {
|
|
24890
|
-
switch (runtime) {
|
|
24891
|
-
case "claude":
|
|
24892
|
-
return env3.CLAUDE_BIN ?? "claude";
|
|
24893
|
-
case "cursor":
|
|
24894
|
-
return env3.CURSOR_BIN ?? "cursor-agent";
|
|
24895
|
-
case "copilot":
|
|
24896
|
-
return env3.COPILOT_BIN ?? "copilot";
|
|
24897
|
-
case "codex":
|
|
24898
|
-
return env3.CODEX_BIN ?? "codex";
|
|
24899
|
-
}
|
|
24900
|
-
}
|
|
24901
|
-
function githubRepo(url) {
|
|
24902
|
-
const match = GITHUB.exec(url.trim());
|
|
24903
|
-
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
24904
|
-
return { owner: match[1], name: match[2] };
|
|
24905
|
-
}
|
|
24906
|
-
function planOnboard(url, opts) {
|
|
24907
|
-
const { owner, name } = githubRepo(url);
|
|
24908
|
-
for (const role of ["architect", "writer", "reviewer"]) {
|
|
24909
|
-
if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
|
|
24910
|
-
}
|
|
24911
|
-
const architectRuntime = opts.architect;
|
|
24912
|
-
const writer = opts.writer;
|
|
24913
|
-
const reviewer = opts.reviewer;
|
|
24914
|
-
for (const r of [architectRuntime, writer, reviewer]) {
|
|
24915
|
-
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
24916
|
-
}
|
|
24917
|
-
const warnings = reviewer === writer ? [`the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`] : [];
|
|
24918
|
-
const poolMax = opts.poolMax ?? 2;
|
|
24919
|
-
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
24920
|
-
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
24921
|
-
}
|
|
24922
|
-
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
24923
|
-
const parent = resolve6(opts.parent ?? join18(opts.home, "github"));
|
|
24924
|
-
const base2 = join18(parent, name);
|
|
24925
|
-
const nodeDir2 = join18(parent, `${name}-node`);
|
|
24926
|
-
const architectWorktree = join18(parent, `${name}-architect`);
|
|
24927
|
-
const review = join18(parent, `${name}-reviewer`);
|
|
24928
|
-
return {
|
|
24929
|
-
owner,
|
|
24930
|
-
name,
|
|
24931
|
-
warnings,
|
|
24932
|
-
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
24933
|
-
base: base2,
|
|
24934
|
-
nodeDir: nodeDir2,
|
|
24935
|
-
specPath: join18(nodeDir2, "spec.json"),
|
|
24936
|
-
configDir: join18(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
24937
|
-
worktrees: [architectWorktree, review],
|
|
24938
|
-
spec: {
|
|
24939
|
-
api: DEFAULT_API_URL,
|
|
24940
|
-
agents: [
|
|
24941
|
-
{ name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
|
|
24942
|
-
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
24943
|
-
{
|
|
24944
|
-
pool: `${id}-pool`,
|
|
24945
|
-
runtime: writer,
|
|
24946
|
-
base: `../${name}`,
|
|
24947
|
-
worktreeRoot: `../${name}-pool`,
|
|
24948
|
-
specialization: "writer",
|
|
24949
|
-
min: 0,
|
|
24950
|
-
max: poolMax
|
|
24951
|
-
}
|
|
24952
|
-
]
|
|
24953
|
-
}
|
|
24954
|
-
};
|
|
24955
|
-
}
|
|
24956
|
-
function onboardOperations(plan2, account) {
|
|
24957
|
-
const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
|
|
24958
|
-
return {
|
|
24959
|
-
...account ? { githubAccount: account } : {},
|
|
24960
|
-
...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
|
|
24961
|
-
...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
|
|
24962
|
-
};
|
|
24963
|
-
}
|
|
24964
|
-
function originHost(origin) {
|
|
24965
|
-
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
24966
|
-
if (url) return url[1].toLowerCase();
|
|
24967
|
-
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
24968
|
-
return scp ? scp[1].toLowerCase() : null;
|
|
24969
|
-
}
|
|
24914
|
+
import { join as join19, resolve as resolve7 } from "node:path";
|
|
24970
24915
|
|
|
24971
24916
|
// src/node/spec.ts
|
|
24972
24917
|
import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
|
|
24973
24918
|
import { homedir as homedir5 } from "node:os";
|
|
24974
|
-
import { join as
|
|
24919
|
+
import { join as join18, parse as parsePath, resolve as resolve6, sep as sep2 } from "node:path";
|
|
24975
24920
|
var MAX_POOL_SIZE = 32;
|
|
24976
24921
|
var MAX_ENV_GRANTS2 = 16;
|
|
24977
24922
|
var NAME2 = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
@@ -24984,7 +24929,7 @@ var canonical = (p) => {
|
|
|
24984
24929
|
try {
|
|
24985
24930
|
return realpathSync4(p);
|
|
24986
24931
|
} catch {
|
|
24987
|
-
return
|
|
24932
|
+
return resolve6(p);
|
|
24988
24933
|
}
|
|
24989
24934
|
};
|
|
24990
24935
|
function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
@@ -24995,7 +24940,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
|
24995
24940
|
const skipped = [];
|
|
24996
24941
|
raw.forEach((entry, i) => {
|
|
24997
24942
|
if (typeof entry !== "string" || entry === "" || entry.includes("\0")) throw new Error(`janitorRoots[${i}] must be a directory path`);
|
|
24998
|
-
const asked =
|
|
24943
|
+
const asked = resolve6(specDir, entry);
|
|
24999
24944
|
let real3;
|
|
25000
24945
|
try {
|
|
25001
24946
|
real3 = realpathSync4(asked);
|
|
@@ -25010,7 +24955,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
|
25010
24955
|
if (real3 === parsePath(real3).root) throw new Error(`janitorRoots[${i}] ${real3} is the filesystem root`);
|
|
25011
24956
|
if (real3 === home) throw new Error(`janitorRoots[${i}] ${real3} is the home directory`);
|
|
25012
24957
|
if (isWithin(home, real3)) throw new Error(`janitorRoots[${i}] ${real3} contains the home directory`);
|
|
25013
|
-
if (existsSync11(
|
|
24958
|
+
if (existsSync11(join18(real3, ".git"))) throw new Error(`janitorRoots[${i}] ${real3} is a git work tree; list the directory that holds worktrees, not a checkout`);
|
|
25014
24959
|
for (const a of allAgents({ api: "", agents, pools })) {
|
|
25015
24960
|
if (isWithin(real3, canonical(a.worktree))) {
|
|
25016
24961
|
throw new Error(`janitorRoots[${i}] ${real3} is or sits inside the worktree of agent "${a.name}"`);
|
|
@@ -25044,7 +24989,7 @@ function poolMembers(pool) {
|
|
|
25044
24989
|
return {
|
|
25045
24990
|
name,
|
|
25046
24991
|
runtime: pool.runtime,
|
|
25047
|
-
worktree:
|
|
24992
|
+
worktree: join18(pool.worktreeRoot, name),
|
|
25048
24993
|
specialization: pool.specialization,
|
|
25049
24994
|
...pool.ownerPool ? { ownerPool: true } : {},
|
|
25050
24995
|
...pool.tier ? { tier: pool.tier } : {},
|
|
@@ -25206,11 +25151,11 @@ function parsePool(a, i, specDir, platform2) {
|
|
|
25206
25151
|
}
|
|
25207
25152
|
burstMax = a.burstMax;
|
|
25208
25153
|
}
|
|
25209
|
-
const worktreeRoot =
|
|
25154
|
+
const worktreeRoot = resolve6(specDir, a.worktreeRoot);
|
|
25210
25155
|
if (ownerPool) {
|
|
25211
25156
|
return { pool: a.pool, runtime, ownerPool: true, worktreeRoot, specialization, ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
|
|
25212
25157
|
}
|
|
25213
|
-
const base2 =
|
|
25158
|
+
const base2 = resolve6(specDir, a.base);
|
|
25214
25159
|
if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
|
|
25215
25160
|
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
|
|
25216
25161
|
}
|
|
@@ -25247,7 +25192,7 @@ function parseSpec(text, specDir, opts = {}) {
|
|
|
25247
25192
|
}
|
|
25248
25193
|
const specialization = specializationOf(a, i);
|
|
25249
25194
|
if (ownerPool) ownerPoolSpecializationCheck(a.name, specialization, "agents");
|
|
25250
|
-
agents.push({ name: a.name, runtime, worktree:
|
|
25195
|
+
agents.push({ name: a.name, runtime, worktree: resolve6(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) });
|
|
25251
25196
|
});
|
|
25252
25197
|
const entries = [
|
|
25253
25198
|
...agents.map((a) => ({ label: `agent "${a.name}"`, ownerPool: a.ownerPool === true, seated: a.runAs !== void 0 })),
|
|
@@ -25291,6 +25236,115 @@ function parseSpec(text, specDir, opts = {}) {
|
|
|
25291
25236
|
};
|
|
25292
25237
|
}
|
|
25293
25238
|
|
|
25239
|
+
// src/node/onboard.ts
|
|
25240
|
+
var MAX_POOL = 30;
|
|
25241
|
+
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
25242
|
+
function runtimeBinary(runtime, env3) {
|
|
25243
|
+
switch (runtime) {
|
|
25244
|
+
case "claude":
|
|
25245
|
+
return env3.CLAUDE_BIN ?? "claude";
|
|
25246
|
+
case "cursor":
|
|
25247
|
+
return env3.CURSOR_BIN ?? "cursor-agent";
|
|
25248
|
+
case "copilot":
|
|
25249
|
+
return env3.COPILOT_BIN ?? "copilot";
|
|
25250
|
+
case "codex":
|
|
25251
|
+
return env3.CODEX_BIN ?? "codex";
|
|
25252
|
+
}
|
|
25253
|
+
}
|
|
25254
|
+
function githubRepo(url) {
|
|
25255
|
+
const match = GITHUB.exec(url.trim());
|
|
25256
|
+
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
25257
|
+
return { owner: match[1], name: match[2] };
|
|
25258
|
+
}
|
|
25259
|
+
function planOnboard(url, opts) {
|
|
25260
|
+
const { owner, name } = githubRepo(url);
|
|
25261
|
+
for (const role of ["architect", "writer", "reviewer"]) {
|
|
25262
|
+
if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
|
|
25263
|
+
}
|
|
25264
|
+
const architectRuntime = opts.architect;
|
|
25265
|
+
const writer = opts.writer;
|
|
25266
|
+
const reviewer = opts.reviewer;
|
|
25267
|
+
for (const r of [architectRuntime, writer, reviewer]) {
|
|
25268
|
+
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
25269
|
+
}
|
|
25270
|
+
const sameVendor = `the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`;
|
|
25271
|
+
const warnings = reviewer === writer ? [sameVendor] : [];
|
|
25272
|
+
const poolMax = opts.poolMax ?? 2;
|
|
25273
|
+
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
25274
|
+
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
25275
|
+
}
|
|
25276
|
+
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
25277
|
+
const parent = resolve7(opts.parent ?? join19(opts.home, "github"));
|
|
25278
|
+
const base2 = join19(parent, name);
|
|
25279
|
+
const nodeDir2 = join19(parent, `${name}-node`);
|
|
25280
|
+
const architectWorktree = join19(parent, `${name}-architect`);
|
|
25281
|
+
const review = join19(parent, `${name}-reviewer`);
|
|
25282
|
+
const writerPool = { runtime: writer, base: `../${name}`, specialization: "writer", min: 0, max: poolMax };
|
|
25283
|
+
const architect = { name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 };
|
|
25284
|
+
const tiers = opts.writerModels ? planWriterTiers(opts.writerModels) : null;
|
|
25285
|
+
const poolName = (tier) => `${id}-${tier === "all" ? "pool" : tier}`;
|
|
25286
|
+
if (tiers && tiers.pools.length * poolMax > MAX_POOL) {
|
|
25287
|
+
throw new Error(`${tiers.pools.length} writer pools of ${poolMax} do not fit one approval of ${MAX_POOL + 2} agents with the architect and reviewer; lower --pool-max`);
|
|
25288
|
+
}
|
|
25289
|
+
const seat = tiers ? planReviewerSeat({ runtime: writer, models: opts.writerModels }, [reviewer]) : null;
|
|
25290
|
+
const tierAgents = tiers && seat && [
|
|
25291
|
+
architect,
|
|
25292
|
+
...tiers.pools.map((p) => ({
|
|
25293
|
+
pool: poolName(p.tier),
|
|
25294
|
+
...writerPool,
|
|
25295
|
+
worktreeRoot: `../${name}-${p.tier === "all" ? "pool" : p.tier}`,
|
|
25296
|
+
...p.model ? { model: p.model } : {},
|
|
25297
|
+
...p.difficulty ? { difficulty: p.difficulty } : {}
|
|
25298
|
+
})),
|
|
25299
|
+
{
|
|
25300
|
+
pool: `${id}-reviewer`,
|
|
25301
|
+
runtime: seat.runtime,
|
|
25302
|
+
base: `../${name}`,
|
|
25303
|
+
worktreeRoot: `../${name}-reviewer-pool`,
|
|
25304
|
+
specialization: "reviewer",
|
|
25305
|
+
...seat.model ? { model: seat.model } : {},
|
|
25306
|
+
min: 0,
|
|
25307
|
+
max: 1
|
|
25308
|
+
}
|
|
25309
|
+
];
|
|
25310
|
+
const spec = {
|
|
25311
|
+
api: DEFAULT_API_URL,
|
|
25312
|
+
agents: tierAgents || [
|
|
25313
|
+
architect,
|
|
25314
|
+
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
25315
|
+
{ pool: `${id}-pool`, ...writerPool, worktreeRoot: `../${name}-pool` }
|
|
25316
|
+
]
|
|
25317
|
+
};
|
|
25318
|
+
parseSpec(JSON.stringify(spec), nodeDir2);
|
|
25319
|
+
return {
|
|
25320
|
+
owner,
|
|
25321
|
+
name,
|
|
25322
|
+
warnings,
|
|
25323
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
25324
|
+
base: base2,
|
|
25325
|
+
nodeDir: nodeDir2,
|
|
25326
|
+
specPath: join19(nodeDir2, "spec.json"),
|
|
25327
|
+
configDir: join19(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
25328
|
+
worktrees: tierAgents ? [architectWorktree] : [architectWorktree, review],
|
|
25329
|
+
poolOrder: tiers?.poolOrder?.map(poolName) ?? null,
|
|
25330
|
+
spec
|
|
25331
|
+
};
|
|
25332
|
+
}
|
|
25333
|
+
function onboardOperations(plan2, account) {
|
|
25334
|
+
const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
|
|
25335
|
+
return {
|
|
25336
|
+
...account ? { githubAccount: account } : {},
|
|
25337
|
+
...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
|
|
25338
|
+
...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
|
|
25339
|
+
};
|
|
25340
|
+
}
|
|
25341
|
+
function originHost(origin) {
|
|
25342
|
+
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
25343
|
+
if (url) return url[1].toLowerCase();
|
|
25344
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
25345
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
25346
|
+
}
|
|
25347
|
+
|
|
25294
25348
|
// src/node/store.ts
|
|
25295
25349
|
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync15, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
|
|
25296
25350
|
import { dirname as dirname10, join as join20 } from "node:path";
|
|
@@ -25392,7 +25446,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
|
25392
25446
|
};
|
|
25393
25447
|
for (const m of stopped) {
|
|
25394
25448
|
if (count >= max) break;
|
|
25395
|
-
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
25449
|
+
if (m.inProgress > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0) take(m);
|
|
25396
25450
|
}
|
|
25397
25451
|
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
25398
25452
|
let demand = pending - running.filter(isIdle).length;
|
|
@@ -25415,7 +25469,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
|
25415
25469
|
count--;
|
|
25416
25470
|
}
|
|
25417
25471
|
if (stop.length > 0) return { start: [], stop, why };
|
|
25418
|
-
const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0).length;
|
|
25472
|
+
const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0).length;
|
|
25419
25473
|
const freed = [];
|
|
25420
25474
|
if (blockedOwners > 0 && count >= max) {
|
|
25421
25475
|
for (const m of running.filter(isIdle)) {
|
|
@@ -25891,7 +25945,8 @@ async function onboardCommand(url, opts) {
|
|
|
25891
25945
|
architect: roles.architect,
|
|
25892
25946
|
writer: roles.writer,
|
|
25893
25947
|
reviewer: roles.reviewer,
|
|
25894
|
-
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
|
|
25948
|
+
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax),
|
|
25949
|
+
writerModels: opts.writerModels === void 0 ? void 0 : opts.writerModels.split(",").map((m) => m.trim())
|
|
25895
25950
|
});
|
|
25896
25951
|
for (const warning of plan2.warnings) console.log(source_default.yellow(` ${warning}`));
|
|
25897
25952
|
const env3 = { ...process.env };
|
|
@@ -25970,6 +26025,8 @@ async function onboardCommand(url, opts) {
|
|
|
25970
26025
|
Start it when you want it running (long-running, foreground):`);
|
|
25971
26026
|
console.log(source_default.cyan(` cd ${shellQuote(plan2.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan2.configDir)} pitboss node up spec.json`));
|
|
25972
26027
|
if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
|
|
26028
|
+
if (plan2.poolOrder) console.log(`
|
|
26029
|
+
Tiers route once the owner or its top-level agent sets each pool's difficulties (set_pool_size) and the pool order (set_pool_order): ${plan2.poolOrder.join(", ")}`);
|
|
25973
26030
|
} catch (err) {
|
|
25974
26031
|
console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
|
|
25975
26032
|
process.exitCode = 1;
|
|
@@ -31620,6 +31677,28 @@ function startActivityLoop(agents, sup, ownerPool = false) {
|
|
|
31620
31677
|
const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
|
|
31621
31678
|
return () => clearInterval(timer);
|
|
31622
31679
|
}
|
|
31680
|
+
var SHUTDOWN_MARK_BOUND_MS = 3e3;
|
|
31681
|
+
async function markPoolsStopped(loops, boundMs = SHUTDOWN_MARK_BOUND_MS) {
|
|
31682
|
+
let timer;
|
|
31683
|
+
const bound = new Promise((resolve19) => {
|
|
31684
|
+
timer = setTimeout(() => {
|
|
31685
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} node: shutdown marking pool members unavailable timed out after ${boundMs}ms`);
|
|
31686
|
+
resolve19();
|
|
31687
|
+
}, boundMs);
|
|
31688
|
+
});
|
|
31689
|
+
try {
|
|
31690
|
+
await Promise.race([Promise.all(loops.map((l) => l.markRunningStopped().catch(() => void 0))), bound]);
|
|
31691
|
+
} finally {
|
|
31692
|
+
clearTimeout(timer);
|
|
31693
|
+
}
|
|
31694
|
+
}
|
|
31695
|
+
async function shutdownNode(opts) {
|
|
31696
|
+
for (const stop of opts.stops) stop();
|
|
31697
|
+
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
31698
|
+
const marking = markPoolsStopped(opts.poolLoops, opts.boundMs);
|
|
31699
|
+
await Promise.all([opts.sup.stop(), marking]);
|
|
31700
|
+
opts.exit(0);
|
|
31701
|
+
}
|
|
31623
31702
|
function startPoolLoop(run4, sup, record, account) {
|
|
31624
31703
|
const { pool, members } = run4;
|
|
31625
31704
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -31640,6 +31719,7 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31640
31719
|
}
|
|
31641
31720
|
};
|
|
31642
31721
|
let busy = false;
|
|
31722
|
+
let closed = false;
|
|
31643
31723
|
const markStopped = async (member) => {
|
|
31644
31724
|
try {
|
|
31645
31725
|
await setAvailable(member.creds, false);
|
|
@@ -31735,12 +31815,17 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31735
31815
|
if (pool.ownerPool) resetOwnerPoolSlot(member.agent.worktree, member.agent.runAs);
|
|
31736
31816
|
else resetWorktree(member.agent.worktree);
|
|
31737
31817
|
}
|
|
31818
|
+
if (closed) return;
|
|
31738
31819
|
await setAvailable(member.creds, true);
|
|
31739
31820
|
markedAt.delete(name);
|
|
31740
31821
|
} catch (err) {
|
|
31741
31822
|
log(`not starting ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31742
31823
|
continue;
|
|
31743
31824
|
}
|
|
31825
|
+
if (closed) {
|
|
31826
|
+
await markStopped(member);
|
|
31827
|
+
return;
|
|
31828
|
+
}
|
|
31744
31829
|
lastBusy.set(name, Date.now());
|
|
31745
31830
|
sup.startAgent(name);
|
|
31746
31831
|
log(`started ${name} (ready ${summary2.ready})`);
|
|
@@ -31766,9 +31851,25 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31766
31851
|
busy = false;
|
|
31767
31852
|
}
|
|
31768
31853
|
};
|
|
31769
|
-
|
|
31770
|
-
const
|
|
31771
|
-
|
|
31854
|
+
let inflight = null;
|
|
31855
|
+
const schedule = () => {
|
|
31856
|
+
if (inflight) return;
|
|
31857
|
+
inflight = tick().finally(() => {
|
|
31858
|
+
inflight = null;
|
|
31859
|
+
});
|
|
31860
|
+
};
|
|
31861
|
+
schedule();
|
|
31862
|
+
const timer = setInterval(schedule, reconcileMs);
|
|
31863
|
+
const stop = (() => {
|
|
31864
|
+
closed = true;
|
|
31865
|
+
clearInterval(timer);
|
|
31866
|
+
});
|
|
31867
|
+
stop.markRunningStopped = async () => {
|
|
31868
|
+
closed = true;
|
|
31869
|
+
const running = members.filter((m) => sup.isActive(m.agent.name));
|
|
31870
|
+
await Promise.all([inflight, ...running.map((m) => markStopped(m))]);
|
|
31871
|
+
};
|
|
31872
|
+
return stop;
|
|
31772
31873
|
}
|
|
31773
31874
|
var MODEL_REFRESH_MS = 15e3;
|
|
31774
31875
|
function desiredIdentity(api, root, nodeToken, log) {
|
|
@@ -31854,10 +31955,12 @@ async function nodeUpDesired() {
|
|
|
31854
31955
|
sup.start();
|
|
31855
31956
|
const identity = createIdentityRefresher(capacityDir());
|
|
31856
31957
|
const loops = [];
|
|
31958
|
+
const poolLoops = [];
|
|
31857
31959
|
const stopAll = [];
|
|
31858
31960
|
let rebuild = null;
|
|
31859
31961
|
const rebuildLoops = () => {
|
|
31860
31962
|
for (const stop of loops.splice(0)) stop();
|
|
31963
|
+
poolLoops.splice(0);
|
|
31861
31964
|
const runnable = allAgents(latest).filter((a) => sup.has(a.name));
|
|
31862
31965
|
const credsOf = (a) => readCreds(a.name);
|
|
31863
31966
|
const byRepo = /* @__PURE__ */ new Map();
|
|
@@ -31875,10 +31978,12 @@ async function nodeUpDesired() {
|
|
|
31875
31978
|
});
|
|
31876
31979
|
if (members.length === 0) continue;
|
|
31877
31980
|
poolStates[pool.pool] = poolStates[pool.pool] ?? { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
|
|
31878
|
-
|
|
31981
|
+
const loop = startPoolLoop({ pool, members, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
|
|
31879
31982
|
poolStates[name] = state;
|
|
31880
31983
|
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
31881
|
-
}, () => identity.current())
|
|
31984
|
+
}, () => identity.current());
|
|
31985
|
+
loops.push(loop);
|
|
31986
|
+
poolLoops.push(loop);
|
|
31882
31987
|
}
|
|
31883
31988
|
for (const name of Object.keys(poolStates)) if (!latest.pools.some((p) => p.pool === name)) delete poolStates[name];
|
|
31884
31989
|
};
|
|
@@ -31957,10 +32062,7 @@ async function nodeUpDesired() {
|
|
|
31957
32062
|
}, 6e4);
|
|
31958
32063
|
const shutdown = async () => {
|
|
31959
32064
|
clearInterval(keepAlive);
|
|
31960
|
-
|
|
31961
|
-
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
31962
|
-
await sup.stop();
|
|
31963
|
-
process.exit(0);
|
|
32065
|
+
await shutdownNode({ stops: [...loops, ...stopAll, stopHeartbeat, janitor.stop], sup, poolLoops: [...poolLoops], exit: (code) => process.exit(code) });
|
|
31964
32066
|
};
|
|
31965
32067
|
const janitor = startJanitorService({ roots: [root], managed: () => managedOfSpec(latest), nodeId: node2.nodeId, log: (line) => console.log(source_default.dim(` ${line}`)) });
|
|
31966
32068
|
const stopHeartbeat = startNodeHeartbeat(
|
|
@@ -32094,14 +32196,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
32094
32196
|
`));
|
|
32095
32197
|
const stopLoops = [];
|
|
32096
32198
|
const ownerBases = createOwnerPoolBases(ownerPoolBasesDepsFor(opts));
|
|
32199
|
+
const poolLoops = [];
|
|
32097
32200
|
let stopping = false;
|
|
32098
32201
|
const shutdown = async () => {
|
|
32099
32202
|
if (stopping) return;
|
|
32100
32203
|
stopping = true;
|
|
32101
|
-
|
|
32102
|
-
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
32103
|
-
await sup.stop();
|
|
32104
|
-
process.exit(0);
|
|
32204
|
+
await shutdownNode({ stops: stopLoops, sup, poolLoops, exit: (code) => process.exit(code) });
|
|
32105
32205
|
};
|
|
32106
32206
|
process.on("SIGINT", shutdown);
|
|
32107
32207
|
process.on("SIGTERM", shutdown);
|
|
@@ -32146,10 +32246,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
32146
32246
|
continue;
|
|
32147
32247
|
}
|
|
32148
32248
|
poolStates[pool.pool] = { members: ready.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null, ...pool.ownerPool ? { ownerPool: true } : {} };
|
|
32149
|
-
|
|
32249
|
+
const loop = startPoolLoop({ pool, members: ready, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
|
|
32150
32250
|
poolStates[name] = state;
|
|
32151
32251
|
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
32152
|
-
}, () => identity.current())
|
|
32252
|
+
}, () => identity.current());
|
|
32253
|
+
stopLoops.push(loop);
|
|
32254
|
+
poolLoops.push(loop);
|
|
32153
32255
|
}
|
|
32154
32256
|
} catch (err) {
|
|
32155
32257
|
console.error(source_default.red(`
|
|
@@ -33964,7 +34066,7 @@ var token = program2.command("token").description("Manage your agent's API token
|
|
|
33964
34066
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
33965
34067
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
33966
34068
|
token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
|
|
33967
|
-
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);
|
|
34069
|
+
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("--writer-models <list>", "One to three writer models, cheapest first, comma-separated: one writer pool per tier and a reviewer pool, all scaling to zero").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
|
|
33968
34070
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
33969
34071
|
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(
|
|
33970
34072
|
"--invite <name=code>",
|