@ashlr/stack 0.3.6 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1610 -48
  3. 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.6", distribution: "npm" }),
1494
- Object.freeze({ id: "cli", name: "@ashlr/stack", version: "0.3.6", distribution: "npm" }),
1495
- Object.freeze({ id: "mcp", name: "ashlr-stack-mcp", version: "0.3.6", distribution: "npm" }),
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 signal = opts.signal;
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, mergeSignal(init, signal));
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
@@ -14555,6 +14734,8 @@ async function readFleetText(path, options = {}) {
14555
14734
  break;
14556
14735
  offset += bytesRead;
14557
14736
  }
14737
+ if (options.requireInitialSize && offset < stat.size)
14738
+ throw new Error("Incomplete source");
14558
14739
  text = buffer.subarray(0, offset).toString("utf8");
14559
14740
  }
14560
14741
  return { status: "ok", text };
@@ -15425,6 +15606,7 @@ async function collectFleetSilent(runs, now = Date.now()) {
15425
15606
  }
15426
15607
  const paths = [...authors, "dispatchd.log", "dispatch-events.log", "metrics/jobs.jsonl"];
15427
15608
  const inputs = await Promise.all(paths.map((path) => readFleetJsonl(join18(runs, path), {
15609
+ requireInitialSize: true,
15428
15610
  parseLine: parse3,
15429
15611
  malformedLines: "flag",
15430
15612
  flagRow: wellFormed
@@ -16256,9 +16438,1130 @@ var init_fleet_graph = __esm(() => {
16256
16438
  });
16257
16439
  });
16258
16440
 
16259
- // src/commands/fleet-bets.ts
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
16260
17475
  import { homedir as homedir12 } from "node:os";
16261
- import { dirname as dirname6, join as join22 } from "node:path";
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";
16262
17565
  function leaderBets(ledger, src) {
16263
17566
  const bets = new Map;
16264
17567
  for (const line of ledger.split(`
@@ -16294,11 +17597,11 @@ function classify(e, leader) {
16294
17597
  return "vanished";
16295
17598
  }
16296
17599
  async function collectFleetBets(queue, options = {}) {
16297
- const runs = dirname6(queue);
17600
+ const runs = dirname7(queue);
16298
17601
  const [router, ready, metrics] = await Promise.all([
16299
- readFleetText(options.router ?? join22(queue, "router-queue.md"), { maxBytes: CAP }),
16300
- readFleetText(options.ready ?? join22(queue, "ready.tsv"), { maxBytes: CAP }),
16301
- readFleetJsonl(options.metrics ?? join22(runs, "metrics", "jobs.jsonl"), {
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"), {
16302
17605
  maxBytes: CAP,
16303
17606
  tail: true,
16304
17607
  malformedLines: "ignore"
@@ -16432,7 +17735,7 @@ var init_fleet_bets = __esm(() => {
16432
17735
  src: { type: "string", description: "Source marker selecting bets (default: leader)." }
16433
17736
  },
16434
17737
  async run({ args }) {
16435
- const report = await collectFleetBets(args.queue ?? join22(homedir12(), ".ashlr-runs", "queue"), args);
17738
+ const report = await collectFleetBets(args.queue ?? join24(homedir13(), ".ashlr-runs", "queue"), args);
16436
17739
  if (args.json)
16437
17740
  console.log(JSON.stringify(report));
16438
17741
  else {
@@ -16446,6 +17749,248 @@ var init_fleet_bets = __esm(() => {
16446
17749
  });
16447
17750
  });
16448
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
+
16449
17994
  // src/commands/fleet-collisions.ts
16450
17995
  function collectFleetCollisions2(input) {
16451
17996
  if (!Array.isArray(input))
@@ -16635,8 +18180,8 @@ var init_fleet_slo2 = __esm(() => {
16635
18180
  });
16636
18181
 
16637
18182
  // src/commands/fleet-waste.ts
16638
- import { homedir as homedir13 } from "node:os";
16639
- import { join as join23 } from "node:path";
18183
+ import { homedir as homedir15 } from "node:os";
18184
+ import { join as join26 } from "node:path";
16640
18185
  import { createInterface as createInterface3 } from "node:readline";
16641
18186
  async function collectFleetWaste2(metrics, hours = 1, now = Date.now()) {
16642
18187
  const start = now - hours * 3600000;
@@ -16756,7 +18301,7 @@ var init_fleet_waste2 = __esm(() => {
16756
18301
  json: { type: "boolean", description: "Emit schemaVersion 1 JSON (also the default)." }
16757
18302
  },
16758
18303
  async run({ args }) {
16759
- console.log(JSON.stringify(await collectFleetWaste2(args.metrics ?? join23(homedir13(), ".ashlr-runs", "metrics", "jobs.jsonl"), Number(args.hours))));
18304
+ console.log(JSON.stringify(await collectFleetWaste2(args.metrics ?? join26(homedir15(), ".ashlr-runs", "metrics", "jobs.jsonl"), Number(args.hours))));
16760
18305
  }
16761
18306
  });
16762
18307
  fleetWasteSubcommand2 = defineCommand2({
@@ -16766,9 +18311,9 @@ var init_fleet_waste2 = __esm(() => {
16766
18311
  });
16767
18312
 
16768
18313
  // src/commands/fleet.ts
16769
- import { execFile as execFile4 } from "node:child_process";
16770
- import { homedir as homedir14 } from "node:os";
16771
- import { join as join24 } from "node:path";
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";
16772
18317
  function fleetLanes(slotsText, log) {
16773
18318
  let slots = {};
16774
18319
  try {
@@ -16803,9 +18348,9 @@ function fleetLanes(slotsText, log) {
16803
18348
  })
16804
18349
  };
16805
18350
  }
16806
- async function collectFleet(queue, runner = locusGh3, logPath = join24(queue, "dispatchd.log")) {
18351
+ async function collectFleet(queue, runner = locusGh3, logPath = join27(queue, "dispatchd.log")) {
16807
18352
  const [slots, log, health] = await Promise.all([
16808
- readFleetText(join24(queue, "slots.json")).then((source) => source.text),
18353
+ readFleetText(join27(queue, "slots.json")).then((source) => source.text),
16809
18354
  readFleetText(logPath, { maxBytes: 65536, tail: true }).then((source) => source.text),
16810
18355
  checkRepos(runner)
16811
18356
  ]);
@@ -16821,7 +18366,7 @@ async function collectFleet(queue, runner = locusGh3, logPath = join24(queue, "d
16821
18366
  };
16822
18367
  }
16823
18368
  var unknown = "unknown", count3 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : unknown, locusGh3 = (args, signal) => new Promise((resolve, reject) => {
16824
- execFile4("locus", ["exec", "--", "gh", ...args], { signal, timeout: 5000, maxBuffer: 1024 * 1024 }, (error, stdout) => error ? reject(error) : resolve(stdout));
18369
+ execFile5("locus", ["exec", "--", "gh", ...args], { signal, timeout: 5000, maxBuffer: 1024 * 1024 }, (error, stdout) => error ? reject(error) : resolve(stdout));
16825
18370
  }), fleetCommand;
16826
18371
  var init_fleet = __esm(() => {
16827
18372
  init_dist3();
@@ -16854,7 +18399,7 @@ var init_fleet = __esm(() => {
16854
18399
  async run({ args, rawArgs }) {
16855
18400
  if (rawArgs.some((a) => !a.startsWith("-")))
16856
18401
  return;
16857
- const report = await collectFleet(args.queue ?? join24(homedir14(), ".ashlr-runs", "queue"), locusGh3, args.log ?? join24(homedir14(), ".ashlr-runs", "dispatchd.log"));
18402
+ const report = await collectFleet(args.queue ?? join27(homedir16(), ".ashlr-runs", "queue"), locusGh3, args.log ?? join27(homedir16(), ".ashlr-runs", "dispatchd.log"));
16858
18403
  if (args.json)
16859
18404
  console.log(JSON.stringify(report));
16860
18405
  else {
@@ -18738,7 +20283,7 @@ var init_apply = __esm(() => {
18738
20283
  });
18739
20284
 
18740
20285
  // src/commands/projects.ts
18741
- import { resolve as resolve11 } from "node:path";
20286
+ import { resolve as resolve12 } from "node:path";
18742
20287
  async function buildProjectsReport() {
18743
20288
  const projects = (await listProjects()).map((p) => ({
18744
20289
  name: p.name,
@@ -18822,7 +20367,7 @@ var init_projects = __esm(() => {
18822
20367
  intro("stack projects remove");
18823
20368
  const target = String(args.target);
18824
20369
  const byName = await findProjectByName(target);
18825
- const removed = await unregisterProject(byName?.path ?? resolve11(target));
20370
+ const removed = await unregisterProject(byName?.path ?? resolve12(target));
18826
20371
  if (!removed) {
18827
20372
  outroError(`No registered project matches "${target}". Run \`stack projects list\` for names and paths.`);
18828
20373
  return;
@@ -18856,7 +20401,7 @@ var init_projects = __esm(() => {
18856
20401
 
18857
20402
  // src/commands/ci.ts
18858
20403
  import { existsSync as existsSync13 } from "node:fs";
18859
- import { join as join25, resolve as resolve12 } from "node:path";
20404
+ import { join as join28, resolve as resolve13 } from "node:path";
18860
20405
  function refuse(message, json, title) {
18861
20406
  if (json) {
18862
20407
  process.stdout.write(`${JSON.stringify({ error: message })}
@@ -18895,7 +20440,7 @@ var init_ci = __esm(() => {
18895
20440
  refuse("No .stack.toml found. Run `stack init` first.", json, "stack ci init");
18896
20441
  return;
18897
20442
  }
18898
- const path = join25(resolve12(join25(process.cwd(), ".github", "workflows")), "stack-ci.yml");
20443
+ const path = join28(resolve13(join28(process.cwd(), ".github", "workflows")), "stack-ci.yml");
18899
20444
  if (existsSync13(path) && !args.force) {
18900
20445
  refuse(`${path} already exists. It was not changed.`, json, "stack ci init");
18901
20446
  return;
@@ -19177,6 +20722,18 @@ var init_completion = __esm(() => {
19177
20722
  "fleet-bets": {
19178
20723
  flags: ["json", "queue", "router", "ready", "metrics", "src"].map((name) => ({ name }))
19179
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
+ },
19180
20737
  fleet: {
19181
20738
  subcommands: ["slo", "collisions", "waste"],
19182
20739
  flags: ["json", "queue", "log", "file", "metrics", "hours"].map((name) => ({ name }))
@@ -19262,6 +20819,7 @@ var init_completion = __esm(() => {
19262
20819
  "fleet-stream",
19263
20820
  "fleet-graph",
19264
20821
  "fleet-bets",
20822
+ "fleet-match",
19265
20823
  "fleet",
19266
20824
  "exec",
19267
20825
  "sync",
@@ -19315,7 +20873,7 @@ var package_default;
19315
20873
  var init_package = __esm(() => {
19316
20874
  package_default = {
19317
20875
  name: "@ashlr/stack",
19318
- version: "0.3.6",
20876
+ version: "0.3.9",
19319
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.",
19320
20878
  license: "MIT",
19321
20879
  author: {
@@ -19369,7 +20927,7 @@ var init_package = __esm(() => {
19369
20927
  typecheck: "bunx tsc --noEmit"
19370
20928
  },
19371
20929
  dependencies: {
19372
- "@ashlr/stack-core": "^0.3.6",
20930
+ "@ashlr/stack-core": "^0.3.9",
19373
20931
  "@clack/prompts": "^0.8.2",
19374
20932
  citty: "^0.1.6",
19375
20933
  picocolors: "^1.1.1"
@@ -19382,19 +20940,19 @@ var init_package = __esm(() => {
19382
20940
  });
19383
20941
 
19384
20942
  // src/lib/upgrade-install.ts
19385
- import { execFile as execFile5 } from "node:child_process";
19386
- import { lstat as lstat2, mkdir as mkdir7, mkdtemp, readFile as readFile12, realpath, rename as rename2, rm as rm2 } from "node:fs/promises";
19387
- import { dirname as dirname7, join as join26 } from "node:path";
19388
- 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";
19389
20947
  function isReleaseVersion(version) {
19390
20948
  return typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
19391
20949
  }
19392
20950
  async function health(directory, version, runner) {
19393
- const manifest = JSON.parse(await readFile12(join26(directory, "package.json"), "utf8"));
20951
+ const manifest = JSON.parse(await readFile13(join29(directory, "package.json"), "utf8"));
19394
20952
  if (manifest.name !== PACKAGE || manifest.version !== version) {
19395
20953
  throw new Error("Installed package identity does not match the requested release.");
19396
20954
  }
19397
- const entry = join26(directory, "dist/index.js");
20955
+ const entry = join29(directory, "dist/index.js");
19398
20956
  const output = JSON.parse(await runner("node", [entry, "--version", "--json"]));
19399
20957
  if (output.version !== version)
19400
20958
  throw new Error("CLI version health check failed.");
@@ -19411,17 +20969,17 @@ async function installUpgrade(current, latest, runner = run) {
19411
20969
  const root = await runner("npm", ["root", "--global"]);
19412
20970
  if (!root.startsWith("/"))
19413
20971
  throw new Error("npm did not return an absolute global root.");
19414
- const target = join26(root, PACKAGE);
19415
- if (!(await lstat2(target)).isDirectory()) {
20972
+ const target = join29(root, PACKAGE);
20973
+ if (!(await lstat4(target)).isDirectory()) {
19416
20974
  throw new Error("Applying upgrades requires an npm global install, not a linked package.");
19417
20975
  }
19418
20976
  const prefix = await runner("npm", ["prefix", "--global"]);
19419
20977
  if (!prefix.startsWith("/"))
19420
20978
  throw new Error("npm did not return an absolute global prefix.");
19421
- if (await realpath(join26(prefix, "bin/stack")) !== join26(await realpath(target), "dist/index.js")) {
20979
+ if (await realpath2(join29(prefix, "bin/stack")) !== join29(await realpath2(target), "dist/index.js")) {
19422
20980
  throw new Error("The npm stack launcher does not point to this installation.");
19423
20981
  }
19424
- const lock = join26(dirname7(target), ".stack-upgrade-lock");
20982
+ const lock = join29(dirname9(target), ".stack-upgrade-lock");
19425
20983
  try {
19426
20984
  await mkdir7(lock);
19427
20985
  } catch {
@@ -19431,7 +20989,7 @@ async function installUpgrade(current, latest, runner = run) {
19431
20989
  let retainBackup = false;
19432
20990
  try {
19433
20991
  await health(target, current, runner);
19434
- workspace = await mkdtemp(join26(dirname7(target), ".stack-upgrade-"));
20992
+ workspace = await mkdtemp(join29(dirname9(target), ".stack-upgrade-"));
19435
20993
  await runner("npm", [
19436
20994
  "install",
19437
20995
  "--global",
@@ -19442,9 +21000,9 @@ async function installUpgrade(current, latest, runner = run) {
19442
21000
  "--no-fund",
19443
21001
  `${PACKAGE}@${latest}`
19444
21002
  ]);
19445
- const candidate = join26(workspace, "lib/node_modules", PACKAGE);
21003
+ const candidate = join29(workspace, "lib/node_modules", PACKAGE);
19446
21004
  await health(candidate, latest, runner);
19447
- const backup = join26(workspace, "previous");
21005
+ const backup = join29(workspace, "previous");
19448
21006
  await rename2(target, backup);
19449
21007
  try {
19450
21008
  await rename2(candidate, target);
@@ -19480,7 +21038,7 @@ var exec2, PACKAGE = "@ashlr/stack", run = async (command, args) => {
19480
21038
  }
19481
21039
  };
19482
21040
  var init_upgrade_install = __esm(() => {
19483
- exec2 = promisify(execFile5);
21041
+ exec2 = promisify2(execFile6);
19484
21042
  });
19485
21043
 
19486
21044
  // src/commands/upgrade.ts
@@ -19605,6 +21163,8 @@ init_detect();
19605
21163
  init_detect_source();
19606
21164
  init_registry();
19607
21165
  init_agent_onboard();
21166
+ init_capability_matrix();
21167
+ init_job_run();
19608
21168
 
19609
21169
  // ../../node_modules/.bun/citty@0.1.6/node_modules/citty/dist/index.mjs
19610
21170
  init_dist2();
@@ -19855,7 +21415,7 @@ async function checkForUpdate(currentVersion, _cacheFile) {
19855
21415
  }
19856
21416
 
19857
21417
  // src/index.ts
19858
- var VERSION = "0.3.6";
21418
+ var VERSION = "0.3.9";
19859
21419
  var main = defineCommand({
19860
21420
  meta: {
19861
21421
  name: "stack",
@@ -19934,7 +21494,9 @@ var main = defineCommand({
19934
21494
  "fleet-act": () => Promise.resolve().then(() => (init_fleet_act(), {})).then((m) => fleetActCommand),
19935
21495
  "fleet-stream": () => Promise.resolve().then(() => (init_fleet_stream(), {})).then((m) => fleetStreamCommand),
19936
21496
  "fleet-graph": () => Promise.resolve().then(() => (init_fleet_graph(), {})).then((m) => fleetGraphCommand),
21497
+ "fleet-gc": () => Promise.resolve().then(() => (init_fleet_gc2(), {})).then((m) => fleetGcCommand),
19937
21498
  "fleet-bets": () => Promise.resolve().then(() => (init_fleet_bets(), {})).then((m) => fleetBetsCommand),
21499
+ "fleet-match": () => Promise.resolve().then(() => (init_fleet_match(), {})).then((m) => fleetMatchCommand),
19938
21500
  fleet: () => Promise.resolve().then(() => (init_fleet(), {})).then((m) => fleetCommand),
19939
21501
  exec: () => Promise.resolve().then(() => (init_exec(), {})).then((m) => execCommand),
19940
21502
  swap: () => Promise.resolve().then(() => (init_swap2(), {})).then((m) => swapCommand),