@ashlr/stack 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1607 -48
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1490,9 +1490,9 @@ var init_stack_manifest = __esm(() => {
|
|
|
1490
1490
|
schema_version: 1,
|
|
1491
1491
|
kind: "ashlr-stack-manifest",
|
|
1492
1492
|
components: Object.freeze([
|
|
1493
|
-
Object.freeze({ id: "core", name: "@ashlr/stack-core", version: "0.3.
|
|
1494
|
-
Object.freeze({ id: "cli", name: "@ashlr/stack", version: "0.3.
|
|
1495
|
-
Object.freeze({ id: "mcp", name: "ashlr-stack-mcp", version: "0.3.
|
|
1493
|
+
Object.freeze({ id: "core", name: "@ashlr/stack-core", version: "0.3.9", distribution: "npm" }),
|
|
1494
|
+
Object.freeze({ id: "cli", name: "@ashlr/stack", version: "0.3.9", distribution: "npm" }),
|
|
1495
|
+
Object.freeze({ id: "mcp", name: "ashlr-stack-mcp", version: "0.3.9", distribution: "npm" }),
|
|
1496
1496
|
Object.freeze({
|
|
1497
1497
|
id: "plugin",
|
|
1498
1498
|
name: "stack",
|
|
@@ -1974,28 +1974,28 @@ async function fetchWithRetry(input, init, opts = {}) {
|
|
|
1974
1974
|
const baseDelayMs = opts.baseDelayMs ?? 400;
|
|
1975
1975
|
const maxDelayMs = opts.maxDelayMs ?? 8000;
|
|
1976
1976
|
const retryOn = opts.retryOn ?? defaultRetryOn;
|
|
1977
|
-
const idempotent = opts.idempotent ?? isIdempotent(init?.method);
|
|
1978
|
-
const
|
|
1977
|
+
const idempotent = opts.idempotent ?? isIdempotent(init?.method ?? (input instanceof Request ? input.method : undefined));
|
|
1978
|
+
const requestSignal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
1979
|
+
const mergedInit = mergeSignal({ ...init, signal: requestSignal }, opts.signal);
|
|
1980
|
+
const signal = mergedInit?.signal ?? undefined;
|
|
1979
1981
|
let attempt = 0;
|
|
1980
|
-
let lastResponse;
|
|
1981
|
-
let lastError;
|
|
1982
1982
|
while (true) {
|
|
1983
1983
|
if (signal?.aborted)
|
|
1984
1984
|
throw abortError(signal);
|
|
1985
1985
|
try {
|
|
1986
|
-
const res = await fetch(input,
|
|
1986
|
+
const res = await fetch(input, mergedInit);
|
|
1987
1987
|
if (res.ok || !idempotent || !retryOn(res))
|
|
1988
1988
|
return res;
|
|
1989
|
-
lastResponse = res;
|
|
1990
1989
|
if (attempt >= retries)
|
|
1991
1990
|
return res;
|
|
1992
1991
|
const waitMs = computeDelay(res, attempt, baseDelayMs, maxDelayMs);
|
|
1993
1992
|
await sleep(waitMs, signal);
|
|
1994
1993
|
attempt++;
|
|
1995
1994
|
} catch (err) {
|
|
1995
|
+
if (signal?.aborted)
|
|
1996
|
+
throw abortError(signal);
|
|
1996
1997
|
if (isAbortError(err))
|
|
1997
1998
|
throw err;
|
|
1998
|
-
lastError = err;
|
|
1999
1999
|
if (!idempotent || attempt >= retries)
|
|
2000
2000
|
throw err;
|
|
2001
2001
|
const waitMs = computeDelay(undefined, attempt, baseDelayMs, maxDelayMs);
|
|
@@ -7524,6 +7524,183 @@ args = [${GATEWAY_ARGS.map((a) => `"${a}"`).join(", ")}]
|
|
|
7524
7524
|
`;
|
|
7525
7525
|
});
|
|
7526
7526
|
|
|
7527
|
+
// ../core/src/fleet/capability-matrix.ts
|
|
7528
|
+
function parseStringList(value, valid) {
|
|
7529
|
+
if (!Array.isArray(value) || value.some((v) => !valid(v)))
|
|
7530
|
+
return null;
|
|
7531
|
+
return [...new Set(value)];
|
|
7532
|
+
}
|
|
7533
|
+
function parseCapabilityDocument(text) {
|
|
7534
|
+
let root;
|
|
7535
|
+
try {
|
|
7536
|
+
root = JSON.parse(text);
|
|
7537
|
+
} catch {
|
|
7538
|
+
return { status: "invalid", document: null, error: "not valid JSON" };
|
|
7539
|
+
}
|
|
7540
|
+
if (!isRecord(root))
|
|
7541
|
+
return { status: "invalid", document: null, error: "not a JSON object" };
|
|
7542
|
+
if (root.schema !== "fleet.capabilities.v1")
|
|
7543
|
+
return { status: "invalid", document: null, error: 'expected schema "fleet.capabilities.v1"' };
|
|
7544
|
+
if (root.resources !== undefined && !isRecord(root.resources))
|
|
7545
|
+
return { status: "invalid", document: null, error: '"resources" must be an object' };
|
|
7546
|
+
const resources = new Map;
|
|
7547
|
+
let dropped = 0;
|
|
7548
|
+
for (const [id, value] of Object.entries(isRecord(root.resources) ? root.resources : {})) {
|
|
7549
|
+
if (!isResourceId(id) || !isRecord(value)) {
|
|
7550
|
+
dropped++;
|
|
7551
|
+
continue;
|
|
7552
|
+
}
|
|
7553
|
+
let roles = null;
|
|
7554
|
+
if (value.roles !== undefined) {
|
|
7555
|
+
const parsed = parseStringList(value.roles, isFleetRole);
|
|
7556
|
+
if (parsed === null) {
|
|
7557
|
+
dropped++;
|
|
7558
|
+
continue;
|
|
7559
|
+
}
|
|
7560
|
+
roles = parsed;
|
|
7561
|
+
}
|
|
7562
|
+
let capabilities = [];
|
|
7563
|
+
if (value.capabilities !== undefined) {
|
|
7564
|
+
const parsed = parseStringList(value.capabilities, isTag);
|
|
7565
|
+
if (parsed === null) {
|
|
7566
|
+
dropped++;
|
|
7567
|
+
continue;
|
|
7568
|
+
}
|
|
7569
|
+
capabilities = parsed;
|
|
7570
|
+
}
|
|
7571
|
+
let tiers = null;
|
|
7572
|
+
if (value.tiers !== undefined) {
|
|
7573
|
+
const parsed = parseStringList(value.tiers, isTier);
|
|
7574
|
+
if (parsed === null) {
|
|
7575
|
+
dropped++;
|
|
7576
|
+
continue;
|
|
7577
|
+
}
|
|
7578
|
+
tiers = parsed;
|
|
7579
|
+
}
|
|
7580
|
+
resources.set(id, { roles, capabilities, tiers });
|
|
7581
|
+
}
|
|
7582
|
+
return {
|
|
7583
|
+
status: "ok",
|
|
7584
|
+
document: { schema: "fleet.capabilities.v1", resources, dropped },
|
|
7585
|
+
error: null
|
|
7586
|
+
};
|
|
7587
|
+
}
|
|
7588
|
+
function effectiveRoles(declaration) {
|
|
7589
|
+
return declaration?.roles ?? DEFAULT_RESOURCE_ROLES;
|
|
7590
|
+
}
|
|
7591
|
+
function buildFleetResource(id, declaration, quota) {
|
|
7592
|
+
return {
|
|
7593
|
+
id,
|
|
7594
|
+
declared: declaration !== undefined,
|
|
7595
|
+
roles: effectiveRoles(declaration),
|
|
7596
|
+
capabilities: declaration?.capabilities ?? [],
|
|
7597
|
+
tiers: declaration?.tiers ?? null,
|
|
7598
|
+
quota
|
|
7599
|
+
};
|
|
7600
|
+
}
|
|
7601
|
+
function capacityBound(quota) {
|
|
7602
|
+
const bounds = [quota.target, quota.ceiling, quota.hardCap].filter((v) => v !== null);
|
|
7603
|
+
return bounds.length ? Math.min(...bounds) : null;
|
|
7604
|
+
}
|
|
7605
|
+
function resourceHeadroom(quota) {
|
|
7606
|
+
if (quota === null || quota.running === null)
|
|
7607
|
+
return null;
|
|
7608
|
+
const bound = capacityBound(quota);
|
|
7609
|
+
return bound === null ? null : Math.max(0, bound - quota.running);
|
|
7610
|
+
}
|
|
7611
|
+
function matchRequirement(requirement, resources, options = {}) {
|
|
7612
|
+
const fleetFull = options.fleet?.hardCap != null && options.fleet.running != null && options.fleet.running >= options.fleet.hardCap;
|
|
7613
|
+
const matches = resources.map((resource) => {
|
|
7614
|
+
const reasons = [];
|
|
7615
|
+
if (!resource.roles.includes(requirement.role))
|
|
7616
|
+
reasons.push(`missing-role:${requirement.role}`);
|
|
7617
|
+
for (const tag of requirement.capabilities)
|
|
7618
|
+
if (!resource.capabilities.includes(tag))
|
|
7619
|
+
reasons.push(`missing-capability:${tag}`);
|
|
7620
|
+
if (requirement.tier !== null && resource.tiers !== null && !resource.tiers.includes(requirement.tier))
|
|
7621
|
+
reasons.push(`tier-unsupported:${requirement.tier}`);
|
|
7622
|
+
if (options.fleetHold === true)
|
|
7623
|
+
reasons.push("fleet-hold");
|
|
7624
|
+
if (fleetFull)
|
|
7625
|
+
reasons.push("fleet-at-capacity");
|
|
7626
|
+
if (resource.quota?.held === true)
|
|
7627
|
+
reasons.push("held");
|
|
7628
|
+
const headroom = resourceHeadroom(resource.quota);
|
|
7629
|
+
if (headroom === 0)
|
|
7630
|
+
reasons.push("at-capacity");
|
|
7631
|
+
return { id: resource.id, eligible: reasons.length === 0, reasons, headroom };
|
|
7632
|
+
});
|
|
7633
|
+
matches.sort((a, b) => {
|
|
7634
|
+
if (a.eligible !== b.eligible)
|
|
7635
|
+
return a.eligible ? -1 : 1;
|
|
7636
|
+
if (a.eligible) {
|
|
7637
|
+
if (a.headroom !== b.headroom) {
|
|
7638
|
+
if (a.headroom === null)
|
|
7639
|
+
return 1;
|
|
7640
|
+
if (b.headroom === null)
|
|
7641
|
+
return -1;
|
|
7642
|
+
return b.headroom - a.headroom;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
return a.id.localeCompare(b.id);
|
|
7646
|
+
});
|
|
7647
|
+
const limited = options.limit === undefined ? matches : matches.slice(0, options.limit);
|
|
7648
|
+
return {
|
|
7649
|
+
requirement,
|
|
7650
|
+
matches: limited,
|
|
7651
|
+
eligible: limited.filter((m) => m.eligible).map((m) => m.id)
|
|
7652
|
+
};
|
|
7653
|
+
}
|
|
7654
|
+
function parseModelMap(text) {
|
|
7655
|
+
const map = new Map;
|
|
7656
|
+
let dropped = 0;
|
|
7657
|
+
let root;
|
|
7658
|
+
try {
|
|
7659
|
+
root = JSON.parse(text);
|
|
7660
|
+
} catch {
|
|
7661
|
+
return { map, dropped: 1, status: "invalid" };
|
|
7662
|
+
}
|
|
7663
|
+
if (!isRecord(root))
|
|
7664
|
+
return { map, dropped: 1, status: "invalid" };
|
|
7665
|
+
for (const [key, value] of Object.entries(root)) {
|
|
7666
|
+
const dot = key.indexOf(".");
|
|
7667
|
+
const lane = dot < 0 ? key : key.slice(0, dot);
|
|
7668
|
+
const tier = dot < 0 ? null : key.slice(dot + 1);
|
|
7669
|
+
if (!MODEL_KEY_RE.test(lane) || tier !== null && !isTier(tier) || typeof value !== "string" || !MODEL_RE.test(value.trim())) {
|
|
7670
|
+
dropped++;
|
|
7671
|
+
continue;
|
|
7672
|
+
}
|
|
7673
|
+
map.set(key, value.trim());
|
|
7674
|
+
}
|
|
7675
|
+
return { map, dropped, status: "ok" };
|
|
7676
|
+
}
|
|
7677
|
+
function resolveModel(map, resourceId, tier) {
|
|
7678
|
+
const family = modelFamily(resourceId);
|
|
7679
|
+
const keys = family === resourceId ? tier === null ? [resourceId] : [`${resourceId}.${tier}`, resourceId] : tier === null ? [resourceId, family] : [`${resourceId}.${tier}`, resourceId, `${family}.${tier}`, family];
|
|
7680
|
+
for (const key of keys) {
|
|
7681
|
+
const model = map.get(key);
|
|
7682
|
+
if (model)
|
|
7683
|
+
return { model, via: key };
|
|
7684
|
+
}
|
|
7685
|
+
return null;
|
|
7686
|
+
}
|
|
7687
|
+
var FLEET_ROLES, ROLE_SET, RESOURCE_ID_RE, TAG_RE, TIER_RE, isFleetRole = (value) => typeof value === "string" && ROLE_SET.has(value), isResourceId = (value) => typeof value === "string" && RESOURCE_ID_RE.test(value), isTag = (value) => typeof value === "string" && TAG_RE.test(value), isTier = (value) => typeof value === "string" && TIER_RE.test(value), isFleetCapability, isFleetTier, isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value), DEFAULT_RESOURCE_ROLES, MODEL_KEY_RE, MODEL_RE, modelFamily = (resourceId) => resourceId.startsWith("codex") && resourceId !== "codex-local" ? "codex" : resourceId;
|
|
7688
|
+
var init_capability_matrix = __esm(() => {
|
|
7689
|
+
FLEET_ROLES = ["planner", "implementer", "reviewer", "release-verifier"];
|
|
7690
|
+
ROLE_SET = new Set(FLEET_ROLES);
|
|
7691
|
+
RESOURCE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7692
|
+
TAG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7693
|
+
TIER_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
7694
|
+
isFleetCapability = isTag;
|
|
7695
|
+
isFleetTier = isTier;
|
|
7696
|
+
DEFAULT_RESOURCE_ROLES = ["implementer"];
|
|
7697
|
+
MODEL_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7698
|
+
MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/;
|
|
7699
|
+
});
|
|
7700
|
+
|
|
7701
|
+
// ../core/src/fleet/job-run.ts
|
|
7702
|
+
var init_job_run = () => {};
|
|
7703
|
+
|
|
7527
7704
|
// ../../node_modules/.bun/consola@3.4.2/node_modules/consola/dist/core.mjs
|
|
7528
7705
|
function isPlainObject$1(value) {
|
|
7529
7706
|
if (value === null || typeof value !== "object") {
|
|
@@ -9978,6 +10155,8 @@ var init_src = __esm(() => {
|
|
|
9978
10155
|
init_detect_source();
|
|
9979
10156
|
init_registry();
|
|
9980
10157
|
init_agent_onboard();
|
|
10158
|
+
init_capability_matrix();
|
|
10159
|
+
init_job_run();
|
|
9981
10160
|
});
|
|
9982
10161
|
|
|
9983
10162
|
// ../../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
|
|
@@ -16259,9 +16438,1130 @@ var init_fleet_graph = __esm(() => {
|
|
|
16259
16438
|
});
|
|
16260
16439
|
});
|
|
16261
16440
|
|
|
16262
|
-
// src/
|
|
16441
|
+
// src/lib/fleet-gc.ts
|
|
16442
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
16443
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
16444
|
+
import { lstat as lstat2, readFile as readFile12, readdir as readdir3, realpath } from "node:fs/promises";
|
|
16445
|
+
import { basename as basename3, dirname as dirname6, join as join22, resolve as resolve11 } from "node:path";
|
|
16446
|
+
import { promisify } from "node:util";
|
|
16447
|
+
function cleanEnv(extra = {}) {
|
|
16448
|
+
const env = {};
|
|
16449
|
+
for (const key of ENV_ALLOW) {
|
|
16450
|
+
const value = process.env[key];
|
|
16451
|
+
if (value !== undefined)
|
|
16452
|
+
env[key] = value;
|
|
16453
|
+
}
|
|
16454
|
+
env.PATH ??= "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
16455
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
16456
|
+
env.GIT_OPTIONAL_LOCKS = "0";
|
|
16457
|
+
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
16458
|
+
env.GIT_CONFIG_GLOBAL = "/dev/null";
|
|
16459
|
+
return { ...env, ...extra };
|
|
16460
|
+
}
|
|
16461
|
+
async function runProc(bin, args, cwd, timeoutMs, maxBuffer) {
|
|
16462
|
+
try {
|
|
16463
|
+
const { stdout } = await execFileAsync(bin, args, {
|
|
16464
|
+
cwd,
|
|
16465
|
+
env: cleanEnv(),
|
|
16466
|
+
timeout: timeoutMs,
|
|
16467
|
+
maxBuffer
|
|
16468
|
+
});
|
|
16469
|
+
return { code: 0, stdout, timedOut: false };
|
|
16470
|
+
} catch (error) {
|
|
16471
|
+
const e = error;
|
|
16472
|
+
return {
|
|
16473
|
+
code: typeof e.code === "number" ? e.code : null,
|
|
16474
|
+
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
|
16475
|
+
timedOut: e.killed === true
|
|
16476
|
+
};
|
|
16477
|
+
}
|
|
16478
|
+
}
|
|
16479
|
+
function parseWorktreePorcelain(text) {
|
|
16480
|
+
const out = [];
|
|
16481
|
+
let cur = null;
|
|
16482
|
+
for (const rec of text.split("\x00")) {
|
|
16483
|
+
if (rec.startsWith("worktree ")) {
|
|
16484
|
+
if (cur)
|
|
16485
|
+
out.push(cur);
|
|
16486
|
+
cur = {
|
|
16487
|
+
path: rec.slice("worktree ".length),
|
|
16488
|
+
head: "",
|
|
16489
|
+
branch: null,
|
|
16490
|
+
detached: false,
|
|
16491
|
+
locked: false,
|
|
16492
|
+
prunable: false
|
|
16493
|
+
};
|
|
16494
|
+
} else if (cur) {
|
|
16495
|
+
if (rec.startsWith("HEAD "))
|
|
16496
|
+
cur.head = rec.slice(5);
|
|
16497
|
+
else if (rec.startsWith("branch "))
|
|
16498
|
+
cur.branch = rec.slice(7);
|
|
16499
|
+
else if (rec === "detached")
|
|
16500
|
+
cur.detached = true;
|
|
16501
|
+
else if (rec === "locked" || rec.startsWith("locked "))
|
|
16502
|
+
cur.locked = true;
|
|
16503
|
+
else if (rec === "prunable" || rec.startsWith("prunable "))
|
|
16504
|
+
cur.prunable = true;
|
|
16505
|
+
}
|
|
16506
|
+
}
|
|
16507
|
+
if (cur)
|
|
16508
|
+
out.push(cur);
|
|
16509
|
+
return out;
|
|
16510
|
+
}
|
|
16511
|
+
function lastResultClass(text) {
|
|
16512
|
+
const lines = text.split(`
|
|
16513
|
+
`).filter((l) => l.startsWith("RESULT"));
|
|
16514
|
+
const line = lines.at(-1) ?? "";
|
|
16515
|
+
if (!line)
|
|
16516
|
+
return { cls: null, line: "" };
|
|
16517
|
+
if (line.startsWith("RESULT PR ")) {
|
|
16518
|
+
return RESULT_PR_RE.test(line) ? { cls: "pr", line } : { cls: "other", line };
|
|
16519
|
+
}
|
|
16520
|
+
if (line.startsWith("RESULT ARCHIVED "))
|
|
16521
|
+
return { cls: "archived", line };
|
|
16522
|
+
if (line === "RESULT EMPTY" || line.startsWith("RESULT EMPTY "))
|
|
16523
|
+
return { cls: "empty", line };
|
|
16524
|
+
if (line.includes("secret-shape"))
|
|
16525
|
+
return { cls: "abort", line };
|
|
16526
|
+
if (line.startsWith("RESULT DENIED"))
|
|
16527
|
+
return { cls: "denied", line };
|
|
16528
|
+
if (line.startsWith("RESULT ABORT "))
|
|
16529
|
+
return { cls: "abort", line };
|
|
16530
|
+
return { cls: "other", line };
|
|
16531
|
+
}
|
|
16532
|
+
async function readFleetBytes(path, maxBytes = 1024 * 1024) {
|
|
16533
|
+
try {
|
|
16534
|
+
const file = await openFleetFile(path);
|
|
16535
|
+
try {
|
|
16536
|
+
const stat = await file.stat();
|
|
16537
|
+
if (stat.size > maxBytes)
|
|
16538
|
+
return { status: "invalid", bytes: null };
|
|
16539
|
+
const buf = Buffer.alloc(stat.size);
|
|
16540
|
+
const { bytesRead } = await file.read(buf, 0, stat.size, 0);
|
|
16541
|
+
return { status: "ok", bytes: buf.subarray(0, bytesRead) };
|
|
16542
|
+
} finally {
|
|
16543
|
+
await file.close();
|
|
16544
|
+
}
|
|
16545
|
+
} catch (error) {
|
|
16546
|
+
const code = error.code;
|
|
16547
|
+
return {
|
|
16548
|
+
status: code === "ENOENT" ? "missing" : "invalid",
|
|
16549
|
+
bytes: null
|
|
16550
|
+
};
|
|
16551
|
+
}
|
|
16552
|
+
}
|
|
16553
|
+
async function loadHoldList(runs, sources) {
|
|
16554
|
+
const rulesPath = join22(runs, "ENGINEER-RULES.md");
|
|
16555
|
+
const shaPath = `${rulesPath}.sha256`;
|
|
16556
|
+
const [rulesL, shaL] = await Promise.all([
|
|
16557
|
+
lstat2(rulesPath).catch(() => null),
|
|
16558
|
+
lstat2(shaPath).catch(() => null)
|
|
16559
|
+
]);
|
|
16560
|
+
const bad = (key, status) => {
|
|
16561
|
+
sources[key] = status;
|
|
16562
|
+
return { entries: [], ok: false };
|
|
16563
|
+
};
|
|
16564
|
+
if (!rulesL || rulesL.isSymbolicLink() || !rulesL.isFile())
|
|
16565
|
+
return bad("rules", rulesL ? "invalid" : "missing");
|
|
16566
|
+
if (!shaL || shaL.isSymbolicLink() || !shaL.isFile())
|
|
16567
|
+
return bad("rulesSha", shaL ? "invalid" : "missing");
|
|
16568
|
+
const rulesBytes = await readFleetBytes(rulesPath);
|
|
16569
|
+
const shaBytes = await readFleetBytes(shaPath, 4096);
|
|
16570
|
+
sources.rules = rulesBytes.status;
|
|
16571
|
+
sources.rulesSha = shaBytes.status;
|
|
16572
|
+
if (rulesBytes.status !== "ok" || shaBytes.status !== "ok" || !rulesBytes.bytes || !shaBytes.bytes)
|
|
16573
|
+
return { entries: [], ok: false };
|
|
16574
|
+
const want = shaBytes.bytes.toString("utf8").trim().split(/\s+/)[0] ?? "";
|
|
16575
|
+
if (!/^[0-9a-f]{64}$/.test(want) || sha256Hex(rulesBytes.bytes) !== want)
|
|
16576
|
+
return { entries: [], ok: false };
|
|
16577
|
+
const text = rulesBytes.bytes.toString("utf8");
|
|
16578
|
+
const lines = text.split(`
|
|
16579
|
+
`).filter((l) => l.startsWith("7. Hold list"));
|
|
16580
|
+
if (lines.length !== 1)
|
|
16581
|
+
return { entries: [], ok: false };
|
|
16582
|
+
const l7 = lines[0];
|
|
16583
|
+
if (!/^7\. Hold list.*:/.test(l7))
|
|
16584
|
+
return { entries: [], ok: false };
|
|
16585
|
+
const body = l7.slice(l7.indexOf(":") + 1).replace(/\.$/, "");
|
|
16586
|
+
const entries = [];
|
|
16587
|
+
for (const seg of body.split(";")) {
|
|
16588
|
+
const tokens = seg.trim().split(/\s+/).filter(Boolean);
|
|
16589
|
+
if (tokens.length < 2)
|
|
16590
|
+
return { entries: [], ok: false };
|
|
16591
|
+
const repo = tokens[0];
|
|
16592
|
+
if (!FLEET_REPOS.has(repo))
|
|
16593
|
+
return { entries: [], ok: false };
|
|
16594
|
+
const prs = new Set;
|
|
16595
|
+
for (const t of tokens.slice(1)) {
|
|
16596
|
+
const m = t.match(/^#(\d+)\.?$/);
|
|
16597
|
+
if (!m)
|
|
16598
|
+
return { entries: [], ok: false };
|
|
16599
|
+
prs.add(m[1]);
|
|
16600
|
+
}
|
|
16601
|
+
entries.push({ repo, prs });
|
|
16602
|
+
}
|
|
16603
|
+
return { entries, ok: entries.length > 0 };
|
|
16604
|
+
}
|
|
16605
|
+
async function loadFleetState(runs, queue) {
|
|
16606
|
+
const sources = {};
|
|
16607
|
+
const readSet = async (path, key) => {
|
|
16608
|
+
const src = await readFleetText(path);
|
|
16609
|
+
sources[key] = src.status;
|
|
16610
|
+
return src.text;
|
|
16611
|
+
};
|
|
16612
|
+
const [keepText, landonlyText, readyText, statusJson, skipsText, g1names] = await Promise.all([
|
|
16613
|
+
readSet(join22(queue, "keep.txt"), "keep"),
|
|
16614
|
+
readSet(join22(runs, "PUBLISH_LANDONLY"), "landonly"),
|
|
16615
|
+
readSet(join22(queue, "ready.tsv"), "ready"),
|
|
16616
|
+
readFleetJson(join22(queue, "status.json"), { object: true }),
|
|
16617
|
+
readSet(join22(queue, "precheck-skips.tsv"), "precheckSkips"),
|
|
16618
|
+
readdir3(join22(runs, "g1-pass")).then((names) => {
|
|
16619
|
+
sources.g1pass = "ok";
|
|
16620
|
+
return names;
|
|
16621
|
+
}, (error) => {
|
|
16622
|
+
sources.g1pass = error.code === "ENOENT" ? "missing" : "unavailable";
|
|
16623
|
+
return [];
|
|
16624
|
+
})
|
|
16625
|
+
]);
|
|
16626
|
+
sources.status = statusJson.status;
|
|
16627
|
+
const hold = await loadHoldList(runs, sources);
|
|
16628
|
+
const queued = new Set;
|
|
16629
|
+
for (const line of readyText.split(`
|
|
16630
|
+
`)) {
|
|
16631
|
+
if (!line.trim() || line.startsWith("#"))
|
|
16632
|
+
continue;
|
|
16633
|
+
const id = line.split("\t")[0]?.trim();
|
|
16634
|
+
if (id && JOB_ID.test(id))
|
|
16635
|
+
queued.add(id);
|
|
16636
|
+
}
|
|
16637
|
+
const statusKept = new Set;
|
|
16638
|
+
const statusReaping = new Set;
|
|
16639
|
+
if (isFleetRow(statusJson.data)) {
|
|
16640
|
+
for (const key of ["kept_worktrees", "reaping"]) {
|
|
16641
|
+
const list = statusJson.data[key];
|
|
16642
|
+
if (Array.isArray(list)) {
|
|
16643
|
+
for (const id of list)
|
|
16644
|
+
if (typeof id === "string")
|
|
16645
|
+
(key === "kept_worktrees" ? statusKept : statusReaping).add(id);
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
const precheckDone = new Set;
|
|
16650
|
+
for (const line of skipsText.split(`
|
|
16651
|
+
`)) {
|
|
16652
|
+
const f = line.split("\t");
|
|
16653
|
+
if (f.length === 4 && f[1] && /^[0-9]{1,2}$/.test(f[2] ?? "")) {
|
|
16654
|
+
const code = Number(f[2]);
|
|
16655
|
+
if (code >= 10 && code <= 13)
|
|
16656
|
+
precheckDone.add(f[1]);
|
|
16657
|
+
}
|
|
16658
|
+
}
|
|
16659
|
+
return {
|
|
16660
|
+
keep: tokenSet(keepText),
|
|
16661
|
+
landonly: tokenSet(landonlyText),
|
|
16662
|
+
queued,
|
|
16663
|
+
statusKept,
|
|
16664
|
+
statusReaping,
|
|
16665
|
+
g1pass: new Set(g1names.filter((n) => n.endsWith(".pass")).map((n) => n.slice(0, -5))),
|
|
16666
|
+
precheckDone,
|
|
16667
|
+
holdEntries: hold.entries,
|
|
16668
|
+
holdListOk: hold.ok,
|
|
16669
|
+
sources
|
|
16670
|
+
};
|
|
16671
|
+
}
|
|
16672
|
+
async function newestTreeMtime(root, deadline) {
|
|
16673
|
+
let newest = 0;
|
|
16674
|
+
let visited = 0;
|
|
16675
|
+
let complete = true;
|
|
16676
|
+
const stack = [{ path: root, depth: 0 }];
|
|
16677
|
+
const note = async (p) => {
|
|
16678
|
+
const s = await lstat2(p).catch((e) => {
|
|
16679
|
+
if (e.code !== "ENOENT")
|
|
16680
|
+
complete = false;
|
|
16681
|
+
return null;
|
|
16682
|
+
});
|
|
16683
|
+
if (s && s.mtimeMs > newest)
|
|
16684
|
+
newest = s.mtimeMs;
|
|
16685
|
+
};
|
|
16686
|
+
while (stack.length) {
|
|
16687
|
+
if (Date.now() > deadline || visited > WALK_MAX_ENTRIES) {
|
|
16688
|
+
complete = false;
|
|
16689
|
+
break;
|
|
16690
|
+
}
|
|
16691
|
+
const { path, depth } = stack.pop();
|
|
16692
|
+
let dirents;
|
|
16693
|
+
try {
|
|
16694
|
+
dirents = await readdir3(path, { withFileTypes: true });
|
|
16695
|
+
} catch (e) {
|
|
16696
|
+
if (e.code !== "ENOENT")
|
|
16697
|
+
complete = false;
|
|
16698
|
+
continue;
|
|
16699
|
+
}
|
|
16700
|
+
for (const d of dirents) {
|
|
16701
|
+
if (++visited > WALK_MAX_ENTRIES) {
|
|
16702
|
+
complete = false;
|
|
16703
|
+
break;
|
|
16704
|
+
}
|
|
16705
|
+
const p = join22(path, d.name);
|
|
16706
|
+
if (d.isSymbolicLink()) {
|
|
16707
|
+
await note(p);
|
|
16708
|
+
continue;
|
|
16709
|
+
}
|
|
16710
|
+
if (d.isDirectory()) {
|
|
16711
|
+
if (WALK_PRUNE_ANYWHERE.has(d.name))
|
|
16712
|
+
continue;
|
|
16713
|
+
if (depth === 0 && WALK_PRUNE_TOP.has(d.name))
|
|
16714
|
+
continue;
|
|
16715
|
+
stack.push({ path: p, depth: depth + 1 });
|
|
16716
|
+
await note(p);
|
|
16717
|
+
continue;
|
|
16718
|
+
}
|
|
16719
|
+
await note(p);
|
|
16720
|
+
}
|
|
16721
|
+
}
|
|
16722
|
+
return { newest, complete };
|
|
16723
|
+
}
|
|
16724
|
+
async function worktreeMapFor(ctx, repoPath) {
|
|
16725
|
+
let pending = ctx.worktreeMaps.get(repoPath);
|
|
16726
|
+
if (!pending) {
|
|
16727
|
+
pending = (async () => {
|
|
16728
|
+
if (Date.now() > ctx.deadline) {
|
|
16729
|
+
ctx.stats.budgetExceeded = true;
|
|
16730
|
+
return null;
|
|
16731
|
+
}
|
|
16732
|
+
ctx.stats.gitChecks++;
|
|
16733
|
+
const res = await runGit(["-C", repoPath, "worktree", "list", "--porcelain", "-z"], repoPath, 20000);
|
|
16734
|
+
if (res.code !== 0)
|
|
16735
|
+
return null;
|
|
16736
|
+
const map = new Map;
|
|
16737
|
+
for (const w of parseWorktreePorcelain(res.stdout))
|
|
16738
|
+
map.set(resolve11(w.path), w);
|
|
16739
|
+
return map;
|
|
16740
|
+
})();
|
|
16741
|
+
ctx.worktreeMaps.set(repoPath, pending);
|
|
16742
|
+
}
|
|
16743
|
+
return pending;
|
|
16744
|
+
}
|
|
16745
|
+
async function resolveRepo(entryPath) {
|
|
16746
|
+
const gitPath = join22(entryPath, ".git");
|
|
16747
|
+
const st = await lstat2(gitPath).catch(() => null);
|
|
16748
|
+
if (!st)
|
|
16749
|
+
return { error: "git-pointer-missing" };
|
|
16750
|
+
if (st.isDirectory())
|
|
16751
|
+
return { error: "not-a-linked-worktree" };
|
|
16752
|
+
if (!st.isFile() || st.isSymbolicLink() || st.size > 8192)
|
|
16753
|
+
return { error: "git-pointer-invalid" };
|
|
16754
|
+
const text = await readFile12(gitPath, "utf8").catch(() => "");
|
|
16755
|
+
const m = text.match(/^gitdir:\s*(.+)$/m);
|
|
16756
|
+
if (!m)
|
|
16757
|
+
return { error: "git-pointer-invalid" };
|
|
16758
|
+
const gitdir = resolve11(entryPath, m[1].trim());
|
|
16759
|
+
const gd = await lstat2(gitdir).catch(() => null);
|
|
16760
|
+
if (!gd?.isDirectory() || gd.isSymbolicLink())
|
|
16761
|
+
return { error: "gitdir-missing" };
|
|
16762
|
+
let common = "../..";
|
|
16763
|
+
const commondirPath = join22(gitdir, "commondir");
|
|
16764
|
+
const cd = await lstat2(commondirPath).catch(() => null);
|
|
16765
|
+
if (cd?.isFile() && !cd.isSymbolicLink() && cd.size <= 4096) {
|
|
16766
|
+
const commondirText = await readFile12(commondirPath, "utf8").catch(() => "");
|
|
16767
|
+
if (commondirText.trim())
|
|
16768
|
+
common = commondirText.trim();
|
|
16769
|
+
}
|
|
16770
|
+
const commonGit = resolve11(gitdir, common);
|
|
16771
|
+
if (basename3(commonGit) !== ".git")
|
|
16772
|
+
return { error: "repo-unresolved" };
|
|
16773
|
+
return { repo: dirname6(commonGit) };
|
|
16774
|
+
}
|
|
16775
|
+
function heldJob(state, job, lineText, repoName) {
|
|
16776
|
+
if (!state.holdListOk)
|
|
16777
|
+
return false;
|
|
16778
|
+
const lineRepo = (lineText.split(`
|
|
16779
|
+
`)[0]?.split("\t")[1] ?? "").trim();
|
|
16780
|
+
const repo = (lineRepo || repoName).replace(/^ashlrai\//, "");
|
|
16781
|
+
if (!FLEET_REPOS.has(repo))
|
|
16782
|
+
return true;
|
|
16783
|
+
const entry = state.holdEntries.find((h) => h.repo === repo);
|
|
16784
|
+
if (!entry)
|
|
16785
|
+
return false;
|
|
16786
|
+
const tokens = `${job} ${lineText}`.match(/\d+/g) ?? [];
|
|
16787
|
+
for (const pr of entry.prs)
|
|
16788
|
+
if (tokens.includes(pr))
|
|
16789
|
+
return true;
|
|
16790
|
+
return false;
|
|
16791
|
+
}
|
|
16792
|
+
function lsofHolds(lsofText, path) {
|
|
16793
|
+
return lsofText.includes(`n${path}/`) || lsofText.includes(`n${path}
|
|
16794
|
+
`) || lsofText.endsWith(`n${path}`);
|
|
16795
|
+
}
|
|
16796
|
+
async function logFinished(runs, name, now, liveMs) {
|
|
16797
|
+
const fail = (reason) => ({ ok: false, reason });
|
|
16798
|
+
const lf = join22(runs, `${name}.log`);
|
|
16799
|
+
const st = await lstat2(lf).catch(() => null);
|
|
16800
|
+
if (!st || !st.isFile() || st.isSymbolicLink())
|
|
16801
|
+
return fail("log-missing");
|
|
16802
|
+
if (st.size <= 0)
|
|
16803
|
+
return fail("log-empty");
|
|
16804
|
+
if (now - st.mtimeMs < liveMs)
|
|
16805
|
+
return fail("log-recent");
|
|
16806
|
+
let tailText;
|
|
16807
|
+
let lastByteNl = false;
|
|
16808
|
+
try {
|
|
16809
|
+
const file = await openFleetFile(lf);
|
|
16810
|
+
try {
|
|
16811
|
+
const size = st.size;
|
|
16812
|
+
const len = Math.min(LOG_TAIL, size);
|
|
16813
|
+
const buf = Buffer.alloc(len);
|
|
16814
|
+
await file.read(buf, 0, len, size - len);
|
|
16815
|
+
tailText = buf.toString("utf8").replace(/\r/g, "");
|
|
16816
|
+
const last = Buffer.alloc(1);
|
|
16817
|
+
await file.read(last, 0, 1, size - 1);
|
|
16818
|
+
lastByteNl = last[0] === 10;
|
|
16819
|
+
} finally {
|
|
16820
|
+
await file.close();
|
|
16821
|
+
}
|
|
16822
|
+
} catch {
|
|
16823
|
+
return fail("log-unreadable");
|
|
16824
|
+
}
|
|
16825
|
+
if (!lastByteNl)
|
|
16826
|
+
return fail("log-truncated");
|
|
16827
|
+
const lines = tailText.split(`
|
|
16828
|
+
`);
|
|
16829
|
+
let endMarked = lines.some((l, i) => /^tokens used[ \t]*$/.test(l) && /^[ \t]*[0-9][0-9,]*[ \t]*$/.test(lines[i + 1] ?? ""));
|
|
16830
|
+
if (!endMarked) {
|
|
16831
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16832
|
+
if (new RegExp(`^API_SPEND ${esc} pool=(gift|monthly) `, "m").test(tailText))
|
|
16833
|
+
endMarked = true;
|
|
16834
|
+
}
|
|
16835
|
+
if (!endMarked) {
|
|
16836
|
+
const errRe = /^[ \t]*(?:ERROR|Error|API Error|stream error)[: ]/;
|
|
16837
|
+
const termRe = /usage limit|you've hit your|weekly limit|credits? (are |have been )?(exhausted|depleted)|out of credits|insufficient credits|no credits remaining/i;
|
|
16838
|
+
endMarked = lines.some((l) => errRe.test(l) && termRe.test(l));
|
|
16839
|
+
}
|
|
16840
|
+
if (!endMarked)
|
|
16841
|
+
return fail("log-no-end-marker");
|
|
16842
|
+
const st2 = await lstat2(lf).catch(() => null);
|
|
16843
|
+
if (!st2 || st2.size !== st.size || st2.mtimeMs !== st.mtimeMs)
|
|
16844
|
+
return fail("log-growing");
|
|
16845
|
+
return { ok: true, reason: "" };
|
|
16846
|
+
}
|
|
16847
|
+
async function classifyEntry(ctx, name) {
|
|
16848
|
+
const entry = { name, path: join22(ctx.root, name), class: "preserve", reasons: [] };
|
|
16849
|
+
const reasons = entry.reasons;
|
|
16850
|
+
const preserve = (reason) => {
|
|
16851
|
+
reasons.push(reason);
|
|
16852
|
+
return { entry };
|
|
16853
|
+
};
|
|
16854
|
+
if (!JOB_ID.test(name))
|
|
16855
|
+
return preserve("bad-name");
|
|
16856
|
+
if (DENIED_NAME.test(name) || DENIED_NAMES.has(name))
|
|
16857
|
+
return preserve("denied-name");
|
|
16858
|
+
const runs = ctx.runs;
|
|
16859
|
+
const queue = ctx.queue;
|
|
16860
|
+
const basePath = join22(runs, `${name}.base`);
|
|
16861
|
+
const baseL = await lstat2(basePath).catch(() => null);
|
|
16862
|
+
if (!baseL)
|
|
16863
|
+
return preserve("no-job-record");
|
|
16864
|
+
if (!baseL.isFile() || baseL.isSymbolicLink())
|
|
16865
|
+
return preserve("base-invalid");
|
|
16866
|
+
const baseText = await readFleetText(basePath);
|
|
16867
|
+
if (baseText.status !== "ok")
|
|
16868
|
+
return preserve("base-invalid");
|
|
16869
|
+
const fields = baseText.text.split(/\s+/).filter(Boolean);
|
|
16870
|
+
if (fields.length < 4)
|
|
16871
|
+
return preserve("base-invalid");
|
|
16872
|
+
const baseSha = fields[0];
|
|
16873
|
+
const branch = fields[1];
|
|
16874
|
+
const baseWt = fields[2];
|
|
16875
|
+
if (!/^[0-9a-f]{40}$/.test(baseSha))
|
|
16876
|
+
return preserve("base-invalid");
|
|
16877
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/.test(branch))
|
|
16878
|
+
return preserve("base-invalid");
|
|
16879
|
+
const baseReal = await realpath(baseWt).catch(() => null);
|
|
16880
|
+
if (baseReal !== entry.path)
|
|
16881
|
+
return preserve("base-mismatch");
|
|
16882
|
+
const runDir = join22(queue, "run");
|
|
16883
|
+
const probe = (p) => lstat2(p).catch(() => null);
|
|
16884
|
+
const [
|
|
16885
|
+
wtStat,
|
|
16886
|
+
dispatched,
|
|
16887
|
+
reaping,
|
|
16888
|
+
runLock,
|
|
16889
|
+
rootLock,
|
|
16890
|
+
logF,
|
|
16891
|
+
launchF,
|
|
16892
|
+
publishF,
|
|
16893
|
+
pidRun,
|
|
16894
|
+
pidRoot,
|
|
16895
|
+
lineF
|
|
16896
|
+
] = await Promise.all([
|
|
16897
|
+
probe(entry.path),
|
|
16898
|
+
probe(join22(runDir, `${name}.dispatched`)),
|
|
16899
|
+
probe(join22(runDir, `${name}.reaping`)),
|
|
16900
|
+
probe(join22(runDir, `${name}.lock`)),
|
|
16901
|
+
probe(join22(runs, `${name}.lock`)),
|
|
16902
|
+
probe(join22(runs, `${name}.log`)),
|
|
16903
|
+
probe(join22(runs, `${name}.launch`)),
|
|
16904
|
+
probe(join22(runs, `${name}.publish`)),
|
|
16905
|
+
probe(join22(runDir, `${name}.pid`)),
|
|
16906
|
+
probe(join22(runs, `${name}.pid`)),
|
|
16907
|
+
probe(join22(runDir, `${name}.line`))
|
|
16908
|
+
]);
|
|
16909
|
+
if (!wtStat?.isDirectory() || wtStat.isSymbolicLink())
|
|
16910
|
+
return preserve("not-a-directory");
|
|
16911
|
+
if (dispatched)
|
|
16912
|
+
reasons.push("dispatch-pending");
|
|
16913
|
+
if (reaping)
|
|
16914
|
+
reasons.push("reap-claimed");
|
|
16915
|
+
if (runLock || rootLock)
|
|
16916
|
+
reasons.push("lock-claimed");
|
|
16917
|
+
const pidStat = pidRun ?? pidRoot;
|
|
16918
|
+
if (pidStat) {
|
|
16919
|
+
const pidPath = pidRun ? join22(runDir, `${name}.pid`) : join22(runs, `${name}.pid`);
|
|
16920
|
+
const pidText = pidStat.isFile() && !pidStat.isSymbolicLink() ? (await readFleetText(pidPath)).text : "";
|
|
16921
|
+
const pid = Number(pidText.trim());
|
|
16922
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
16923
|
+
reasons.push("pid-unknown");
|
|
16924
|
+
else {
|
|
16925
|
+
try {
|
|
16926
|
+
process.kill(pid, 0);
|
|
16927
|
+
reasons.push("process-live");
|
|
16928
|
+
} catch (error) {
|
|
16929
|
+
const code = error.code;
|
|
16930
|
+
reasons.push(code === "EPERM" ? "process-live" : "pid-stale");
|
|
16931
|
+
}
|
|
16932
|
+
}
|
|
16933
|
+
}
|
|
16934
|
+
if (ctx.state.statusKept.has(name))
|
|
16935
|
+
reasons.push("status-kept");
|
|
16936
|
+
if (ctx.state.statusReaping.has(name))
|
|
16937
|
+
reasons.push("status-reaping");
|
|
16938
|
+
if (ctx.state.keep.has(name))
|
|
16939
|
+
reasons.push("kept");
|
|
16940
|
+
if (ctx.state.queued.has(name))
|
|
16941
|
+
reasons.push("job-queued");
|
|
16942
|
+
const now = ctx.now;
|
|
16943
|
+
const recent = Math.max(baseL.mtimeMs, logF?.mtimeMs ?? 0, launchF?.mtimeMs ?? 0, publishF?.mtimeMs ?? 0, wtStat.mtimeMs);
|
|
16944
|
+
if (now - recent < LIVE_WINDOW_MS)
|
|
16945
|
+
reasons.push("recent-activity");
|
|
16946
|
+
if (reasons.length)
|
|
16947
|
+
return { entry };
|
|
16948
|
+
let basis;
|
|
16949
|
+
let resultCls = null;
|
|
16950
|
+
let resultPr = null;
|
|
16951
|
+
const lineText = lineF?.isFile() && !lineF.isSymbolicLink() ? (await readFleetText(join22(runDir, `${name}.line`))).text : "";
|
|
16952
|
+
if (publishF) {
|
|
16953
|
+
const pub = await readFleetText(join22(runs, `${name}.publish`));
|
|
16954
|
+
if (pub.status !== "ok")
|
|
16955
|
+
return preserve("publish-unreadable");
|
|
16956
|
+
const { cls, line } = lastResultClass(pub.text);
|
|
16957
|
+
if (!cls)
|
|
16958
|
+
return preserve("publish-unresulted");
|
|
16959
|
+
resultCls = cls;
|
|
16960
|
+
entry.resultClass = cls;
|
|
16961
|
+
if (cls !== "pr" && cls !== "empty") {
|
|
16962
|
+
reasons.push(`kept-supervisor:${cls}`);
|
|
16963
|
+
entry.jobState = "kept";
|
|
16964
|
+
return { entry };
|
|
16965
|
+
}
|
|
16966
|
+
basis = cls === "pr" ? "result-pr" : "result-empty";
|
|
16967
|
+
entry.jobState = "finished";
|
|
16968
|
+
if (cls === "pr") {
|
|
16969
|
+
const m = line.match(RESULT_PR_RE);
|
|
16970
|
+
if (m)
|
|
16971
|
+
resultPr = { repo: m[1], pr: m[2] };
|
|
16972
|
+
entry.pr = resultPr ? `ashlrai/${resultPr.repo}#${resultPr.pr}` : undefined;
|
|
16973
|
+
}
|
|
16974
|
+
} else if (ctx.state.precheckDone.has(name)) {
|
|
16975
|
+
basis = "done-noop";
|
|
16976
|
+
entry.jobState = "done-noop";
|
|
16977
|
+
} else {
|
|
16978
|
+
const fin = await logFinished(runs, name, now, LIVE_WINDOW_MS);
|
|
16979
|
+
if (!fin.ok)
|
|
16980
|
+
return preserve(fin.reason);
|
|
16981
|
+
basis = "empty-inferred";
|
|
16982
|
+
entry.jobState = "finished-inferred";
|
|
16983
|
+
}
|
|
16984
|
+
const idleThresholdMs = basis === "result-pr" ? ctx.reapIdleMs : ctx.emptyIdleMs;
|
|
16985
|
+
const newestRuns = Math.max(baseL.mtimeMs, logF?.mtimeMs ?? 0, launchF?.mtimeMs ?? 0, publishF?.mtimeMs ?? 0, wtStat.mtimeMs);
|
|
16986
|
+
return {
|
|
16987
|
+
entry,
|
|
16988
|
+
deep: {
|
|
16989
|
+
basis,
|
|
16990
|
+
baseSha,
|
|
16991
|
+
branch,
|
|
16992
|
+
idleThresholdMs,
|
|
16993
|
+
newestRuns,
|
|
16994
|
+
publishMtimeMs: publishF?.mtimeMs ?? 0,
|
|
16995
|
+
resultPr,
|
|
16996
|
+
lineText
|
|
16997
|
+
}
|
|
16998
|
+
};
|
|
16999
|
+
}
|
|
17000
|
+
async function deepCheck(ctx, entry, d) {
|
|
17001
|
+
const reasons = entry.reasons;
|
|
17002
|
+
const bail = (reason) => {
|
|
17003
|
+
reasons.push(reason);
|
|
17004
|
+
};
|
|
17005
|
+
if (Date.now() > ctx.deadline) {
|
|
17006
|
+
ctx.stats.budgetExceeded = true;
|
|
17007
|
+
bail("budget-exceeded");
|
|
17008
|
+
return;
|
|
17009
|
+
}
|
|
17010
|
+
const resolved = await resolveRepo(entry.path);
|
|
17011
|
+
if ("error" in resolved) {
|
|
17012
|
+
bail(resolved.error);
|
|
17013
|
+
return;
|
|
17014
|
+
}
|
|
17015
|
+
entry.repo = resolved.repo;
|
|
17016
|
+
const repoName = basename3(resolved.repo);
|
|
17017
|
+
const map = await worktreeMapFor(ctx, resolved.repo);
|
|
17018
|
+
if (!map) {
|
|
17019
|
+
bail("repo-scan-failed");
|
|
17020
|
+
return;
|
|
17021
|
+
}
|
|
17022
|
+
const info = map.get(resolve11(entry.path));
|
|
17023
|
+
if (!info) {
|
|
17024
|
+
bail("unregistered");
|
|
17025
|
+
return;
|
|
17026
|
+
}
|
|
17027
|
+
if (info.prunable) {
|
|
17028
|
+
bail("prunable");
|
|
17029
|
+
return;
|
|
17030
|
+
}
|
|
17031
|
+
if (info.locked) {
|
|
17032
|
+
bail("locked");
|
|
17033
|
+
return;
|
|
17034
|
+
}
|
|
17035
|
+
entry.head = info.head || undefined;
|
|
17036
|
+
entry.branch = info.branch ? info.branch.replace(/^refs\/heads\//, "") : null;
|
|
17037
|
+
entry.detached = info.detached || undefined;
|
|
17038
|
+
if (!ctx.lsofOk) {
|
|
17039
|
+
bail("lsof-unavailable");
|
|
17040
|
+
return;
|
|
17041
|
+
}
|
|
17042
|
+
const realEntry = await realpath(entry.path).catch(() => entry.path);
|
|
17043
|
+
if (lsofHolds(ctx.lsofText, entry.path) || realEntry !== entry.path && lsofHolds(ctx.lsofText, realEntry)) {
|
|
17044
|
+
bail("live-process");
|
|
17045
|
+
return;
|
|
17046
|
+
}
|
|
17047
|
+
if (d.basis === "result-pr") {
|
|
17048
|
+
if (!d.resultPr || !FLEET_REPOS.has(d.resultPr.repo)) {
|
|
17049
|
+
bail("repo-unlisted");
|
|
17050
|
+
return;
|
|
17051
|
+
}
|
|
17052
|
+
const rp = d.resultPr;
|
|
17053
|
+
const h = rp ? ctx.state.holdEntries.find((e) => e.repo === rp.repo) : undefined;
|
|
17054
|
+
if (rp && h?.prs.has(rp.pr)) {
|
|
17055
|
+
bail("hold-list");
|
|
17056
|
+
return;
|
|
17057
|
+
}
|
|
17058
|
+
if (PROTECTED_PR.test(d.branch)) {
|
|
17059
|
+
bail("protected-branch");
|
|
17060
|
+
return;
|
|
17061
|
+
}
|
|
17062
|
+
if (CLAUDE_BRANCH.test(d.branch) && ctx.now - d.publishMtimeMs < CLAUDE_PUSH_MS) {
|
|
17063
|
+
bail("claude-recent-push");
|
|
17064
|
+
return;
|
|
17065
|
+
}
|
|
17066
|
+
} else {
|
|
17067
|
+
if (ctx.state.landonly.has(entry.name) || ctx.state.g1pass.has(entry.name)) {
|
|
17068
|
+
bail("publish-queued");
|
|
17069
|
+
return;
|
|
17070
|
+
}
|
|
17071
|
+
if (PROTECTED_EMPTY.test(d.branch)) {
|
|
17072
|
+
bail("protected-branch");
|
|
17073
|
+
return;
|
|
17074
|
+
}
|
|
17075
|
+
if (heldJob(ctx.state, entry.name, d.lineText, repoName)) {
|
|
17076
|
+
bail("held");
|
|
17077
|
+
return;
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
if (Date.now() > ctx.deadline) {
|
|
17081
|
+
ctx.stats.budgetExceeded = true;
|
|
17082
|
+
bail("budget-exceeded");
|
|
17083
|
+
return;
|
|
17084
|
+
}
|
|
17085
|
+
ctx.stats.gitChecks++;
|
|
17086
|
+
const status = await runGit([
|
|
17087
|
+
"--no-optional-locks",
|
|
17088
|
+
"-C",
|
|
17089
|
+
entry.path,
|
|
17090
|
+
"-c",
|
|
17091
|
+
"core.fsmonitor=false",
|
|
17092
|
+
"-c",
|
|
17093
|
+
"core.untrackedCache=false",
|
|
17094
|
+
"status",
|
|
17095
|
+
"--porcelain=v1",
|
|
17096
|
+
"--untracked-files=all",
|
|
17097
|
+
"--ignored=traditional",
|
|
17098
|
+
"--ignore-submodules=all"
|
|
17099
|
+
], entry.path);
|
|
17100
|
+
if (status.code !== 0) {
|
|
17101
|
+
bail(status.timedOut ? "git-timeout" : "git-error");
|
|
17102
|
+
return;
|
|
17103
|
+
}
|
|
17104
|
+
let dirty = false;
|
|
17105
|
+
let badIgnored = false;
|
|
17106
|
+
const checkIgnored = d.basis === "empty-inferred";
|
|
17107
|
+
for (const line of status.stdout.split(`
|
|
17108
|
+
`)) {
|
|
17109
|
+
if (!line)
|
|
17110
|
+
continue;
|
|
17111
|
+
if (line.startsWith("!!")) {
|
|
17112
|
+
if (!checkIgnored)
|
|
17113
|
+
continue;
|
|
17114
|
+
const raw = line.slice(3);
|
|
17115
|
+
if (raw.startsWith('"')) {
|
|
17116
|
+
badIgnored = true;
|
|
17117
|
+
continue;
|
|
17118
|
+
}
|
|
17119
|
+
const isDir = raw.endsWith("/");
|
|
17120
|
+
const body = isDir ? raw.slice(0, -1) : raw;
|
|
17121
|
+
const top = body.split("/")[0] ?? "";
|
|
17122
|
+
if (!BUILD_ALLOW.has(top) || !isDir && !body.includes("/"))
|
|
17123
|
+
badIgnored = true;
|
|
17124
|
+
continue;
|
|
17125
|
+
}
|
|
17126
|
+
dirty = true;
|
|
17127
|
+
}
|
|
17128
|
+
if (dirty) {
|
|
17129
|
+
bail("dirty");
|
|
17130
|
+
return;
|
|
17131
|
+
}
|
|
17132
|
+
if (badIgnored) {
|
|
17133
|
+
bail("ignored-files");
|
|
17134
|
+
return;
|
|
17135
|
+
}
|
|
17136
|
+
if (d.basis === "result-pr") {
|
|
17137
|
+
if (Date.now() > ctx.deadline) {
|
|
17138
|
+
ctx.stats.budgetExceeded = true;
|
|
17139
|
+
bail("budget-exceeded");
|
|
17140
|
+
return;
|
|
17141
|
+
}
|
|
17142
|
+
ctx.stats.gitChecks++;
|
|
17143
|
+
const unpushed = await runGit(["-C", entry.path, "rev-list", "--count", "HEAD", "--not", "--remotes"], entry.path);
|
|
17144
|
+
if (unpushed.code !== 0) {
|
|
17145
|
+
bail(unpushed.timedOut ? "git-timeout" : "git-error");
|
|
17146
|
+
return;
|
|
17147
|
+
}
|
|
17148
|
+
if (unpushed.stdout.trim() !== "0") {
|
|
17149
|
+
bail("unpushed-commits");
|
|
17150
|
+
return;
|
|
17151
|
+
}
|
|
17152
|
+
} else {
|
|
17153
|
+
if (Date.now() > ctx.deadline) {
|
|
17154
|
+
ctx.stats.budgetExceeded = true;
|
|
17155
|
+
bail("budget-exceeded");
|
|
17156
|
+
return;
|
|
17157
|
+
}
|
|
17158
|
+
ctx.stats.gitChecks++;
|
|
17159
|
+
const ahead = await runGit(["-C", entry.path, "rev-list", "--count", `${d.baseSha}..HEAD`], entry.path);
|
|
17160
|
+
if (ahead.code !== 0) {
|
|
17161
|
+
bail(ahead.timedOut ? "git-timeout" : "git-error");
|
|
17162
|
+
return;
|
|
17163
|
+
}
|
|
17164
|
+
if (ahead.stdout.trim() !== "0") {
|
|
17165
|
+
bail("unpublished-work");
|
|
17166
|
+
return;
|
|
17167
|
+
}
|
|
17168
|
+
ctx.stats.gitChecks++;
|
|
17169
|
+
const diff = await runGit([
|
|
17170
|
+
"-C",
|
|
17171
|
+
entry.path,
|
|
17172
|
+
"diff",
|
|
17173
|
+
"--quiet",
|
|
17174
|
+
"--no-ext-diff",
|
|
17175
|
+
"--no-textconv",
|
|
17176
|
+
"--ignore-submodules=all",
|
|
17177
|
+
d.baseSha,
|
|
17178
|
+
"--"
|
|
17179
|
+
], entry.path);
|
|
17180
|
+
if (diff.code === null || diff.code !== 0 && diff.code !== 1) {
|
|
17181
|
+
bail(diff.timedOut ? "git-timeout" : "git-error");
|
|
17182
|
+
return;
|
|
17183
|
+
}
|
|
17184
|
+
if (diff.code === 1) {
|
|
17185
|
+
bail("unpublished-work");
|
|
17186
|
+
return;
|
|
17187
|
+
}
|
|
17188
|
+
}
|
|
17189
|
+
if (Date.now() > ctx.deadline) {
|
|
17190
|
+
ctx.stats.budgetExceeded = true;
|
|
17191
|
+
bail("budget-exceeded");
|
|
17192
|
+
return;
|
|
17193
|
+
}
|
|
17194
|
+
ctx.stats.walks++;
|
|
17195
|
+
const walk = await newestTreeMtime(entry.path, ctx.deadline);
|
|
17196
|
+
if (!walk.complete) {
|
|
17197
|
+
bail("walk-incomplete");
|
|
17198
|
+
return;
|
|
17199
|
+
}
|
|
17200
|
+
const newest = Math.max(d.newestRuns, walk.newest, (await lstat2(entry.path).catch(() => null))?.mtimeMs ?? 0);
|
|
17201
|
+
const idleHours = (ctx.now - newest) / 3600000;
|
|
17202
|
+
entry.idleHours = Math.round(idleHours * 100) / 100;
|
|
17203
|
+
if (idleHours * 3600000 < d.idleThresholdMs) {
|
|
17204
|
+
bail("idle-below-threshold");
|
|
17205
|
+
return;
|
|
17206
|
+
}
|
|
17207
|
+
if (d.basis === "result-pr") {
|
|
17208
|
+
if (!d.resultPr) {
|
|
17209
|
+
bail("pr-unparsed");
|
|
17210
|
+
return;
|
|
17211
|
+
}
|
|
17212
|
+
if (ctx.ghCalls >= ctx.ghMax) {
|
|
17213
|
+
bail("gh-budget");
|
|
17214
|
+
return;
|
|
17215
|
+
}
|
|
17216
|
+
ctx.ghCalls++;
|
|
17217
|
+
const gh = await runProc(ctx.ghBin, [
|
|
17218
|
+
"pr",
|
|
17219
|
+
"view",
|
|
17220
|
+
d.resultPr.pr,
|
|
17221
|
+
"--repo",
|
|
17222
|
+
`ashlrai/${d.resultPr.repo}`,
|
|
17223
|
+
"--json",
|
|
17224
|
+
"state",
|
|
17225
|
+
"-q",
|
|
17226
|
+
".state"
|
|
17227
|
+
], entry.path, 15000, 1024 * 1024);
|
|
17228
|
+
if (gh.code === null) {
|
|
17229
|
+
bail("gh-unavailable");
|
|
17230
|
+
return;
|
|
17231
|
+
}
|
|
17232
|
+
if (gh.code === 0)
|
|
17233
|
+
ctx.ghSawOk = true;
|
|
17234
|
+
if (gh.code !== 0) {
|
|
17235
|
+
bail(gh.timedOut ? "gh-timeout" : "pr-state-unknown");
|
|
17236
|
+
return;
|
|
17237
|
+
}
|
|
17238
|
+
const state = gh.stdout.trim().toUpperCase();
|
|
17239
|
+
if (state === "OPEN") {
|
|
17240
|
+
bail("pr-open");
|
|
17241
|
+
return;
|
|
17242
|
+
}
|
|
17243
|
+
if (state !== "MERGED" && state !== "CLOSED") {
|
|
17244
|
+
bail("pr-state-unknown");
|
|
17245
|
+
return;
|
|
17246
|
+
}
|
|
17247
|
+
}
|
|
17248
|
+
entry.class = "collect-candidate";
|
|
17249
|
+
entry.basis = d.basis;
|
|
17250
|
+
entry.reasons = [];
|
|
17251
|
+
}
|
|
17252
|
+
async function firstExisting(paths) {
|
|
17253
|
+
for (const p of paths) {
|
|
17254
|
+
const st = await lstat2(p).catch(() => null);
|
|
17255
|
+
if (st)
|
|
17256
|
+
return p;
|
|
17257
|
+
}
|
|
17258
|
+
return null;
|
|
17259
|
+
}
|
|
17260
|
+
async function collectFleetGc(options) {
|
|
17261
|
+
const started = Date.now();
|
|
17262
|
+
const now = options.now ?? started;
|
|
17263
|
+
const root = await realpath(resolve11(options.root)).catch(() => resolve11(options.root));
|
|
17264
|
+
const runs = resolve11(options.runs);
|
|
17265
|
+
const queue = resolve11(options.queue ?? join22(runs, "queue"));
|
|
17266
|
+
const reapIdleHours = options.reapIdleHours ?? 2;
|
|
17267
|
+
const emptyIdleHours = options.emptyIdleHours ?? 6;
|
|
17268
|
+
const budgetSeconds = options.budgetSeconds ?? 120;
|
|
17269
|
+
const ghMax = options.ghMax ?? GH_MAX_DEFAULT;
|
|
17270
|
+
const sources = {};
|
|
17271
|
+
const dirents = await readdir3(root, { withFileTypes: true }).then((d) => {
|
|
17272
|
+
sources.root = "ok";
|
|
17273
|
+
return d;
|
|
17274
|
+
}, (error) => {
|
|
17275
|
+
sources.root = error.code === "ENOENT" ? "missing" : "unavailable";
|
|
17276
|
+
return [];
|
|
17277
|
+
});
|
|
17278
|
+
sources.runs = await lstat2(runs).then((s) => s.isDirectory() ? "ok" : "invalid").catch((e) => e.code === "ENOENT" ? "missing" : "unavailable");
|
|
17279
|
+
const names = dirents.filter((d) => d.isDirectory() && !d.isSymbolicLink()).map((d) => d.name).sort();
|
|
17280
|
+
const skippedNonDirs = dirents.length - names.length;
|
|
17281
|
+
const state = await loadFleetState(runs, queue);
|
|
17282
|
+
Object.assign(sources, state.sources);
|
|
17283
|
+
const repoPaths = new Set;
|
|
17284
|
+
const parent = dirname6(root);
|
|
17285
|
+
const siblings = await readdir3(parent, { withFileTypes: true }).catch(() => []);
|
|
17286
|
+
for (const d of siblings) {
|
|
17287
|
+
if (!d.isDirectory() || d.isSymbolicLink())
|
|
17288
|
+
continue;
|
|
17289
|
+
const candidate = join22(parent, d.name);
|
|
17290
|
+
const gitEntry = await lstat2(join22(candidate, ".git")).catch(() => null);
|
|
17291
|
+
if (gitEntry && (gitEntry.isDirectory() || gitEntry.isFile()))
|
|
17292
|
+
repoPaths.add(candidate);
|
|
17293
|
+
}
|
|
17294
|
+
const lsofBin = options.lsofBin ?? await firstExisting(["/usr/sbin/lsof", "/usr/bin/lsof", "/usr/local/bin/lsof"]);
|
|
17295
|
+
let lsofOk = false;
|
|
17296
|
+
let lsofText = "";
|
|
17297
|
+
if (lsofBin) {
|
|
17298
|
+
const lo = await runProc(lsofBin, ["-nP", "-w", "-Fn"], root, 30000, 64 * 1024 * 1024);
|
|
17299
|
+
lsofOk = lo.code === 0 || lo.stdout.length > 0;
|
|
17300
|
+
lsofText = lo.stdout;
|
|
17301
|
+
}
|
|
17302
|
+
sources.lsof = lsofOk ? "ok" : "unavailable";
|
|
17303
|
+
const ghCandidates = ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"];
|
|
17304
|
+
if (process.env.HOME)
|
|
17305
|
+
ghCandidates.push(join22(process.env.HOME, ".local", "bin", "gh"));
|
|
17306
|
+
const ghBin = options.ghBin ?? await firstExisting(ghCandidates) ?? "gh";
|
|
17307
|
+
const deadline = started + budgetSeconds * 1000;
|
|
17308
|
+
const ctx = {
|
|
17309
|
+
root,
|
|
17310
|
+
runs,
|
|
17311
|
+
queue,
|
|
17312
|
+
state,
|
|
17313
|
+
now,
|
|
17314
|
+
reapIdleMs: reapIdleHours * 3600000,
|
|
17315
|
+
emptyIdleMs: emptyIdleHours * 3600000,
|
|
17316
|
+
deadline,
|
|
17317
|
+
lsofOk,
|
|
17318
|
+
lsofText,
|
|
17319
|
+
ghBin,
|
|
17320
|
+
ghMax,
|
|
17321
|
+
ghCalls: 0,
|
|
17322
|
+
ghSawOk: false,
|
|
17323
|
+
worktreeMaps: new Map,
|
|
17324
|
+
stats: { gitChecks: 0, walks: 0, budgetExceeded: false }
|
|
17325
|
+
};
|
|
17326
|
+
const repoList = [...repoPaths];
|
|
17327
|
+
let ri = 0;
|
|
17328
|
+
await Promise.all(Array.from({ length: Math.min(8, repoList.length) }, async () => {
|
|
17329
|
+
while (ri < repoList.length) {
|
|
17330
|
+
const repo = repoList[ri++];
|
|
17331
|
+
await worktreeMapFor(ctx, repo);
|
|
17332
|
+
}
|
|
17333
|
+
}));
|
|
17334
|
+
const classified = [];
|
|
17335
|
+
const CHUNK = 64;
|
|
17336
|
+
for (let i = 0;i < names.length; i += CHUNK) {
|
|
17337
|
+
const chunk = names.slice(i, i + CHUNK);
|
|
17338
|
+
classified.push(...await Promise.all(chunk.map((n) => classifyEntry(ctx, n))));
|
|
17339
|
+
}
|
|
17340
|
+
const deepOnes = classified.filter((c) => c.deep);
|
|
17341
|
+
let di = 0;
|
|
17342
|
+
await Promise.all(Array.from({ length: Math.min(8, deepOnes.length) }, async () => {
|
|
17343
|
+
while (di < deepOnes.length) {
|
|
17344
|
+
const c = deepOnes[di++];
|
|
17345
|
+
if (!state.holdListOk) {
|
|
17346
|
+
c.entry.reasons.push("hold-list-unknown");
|
|
17347
|
+
continue;
|
|
17348
|
+
}
|
|
17349
|
+
await deepCheck(ctx, c.entry, c.deep);
|
|
17350
|
+
}
|
|
17351
|
+
}));
|
|
17352
|
+
const staleRegistrations = [];
|
|
17353
|
+
const nameSet = new Set(names);
|
|
17354
|
+
for (const pending of ctx.worktreeMaps.values()) {
|
|
17355
|
+
const map = await pending;
|
|
17356
|
+
if (!map)
|
|
17357
|
+
continue;
|
|
17358
|
+
for (const p of map.keys()) {
|
|
17359
|
+
if (dirname6(p) !== root)
|
|
17360
|
+
continue;
|
|
17361
|
+
const rel = p.slice(root.length + 1);
|
|
17362
|
+
if (!rel || rel.includes("/"))
|
|
17363
|
+
continue;
|
|
17364
|
+
if (nameSet.has(rel))
|
|
17365
|
+
continue;
|
|
17366
|
+
const exists = await lstat2(p).then(() => true, () => false);
|
|
17367
|
+
if (!exists)
|
|
17368
|
+
staleRegistrations.push(rel);
|
|
17369
|
+
}
|
|
17370
|
+
}
|
|
17371
|
+
staleRegistrations.sort();
|
|
17372
|
+
const entries = classified.map((c) => c.entry);
|
|
17373
|
+
const byClass = { preserve: 0, "collect-candidate": 0 };
|
|
17374
|
+
const byBasis = {};
|
|
17375
|
+
const byReason = {};
|
|
17376
|
+
const byResultClass = {};
|
|
17377
|
+
for (const e of entries) {
|
|
17378
|
+
byClass[e.class]++;
|
|
17379
|
+
if (e.basis)
|
|
17380
|
+
byBasis[e.basis] = (byBasis[e.basis] ?? 0) + 1;
|
|
17381
|
+
if (e.resultClass)
|
|
17382
|
+
byResultClass[e.resultClass] = (byResultClass[e.resultClass] ?? 0) + 1;
|
|
17383
|
+
for (const r of e.reasons)
|
|
17384
|
+
byReason[r] = (byReason[r] ?? 0) + 1;
|
|
17385
|
+
}
|
|
17386
|
+
return {
|
|
17387
|
+
schemaVersion: 1,
|
|
17388
|
+
mode: "dry-run",
|
|
17389
|
+
generatedAt: new Date(now).toISOString(),
|
|
17390
|
+
root,
|
|
17391
|
+
runs,
|
|
17392
|
+
queue,
|
|
17393
|
+
thresholds: { reapIdleHours, emptyIdleHours, budgetSeconds },
|
|
17394
|
+
sources,
|
|
17395
|
+
tools: {
|
|
17396
|
+
lsof: lsofOk ? "ok" : "unavailable",
|
|
17397
|
+
gh: ctx.ghCalls === 0 ? "unused" : ctx.ghSawOk ? "ok" : "unavailable"
|
|
17398
|
+
},
|
|
17399
|
+
reposScanned: ctx.worktreeMaps.size,
|
|
17400
|
+
scan: {
|
|
17401
|
+
complete: !ctx.stats.budgetExceeded,
|
|
17402
|
+
entries: entries.length,
|
|
17403
|
+
skippedNonDirs,
|
|
17404
|
+
gitChecks: ctx.stats.gitChecks,
|
|
17405
|
+
walks: ctx.stats.walks,
|
|
17406
|
+
ghCalls: ctx.ghCalls,
|
|
17407
|
+
budgetExceeded: ctx.stats.budgetExceeded,
|
|
17408
|
+
durationMs: Date.now() - started
|
|
17409
|
+
},
|
|
17410
|
+
summary: {
|
|
17411
|
+
total: entries.length,
|
|
17412
|
+
byClass,
|
|
17413
|
+
byBasis,
|
|
17414
|
+
byReason,
|
|
17415
|
+
byResultClass
|
|
17416
|
+
},
|
|
17417
|
+
staleRegistrations: {
|
|
17418
|
+
count: staleRegistrations.length,
|
|
17419
|
+
sample: staleRegistrations.slice(0, 50)
|
|
17420
|
+
},
|
|
17421
|
+
entries
|
|
17422
|
+
};
|
|
17423
|
+
}
|
|
17424
|
+
var execFileAsync, JOB_ID, LIVE_WINDOW_MS = 900000, CLAUDE_PUSH_MS = 1800000, LOG_TAIL = 8192, GH_MAX_DEFAULT = 40, FLEET_REPOS, BUILD_ALLOW, PROTECTED_EMPTY, PROTECTED_PR, CLAUDE_BRANCH, DENIED_NAME, DENIED_NAMES, WALK_PRUNE_ANYWHERE, WALK_PRUNE_TOP, WALK_MAX_ENTRIES = 60000, RESULT_PR_RE, ENV_ALLOW, runGit = (args, cwd, timeoutMs = 15000) => runProc("git", args, cwd, timeoutMs, 8 * 1024 * 1024), tokenSet = (text) => new Set(text.split(/\s+/).filter(Boolean)), sha256Hex = (buf) => createHash4("sha256").update(buf).digest("hex");
|
|
17425
|
+
var init_fleet_gc = __esm(() => {
|
|
17426
|
+
init_fleet_jsonl();
|
|
17427
|
+
execFileAsync = promisify(execFile4);
|
|
17428
|
+
JOB_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
17429
|
+
FLEET_REPOS = new Set([
|
|
17430
|
+
"phantom",
|
|
17431
|
+
"phantom-secrets",
|
|
17432
|
+
"ashlr-plugin",
|
|
17433
|
+
"locus",
|
|
17434
|
+
"lexicon",
|
|
17435
|
+
"ashlr-md",
|
|
17436
|
+
"ashlr-stack"
|
|
17437
|
+
]);
|
|
17438
|
+
BUILD_ALLOW = new Set([
|
|
17439
|
+
"node_modules",
|
|
17440
|
+
"dist",
|
|
17441
|
+
"build",
|
|
17442
|
+
"target",
|
|
17443
|
+
".next",
|
|
17444
|
+
".turbo",
|
|
17445
|
+
"coverage",
|
|
17446
|
+
"__pycache__",
|
|
17447
|
+
".pytest_cache",
|
|
17448
|
+
".vite",
|
|
17449
|
+
"out",
|
|
17450
|
+
".cache"
|
|
17451
|
+
]);
|
|
17452
|
+
PROTECTED_EMPTY = /^(?:codex|claude)\//;
|
|
17453
|
+
PROTECTED_PR = /^codex\//;
|
|
17454
|
+
CLAUDE_BRANCH = /^claude\//;
|
|
17455
|
+
DENIED_NAME = /vault|keychains/i;
|
|
17456
|
+
DENIED_NAMES = new Set(["g1-pass", "quarantine", ".codex", ".codex-2", ".ashlr-runs"]);
|
|
17457
|
+
WALK_PRUNE_ANYWHERE = new Set([".git", "node_modules"]);
|
|
17458
|
+
WALK_PRUNE_TOP = new Set([...BUILD_ALLOW].filter((d) => d !== "node_modules"));
|
|
17459
|
+
RESULT_PR_RE = /^RESULT PR https:\/\/github\.com\/ashlrai\/([A-Za-z0-9._-]+)\/pull\/([0-9]{1,7})(?: |$)/;
|
|
17460
|
+
ENV_ALLOW = [
|
|
17461
|
+
"HOME",
|
|
17462
|
+
"PATH",
|
|
17463
|
+
"LANG",
|
|
17464
|
+
"LC_ALL",
|
|
17465
|
+
"USER",
|
|
17466
|
+
"LOGNAME",
|
|
17467
|
+
"TMPDIR",
|
|
17468
|
+
"GH_TOKEN",
|
|
17469
|
+
"GITHUB_TOKEN",
|
|
17470
|
+
"GH_CONFIG_DIR"
|
|
17471
|
+
];
|
|
17472
|
+
});
|
|
17473
|
+
|
|
17474
|
+
// src/commands/fleet-gc.ts
|
|
16263
17475
|
import { homedir as homedir12 } from "node:os";
|
|
16264
|
-
import {
|
|
17476
|
+
import { join as join23 } from "node:path";
|
|
17477
|
+
var fleetGcCommand;
|
|
17478
|
+
var init_fleet_gc2 = __esm(() => {
|
|
17479
|
+
init_dist3();
|
|
17480
|
+
init_fleet_gc();
|
|
17481
|
+
fleetGcCommand = defineCommand2({
|
|
17482
|
+
meta: {
|
|
17483
|
+
name: "fleet-gc",
|
|
17484
|
+
description: "Dry-run report classifying fleet worktrees as preserved or collect-candidates (read-only)."
|
|
17485
|
+
},
|
|
17486
|
+
args: {
|
|
17487
|
+
json: { type: "boolean", description: "Emit schemaVersion 1 JSON." },
|
|
17488
|
+
root: {
|
|
17489
|
+
type: "string",
|
|
17490
|
+
description: "Worktree root (default: ~/Projects/ashlrai/wt)."
|
|
17491
|
+
},
|
|
17492
|
+
runs: {
|
|
17493
|
+
type: "string",
|
|
17494
|
+
description: "Fleet runs dir (default: ~/.ashlr-runs); queue defaults to <runs>/queue."
|
|
17495
|
+
},
|
|
17496
|
+
queue: { type: "string", description: "Queue dir override (default: <runs>/queue)." },
|
|
17497
|
+
reapIdleHours: {
|
|
17498
|
+
type: "string",
|
|
17499
|
+
description: "Idle hours before RESULT PR worktrees are candidates (default 2)."
|
|
17500
|
+
},
|
|
17501
|
+
emptyIdleHours: {
|
|
17502
|
+
type: "string",
|
|
17503
|
+
description: "Idle hours before done/empty worktrees are candidates (default 6)."
|
|
17504
|
+
},
|
|
17505
|
+
budgetSeconds: {
|
|
17506
|
+
type: "string",
|
|
17507
|
+
description: "Seconds allowed for deep git/tree/gh inspection (default 120)."
|
|
17508
|
+
},
|
|
17509
|
+
ghBin: { type: "string", description: "gh binary override (default: PATH/install paths)." },
|
|
17510
|
+
lsofBin: { type: "string", description: "lsof binary override (default: system lsof)." },
|
|
17511
|
+
ghMax: {
|
|
17512
|
+
type: "string",
|
|
17513
|
+
description: "Cap on gh pr view calls (default 40)."
|
|
17514
|
+
},
|
|
17515
|
+
dryRun: {
|
|
17516
|
+
type: "boolean",
|
|
17517
|
+
description: "Accepted for explicitness; this command is always a dry run."
|
|
17518
|
+
}
|
|
17519
|
+
},
|
|
17520
|
+
async run({ args }) {
|
|
17521
|
+
const num = (v, fallback) => {
|
|
17522
|
+
if (v === undefined)
|
|
17523
|
+
return fallback;
|
|
17524
|
+
const n = Number(v);
|
|
17525
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
17526
|
+
};
|
|
17527
|
+
const report = await collectFleetGc({
|
|
17528
|
+
root: args.root ?? join23(homedir12(), "Projects", "ashlrai", "wt"),
|
|
17529
|
+
runs: args.runs ?? join23(homedir12(), ".ashlr-runs"),
|
|
17530
|
+
queue: args.queue,
|
|
17531
|
+
reapIdleHours: num(args.reapIdleHours, 2),
|
|
17532
|
+
emptyIdleHours: num(args.emptyIdleHours, 6),
|
|
17533
|
+
budgetSeconds: num(args.budgetSeconds, 120),
|
|
17534
|
+
ghBin: args.ghBin,
|
|
17535
|
+
lsofBin: args.lsofBin,
|
|
17536
|
+
ghMax: num(args.ghMax, 40)
|
|
17537
|
+
});
|
|
17538
|
+
if (args.json) {
|
|
17539
|
+
console.log(JSON.stringify(report));
|
|
17540
|
+
return;
|
|
17541
|
+
}
|
|
17542
|
+
const s = report.summary;
|
|
17543
|
+
console.log(`fleet-gc dry-run: ${s.total} worktrees under ${report.root} — ${s.byClass["collect-candidate"]} collect-candidates, ${s.byClass.preserve} preserved (${report.scan.durationMs}ms)`);
|
|
17544
|
+
for (const [basis, n] of Object.entries(s.byBasis).sort((a, b) => b[1] - a[1]))
|
|
17545
|
+
console.log(` basis ${basis}: ${n}`);
|
|
17546
|
+
const topReasons = Object.entries(s.byReason).sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
17547
|
+
for (const [reason, n] of topReasons)
|
|
17548
|
+
console.log(` preserve ${reason}: ${n}`);
|
|
17549
|
+
if (report.staleRegistrations.count)
|
|
17550
|
+
console.log(` stale git registrations under root (missing dirs): ${report.staleRegistrations.count}`);
|
|
17551
|
+
if (!report.scan.complete)
|
|
17552
|
+
console.log(" WARNING: scan incomplete (deep-inspection budget exhausted); candidates are a lower bound.");
|
|
17553
|
+
const candidates = report.entries.filter((e) => e.class === "collect-candidate");
|
|
17554
|
+
for (const c of candidates.slice(0, 50))
|
|
17555
|
+
console.log(` candidate ${c.name} basis=${c.basis} idle=${c.idleHours ?? "?"}h result=${c.resultClass ?? "none"}`);
|
|
17556
|
+
if (candidates.length > 50)
|
|
17557
|
+
console.log(` … and ${candidates.length - 50} more (--json for all)`);
|
|
17558
|
+
}
|
|
17559
|
+
});
|
|
17560
|
+
});
|
|
17561
|
+
|
|
17562
|
+
// src/commands/fleet-bets.ts
|
|
17563
|
+
import { homedir as homedir13 } from "node:os";
|
|
17564
|
+
import { dirname as dirname7, join as join24 } from "node:path";
|
|
16265
17565
|
function leaderBets(ledger, src) {
|
|
16266
17566
|
const bets = new Map;
|
|
16267
17567
|
for (const line of ledger.split(`
|
|
@@ -16297,11 +17597,11 @@ function classify(e, leader) {
|
|
|
16297
17597
|
return "vanished";
|
|
16298
17598
|
}
|
|
16299
17599
|
async function collectFleetBets(queue, options = {}) {
|
|
16300
|
-
const runs =
|
|
17600
|
+
const runs = dirname7(queue);
|
|
16301
17601
|
const [router, ready, metrics] = await Promise.all([
|
|
16302
|
-
readFleetText(options.router ??
|
|
16303
|
-
readFleetText(options.ready ??
|
|
16304
|
-
readFleetJsonl(options.metrics ??
|
|
17602
|
+
readFleetText(options.router ?? join24(queue, "router-queue.md"), { maxBytes: CAP }),
|
|
17603
|
+
readFleetText(options.ready ?? join24(queue, "ready.tsv"), { maxBytes: CAP }),
|
|
17604
|
+
readFleetJsonl(options.metrics ?? join24(runs, "metrics", "jobs.jsonl"), {
|
|
16305
17605
|
maxBytes: CAP,
|
|
16306
17606
|
tail: true,
|
|
16307
17607
|
malformedLines: "ignore"
|
|
@@ -16435,7 +17735,7 @@ var init_fleet_bets = __esm(() => {
|
|
|
16435
17735
|
src: { type: "string", description: "Source marker selecting bets (default: leader)." }
|
|
16436
17736
|
},
|
|
16437
17737
|
async run({ args }) {
|
|
16438
|
-
const report = await collectFleetBets(args.queue ??
|
|
17738
|
+
const report = await collectFleetBets(args.queue ?? join24(homedir13(), ".ashlr-runs", "queue"), args);
|
|
16439
17739
|
if (args.json)
|
|
16440
17740
|
console.log(JSON.stringify(report));
|
|
16441
17741
|
else {
|
|
@@ -16449,6 +17749,248 @@ var init_fleet_bets = __esm(() => {
|
|
|
16449
17749
|
});
|
|
16450
17750
|
});
|
|
16451
17751
|
|
|
17752
|
+
// src/commands/fleet-match.ts
|
|
17753
|
+
import { lstat as lstat3 } from "node:fs/promises";
|
|
17754
|
+
import { homedir as homedir14 } from "node:os";
|
|
17755
|
+
import { dirname as dirname8, join as join25 } from "node:path";
|
|
17756
|
+
function latestJobRows(rows) {
|
|
17757
|
+
const latest = new Map;
|
|
17758
|
+
for (const row of rows) {
|
|
17759
|
+
const id = row.id ?? row.job_id;
|
|
17760
|
+
if (typeof id === "string")
|
|
17761
|
+
latest.set(id, { ...latest.get(id), ...row });
|
|
17762
|
+
}
|
|
17763
|
+
return latest;
|
|
17764
|
+
}
|
|
17765
|
+
function poolCounts(latest) {
|
|
17766
|
+
const counts = new Map;
|
|
17767
|
+
for (const row of latest.values()) {
|
|
17768
|
+
const pool = row.pool;
|
|
17769
|
+
const status = row.status;
|
|
17770
|
+
if (typeof pool !== "string" || !isResourceId(pool))
|
|
17771
|
+
continue;
|
|
17772
|
+
if (status !== "running" && status !== "queued")
|
|
17773
|
+
continue;
|
|
17774
|
+
const entry = counts.get(pool) ?? { running: 0, queued: 0 };
|
|
17775
|
+
entry[status]++;
|
|
17776
|
+
counts.set(pool, entry);
|
|
17777
|
+
}
|
|
17778
|
+
return counts;
|
|
17779
|
+
}
|
|
17780
|
+
function readyRow(text, job) {
|
|
17781
|
+
for (const line of text.split(/\r?\n/)) {
|
|
17782
|
+
if (!line || line.startsWith("#"))
|
|
17783
|
+
continue;
|
|
17784
|
+
const f = line.split("\t");
|
|
17785
|
+
if (f[0] !== job)
|
|
17786
|
+
continue;
|
|
17787
|
+
return {
|
|
17788
|
+
lane: typeof f[2] === "string" && isResourceId(f[2]) ? f[2] : null,
|
|
17789
|
+
tier: typeof f[3] === "string" && isFleetTier(f[3]) ? f[3] : null
|
|
17790
|
+
};
|
|
17791
|
+
}
|
|
17792
|
+
return null;
|
|
17793
|
+
}
|
|
17794
|
+
function deriveFleetRequirement(flags, jobId, jobRow, ready) {
|
|
17795
|
+
const found = jobRow !== undefined || ready !== null;
|
|
17796
|
+
const job = jobId ? {
|
|
17797
|
+
id: jobId,
|
|
17798
|
+
found,
|
|
17799
|
+
proposedResource: (typeof jobRow?.pool === "string" && isResourceId(jobRow.pool) ? jobRow.pool : null) ?? ready?.lane ?? null
|
|
17800
|
+
} : null;
|
|
17801
|
+
const flagGiven = flags.role !== undefined || flags.capabilities !== undefined || flags.tier !== undefined;
|
|
17802
|
+
if (!flagGiven && (jobId === undefined || !found))
|
|
17803
|
+
return { requirement: null, error: null, job };
|
|
17804
|
+
const rowRole = jobRow?.role;
|
|
17805
|
+
const rowCaps = Array.isArray(jobRow?.capabilities) ? jobRow.capabilities.filter((c) => typeof c === "string" && isFleetCapability(c)) : [];
|
|
17806
|
+
const rowTier = typeof jobRow?.tier === "string" && isFleetTier(jobRow.tier) ? jobRow.tier : ready?.tier ?? null;
|
|
17807
|
+
const role = flags.role ?? (typeof rowRole === "string" ? rowRole : "implementer");
|
|
17808
|
+
if (!isFleetRole(role)) {
|
|
17809
|
+
return {
|
|
17810
|
+
requirement: null,
|
|
17811
|
+
error: rowRole !== undefined && typeof rowRole === "string" ? `job ${jobId} declares role "${rowRole.slice(0, 64)}", which is not a fleet role (${FLEET_ROLES_HINT})` : `role "${String(role).slice(0, 64)}" is not a fleet role (${FLEET_ROLES_HINT})`,
|
|
17812
|
+
job
|
|
17813
|
+
};
|
|
17814
|
+
}
|
|
17815
|
+
return {
|
|
17816
|
+
requirement: {
|
|
17817
|
+
role,
|
|
17818
|
+
capabilities: [...new Set(flags.capabilities ?? rowCaps)],
|
|
17819
|
+
tier: flags.tier ?? rowTier
|
|
17820
|
+
},
|
|
17821
|
+
error: null,
|
|
17822
|
+
job
|
|
17823
|
+
};
|
|
17824
|
+
}
|
|
17825
|
+
async function collectFleetMatch(queue, options = {}) {
|
|
17826
|
+
const runsDir = dirname8(queue);
|
|
17827
|
+
const capabilitiesPath = options.capabilitiesPath ?? join25(runsDir, "capabilities.json");
|
|
17828
|
+
const modelsPath = options.modelsPath ?? join25(runsDir, "models.json");
|
|
17829
|
+
const [slots, queueJournal, jobsJournal, ready, hold, capsText, modelsText] = await Promise.all([
|
|
17830
|
+
readFleetJson(join25(queue, "slots.json"), { object: true }),
|
|
17831
|
+
readFleetJsonl(join25(queue, "queue.jsonl"), { allowArray: true }),
|
|
17832
|
+
readFleetJsonl(join25(queue, "jobs.jsonl"), { allowArray: true }),
|
|
17833
|
+
readFleetText(join25(queue, "ready.tsv"), { maxBytes: 4 * 1024 * 1024 }),
|
|
17834
|
+
lstat3(join25(queue, "HOLD")).then(() => "present", (error) => isFleetRow(error) && error.code === "ENOENT" ? "absent" : "unavailable"),
|
|
17835
|
+
readFleetText(capabilitiesPath),
|
|
17836
|
+
readFleetText(modelsPath)
|
|
17837
|
+
]);
|
|
17838
|
+
const journalsKnown = queueJournal.status === "ok" && jobsJournal.status === "ok";
|
|
17839
|
+
const latest = latestJobRows([...queueJournal.rows, ...jobsJournal.rows]);
|
|
17840
|
+
const counts = journalsKnown ? poolCounts(latest) : new Map;
|
|
17841
|
+
const declarations = new Map;
|
|
17842
|
+
let capsDropped = 0;
|
|
17843
|
+
let capsStatus = capsText.status === "ok" ? "ok" : capsText.status;
|
|
17844
|
+
if (capsText.status === "ok") {
|
|
17845
|
+
const parsed = parseCapabilityDocument(capsText.text);
|
|
17846
|
+
if (parsed.status === "ok") {
|
|
17847
|
+
for (const [id, decl] of parsed.document.resources)
|
|
17848
|
+
declarations.set(id, decl);
|
|
17849
|
+
capsDropped = parsed.document.dropped;
|
|
17850
|
+
} else {
|
|
17851
|
+
capsStatus = "invalid";
|
|
17852
|
+
}
|
|
17853
|
+
}
|
|
17854
|
+
const modelsParsed = modelsText.status === "ok" ? parseModelMap(modelsText.text) : { map: new Map, dropped: 0, status: "ok" };
|
|
17855
|
+
const slotRows = isFleetRow(slots.data) ? slots.data : {};
|
|
17856
|
+
const fleetHardCap = nonNegInt(slotRows.hard_cap);
|
|
17857
|
+
const ids = new Set;
|
|
17858
|
+
for (const [key, value] of Object.entries(slotRows))
|
|
17859
|
+
if (isFleetRow(value) && isResourceId(key))
|
|
17860
|
+
ids.add(key);
|
|
17861
|
+
for (const id of declarations.keys())
|
|
17862
|
+
ids.add(id);
|
|
17863
|
+
if (journalsKnown)
|
|
17864
|
+
for (const id of counts.keys())
|
|
17865
|
+
ids.add(id);
|
|
17866
|
+
const derived = deriveFleetRequirement({ role: options.role, capabilities: options.capabilities, tier: options.tier }, options.job, options.job ? latest.get(options.job) : undefined, options.job && ready.status === "ok" ? readyRow(ready.text, options.job) : null);
|
|
17867
|
+
const tierForModel = derived.requirement?.tier ?? null;
|
|
17868
|
+
const resources = [...ids].sort().map((id) => {
|
|
17869
|
+
const slot = isFleetRow(slotRows[id]) ? slotRows[id] : null;
|
|
17870
|
+
const quota = slots.status === "ok" && slot !== null ? {
|
|
17871
|
+
target: nonNegInt(slot.target),
|
|
17872
|
+
ceiling: nonNegInt(slot.ceiling),
|
|
17873
|
+
hardCap: nonNegInt(slot.hard_cap),
|
|
17874
|
+
held: slot.held === true,
|
|
17875
|
+
running: journalsKnown ? counts.get(id)?.running ?? 0 : null,
|
|
17876
|
+
queued: journalsKnown ? counts.get(id)?.queued ?? 0 : null,
|
|
17877
|
+
quotaRemaining: numOrNull(slot.quota)
|
|
17878
|
+
} : null;
|
|
17879
|
+
const resolved = resolveModel(modelsParsed.map, id, tierForModel);
|
|
17880
|
+
return {
|
|
17881
|
+
...buildFleetResource(id, declarations.get(id), quota),
|
|
17882
|
+
model: resolved?.model ?? null,
|
|
17883
|
+
modelVia: resolved?.via ?? null
|
|
17884
|
+
};
|
|
17885
|
+
});
|
|
17886
|
+
const fleetRunning = journalsKnown ? [...counts.values()].reduce((sum, c) => sum + c.running, 0) : null;
|
|
17887
|
+
const match = derived.requirement === null ? null : matchRequirement(derived.requirement, resources, {
|
|
17888
|
+
fleetHold: hold === "present",
|
|
17889
|
+
fleet: { running: fleetRunning, hardCap: fleetHardCap }
|
|
17890
|
+
});
|
|
17891
|
+
const matchById = new Map((match?.matches ?? []).map((m) => [m.id, m]));
|
|
17892
|
+
return {
|
|
17893
|
+
schemaVersion: 1,
|
|
17894
|
+
generatedAt: (options.now ?? (() => new Date))().toISOString(),
|
|
17895
|
+
sources: {
|
|
17896
|
+
slots: slots.status,
|
|
17897
|
+
queue: queueJournal.status,
|
|
17898
|
+
jobs: jobsJournal.status,
|
|
17899
|
+
capabilities: capsStatus,
|
|
17900
|
+
capabilitiesDropped: capsDropped,
|
|
17901
|
+
models: modelsText.status === "ok" && modelsParsed.status === "invalid" ? "invalid" : modelsText.status,
|
|
17902
|
+
modelsDropped: modelsParsed.dropped,
|
|
17903
|
+
ready: ready.status,
|
|
17904
|
+
hold
|
|
17905
|
+
},
|
|
17906
|
+
fleet: { running: fleetRunning, hardCap: fleetHardCap, hold: hold === "present" },
|
|
17907
|
+
requirement: derived.requirement,
|
|
17908
|
+
requirementError: derived.error,
|
|
17909
|
+
job: derived.job,
|
|
17910
|
+
resources: resources.map((r) => {
|
|
17911
|
+
const { model, modelVia, ...rest } = r;
|
|
17912
|
+
const m = matchById.get(r.id);
|
|
17913
|
+
return {
|
|
17914
|
+
...rest,
|
|
17915
|
+
quota: r.quota === null ? null : { ...r.quota, headroom: resourceHeadroom(r.quota) },
|
|
17916
|
+
model,
|
|
17917
|
+
modelVia,
|
|
17918
|
+
match: m ? { eligible: m.eligible, reasons: m.reasons, headroom: m.headroom } : null
|
|
17919
|
+
};
|
|
17920
|
+
}),
|
|
17921
|
+
eligible: match?.eligible ?? null
|
|
17922
|
+
};
|
|
17923
|
+
}
|
|
17924
|
+
var FLEET_ROLES_HINT = "planner, implementer, reviewer, release-verifier", nonNegInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0 ? v : null, numOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null, fleetMatchCommand;
|
|
17925
|
+
var init_fleet_match = __esm(() => {
|
|
17926
|
+
init_src();
|
|
17927
|
+
init_dist3();
|
|
17928
|
+
init_fleet_jsonl();
|
|
17929
|
+
fleetMatchCommand = defineCommand2({
|
|
17930
|
+
meta: {
|
|
17931
|
+
name: "fleet-match",
|
|
17932
|
+
description: "Map a job requirement to eligible fleet resources by capability, quota and availability."
|
|
17933
|
+
},
|
|
17934
|
+
args: {
|
|
17935
|
+
json: { type: "boolean", description: "Emit schemaVersion 1 JSON." },
|
|
17936
|
+
queue: { type: "string", description: "Fleet queue directory (default: ~/.ashlr-runs/queue)." },
|
|
17937
|
+
role: { type: "string", description: `Required role: ${FLEET_ROLES_HINT}.` },
|
|
17938
|
+
capability: {
|
|
17939
|
+
type: "string",
|
|
17940
|
+
description: "Comma-separated capability tags the resource must declare."
|
|
17941
|
+
},
|
|
17942
|
+
tier: { type: "string", description: "Required tier (e.g. frontier, work, mid, fast)." },
|
|
17943
|
+
job: {
|
|
17944
|
+
type: "string",
|
|
17945
|
+
description: "Derive the requirement from a queued job id (ready.tsv + journals)."
|
|
17946
|
+
},
|
|
17947
|
+
capabilitiesFile: {
|
|
17948
|
+
type: "string",
|
|
17949
|
+
description: "Capabilities file (default: capabilities.json beside the queue directory)."
|
|
17950
|
+
},
|
|
17951
|
+
modelsFile: {
|
|
17952
|
+
type: "string",
|
|
17953
|
+
description: "Models file (default: models.json beside the queue directory)."
|
|
17954
|
+
}
|
|
17955
|
+
},
|
|
17956
|
+
async run({ args }) {
|
|
17957
|
+
const capabilities = args.capability?.split(",").map((c) => c.trim()).filter(Boolean);
|
|
17958
|
+
if (capabilities?.some((c) => !isFleetCapability(c)))
|
|
17959
|
+
throw new Error("Invalid capability tag; expected lowercase tags like local-inference.");
|
|
17960
|
+
if (args.role !== undefined && !isFleetRole(args.role))
|
|
17961
|
+
throw new Error(`Invalid role; expected one of: ${FLEET_ROLES_HINT}.`);
|
|
17962
|
+
if (args.tier !== undefined && !isFleetTier(args.tier))
|
|
17963
|
+
throw new Error("Invalid tier; expected a lowercase tier name.");
|
|
17964
|
+
if (args.job !== undefined && !isResourceId(args.job))
|
|
17965
|
+
throw new Error("Invalid job id.");
|
|
17966
|
+
const report = await collectFleetMatch(args.queue ?? join25(homedir14(), ".ashlr-runs", "queue"), {
|
|
17967
|
+
role: args.role,
|
|
17968
|
+
capabilities,
|
|
17969
|
+
tier: args.tier,
|
|
17970
|
+
job: args.job,
|
|
17971
|
+
capabilitiesPath: typeof args.capabilitiesFile === "string" ? args.capabilitiesFile : undefined,
|
|
17972
|
+
modelsPath: typeof args.modelsFile === "string" ? args.modelsFile : undefined
|
|
17973
|
+
});
|
|
17974
|
+
if (args.json) {
|
|
17975
|
+
console.log(JSON.stringify(report));
|
|
17976
|
+
return;
|
|
17977
|
+
}
|
|
17978
|
+
const req = report.requirement;
|
|
17979
|
+
console.log(req ? `Requirement: role=${req.role} capabilities=[${req.capabilities.join(", ")}] tier=${req.tier ?? "any"}${report.job?.proposedResource ? ` (job ${report.job.id} proposed ${report.job.proposedResource})` : ""}` : report.requirementError ? `Requirement: invalid — ${report.requirementError}` : "Capability matrix (no requirement given; pass --role/--job to match)");
|
|
17980
|
+
console.log(`Sources: slots=${report.sources.slots} queue=${report.sources.queue} jobs=${report.sources.jobs} capabilities=${report.sources.capabilities} models=${report.sources.models} hold=${report.sources.hold} ready=${report.sources.ready}`);
|
|
17981
|
+
console.log(`Fleet: running=${report.fleet.running ?? "unknown"} hardCap=${report.fleet.hardCap ?? "unknown"} hold=${report.fleet.hold}`);
|
|
17982
|
+
for (const r of report.resources) {
|
|
17983
|
+
const q = r.quota;
|
|
17984
|
+
const quotaText = q ? `target=${q.target ?? "?"} running=${q.running ?? "?"} headroom=${q.headroom ?? "?"}${q.held ? " held" : ""}` : "quota=unknown";
|
|
17985
|
+
const matchText = r.match ? r.match.eligible ? "ELIGIBLE" : `ineligible: ${r.match.reasons.join(", ")}` : "";
|
|
17986
|
+
console.log(`${r.id}: roles=[${r.roles.join(", ")}] caps=[${r.capabilities.join(", ")}]${r.tiers ? ` tiers=[${r.tiers.join(", ")}]` : ""} ${quotaText}${r.model ? ` model=${r.model}` : ""} ${matchText}`.trimEnd());
|
|
17987
|
+
}
|
|
17988
|
+
if (report.eligible)
|
|
17989
|
+
console.log(`Eligible: ${report.eligible.join(", ") || "none"}`);
|
|
17990
|
+
}
|
|
17991
|
+
});
|
|
17992
|
+
});
|
|
17993
|
+
|
|
16452
17994
|
// src/commands/fleet-collisions.ts
|
|
16453
17995
|
function collectFleetCollisions2(input) {
|
|
16454
17996
|
if (!Array.isArray(input))
|
|
@@ -16638,8 +18180,8 @@ var init_fleet_slo2 = __esm(() => {
|
|
|
16638
18180
|
});
|
|
16639
18181
|
|
|
16640
18182
|
// src/commands/fleet-waste.ts
|
|
16641
|
-
import { homedir as
|
|
16642
|
-
import { join as
|
|
18183
|
+
import { homedir as homedir15 } from "node:os";
|
|
18184
|
+
import { join as join26 } from "node:path";
|
|
16643
18185
|
import { createInterface as createInterface3 } from "node:readline";
|
|
16644
18186
|
async function collectFleetWaste2(metrics, hours = 1, now = Date.now()) {
|
|
16645
18187
|
const start = now - hours * 3600000;
|
|
@@ -16759,7 +18301,7 @@ var init_fleet_waste2 = __esm(() => {
|
|
|
16759
18301
|
json: { type: "boolean", description: "Emit schemaVersion 1 JSON (also the default)." }
|
|
16760
18302
|
},
|
|
16761
18303
|
async run({ args }) {
|
|
16762
|
-
console.log(JSON.stringify(await collectFleetWaste2(args.metrics ??
|
|
18304
|
+
console.log(JSON.stringify(await collectFleetWaste2(args.metrics ?? join26(homedir15(), ".ashlr-runs", "metrics", "jobs.jsonl"), Number(args.hours))));
|
|
16763
18305
|
}
|
|
16764
18306
|
});
|
|
16765
18307
|
fleetWasteSubcommand2 = defineCommand2({
|
|
@@ -16769,9 +18311,9 @@ var init_fleet_waste2 = __esm(() => {
|
|
|
16769
18311
|
});
|
|
16770
18312
|
|
|
16771
18313
|
// src/commands/fleet.ts
|
|
16772
|
-
import { execFile as
|
|
16773
|
-
import { homedir as
|
|
16774
|
-
import { join as
|
|
18314
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
18315
|
+
import { homedir as homedir16 } from "node:os";
|
|
18316
|
+
import { join as join27 } from "node:path";
|
|
16775
18317
|
function fleetLanes(slotsText, log) {
|
|
16776
18318
|
let slots = {};
|
|
16777
18319
|
try {
|
|
@@ -16806,9 +18348,9 @@ function fleetLanes(slotsText, log) {
|
|
|
16806
18348
|
})
|
|
16807
18349
|
};
|
|
16808
18350
|
}
|
|
16809
|
-
async function collectFleet(queue, runner = locusGh3, logPath =
|
|
18351
|
+
async function collectFleet(queue, runner = locusGh3, logPath = join27(queue, "dispatchd.log")) {
|
|
16810
18352
|
const [slots, log, health] = await Promise.all([
|
|
16811
|
-
readFleetText(
|
|
18353
|
+
readFleetText(join27(queue, "slots.json")).then((source) => source.text),
|
|
16812
18354
|
readFleetText(logPath, { maxBytes: 65536, tail: true }).then((source) => source.text),
|
|
16813
18355
|
checkRepos(runner)
|
|
16814
18356
|
]);
|
|
@@ -16824,7 +18366,7 @@ async function collectFleet(queue, runner = locusGh3, logPath = join24(queue, "d
|
|
|
16824
18366
|
};
|
|
16825
18367
|
}
|
|
16826
18368
|
var unknown = "unknown", count3 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : unknown, locusGh3 = (args, signal) => new Promise((resolve, reject) => {
|
|
16827
|
-
|
|
18369
|
+
execFile5("locus", ["exec", "--", "gh", ...args], { signal, timeout: 5000, maxBuffer: 1024 * 1024 }, (error, stdout) => error ? reject(error) : resolve(stdout));
|
|
16828
18370
|
}), fleetCommand;
|
|
16829
18371
|
var init_fleet = __esm(() => {
|
|
16830
18372
|
init_dist3();
|
|
@@ -16857,7 +18399,7 @@ var init_fleet = __esm(() => {
|
|
|
16857
18399
|
async run({ args, rawArgs }) {
|
|
16858
18400
|
if (rawArgs.some((a) => !a.startsWith("-")))
|
|
16859
18401
|
return;
|
|
16860
|
-
const report = await collectFleet(args.queue ??
|
|
18402
|
+
const report = await collectFleet(args.queue ?? join27(homedir16(), ".ashlr-runs", "queue"), locusGh3, args.log ?? join27(homedir16(), ".ashlr-runs", "dispatchd.log"));
|
|
16861
18403
|
if (args.json)
|
|
16862
18404
|
console.log(JSON.stringify(report));
|
|
16863
18405
|
else {
|
|
@@ -18741,7 +20283,7 @@ var init_apply = __esm(() => {
|
|
|
18741
20283
|
});
|
|
18742
20284
|
|
|
18743
20285
|
// src/commands/projects.ts
|
|
18744
|
-
import { resolve as
|
|
20286
|
+
import { resolve as resolve12 } from "node:path";
|
|
18745
20287
|
async function buildProjectsReport() {
|
|
18746
20288
|
const projects = (await listProjects()).map((p) => ({
|
|
18747
20289
|
name: p.name,
|
|
@@ -18825,7 +20367,7 @@ var init_projects = __esm(() => {
|
|
|
18825
20367
|
intro("stack projects remove");
|
|
18826
20368
|
const target = String(args.target);
|
|
18827
20369
|
const byName = await findProjectByName(target);
|
|
18828
|
-
const removed = await unregisterProject(byName?.path ??
|
|
20370
|
+
const removed = await unregisterProject(byName?.path ?? resolve12(target));
|
|
18829
20371
|
if (!removed) {
|
|
18830
20372
|
outroError(`No registered project matches "${target}". Run \`stack projects list\` for names and paths.`);
|
|
18831
20373
|
return;
|
|
@@ -18859,7 +20401,7 @@ var init_projects = __esm(() => {
|
|
|
18859
20401
|
|
|
18860
20402
|
// src/commands/ci.ts
|
|
18861
20403
|
import { existsSync as existsSync13 } from "node:fs";
|
|
18862
|
-
import { join as
|
|
20404
|
+
import { join as join28, resolve as resolve13 } from "node:path";
|
|
18863
20405
|
function refuse(message, json, title) {
|
|
18864
20406
|
if (json) {
|
|
18865
20407
|
process.stdout.write(`${JSON.stringify({ error: message })}
|
|
@@ -18898,7 +20440,7 @@ var init_ci = __esm(() => {
|
|
|
18898
20440
|
refuse("No .stack.toml found. Run `stack init` first.", json, "stack ci init");
|
|
18899
20441
|
return;
|
|
18900
20442
|
}
|
|
18901
|
-
const path =
|
|
20443
|
+
const path = join28(resolve13(join28(process.cwd(), ".github", "workflows")), "stack-ci.yml");
|
|
18902
20444
|
if (existsSync13(path) && !args.force) {
|
|
18903
20445
|
refuse(`${path} already exists. It was not changed.`, json, "stack ci init");
|
|
18904
20446
|
return;
|
|
@@ -19180,6 +20722,18 @@ var init_completion = __esm(() => {
|
|
|
19180
20722
|
"fleet-bets": {
|
|
19181
20723
|
flags: ["json", "queue", "router", "ready", "metrics", "src"].map((name) => ({ name }))
|
|
19182
20724
|
},
|
|
20725
|
+
"fleet-match": {
|
|
20726
|
+
flags: [
|
|
20727
|
+
"json",
|
|
20728
|
+
"queue",
|
|
20729
|
+
"role",
|
|
20730
|
+
"capability",
|
|
20731
|
+
"tier",
|
|
20732
|
+
"job",
|
|
20733
|
+
"capabilitiesFile",
|
|
20734
|
+
"modelsFile"
|
|
20735
|
+
].map((name) => ({ name }))
|
|
20736
|
+
},
|
|
19183
20737
|
fleet: {
|
|
19184
20738
|
subcommands: ["slo", "collisions", "waste"],
|
|
19185
20739
|
flags: ["json", "queue", "log", "file", "metrics", "hours"].map((name) => ({ name }))
|
|
@@ -19265,6 +20819,7 @@ var init_completion = __esm(() => {
|
|
|
19265
20819
|
"fleet-stream",
|
|
19266
20820
|
"fleet-graph",
|
|
19267
20821
|
"fleet-bets",
|
|
20822
|
+
"fleet-match",
|
|
19268
20823
|
"fleet",
|
|
19269
20824
|
"exec",
|
|
19270
20825
|
"sync",
|
|
@@ -19318,7 +20873,7 @@ var package_default;
|
|
|
19318
20873
|
var init_package = __esm(() => {
|
|
19319
20874
|
package_default = {
|
|
19320
20875
|
name: "@ashlr/stack",
|
|
19321
|
-
version: "0.3.
|
|
20876
|
+
version: "0.3.9",
|
|
19322
20877
|
description: "Ashlr Stack CLI — the control plane for your entire dev stack. One command to provision, wire, and operate every third-party service in your project. 39 providers, MCP-native.",
|
|
19323
20878
|
license: "MIT",
|
|
19324
20879
|
author: {
|
|
@@ -19372,7 +20927,7 @@ var init_package = __esm(() => {
|
|
|
19372
20927
|
typecheck: "bunx tsc --noEmit"
|
|
19373
20928
|
},
|
|
19374
20929
|
dependencies: {
|
|
19375
|
-
"@ashlr/stack-core": "^0.3.
|
|
20930
|
+
"@ashlr/stack-core": "^0.3.9",
|
|
19376
20931
|
"@clack/prompts": "^0.8.2",
|
|
19377
20932
|
citty: "^0.1.6",
|
|
19378
20933
|
picocolors: "^1.1.1"
|
|
@@ -19385,19 +20940,19 @@ var init_package = __esm(() => {
|
|
|
19385
20940
|
});
|
|
19386
20941
|
|
|
19387
20942
|
// src/lib/upgrade-install.ts
|
|
19388
|
-
import { execFile as
|
|
19389
|
-
import { lstat as
|
|
19390
|
-
import { dirname as
|
|
19391
|
-
import { promisify } from "node:util";
|
|
20943
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
20944
|
+
import { lstat as lstat4, mkdir as mkdir7, mkdtemp, readFile as readFile13, realpath as realpath2, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
20945
|
+
import { dirname as dirname9, join as join29 } from "node:path";
|
|
20946
|
+
import { promisify as promisify2 } from "node:util";
|
|
19392
20947
|
function isReleaseVersion(version) {
|
|
19393
20948
|
return typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
|
|
19394
20949
|
}
|
|
19395
20950
|
async function health(directory, version, runner) {
|
|
19396
|
-
const manifest = JSON.parse(await
|
|
20951
|
+
const manifest = JSON.parse(await readFile13(join29(directory, "package.json"), "utf8"));
|
|
19397
20952
|
if (manifest.name !== PACKAGE || manifest.version !== version) {
|
|
19398
20953
|
throw new Error("Installed package identity does not match the requested release.");
|
|
19399
20954
|
}
|
|
19400
|
-
const entry =
|
|
20955
|
+
const entry = join29(directory, "dist/index.js");
|
|
19401
20956
|
const output = JSON.parse(await runner("node", [entry, "--version", "--json"]));
|
|
19402
20957
|
if (output.version !== version)
|
|
19403
20958
|
throw new Error("CLI version health check failed.");
|
|
@@ -19414,17 +20969,17 @@ async function installUpgrade(current, latest, runner = run) {
|
|
|
19414
20969
|
const root = await runner("npm", ["root", "--global"]);
|
|
19415
20970
|
if (!root.startsWith("/"))
|
|
19416
20971
|
throw new Error("npm did not return an absolute global root.");
|
|
19417
|
-
const target =
|
|
19418
|
-
if (!(await
|
|
20972
|
+
const target = join29(root, PACKAGE);
|
|
20973
|
+
if (!(await lstat4(target)).isDirectory()) {
|
|
19419
20974
|
throw new Error("Applying upgrades requires an npm global install, not a linked package.");
|
|
19420
20975
|
}
|
|
19421
20976
|
const prefix = await runner("npm", ["prefix", "--global"]);
|
|
19422
20977
|
if (!prefix.startsWith("/"))
|
|
19423
20978
|
throw new Error("npm did not return an absolute global prefix.");
|
|
19424
|
-
if (await
|
|
20979
|
+
if (await realpath2(join29(prefix, "bin/stack")) !== join29(await realpath2(target), "dist/index.js")) {
|
|
19425
20980
|
throw new Error("The npm stack launcher does not point to this installation.");
|
|
19426
20981
|
}
|
|
19427
|
-
const lock =
|
|
20982
|
+
const lock = join29(dirname9(target), ".stack-upgrade-lock");
|
|
19428
20983
|
try {
|
|
19429
20984
|
await mkdir7(lock);
|
|
19430
20985
|
} catch {
|
|
@@ -19434,7 +20989,7 @@ async function installUpgrade(current, latest, runner = run) {
|
|
|
19434
20989
|
let retainBackup = false;
|
|
19435
20990
|
try {
|
|
19436
20991
|
await health(target, current, runner);
|
|
19437
|
-
workspace = await mkdtemp(
|
|
20992
|
+
workspace = await mkdtemp(join29(dirname9(target), ".stack-upgrade-"));
|
|
19438
20993
|
await runner("npm", [
|
|
19439
20994
|
"install",
|
|
19440
20995
|
"--global",
|
|
@@ -19445,9 +21000,9 @@ async function installUpgrade(current, latest, runner = run) {
|
|
|
19445
21000
|
"--no-fund",
|
|
19446
21001
|
`${PACKAGE}@${latest}`
|
|
19447
21002
|
]);
|
|
19448
|
-
const candidate =
|
|
21003
|
+
const candidate = join29(workspace, "lib/node_modules", PACKAGE);
|
|
19449
21004
|
await health(candidate, latest, runner);
|
|
19450
|
-
const backup =
|
|
21005
|
+
const backup = join29(workspace, "previous");
|
|
19451
21006
|
await rename2(target, backup);
|
|
19452
21007
|
try {
|
|
19453
21008
|
await rename2(candidate, target);
|
|
@@ -19483,7 +21038,7 @@ var exec2, PACKAGE = "@ashlr/stack", run = async (command, args) => {
|
|
|
19483
21038
|
}
|
|
19484
21039
|
};
|
|
19485
21040
|
var init_upgrade_install = __esm(() => {
|
|
19486
|
-
exec2 =
|
|
21041
|
+
exec2 = promisify2(execFile6);
|
|
19487
21042
|
});
|
|
19488
21043
|
|
|
19489
21044
|
// src/commands/upgrade.ts
|
|
@@ -19608,6 +21163,8 @@ init_detect();
|
|
|
19608
21163
|
init_detect_source();
|
|
19609
21164
|
init_registry();
|
|
19610
21165
|
init_agent_onboard();
|
|
21166
|
+
init_capability_matrix();
|
|
21167
|
+
init_job_run();
|
|
19611
21168
|
|
|
19612
21169
|
// ../../node_modules/.bun/citty@0.1.6/node_modules/citty/dist/index.mjs
|
|
19613
21170
|
init_dist2();
|
|
@@ -19858,7 +21415,7 @@ async function checkForUpdate(currentVersion, _cacheFile) {
|
|
|
19858
21415
|
}
|
|
19859
21416
|
|
|
19860
21417
|
// src/index.ts
|
|
19861
|
-
var VERSION = "0.3.
|
|
21418
|
+
var VERSION = "0.3.9";
|
|
19862
21419
|
var main = defineCommand({
|
|
19863
21420
|
meta: {
|
|
19864
21421
|
name: "stack",
|
|
@@ -19937,7 +21494,9 @@ var main = defineCommand({
|
|
|
19937
21494
|
"fleet-act": () => Promise.resolve().then(() => (init_fleet_act(), {})).then((m) => fleetActCommand),
|
|
19938
21495
|
"fleet-stream": () => Promise.resolve().then(() => (init_fleet_stream(), {})).then((m) => fleetStreamCommand),
|
|
19939
21496
|
"fleet-graph": () => Promise.resolve().then(() => (init_fleet_graph(), {})).then((m) => fleetGraphCommand),
|
|
21497
|
+
"fleet-gc": () => Promise.resolve().then(() => (init_fleet_gc2(), {})).then((m) => fleetGcCommand),
|
|
19940
21498
|
"fleet-bets": () => Promise.resolve().then(() => (init_fleet_bets(), {})).then((m) => fleetBetsCommand),
|
|
21499
|
+
"fleet-match": () => Promise.resolve().then(() => (init_fleet_match(), {})).then((m) => fleetMatchCommand),
|
|
19941
21500
|
fleet: () => Promise.resolve().then(() => (init_fleet(), {})).then((m) => fleetCommand),
|
|
19942
21501
|
exec: () => Promise.resolve().then(() => (init_exec(), {})).then((m) => execCommand),
|
|
19943
21502
|
swap: () => Promise.resolve().then(() => (init_swap2(), {})).then((m) => swapCommand),
|