@mutmutco/cli 3.48.1 → 3.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +528 -96
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4478,6 +4478,52 @@ async function runSessionStart(parallel, sequential, io) {
4478
4478
  for (const lines of buffered) flush(lines, io);
4479
4479
  for (const step of sequential) flush(await runBufferedStep(step), io);
4480
4480
  }
4481
+ var SESSION_PAYLOAD_CAP_CHARS = 1e4;
4482
+ var SESSION_PAYLOAD_BUDGET_CHARS = 8e3;
4483
+ function sessionPayloadStatus(chars) {
4484
+ if (chars > SESSION_PAYLOAD_CAP_CHARS) return "red";
4485
+ if (chars > SESSION_PAYLOAD_BUDGET_CHARS) return "amber";
4486
+ return "ok";
4487
+ }
4488
+ function measuredSessionIo(io) {
4489
+ let chars = 0;
4490
+ return {
4491
+ io: {
4492
+ log: (text) => {
4493
+ chars += text.length + 1;
4494
+ io.log(text);
4495
+ },
4496
+ err: (text) => io.err(text)
4497
+ },
4498
+ chars: () => chars
4499
+ };
4500
+ }
4501
+ function sessionPayloadStatePath(cwd) {
4502
+ return repoRuntimeStatePath(cwd, "session-payload.json");
4503
+ }
4504
+ function recordSessionPayload(cwd, chars, now = /* @__PURE__ */ new Date()) {
4505
+ try {
4506
+ const path2 = sessionPayloadStatePath(cwd);
4507
+ (0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
4508
+ const record = { chars, status: sessionPayloadStatus(chars), at: now.toISOString() };
4509
+ (0, import_node_fs8.writeFileSync)(path2, `${JSON.stringify(record)}
4510
+ `, "utf8");
4511
+ } catch {
4512
+ }
4513
+ }
4514
+ function readSessionPayload(cwd) {
4515
+ try {
4516
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(sessionPayloadStatePath(cwd), "utf8"));
4517
+ if (typeof parsed?.chars !== "number" || !Number.isFinite(parsed.chars)) return void 0;
4518
+ return {
4519
+ chars: parsed.chars,
4520
+ status: sessionPayloadStatus(parsed.chars),
4521
+ at: typeof parsed.at === "string" ? parsed.at : ""
4522
+ };
4523
+ } catch {
4524
+ return void 0;
4525
+ }
4526
+ }
4481
4527
  function buildSessionStartPlan(verbs) {
4482
4528
  return {
4483
4529
  parallel: [
@@ -5375,7 +5421,33 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
5375
5421
  }
5376
5422
  function siblingMmiWorktreesRoot(repoRoot2) {
5377
5423
  const parent = (0, import_node_path7.dirname)(repoRoot2);
5378
- return (0, import_node_path7.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path7.join)(parent, "mmi-worktrees");
5424
+ if ((0, import_node_path7.basename)(parent).toLowerCase() === "mmi-worktrees") return parent;
5425
+ const grandparent = (0, import_node_path7.dirname)(parent);
5426
+ if ((0, import_node_path7.basename)(grandparent).toLowerCase() === "mmi-worktrees") return grandparent;
5427
+ return (0, import_node_path7.join)(parent, "mmi-worktrees");
5428
+ }
5429
+ function worktreeScanDirs(root, repoRoot2, listDirs, isRepoCheckout) {
5430
+ const projectsDir = (0, import_node_path7.dirname)(root);
5431
+ const ownName = (0, import_node_path7.basename)(repoRoot2).toLowerCase();
5432
+ const flat = [];
5433
+ let ownContainer = null;
5434
+ for (const dir of listDirs(root)) {
5435
+ const name = (0, import_node_path7.basename)(dir);
5436
+ if (isRepoCheckout((0, import_node_path7.join)(projectsDir, name))) {
5437
+ if (name.toLowerCase() === ownName) ownContainer = dir;
5438
+ continue;
5439
+ }
5440
+ flat.push(dir);
5441
+ }
5442
+ return ownContainer ? [...flat, ...listDirs(ownContainer)] : flat;
5443
+ }
5444
+ function explicitRepoWorktreesRoot(root, repoRoot2, rootDirs) {
5445
+ const repoName = (0, import_node_path7.basename)(repoRoot2);
5446
+ const repoDir = rootDirs.find((name) => name.toLowerCase() === repoName.toLowerCase());
5447
+ return repoDir ? (0, import_node_path7.join)(root, repoDir) : root;
5448
+ }
5449
+ function strayWorktreeRootPaths(container, names, authoritativeRoot) {
5450
+ return names.filter((name) => /worktrees/i.test(name)).map((name) => (0, import_node_path7.join)(container, name)).filter((path2) => !samePath(path2, authoritativeRoot));
5379
5451
  }
5380
5452
  function classifySiblingWorktreeDir(entry) {
5381
5453
  if (!entry.ownedByCurrentRepo) {
@@ -6235,6 +6307,7 @@ var import_node_child_process5 = require("node:child_process");
6235
6307
  var OWNER = "mutmutco";
6236
6308
  var SSM_ROOT = "/mmi-future";
6237
6309
  var PROJECT_TIER_SEGMENT = "dev";
6310
+ var ORG_INFRA_SLUG = "_org";
6238
6311
  var KEY_RE = /^(?:[a-z][a-z0-9-]*\/)?[A-Za-z][A-Za-z0-9_]*$/;
6239
6312
  function isValidSecretKey(key) {
6240
6313
  if (!key || key.length > 256) return false;
@@ -6445,7 +6518,7 @@ no vault credentials visible`;
6445
6518
  ...lines,
6446
6519
  "",
6447
6520
  "+ = usable now. Consume keyless: `mmi-cli secrets use <KEY> -- <cmd>` (inject, never printed); validate: `mmi-cli secrets verify <KEY>`. No command prints a raw value (#2844).",
6448
- " Cross-slug (org-infra): `mmi-cli secrets use GLOBAL_API_KEY --slug cloudflare -- <cmd>` consumes it keyless (master-gated)."
6521
+ ` Org-infra (#3463): \`--slug\` is ALWAYS \`${ORG_INFRA_SLUG}\`; the provider is the key's FIRST SEGMENT \u2014 \`mmi-cli secrets use cloudflare/GLOBAL_API_KEY --slug ${ORG_INFRA_SLUG} -- <cmd>\` (master-gated).`
6449
6522
  ].join("\n");
6450
6523
  }
6451
6524
  async function secretsCapabilities(deps, opts) {
@@ -6487,6 +6560,81 @@ async function secretsCapabilities(deps, opts) {
6487
6560
  }
6488
6561
  deps.log(formatCapabilities(report));
6489
6562
  }
6563
+ var PROBE_TIMEOUT_MS = 4e3;
6564
+ async function probeCapabilities(deps, repo) {
6565
+ try {
6566
+ const qs = new URLSearchParams({ repo }).toString();
6567
+ const res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
6568
+ method: "GET",
6569
+ headers: await deps.headers(),
6570
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
6571
+ });
6572
+ if (!res.ok) return void 0;
6573
+ return await res.json();
6574
+ } catch {
6575
+ return void 0;
6576
+ }
6577
+ }
6578
+ function secretKeyLeaf(key) {
6579
+ const slash = key.lastIndexOf("/");
6580
+ return slash === -1 ? key : key.slice(slash + 1);
6581
+ }
6582
+ function resolveNotFoundGuidance(input) {
6583
+ const { key, repo, slug, report } = input;
6584
+ const tried = secretParamName(slug ?? "<repo-slug>", key);
6585
+ const isMaster = report?.role === "master";
6586
+ const mnemonic = `Org-infra shape: \`--slug\` is ALWAYS \`${ORG_INFRA_SLUG}\`; the provider is the key's FIRST SEGMENT (e.g. \`cloudflare/API_TOKEN --slug ${ORG_INFRA_SLUG}\`).`;
6587
+ const escalate = (target) => target && target.slug === ORG_INFRA_SLUG ? `Approved escalation: mmi-cli secrets request ${key} --repo ${repo} --reason "org-infra grant for ${target.scope} (key ${target.key}, slug ${ORG_INFRA_SLUG}) \u2014 <why it is needed>"` : `Approved escalation: mmi-cli secrets request ${key} --repo ${repo} --reason "<why it is needed>"`;
6588
+ const leaf = secretKeyLeaf(key);
6589
+ const sameLeaf = (report?.capabilities ?? []).filter(
6590
+ (c) => secretKeyLeaf(c.key) === leaf && !(c.slug === slug && c.key === key)
6591
+ );
6592
+ const exact = sameLeaf.filter((c) => slug ? c.key === `${slug}/${leaf}` : false);
6593
+ const unambiguous = exact.length === 1 ? exact : sameLeaf.length === 1 ? sameLeaf : [];
6594
+ const usable = unambiguous.find((c) => c.accessible);
6595
+ if (usable) {
6596
+ return {
6597
+ reachable: true,
6598
+ lines: [
6599
+ `Secret ${key} was not found at ${tried}.`,
6600
+ `It lives at ${usable.scope} and you can already consume it \u2014 no request needed:`,
6601
+ ` mmi-cli secrets use ${usable.key} --slug ${usable.slug} -- <cmd>`,
6602
+ mnemonic
6603
+ ]
6604
+ };
6605
+ }
6606
+ if (!unambiguous.length && sameLeaf.length > 1) {
6607
+ const lines2 = [
6608
+ `Secret ${key} was not found at ${tried}.`,
6609
+ `${sameLeaf.length} different keys are named ${leaf} \u2014 pick the one you meant, this command cannot:`,
6610
+ ...sameLeaf.map((c) => ` ${c.accessible ? "+" : " "} mmi-cli secrets use ${c.key} --slug ${c.slug} -- <cmd> (${c.scope})`),
6611
+ mnemonic
6612
+ ];
6613
+ if (!isMaster) lines2.push(escalate(sameLeaf.find((c) => !c.accessible)));
6614
+ return { reachable: false, lines: lines2 };
6615
+ }
6616
+ if (unambiguous.length) {
6617
+ const other = unambiguous[0];
6618
+ const lines2 = [
6619
+ `Secret ${key} was not found at ${tried}.`,
6620
+ `A key named ${leaf} exists at ${other.scope}, but it is not granted to you (this is an ACCESS gap, not a missing key).`
6621
+ ];
6622
+ if (isMaster) lines2.push("You are master \u2014 grant yourself directly: `mmi-cli secrets grant <repo> <login> <provider>/<KEY>`.");
6623
+ else lines2.push(escalate(other));
6624
+ return { reachable: false, lines: lines2 };
6625
+ }
6626
+ const lines = [
6627
+ `Secret ${key} was not found at ${tried}, and no key named ${leaf} is visible to you.`,
6628
+ `Locate it by intent first: \`mmi-cli secrets find "<what it is for>"\` \u2014 it returns the exact keyless-use command.`,
6629
+ mnemonic
6630
+ ];
6631
+ if (isMaster) {
6632
+ lines.push("You are master: if it truly does not exist yet, create it with `mmi-cli secrets set` rather than requesting a grant from yourself.");
6633
+ } else {
6634
+ lines.push(escalate());
6635
+ }
6636
+ return { reachable: false, lines };
6637
+ }
6490
6638
  async function fetchCatalog(deps, label, repo, extra) {
6491
6639
  const qs = new URLSearchParams({ repo, ...extra }).toString();
6492
6640
  let res;
@@ -6754,11 +6902,17 @@ async function secretsVerify(deps, key, opts) {
6754
6902
  deps.err(`invalid secret key ${JSON.stringify(key)}`);
6755
6903
  return false;
6756
6904
  }
6757
- if (!providerForSecretKey(key)) {
6758
- deps.err(`no provider verifier configured for ${secretLeafName(key)}`);
6759
- return false;
6760
- }
6905
+ const hasProviderVerifier = Boolean(providerForSecretKey(key));
6761
6906
  const value = await fetchSecretValue(deps, key, opts);
6907
+ if (!hasProviderVerifier) {
6908
+ if (!value) {
6909
+ deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
6910
+ deps.err(`Locate a key across every vault you can reach: mmi-cli org access capabilities${opts.repo ? ` --repo ${opts.repo}` : ""}`);
6911
+ return false;
6912
+ }
6913
+ deps.log(`${key}: PRESENT (${value.length} chars) \u2014 reachability only; no provider verifier is configured for ${secretLeafName(key)}, so the credential itself was not validated`);
6914
+ return true;
6915
+ }
6762
6916
  if (!value) {
6763
6917
  deps.err(`secrets verify: could not read ${key}; no value was printed`);
6764
6918
  return false;
@@ -6777,6 +6931,16 @@ async function secretsRequest(deps, key, opts) {
6777
6931
  return false;
6778
6932
  }
6779
6933
  const repo = await targetRepo(deps, opts);
6934
+ const requester = await probeCapabilities(deps, repo);
6935
+ if (requester?.role === "master") {
6936
+ deps.err(`secrets request: you are master on ${repo} \u2014 a request would file an issue and notify yourself.`);
6937
+ const candidates = (requester.capabilities ?? []).filter((c) => secretKeyLeaf(c.key) === secretKeyLeaf(key) && c.accessible);
6938
+ const reachable = candidates.length === 1 ? candidates[0] : void 0;
6939
+ if (reachable) deps.err(`${key} is already reachable: \`mmi-cli secrets use ${reachable.key} --slug ${reachable.slug} -- <cmd>\``);
6940
+ else if (candidates.length > 1) deps.err(`${candidates.length} keys are named ${secretKeyLeaf(key)} \u2014 pick one with \`mmi-cli secrets find "<what it is for>"\`.`);
6941
+ else deps.err(`Grant directly with \`mmi-cli secrets grant <repo> <login> <provider>/<KEY>\`, or create it with \`mmi-cli secrets set\` if it does not exist yet.`);
6942
+ return false;
6943
+ }
6780
6944
  const res = await deps.fetch(`${deps.apiUrl}/secrets/request`, {
6781
6945
  method: "POST",
6782
6946
  headers: await deps.headers({ "content-type": "application/json" }),
@@ -6903,6 +7067,11 @@ async function secretsDeclare(deps, key, opts) {
6903
7067
  const body = await readJsonBody(res);
6904
7068
  if (!res.ok) {
6905
7069
  deps.err(await upgradeMessage(res, body) ?? `secrets declare failed: HTTP ${res.status}${errorDetail(body)}`);
7070
+ if (res.status === 404 && !errorDetail(body)) {
7071
+ deps.err(
7072
+ `The Hub did not recognize the declare route (no error body \u2014 an API-gateway 404, not an authority refusal). Fall back to the master-side catalog write: mmi-cli org project set ${repo} --secrets-file <catalog.json> (shape: keyed by env name; each entry {key,purpose,group,owner,stages[],consumers[]} \u2014 see docs/Guides/oauth-provision.md \xA7 Cred catalog shape).`
7073
+ );
7074
+ }
6906
7075
  return false;
6907
7076
  }
6908
7077
  if (opts.json) {
@@ -7115,7 +7284,12 @@ async function secretsUse(deps, key, opts) {
7115
7284
  " \u2022 Runtime: the central deploy injects the declared stage env; broker consumers use the workload's scoped runtime token. Never bake a value into an image or commit it.",
7116
7285
  ` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
7117
7286
  " \u2022 Own project: the stageless canonical and any declared dev/rc/main overrides are project-admin self-service.",
7118
- " \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted."
7287
+ " \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted.",
7288
+ "",
7289
+ "Answer it without consuming it (#3436):",
7290
+ " \u2022 WHERE does a key live, across every vault you can reach: `mmi-cli org access capabilities` (names + tier + scope, never values). `secrets list` is this repo only and cannot see _org.",
7291
+ ` \u2022 DOES it exist here: \`mmi-cli secrets verify ${key}${opts.slug ? ` --slug ${opts.slug}` : ""}\` \u2014 PRESENT/ABSENT even with no provider verifier configured.`,
7292
+ " \u2022 PowerShell note: `mmi-cli` resolves to npm's mmi-cli.ps1, whose binder eats the `--` separator. The wrapped command works either way; if a flag still misbinds, call `mmi-cli.cmd` explicitly."
7119
7293
  ].join("\n")
7120
7294
  );
7121
7295
  return;
@@ -7135,8 +7309,8 @@ async function secretsUse(deps, key, opts) {
7135
7309
  if (!res.ok) {
7136
7310
  const body = await readJsonBody(res);
7137
7311
  if (res.status === 404 && body.code === "secret_not_found") {
7138
- deps.err(`Secret ${key} was not found.`);
7139
- deps.err(`Approved escalation: mmi-cli secrets request ${key} --repo ${repo} --reason "<why it is needed>"`);
7312
+ const guidance = resolveNotFoundGuidance({ key, repo, slug, report: await probeCapabilities(deps, repo) });
7313
+ for (const line of guidance.lines) deps.err(line);
7140
7314
  return false;
7141
7315
  }
7142
7316
  deps.err(
@@ -7361,18 +7535,37 @@ async function preservedBranches() {
7361
7535
  return [];
7362
7536
  }
7363
7537
  }
7364
- async function siblingWorktreeDirs() {
7538
+ async function siblingWorktreeDirs(explicitRoot) {
7365
7539
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
7366
7540
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
7367
- const siblingRoot = siblingMmiWorktreesRoot(repoRoot2);
7541
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path8.dirname)((0, import_node_path8.dirname)(worktreeGitRoot)) : repoRoot2;
7368
7542
  try {
7369
- const entries = (0, import_node_fs10.readdirSync)(siblingRoot, { withFileTypes: true });
7370
- return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path8.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
7543
+ const dirs = explicitRoot ? listDirsIn(resolveExplicitScanRoot(explicitRoot, primaryRepoRoot)) : worktreeScanDirs(siblingMmiWorktreesRoot(primaryRepoRoot), primaryRepoRoot, listDirsIn, isRepoCheckoutDir);
7544
+ return dirs.map((dir) => inspectSiblingWorktreeDir(dir, worktreeGitRoot)).filter((entry) => Boolean(entry));
7371
7545
  } catch {
7372
7546
  return [];
7373
7547
  }
7374
7548
  }
7375
- async function gcPlan(remote, limit) {
7549
+ function listDirsIn(dir) {
7550
+ try {
7551
+ return (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0, import_node_path8.join)(dir, ent.name));
7552
+ } catch {
7553
+ return [];
7554
+ }
7555
+ }
7556
+ function isRepoCheckoutDir(dir) {
7557
+ return (0, import_node_fs10.existsSync)((0, import_node_path8.join)(dir, ".git"));
7558
+ }
7559
+ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
7560
+ let rootDirs;
7561
+ try {
7562
+ rootDirs = (0, import_node_fs10.readdirSync)(explicitRoot, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => ent.name);
7563
+ } catch {
7564
+ return explicitRoot;
7565
+ }
7566
+ return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
7567
+ }
7568
+ async function gcPlan(remote, limit, opts = {}) {
7376
7569
  const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved] = await Promise.all([
7377
7570
  gitOut(["branch", "--format=%(refname:short)"]),
7378
7571
  localBranchHeads(),
@@ -7382,7 +7575,7 @@ async function gcPlan(remote, limit) {
7382
7575
  }),
7383
7576
  ghPrs(limit),
7384
7577
  worktreeBranches(),
7385
- siblingWorktreeDirs(),
7578
+ siblingWorktreeDirs(opts.root),
7386
7579
  preservedBranches()
7387
7580
  ]);
7388
7581
  return buildGcPlan({
@@ -7397,6 +7590,62 @@ async function gcPlan(remote, limit) {
7397
7590
  preservedBranches: preserved
7398
7591
  });
7399
7592
  }
7593
+ var STRAY_ROOT_WALK_BUDGET_MS = 4e3;
7594
+ function measureWorktreeRoot(root, deadline) {
7595
+ let dirs = 0;
7596
+ let bytes = 0;
7597
+ let partial = false;
7598
+ const stack = [root];
7599
+ let depth0 = true;
7600
+ while (stack.length) {
7601
+ if (Date.now() > deadline) {
7602
+ partial = true;
7603
+ break;
7604
+ }
7605
+ const current = stack.pop();
7606
+ let entries;
7607
+ try {
7608
+ entries = (0, import_node_fs10.readdirSync)(current, { withFileTypes: true });
7609
+ } catch {
7610
+ continue;
7611
+ }
7612
+ for (const ent of entries) {
7613
+ const child2 = (0, import_node_path8.join)(current, ent.name);
7614
+ if (ent.isDirectory()) {
7615
+ if (depth0) dirs++;
7616
+ let isLink = false;
7617
+ try {
7618
+ (0, import_node_fs10.readlinkSync)(child2);
7619
+ isLink = true;
7620
+ } catch {
7621
+ }
7622
+ if (!isLink) stack.push(child2);
7623
+ } else if (ent.isFile()) {
7624
+ try {
7625
+ bytes += (0, import_node_fs10.statSync)(child2).size;
7626
+ } catch {
7627
+ }
7628
+ }
7629
+ }
7630
+ depth0 = false;
7631
+ }
7632
+ return partial ? { dirs, bytes, partial } : { dirs, bytes };
7633
+ }
7634
+ function worktreeRootsProbe(repoRoot2) {
7635
+ const authoritative = siblingMmiWorktreesRoot(repoRoot2);
7636
+ const container = (0, import_node_path8.dirname)(authoritative);
7637
+ let names;
7638
+ try {
7639
+ names = (0, import_node_fs10.readdirSync)(container, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => ent.name);
7640
+ } catch {
7641
+ return { authoritative, stray: [] };
7642
+ }
7643
+ const deadline = Date.now() + STRAY_ROOT_WALK_BUDGET_MS;
7644
+ return {
7645
+ authoritative,
7646
+ stray: strayWorktreeRootPaths(container, names, authoritative).map((path2) => ({ path: path2, ...measureWorktreeRoot(path2, deadline) }))
7647
+ };
7648
+ }
7400
7649
 
7401
7650
  // src/repo-resolve.ts
7402
7651
  function slugOf(repoOrSlug) {
@@ -8873,7 +9122,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8873
9122
  }
8874
9123
  function defaultWorktreePath(repoRoot2, branch) {
8875
9124
  const safe = branch.replace(/[/\\]+/g, "-");
8876
- return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", safe);
9125
+ return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path11.basename)(repoRoot2), safe);
8877
9126
  }
8878
9127
  async function primaryCheckoutRootOf(git) {
8879
9128
  try {
@@ -8984,7 +9233,7 @@ function commandLadderHint() {
8984
9233
  }
8985
9234
 
8986
9235
  // src/index.ts
8987
- var import_node_path26 = require("node:path");
9236
+ var import_node_path27 = require("node:path");
8988
9237
 
8989
9238
  // src/merge-ci-policy.ts
8990
9239
  function resolveMergeCiPolicy(input) {
@@ -9179,8 +9428,11 @@ async function waitForPrChecks(deps) {
9179
9428
  failureStreak += 1;
9180
9429
  if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
9181
9430
  const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
9182
- if (diagnosis?.cause === "ci-budget-kill") {
9183
- return { policy, status: "failure", reason: diagnosis.reason, detail: "ci-budget-kill" };
9431
+ if (diagnosis?.cause === "stale-head") {
9432
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head" };
9433
+ }
9434
+ if (diagnosis?.cause === "runner-infra") {
9435
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra" };
9184
9436
  }
9185
9437
  return { policy, status: "failure", reason, detail: "checks-failure" };
9186
9438
  }
@@ -9434,7 +9686,7 @@ var MANAGED_GITIGNORE_LINES = [
9434
9686
  // and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
9435
9687
  ".codex/",
9436
9688
  ".agents/",
9437
- // #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<branch>` (outside the tree,
9689
+ // #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
9438
9690
  // via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
9439
9691
  // doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
9440
9692
  // into every repo's .gitignore.
@@ -11774,6 +12026,10 @@ var PATH_OVERRIDES = {
11774
12026
  "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
11775
12027
  "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
11776
12028
  };
12029
+ var DEFAULT_ADMIN_MARKER = "(master-only)";
12030
+ var ADMIN_MARKERS = {
12031
+ "secrets org-catalog": "(writes master-only)"
12032
+ };
11777
12033
  var HIDDEN_OPTIONS = {
11778
12034
  "worktree create": ["--base"],
11779
12035
  "org project set": ["--set"]
@@ -11808,27 +12064,42 @@ function setCommandMetadata(command, metadata) {
11808
12064
  function commandMetadata(command) {
11809
12065
  return command[COMMAND_METADATA];
11810
12066
  }
11811
- function classifyTree(command, path2, inherited) {
11812
- const metadata = PATH_OVERRIDES[path2] ?? inherited;
12067
+ var REQUIRED_OPTION_HELP = {
12068
+ optionDescription(option) {
12069
+ const described = Help.prototype.optionDescription.call(this, option);
12070
+ return option.mandatory ? `(required) ${described}` : described;
12071
+ }
12072
+ };
12073
+ function classifyTree(command, path2, inherited, hideFromParent) {
12074
+ const override = PATH_OVERRIDES[path2];
12075
+ const metadata = override ?? inherited;
11813
12076
  setCommandMetadata(command, metadata);
11814
- if (metadata.discovery !== "primary") {
12077
+ const hideByOverride = override !== void 0 && metadata.discovery === "hidden";
12078
+ if ((hideByOverride || hideFromParent) && metadata.discovery !== "primary") {
11815
12079
  command._hidden = true;
11816
12080
  }
12081
+ if (override !== void 0 && metadata.discovery === "all-only" && metadata.category === "admin" && !command.summary()) {
12082
+ const marker = ADMIN_MARKERS[path2] ?? DEFAULT_ADMIN_MARKER;
12083
+ const described = command.description();
12084
+ command.summary(described ? `${marker} ${described}` : marker);
12085
+ }
11817
12086
  for (const option of command.options) {
11818
12087
  if ((HIDDEN_OPTIONS[path2] ?? []).includes(option.long ?? option.short ?? "")) option.hideHelp();
11819
12088
  }
12089
+ command.configureHelp(REQUIRED_OPTION_HELP);
11820
12090
  for (const child2 of command.commands) {
11821
12091
  const childPath = path2 ? `${path2} ${child2.name()}` : child2.name();
11822
- classifyTree(child2, childPath, PATH_OVERRIDES[childPath] ?? metadata);
12092
+ classifyTree(child2, childPath, metadata, false);
11823
12093
  }
11824
12094
  }
11825
12095
  function applyCommandTaxonomy(program3) {
11826
12096
  for (const command of program3.commands) {
11827
12097
  const metadata = topLevelMetadata(command.name());
11828
12098
  command.helpGroup(metadata.help_group);
11829
- classifyTree(command, command.name(), metadata);
12099
+ classifyTree(command, command.name(), metadata, true);
11830
12100
  }
11831
12101
  program3.configureHelp({
12102
+ ...REQUIRED_OPTION_HELP,
11832
12103
  visibleCommands(command) {
11833
12104
  const visible = command.commands.filter((child2) => !child2._hidden);
11834
12105
  const helpCommand = command._getHelpCommand();
@@ -16872,7 +17143,7 @@ async function withSecrets(run) {
16872
17143
  function registerSecretsCommands(program3) {
16873
17144
  const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
16874
17145
  secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
16875
- secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
17146
+ secrets.command("list").description("list secret NAMES + tier for THIS repo (never values). To locate a key you cannot place \u2014 including org-infra (_org) \u2014 use the cross-slug view: `mmi-cli org access capabilities` (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
16876
17147
  secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16877
17148
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16878
17149
  }));
@@ -16995,7 +17266,7 @@ function registerSecretsCommands(program3) {
16995
17266
  });
16996
17267
  if (!ok) process.exitCode = 1;
16997
17268
  }));
16998
- secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
17269
+ secrets.command("verify <key>").description("validate a known provider secret without printing its value; a key with no configured provider verifier falls back to a value-free PRESENT/ABSENT reachability check (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").action((key, o) => withSecrets(async (d) => {
16999
17270
  const ok = await secretsVerify(d, key, o);
17000
17271
  if (!ok) process.exitCode = 1;
17001
17272
  }));
@@ -17045,7 +17316,7 @@ function registerSecretsCommands(program3) {
17045
17316
  if (!ok) process.exitCode = 1;
17046
17317
  }));
17047
17318
  secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
17048
- secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
17319
+ secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it. The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
17049
17320
  const ok = await secretsUse(d, key, { ...o, command });
17050
17321
  if (ok === false) process.exitCode = 1;
17051
17322
  }));
@@ -17459,9 +17730,9 @@ function extractWorkflowCrons(yamlText) {
17459
17730
  function workflowEntry(repo, workflowPath, yamlText) {
17460
17731
  const crons = extractWorkflowCrons(yamlText);
17461
17732
  if (!crons.length) return null;
17462
- const basename3 = workflowPath.split("/").pop() ?? workflowPath;
17733
+ const basename4 = workflowPath.split("/").pop() ?? workflowPath;
17463
17734
  return {
17464
- name: `${repo}/${basename3.replace(/\.ya?ml$/, "")}`,
17735
+ name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
17465
17736
  cadence: crons.join(" + "),
17466
17737
  executor: "github-actions",
17467
17738
  llm: llmFromHeader(yamlText),
@@ -17493,16 +17764,30 @@ function awsRuleEntries(payload) {
17493
17764
  }
17494
17765
  return entries;
17495
17766
  }
17767
+ function lambdaTargetFromArn(arn) {
17768
+ const marker = ":function:";
17769
+ const i = arn.indexOf(marker);
17770
+ if (i < 0) return { functionName: arn.split(":").pop() ?? "" };
17771
+ const rest = arn.slice(i + marker.length);
17772
+ const q = rest.indexOf(":");
17773
+ if (q < 0) return { functionName: rest };
17774
+ return { functionName: rest.slice(0, q), qualifier: rest.slice(q + 1) };
17775
+ }
17496
17776
  function awsScheduleEntry(payload) {
17497
17777
  if (!payload || typeof payload !== "object") return null;
17498
17778
  const { Name, ScheduleExpression, State, Target, Description } = payload;
17499
17779
  if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
17500
17780
  if (State !== "ENABLED" || isJervResource(Name)) return null;
17501
17781
  let target = "";
17782
+ let qualifier;
17502
17783
  let scheduleId;
17503
17784
  if (Target && typeof Target === "object") {
17504
17785
  const { Arn, Input } = Target;
17505
- if (typeof Arn === "string") target = Arn.split(":").pop() ?? "";
17786
+ if (typeof Arn === "string") {
17787
+ const parsed = lambdaTargetFromArn(Arn);
17788
+ target = parsed.functionName;
17789
+ qualifier = parsed.qualifier;
17790
+ }
17506
17791
  if (typeof Input === "string") {
17507
17792
  try {
17508
17793
  const parsed = JSON.parse(Input);
@@ -17511,13 +17796,15 @@ function awsScheduleEntry(payload) {
17511
17796
  }
17512
17797
  }
17513
17798
  }
17799
+ const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
17800
+ const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
17514
17801
  return {
17515
17802
  name: Name,
17516
17803
  cadence: ScheduleExpression,
17517
17804
  executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
17518
17805
  llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
17519
17806
  resolved: "live",
17520
- source: `aws scheduler schedule ${Name} (eu-central-1)`,
17807
+ source,
17521
17808
  ...scheduleId ? { scheduleId } : {}
17522
17809
  };
17523
17810
  }
@@ -17673,7 +17960,7 @@ function strayCronDrifts(workflows) {
17673
17960
  function unlauncheredLlmDrift(entry) {
17674
17961
  if (entry.llm !== "yes") return null;
17675
17962
  if (isHarbourLlmLauncher(entry.executor)) return null;
17676
- if (entry.executor.startsWith("aws-scheduler") && entry.executor.includes("mmi-hub-schedule-dispatcher")) return null;
17963
+ if (entry.executor === "aws-scheduler \u2192 mmi-hub-schedule-dispatcher") return null;
17677
17964
  const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
17678
17965
  return {
17679
17966
  class: "unlaunchered-llm",
@@ -17766,8 +18053,8 @@ async function repoWorkflowEntries(client, repo) {
17766
18053
  );
17767
18054
  if (typeof contents?.content !== "string" || contents.encoding !== "base64") continue;
17768
18055
  const text = Buffer.from(contents.content, "base64").toString("utf8");
17769
- const basename3 = wf.path.split("/").pop() ?? wf.path;
17770
- const name = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
18056
+ const basename4 = wf.path.split("/").pop() ?? wf.path;
18057
+ const name = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
17771
18058
  workflows.push({ name, yamlText: text });
17772
18059
  const entry = workflowEntry(repo, wf.path, text);
17773
18060
  if (entry) entries.push(entry);
@@ -17990,8 +18277,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
17990
18277
  const crons = extractWorkflowCrons(yamlText);
17991
18278
  const header = parseScheduleHeader(yamlText);
17992
18279
  if (!crons.length && !header.schedule) return null;
17993
- const basename3 = workflowPath.split("/").pop() ?? workflowPath;
17994
- const expectedId = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
18280
+ const basename4 = workflowPath.split("/").pop() ?? workflowPath;
18281
+ const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
17995
18282
  const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
17996
18283
  if (missing.length) {
17997
18284
  throw new Error(`schedules lift: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
@@ -18179,7 +18466,9 @@ function planInfraTunnel(hostname, upstream) {
18179
18466
  ].join("\n");
18180
18467
  const steps = [
18181
18468
  "Install cloudflared on the box (or run locally for dev-only tunnels).",
18182
- "Fetch CF account creds (master-gated): mmi-cli secrets use --slug cloudflare API_TOKEN -- <cmd>",
18469
+ // #3463: `--slug <provider> <KEY>` is the retired shape and resolves to nothing. Org-infra is one
18470
+ // slug (`_org`) with the provider as the key's first segment.
18471
+ "Fetch CF account creds (master-gated): mmi-cli secrets use cloudflare/API_TOKEN --slug _org -- <cmd>",
18183
18472
  "Create tunnel: cloudflared tunnel create " + tunnelName,
18184
18473
  "Route DNS: cloudflared tunnel route dns " + tunnelName + " " + host,
18185
18474
  "Write config (below) to /etc/cloudflared/config.yml and run: cloudflared tunnel run " + tunnelName,
@@ -18204,7 +18493,7 @@ function formatInfraTunnelPlan(plan) {
18204
18493
  function registerEdgeCommands(program3) {
18205
18494
  const edge = program3.command("edge").description("infra-service edge helpers (non-tenant loopback services)");
18206
18495
  const tunnel = edge.command("tunnel").description("Cloudflare Tunnel planning for loopback infra services");
18207
- tunnel.command("plan").description("print a cloudflared tunnel plan (no API calls \u2014 use with secrets use --slug cloudflare)").requiredOption("--hostname <host>", "public hostname (e.g. open-meteo.tools.mutatismutandis.co)").requiredOption("--upstream <url>", "loopback origin (e.g. http://127.0.0.1:8080)").option("--json", "machine-readable output").action((o) => {
18496
+ tunnel.command("plan").description("print a cloudflared tunnel plan (no API calls \u2014 pair with `secrets use cloudflare/API_TOKEN --slug _org`)").requiredOption("--hostname <host>", "public hostname (e.g. open-meteo.tools.mutatismutandis.co)").requiredOption("--upstream <url>", "loopback origin (e.g. http://127.0.0.1:8080)").option("--json", "machine-readable output").action((o) => {
18208
18497
  try {
18209
18498
  const plan = planInfraTunnel(o.hostname, o.upstream);
18210
18499
  if (o.json) {
@@ -20101,6 +20390,7 @@ function registerBoardCommands(program3) {
20101
20390
  // src/merge-cleanup.ts
20102
20391
  var import_node_fs24 = require("node:fs");
20103
20392
  var import_promises5 = require("node:fs/promises");
20393
+ var import_node_path22 = require("node:path");
20104
20394
  var import_node_child_process11 = require("node:child_process");
20105
20395
 
20106
20396
  // src/board-advance.ts
@@ -20410,7 +20700,7 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
20410
20700
  if (verdict.blocked) throw new Error(verdict.reason);
20411
20701
  return housekeeping;
20412
20702
  }
20413
- async function applyGcPlan(plan, remote) {
20703
+ async function applyGcPlan(plan, remote, opts = {}) {
20414
20704
  const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], failed: [], pruned: false };
20415
20705
  const beforeWorktrees = parseWorktreePorcelain(
20416
20706
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
@@ -20444,8 +20734,9 @@ async function applyGcPlan(plan, remote) {
20444
20734
  result.failed.push(...branchTracking.failed);
20445
20735
  const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
20446
20736
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20447
- const siblingRoot = siblingMmiWorktreesRoot(repoRoot2);
20448
20737
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
20738
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path22.dirname)((0, import_node_path22.dirname)(worktreeGitRoot)) : repoRoot2;
20739
+ const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
20449
20740
  for (const wt of plan.worktreeDirs) {
20450
20741
  try {
20451
20742
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
@@ -20748,14 +21039,30 @@ function parseRestPrSnapshot(json) {
20748
21039
  const pr2 = json ?? {};
20749
21040
  const headSha = pr2.head?.sha ?? "";
20750
21041
  if (!headSha) throw new Error("pulls endpoint returned no head SHA");
21042
+ const headRepo = pr2.head?.repo?.full_name;
21043
+ const baseRepo = pr2.base?.repo?.full_name;
20751
21044
  return {
20752
21045
  headSha,
21046
+ headRef: pr2.head?.ref ?? "",
21047
+ // Only call it a fork when BOTH names are known and differ — an absent name (private/deleted fork)
21048
+ // must not read as same-repo and send a git-ref probe at a branch the base repo does not have.
21049
+ headIsFork: Boolean(headRepo && baseRepo && headRepo !== baseRepo),
20753
21050
  mergeable: pr2.mergeable === true ? "MERGEABLE" : pr2.mergeable === false ? "CONFLICTING" : "UNKNOWN",
20754
21051
  baseRef: pr2.base?.ref ?? "development",
20755
21052
  merged: pr2.merged === true,
20756
21053
  state: pr2.state ?? ""
20757
21054
  };
20758
21055
  }
21056
+ async function fetchBranchTipSha(repo, ref, gh = defaultGhApi) {
21057
+ if (!ref) return null;
21058
+ try {
21059
+ const parsed = JSON.parse(await gh([`repos/${repo}/git/ref/heads/${ref}`]));
21060
+ const sha = parsed.object?.sha;
21061
+ return typeof sha === "string" && sha ? sha : null;
21062
+ } catch {
21063
+ return null;
21064
+ }
21065
+ }
20759
21066
  async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
20760
21067
  return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
20761
21068
  }
@@ -20790,36 +21097,54 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
20790
21097
  }
20791
21098
  }
20792
21099
  var CI_BUDGET_ANNOTATION_TITLE = "CI budget exceeded";
21100
+ var CI_DISK_ANNOTATION_TITLE = "CI runner disk";
21101
+ var CI_TOOLCHAIN_ANNOTATION_TITLE = "CI runner toolchain";
21102
+ var RUNNER_INFRA_ANNOTATION_TITLES = /* @__PURE__ */ new Set([
21103
+ CI_BUDGET_ANNOTATION_TITLE,
21104
+ CI_DISK_ANNOTATION_TITLE,
21105
+ CI_TOOLCHAIN_ANNOTATION_TITLE
21106
+ ]);
20793
21107
  function isErrorAnnotation(a) {
20794
21108
  const level = a.annotation_level?.toLowerCase();
20795
21109
  return level === "failure" || level === "error";
20796
21110
  }
20797
21111
  function classifyFailedChecks(failing) {
20798
- const budgetKilled = [];
21112
+ const infraFailures = [];
20799
21113
  const otherFailures = [];
20800
21114
  for (const check of failing) {
20801
21115
  const errors = check.annotations.filter(isErrorAnnotation);
20802
- const allBudget = errors.length > 0 && errors.every((a) => a.title?.trim() === CI_BUDGET_ANNOTATION_TITLE);
20803
- (allBudget ? budgetKilled : otherFailures).push(check.name);
21116
+ const allInfra = errors.length > 0 && errors.every((a) => RUNNER_INFRA_ANNOTATION_TITLES.has(a.title?.trim() ?? ""));
21117
+ (allInfra ? infraFailures : otherFailures).push(check.name);
20804
21118
  }
20805
- if (budgetKilled.length && !otherFailures.length) {
21119
+ if (infraFailures.length && !otherFailures.length) {
20806
21120
  return {
20807
- cause: "ci-budget-kill",
20808
- budgetKilled,
21121
+ cause: "runner-infra",
21122
+ infraFailures,
20809
21123
  otherFailures,
20810
- reason: `${budgetKilled.length} check(s) were KILLED for wall-clock, not failed: ${budgetKilled.join(", ")}. No test failed \u2014 this is runner contention (concurrent gates sharing one runner). Rerun once the other gates drain (gh run rerun --failed); do not debug the diff.`
21124
+ reason: `${infraFailures.length} check(s) failed on RUNNER INFRASTRUCTURE, not your diff: ${infraFailures.join(", ")}. No test failed \u2014 the shared runner hit a wall-clock budget kill, ran out of disk, or was missing a toolchain (the check annotation names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
20811
21125
  };
20812
21126
  }
20813
21127
  return {
20814
21128
  cause: "checks-failure",
20815
- budgetKilled,
21129
+ infraFailures,
20816
21130
  otherFailures,
20817
- reason: budgetKilled.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${budgetKilled.join(", ")} was killed for wall-clock, not failed.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
21131
+ reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
20818
21132
  };
20819
21133
  }
20820
21134
  async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
20821
21135
  try {
20822
21136
  const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh);
21137
+ if (!snapshot.headIsFork) {
21138
+ const branchTip = await fetchBranchTipSha(repo, snapshot.headRef, gh);
21139
+ if (branchTip && branchTip !== snapshot.headSha) {
21140
+ return {
21141
+ cause: "stale-head",
21142
+ infraFailures: [],
21143
+ otherFailures: [],
21144
+ reason: `the PR head (${snapshot.headSha.slice(0, 7)}) is BEHIND the branch tip (${branchTip.slice(0, 7)}) \u2014 GitHub likely dropped a \`synchronize\` event, so the reported red is a superseded commit and NO run exists for your current push. This is not your diff failing. Re-push (an empty commit is enough) or close and reopen the PR to re-sync the head and trigger a fresh run.`
21145
+ };
21146
+ }
21147
+ }
20823
21148
  const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
20824
21149
  if (!failing.length) return null;
20825
21150
  const annotated = await Promise.all(failing.map(async (run) => ({
@@ -20844,7 +21169,7 @@ async function fetchRestCorePool(gh = defaultGhApi) {
20844
21169
 
20845
21170
  // src/worktree-lifecycle-commands.ts
20846
21171
  var import_node_fs25 = require("node:fs");
20847
- var import_node_path22 = require("node:path");
21172
+ var import_node_path23 = require("node:path");
20848
21173
  var GH_TIMEOUT_MS = 2e4;
20849
21174
  var DEFAULT_BASE = "origin/development";
20850
21175
  var DEFAULT_REMOTE = "origin";
@@ -20947,7 +21272,7 @@ function classifyStaleLeaks(input) {
20947
21272
  var defaultOrphanDirScanDeps = {
20948
21273
  listDirs: (root) => {
20949
21274
  try {
20950
- return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path22.join)(root, e.name));
21275
+ return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path23.join)(root, e.name));
20951
21276
  } catch {
20952
21277
  return [];
20953
21278
  }
@@ -21003,13 +21328,13 @@ function registerWorktreeCommands(program3) {
21003
21328
  if (PROTECTED_BRANCHES2.has(branch)) {
21004
21329
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
21005
21330
  }
21006
- const gitFile = (0, import_node_path22.join)(wtPath, ".git");
21331
+ const gitFile = (0, import_node_path23.join)(wtPath, ".git");
21007
21332
  const isLinked = (0, import_node_fs25.existsSync)(gitFile) && (0, import_node_fs25.statSync)(gitFile).isFile();
21008
21333
  if (apply && !isLinked) {
21009
21334
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
21010
21335
  }
21011
21336
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
21012
- const primaryCheckout = commonDir ? (0, import_node_path22.dirname)(commonDir) : wtPath;
21337
+ const primaryCheckout = commonDir ? (0, import_node_path23.dirname)(commonDir) : wtPath;
21013
21338
  const stageSummary = readStageSummary(wtPath);
21014
21339
  const hasStage = stageSummary != null;
21015
21340
  const steps = landSteps(branch, true, hasStage);
@@ -21119,10 +21444,15 @@ async function gatherWorktreeContext() {
21119
21444
  const s = readStageSummary(wt.path);
21120
21445
  if (s) stages.push({ path: wt.path, port: s.port });
21121
21446
  }
21122
- const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
21447
+ const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
21448
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path23.dirname)((0, import_node_path23.dirname)(worktreeGitRoot)) : repoRoot2;
21449
+ const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
21123
21450
  let orphanDirs = [];
21124
21451
  if ((0, import_node_fs25.existsSync)(wtRoot)) {
21125
- orphanDirs = scanOrphanDirs(wtRoot, await currentRepoWorktreeGitRoot(repoRoot2));
21452
+ orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
21453
+ ...defaultOrphanDirScanDeps,
21454
+ listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
21455
+ });
21126
21456
  }
21127
21457
  return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
21128
21458
  }
@@ -21711,11 +22041,11 @@ ${lines}`);
21711
22041
 
21712
22042
  // src/train-commands.ts
21713
22043
  var import_node_fs28 = require("node:fs");
21714
- var import_node_path24 = require("node:path");
22044
+ var import_node_path25 = require("node:path");
21715
22045
 
21716
22046
  // src/plugin-guard-io.ts
21717
22047
  var import_node_fs27 = require("node:fs");
21718
- var import_node_path23 = require("node:path");
22048
+ var import_node_path24 = require("node:path");
21719
22049
  var import_node_os6 = require("node:os");
21720
22050
 
21721
22051
  // src/plugin-guard.ts
@@ -21845,7 +22175,7 @@ function runHostBin(bin, args, opts) {
21845
22175
  }
21846
22176
  var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
21847
22177
  const homeDir = surface === "codex" ? ".codex" : ".claude";
21848
- return (0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
22178
+ return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21849
22179
  };
21850
22180
  function readInstalledPlugins(surface = detectSurface(process.env)) {
21851
22181
  try {
@@ -21857,11 +22187,11 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
21857
22187
  function marketplaceCloneCandidates(surface, home) {
21858
22188
  if (surface === "codex") {
21859
22189
  return [
21860
- (0, import_node_path23.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
21861
- (0, import_node_path23.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
22190
+ (0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
22191
+ (0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
21862
22192
  ];
21863
22193
  }
21864
- return [(0, import_node_path23.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
22194
+ return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21865
22195
  }
21866
22196
  function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
21867
22197
  return marketplaceCloneCandidates(surface, home).some(exists);
@@ -21891,7 +22221,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
21891
22221
  isOrgRepo,
21892
22222
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
21893
22223
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
21894
- pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
22224
+ pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21895
22225
  };
21896
22226
  }
21897
22227
  function claudePluginGuardState(isOrgRepo) {
@@ -22026,7 +22356,7 @@ function formatTrainStatus(r) {
22026
22356
  // src/train-commands.ts
22027
22357
  function readRepoVersion() {
22028
22358
  try {
22029
- return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
22359
+ return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
22030
22360
  } catch {
22031
22361
  return void 0;
22032
22362
  }
@@ -23169,6 +23499,28 @@ function checkRepoWorktrees(probe) {
23169
23499
  ]
23170
23500
  };
23171
23501
  }
23502
+ function formatBytes(bytes, partial) {
23503
+ const prefix = partial ? "\u2265" : "";
23504
+ return bytes >= 1e9 ? `${prefix}${(bytes / 1e9).toFixed(2)} GB` : `${prefix}${(bytes / 1e6).toFixed(1)} MB`;
23505
+ }
23506
+ function checkWorktreeRoots(probe) {
23507
+ if (!probe) return null;
23508
+ const evidence = [
23509
+ `scanned by worktree gc/list: ${probe.authoritative}`,
23510
+ ...probe.stray.length ? probe.stray.map((s) => `unreachable by any gc: ${s.path} \u2014 ${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)}`) : ["no other *worktrees* directory beside this repo"]
23511
+ ];
23512
+ if (!probe.stray.length) return { ok: true, label: "worktree roots", verbose: evidence };
23513
+ const named = probe.stray.map((s) => `${s.path} (${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)})`).join("; ");
23514
+ const cutShort = probe.stray.some((s) => s.partial) ? " (a \u2265 figure is a floor \u2014 the size walk hit its time budget, so the real total is larger)" : "";
23515
+ return {
23516
+ ok: false,
23517
+ label: "worktree roots",
23518
+ // The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
23519
+ // and doctor itself — with or without --apply — never touches these directories.
23520
+ fix: `${probe.stray.length} worktrees root(s) outside ${probe.authoritative} \u2014 ${named}; no gc scans them. Review, then sweep deliberately from the owning repo with \`mmi-cli worktree gc --root <path> --apply\` (report-only here: doctor never deletes these)${cutShort}`,
23521
+ verbose: evidence
23522
+ };
23523
+ }
23172
23524
  function pluginHealTrigger(probe) {
23173
23525
  if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
23174
23526
  const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
@@ -23306,6 +23658,45 @@ function checkDocsAudit(probe) {
23306
23658
  }
23307
23659
  return { ok: true, label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
23308
23660
  }
23661
+ function checkSessionPayload(probe) {
23662
+ if (!probe) return null;
23663
+ const { chars } = probe;
23664
+ const evidence = [
23665
+ `last emitted: ${chars} characters${probe.at ? ` (${probe.at})` : ""}`,
23666
+ `budget ${SESSION_PAYLOAD_BUDGET_CHARS}, Claude Code cap ${SESSION_PAYLOAD_CAP_CHARS}`,
23667
+ // Named in the evidence because the units are the whole trap: the cap is documented in characters
23668
+ // while the runtime's overflow message reports bytes, and they diverge on the non-ASCII the doctrine
23669
+ // blocks preserve. A reader comparing this number to a byte figure is comparing two different things.
23670
+ "measured in CHARACTERS (the documented unit of the cap), never bytes"
23671
+ ];
23672
+ const status = sessionPayloadStatus(chars);
23673
+ if (status === "red") {
23674
+ return {
23675
+ ok: false,
23676
+ label: "SessionStart payload",
23677
+ fix: `${chars} characters exceeds the ${SESSION_PAYLOAD_CAP_CHARS}-character additionalContext cap \u2014 Claude Code truncates it to a ~2,000-character preview with no error, so agents are starting on partial doctrine; cut a block from the session-start banner`,
23678
+ verbose: evidence
23679
+ };
23680
+ }
23681
+ if (status === "amber") {
23682
+ return {
23683
+ ok: true,
23684
+ // warn: the banner lane must print this too. Without it the banner's failing-only filter made the
23685
+ // amber tier dead code on the one lane that runs every session — drift past the budget was
23686
+ // invisible until it became a truncation. Still ✓, still never moves the exit code.
23687
+ warn: true,
23688
+ label: "SessionStart payload",
23689
+ detail: `amber \u2014 ${chars} characters, over the ${SESSION_PAYLOAD_BUDGET_CHARS}-character budget and ${SESSION_PAYLOAD_CAP_CHARS - chars} from the hard cap`,
23690
+ verbose: evidence
23691
+ };
23692
+ }
23693
+ return {
23694
+ ok: true,
23695
+ label: "SessionStart payload",
23696
+ detail: `${chars} of ${SESSION_PAYLOAD_BUDGET_CHARS} budgeted characters`,
23697
+ verbose: evidence
23698
+ };
23699
+ }
23309
23700
  function planGitignore(current) {
23310
23701
  const { content, changed } = upsertManagedGitignoreBlock(current);
23311
23702
  return changed ? { ok: false, content } : { ok: true };
@@ -23401,6 +23792,10 @@ async function runDoctorClean(opts, io, deps) {
23401
23792
  checks.push(checkCliVersion(cliInput));
23402
23793
  }
23403
23794
  checks.push(checkPluginCache(deps.pluginCache()));
23795
+ if (deps.sessionPayload) {
23796
+ const payload = checkSessionPayload(deps.sessionPayload());
23797
+ if (payload) checks.push(payload);
23798
+ }
23404
23799
  if (!opts.fast && !opts.banner && !opts.preflight) {
23405
23800
  const probe = await deps.schedulesNotebook().catch((e) => ({
23406
23801
  armed: 0,
@@ -23421,6 +23816,11 @@ async function runDoctorClean(opts, io, deps) {
23421
23816
  const docsAudit2 = checkDocsAudit(probe);
23422
23817
  if (docsAudit2) checks.push(docsAudit2);
23423
23818
  }
23819
+ if (!opts.fast && !opts.banner && !opts.preflight && deps.worktreeRoots) {
23820
+ const probe = await deps.worktreeRoots().catch(() => void 0);
23821
+ const roots = checkWorktreeRoots(probe);
23822
+ if (roots) checks.push(roots);
23823
+ }
23424
23824
  if (isOrgRepo && !opts.fast && !opts.banner) {
23425
23825
  try {
23426
23826
  checks.push(checkTrainSync(await deps.syncTrain()));
@@ -23492,7 +23892,7 @@ async function runDoctorClean(opts, io, deps) {
23492
23892
  return exitCode;
23493
23893
  }
23494
23894
  if (opts.banner) {
23495
- const actionable = checks.filter((c) => !c.ok);
23895
+ const actionable = checks.filter((c) => !c.ok || c.warn);
23496
23896
  for (const c of actionable) {
23497
23897
  io.log(renderReport([c], { restartPending: false }));
23498
23898
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
@@ -23585,7 +23985,7 @@ function ghAccountCaveat(announcedLogin, accounts) {
23585
23985
  // src/doctor-io.ts
23586
23986
  var import_node_fs29 = require("node:fs");
23587
23987
  var import_node_os7 = require("node:os");
23588
- var import_node_path25 = require("node:path");
23988
+ var import_node_path26 = require("node:path");
23589
23989
  var import_node_child_process12 = require("node:child_process");
23590
23990
  var import_node_util8 = require("node:util");
23591
23991
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
@@ -23593,7 +23993,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
23593
23993
  function installedClaudePluginVersion() {
23594
23994
  try {
23595
23995
  const file = JSON.parse(
23596
- (0, import_node_fs29.readFileSync)((0, import_node_path25.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23996
+ (0, import_node_fs29.readFileSync)((0, import_node_path26.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23597
23997
  );
23598
23998
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
23599
23999
  if (versions.length === 0) return void 0;
@@ -23614,7 +24014,7 @@ function worktreeRootSync() {
23614
24014
  }
23615
24015
  var gitignorePath = () => {
23616
24016
  const root = worktreeRootSync();
23617
- return root === null ? null : (0, import_node_path25.join)(root, ".gitignore");
24017
+ return root === null ? null : (0, import_node_path26.join)(root, ".gitignore");
23618
24018
  };
23619
24019
  function readGitignore() {
23620
24020
  const path2 = gitignorePath();
@@ -23653,7 +24053,7 @@ async function repoRoot() {
23653
24053
  }
23654
24054
  function hasRepoLocalWorktrees() {
23655
24055
  const root = worktreeRootSync();
23656
- return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path25.join)(root, ".worktrees"));
24056
+ return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path26.join)(root, ".worktrees"));
23657
24057
  }
23658
24058
 
23659
24059
  // src/index.ts
@@ -23776,7 +24176,14 @@ function mmiDoctorDeps() {
23776
24176
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
23777
24177
  const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
23778
24178
  return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
23779
- }
24179
+ },
24180
+ // #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
24181
+ // fast/banner/preflight), and READ-ONLY by construction — it stats, it never removes. Nothing here is
24182
+ // wired to a reaper, in either mode.
24183
+ worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
24184
+ // #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
24185
+ // A local record read — cheap enough for every lane, including the banner.
24186
+ sessionPayload: () => readSessionPayload(process.cwd())
23780
24187
  };
23781
24188
  }
23782
24189
  async function requireFreshTrainCli(commandName) {
@@ -23901,7 +24308,7 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
23901
24308
  });
23902
24309
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
23903
24310
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
23904
- const path2 = (0, import_node_path26.join)(process.cwd(), ".gitignore");
24311
+ const path2 = (0, import_node_path27.join)(process.cwd(), ".gitignore");
23905
24312
  const current = (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null;
23906
24313
  const plan = planManagedGitignore(current);
23907
24314
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
@@ -24023,12 +24430,12 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
24023
24430
  process.exit(process.exitCode ?? 0);
24024
24431
  });
24025
24432
  });
24026
- gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
24433
+ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) \u2014 descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
24027
24434
  if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
24028
24435
  if (o.scratch) {
24029
24436
  try {
24030
- const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
24031
- const run = executeScratchGc(root, { apply: Boolean(o.apply) });
24437
+ const root2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
24438
+ const run = executeScratchGc(root2, { apply: Boolean(o.apply) });
24032
24439
  if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
24033
24440
  return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply), run.applied));
24034
24441
  } catch (e) {
@@ -24037,8 +24444,19 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
24037
24444
  }
24038
24445
  const limit = Number.parseInt(o.limit, 10);
24039
24446
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
24447
+ let root;
24448
+ if (o.root !== void 0) {
24449
+ root = (0, import_node_path27.resolve)(o.root);
24450
+ if (!(0, import_node_fs30.existsSync)(root) || !(0, import_node_fs30.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
24451
+ const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
24452
+ if (isPathUnderDirectory(gcRepoRoot, root)) {
24453
+ return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
24454
+ }
24455
+ }
24040
24456
  try {
24041
- const plan = await gcPlan(o.remote, limit);
24457
+ const plan = await gcPlan(o.remote, limit, { root });
24458
+ if (root && !o.json) console.log(`worktree gc: scanning the explicitly named root ${root} for worktrees proven to belong to this repo
24459
+ `);
24042
24460
  if (o.apply && !o.json) console.log(formatGcPlan(plan, true));
24043
24461
  let applyResult;
24044
24462
  if (o.apply) {
@@ -24049,10 +24467,10 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
24049
24467
  // git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
24050
24468
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout)
24051
24469
  ).catch(() => void 0);
24052
- applyResult = await applyGcPlan(plan, o.remote);
24470
+ applyResult = await applyGcPlan(plan, o.remote, { root });
24053
24471
  }
24054
24472
  if (o.json) {
24055
- console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan, applyResult }, null, 2));
24473
+ console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, ...root ? { root } : {}, plan, applyResult }, null, 2));
24056
24474
  } else if (!o.apply) {
24057
24475
  console.log(formatGcPlan(plan, false));
24058
24476
  } else {
@@ -24117,7 +24535,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
24117
24535
  };
24118
24536
  };
24119
24537
  try {
24120
- (0, import_node_fs30.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
24538
+ (0, import_node_fs30.mkdirSync)((0, import_node_path27.dirname)(lockPath), { recursive: true });
24121
24539
  return take();
24122
24540
  } catch {
24123
24541
  try {
@@ -24132,7 +24550,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
24132
24550
  }
24133
24551
  var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
24134
24552
  withExamples(mutating(
24135
- worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch> \u2014 authoritative; worktree gc/list scan only this root)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--slug <slug>", "issue-ref form: override the slug derived from the issue title").option("--claim", "issue-ref form: claim the issue's board item after provisioning").option("--for <login>", "with --claim: assign the board item to this login instead of @me"),
24553
+ worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<RepoName>/<branch> \u2014 authoritative; the <RepoName> segment is what makes ownership provable from the path, #3471. gc/list read this root in BOTH the nested and the legacy flat shape)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--slug <slug>", "issue-ref form: override the slug derived from the issue title").option("--claim", "issue-ref form: claim the issue's board item after provisioning").option("--for <login>", "with --claim: assign the board item to this login instead of @me"),
24136
24554
  worktreeCreatePlan
24137
24555
  ).action(async (target, o, cmd) => {
24138
24556
  try {
@@ -24548,7 +24966,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
24548
24966
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
24549
24967
  console.log(JSON.stringify(payload));
24550
24968
  });
24551
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
24969
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|\u2026], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
24552
24970
  const cfg = await loadConfig();
24553
24971
  let target;
24554
24972
  try {
@@ -24735,7 +25153,15 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
24735
25153
  try {
24736
25154
  oc = parseOauthConfig(meta ?? {}, slug);
24737
25155
  } catch (e) {
24738
- return failGraceful(`org oauth plan: ${e.message}`);
25156
+ const message = e.message;
25157
+ if (/^oauth is not configured for /.test(message)) {
25158
+ return failGraceful(
25159
+ `org oauth plan: ${message}. Declare it in the registry META first:
25160
+ mmi-cli org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
25161
+ New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \xA7 New repo.`
25162
+ );
25163
+ }
25164
+ return failGraceful(`org oauth plan: ${message}`);
24739
25165
  }
24740
25166
  const origins = expectedJsOrigins(oc);
24741
25167
  const redirects = expectedRedirectUris(oc);
@@ -25212,11 +25638,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
25212
25638
  }
25213
25639
  });
25214
25640
  async function listCiWorkflowPaths(cwd = process.cwd()) {
25215
- const wfDir = (0, import_node_path26.join)(cwd, ".github", "workflows");
25641
+ const wfDir = (0, import_node_path27.join)(cwd, ".github", "workflows");
25216
25642
  if (!(0, import_node_fs30.existsSync)(wfDir)) return [];
25217
25643
  return (0, import_node_fs30.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
25218
25644
  try {
25219
- return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path26.join)(wfDir, name), "utf8"));
25645
+ return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path27.join)(wfDir, name), "utf8"));
25220
25646
  } catch {
25221
25647
  return true;
25222
25648
  }
@@ -25243,16 +25669,16 @@ function ciAuditDeps() {
25243
25669
  // gate re-seed step is skipped gracefully rather than failing mid-run.
25244
25670
  readSeedFile: (path2) => {
25245
25671
  if (!root) return null;
25246
- const fullPath = (0, import_node_path26.join)(root, path2);
25672
+ const fullPath = (0, import_node_path27.join)(root, path2);
25247
25673
  return (0, import_node_fs30.existsSync)(fullPath) ? (0, import_node_fs30.readFileSync)(fullPath, "utf8") : null;
25248
25674
  }
25249
25675
  };
25250
25676
  }
25251
25677
  function hubRoot() {
25252
- const fromPkg = (0, import_node_path26.join)(__dirname, "..", "..");
25678
+ const fromPkg = (0, import_node_path27.join)(__dirname, "..", "..");
25253
25679
  const marker = "skills/bootstrap/seeds/manifest.json";
25254
- if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(fromPkg, marker))) return fromPkg;
25255
- if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(process.cwd(), marker))) return process.cwd();
25680
+ if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(fromPkg, marker))) return fromPkg;
25681
+ if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(process.cwd(), marker))) return process.cwd();
25256
25682
  return null;
25257
25683
  }
25258
25684
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
@@ -25292,8 +25718,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
25292
25718
  printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
25293
25719
  } else if (result.status === "rate-limited") {
25294
25720
  printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
25295
- } else if (result.detail === "ci-budget-kill") {
25296
- printLine(`pr checks-wait: failure (wall-clock budget kill, NOT a test failure) \u2014 ${result.reason}`);
25721
+ } else if (result.detail === "runner-infra") {
25722
+ printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
25723
+ } else if (result.detail === "stale-head") {
25724
+ printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
25297
25725
  } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
25298
25726
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
25299
25727
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
@@ -25883,7 +26311,7 @@ function directoryBytes(path2) {
25883
26311
  return 0;
25884
26312
  }
25885
26313
  for (const entry of entries) {
25886
- const child2 = (0, import_node_path26.join)(path2, entry.name);
26314
+ const child2 = (0, import_node_path27.join)(path2, entry.name);
25887
26315
  if (entry.isDirectory()) total += directoryBytes(child2);
25888
26316
  else {
25889
26317
  try {
@@ -25913,7 +26341,7 @@ function pluginCacheFsDeps(home, dirBytes) {
25913
26341
  dirBytes,
25914
26342
  listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
25915
26343
  try {
25916
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path26.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
26344
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path27.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
25917
26345
  } catch {
25918
26346
  return { name: d.name, mtimeMs: Date.now() };
25919
26347
  }
@@ -25927,7 +26355,7 @@ function stagingApplyFsGuard(home) {
25927
26355
  return {
25928
26356
  referencedPaths: () => readInstalledPluginRefs(home),
25929
26357
  mtimeMs: (name) => {
25930
- const p = (0, import_node_path26.join)(stagingRoot, name);
26358
+ const p = (0, import_node_path27.join)(stagingRoot, name);
25931
26359
  if (!(0, import_node_fs30.existsSync)(p)) return null;
25932
26360
  try {
25933
26361
  return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
@@ -25963,6 +26391,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
25963
26391
  return;
25964
26392
  }
25965
26393
  spawnDeferredGcSweep();
26394
+ const measured = measuredSessionIo(consoleIo);
26395
+ const bannerIo = measured.io;
25966
26396
  const { parallel, sequential } = buildSessionStartPlan({
25967
26397
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
25968
26398
  // when unknown — a missing gh login must not noise or fail the banner.
@@ -26006,14 +26436,16 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
26006
26436
  await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
26007
26437
  }
26008
26438
  });
26009
- await runSessionStart(parallel, sequential, consoleIo);
26010
- for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
26439
+ await runSessionStart(parallel, sequential, bannerIo);
26440
+ for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
26011
26441
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
26012
26442
  if (worktreeBanner) {
26013
26443
  spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26014
- consoleIo.log(worktreeBanner);
26444
+ bannerIo.log(worktreeBanner);
26015
26445
  }
26016
- appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
26446
+ const payloadChars = measured.chars();
26447
+ recordSessionPayload(process.cwd(), payloadChars);
26448
+ appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed", payloadChars });
26017
26449
  });
26018
26450
  installProcessBackstop();
26019
26451
  consolidateCommandNamespaces(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.48.1",
3
+ "version": "3.50.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",