@mutmutco/cli 3.43.0 → 3.44.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 +518 -156
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3410,7 +3410,7 @@ var program = new Command();
3410
3410
 
3411
3411
  // src/index.ts
3412
3412
  var import_promises7 = require("node:fs/promises");
3413
- var import_node_fs29 = require("node:fs");
3413
+ var import_node_fs30 = require("node:fs");
3414
3414
  var import_node_child_process13 = require("node:child_process");
3415
3415
 
3416
3416
  // src/cli-shared.ts
@@ -4061,6 +4061,12 @@ function commandPath(cmd) {
4061
4061
  }
4062
4062
  return parts.join(" ") || cmd.name();
4063
4063
  }
4064
+ function jsonParity(cmd) {
4065
+ if (!cmd.options.some((o) => o.long === "--json")) {
4066
+ cmd.option("--json", "machine-readable output (default; accepted for parity)");
4067
+ }
4068
+ return cmd;
4069
+ }
4064
4070
  function mutating(cmd, planFn) {
4065
4071
  const marker = cmd;
4066
4072
  if (marker.__mmiMutating) return cmd;
@@ -5604,6 +5610,9 @@ function samePath(a, b) {
5604
5610
  function normPath(p) {
5605
5611
  return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
5606
5612
  }
5613
+ function toNativePath(p) {
5614
+ return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
5615
+ }
5607
5616
  function parseComposeLs(stdout) {
5608
5617
  const text = stdout.trim();
5609
5618
  if (!text) return [];
@@ -8910,9 +8919,10 @@ async function resolveWhoami(deps) {
8910
8919
  if (ghLogin) return { login: ghLogin, source: "github", sessionExpiresAt: session?.expiresAt };
8911
8920
  return { source: "unknown" };
8912
8921
  }
8913
- function whoamiLine(report) {
8922
+ function whoamiLine(report, caveat) {
8914
8923
  if (!report.login) return null;
8915
- return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
8924
+ const line = `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
8925
+ return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
8916
8926
  }
8917
8927
 
8918
8928
  // src/command-ladder-hint.ts
@@ -9143,6 +9153,10 @@ async function waitForPrChecks(deps) {
9143
9153
  if (state === "failure") {
9144
9154
  failureStreak += 1;
9145
9155
  if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
9156
+ const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
9157
+ if (diagnosis?.cause === "ci-budget-kill") {
9158
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "ci-budget-kill" };
9159
+ }
9146
9160
  return { policy, status: "failure", reason, detail: "checks-failure" };
9147
9161
  }
9148
9162
  lastDetail = "confirming-failure";
@@ -9459,7 +9473,7 @@ function planManagedGitignore(current) {
9459
9473
  }
9460
9474
 
9461
9475
  // src/gate-budget.ts
9462
- var BLESSED_RUN_WITH_BUDGET_SHA = "b6ec6e4eafcda73b543fc0754a6356551fe762a1";
9476
+ var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
9463
9477
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
9464
9478
  var LOCAL_USE = "./.github/actions/run-with-budget";
9465
9479
  var FULL_SHA = /^[0-9a-f]{40}$/;
@@ -11229,7 +11243,9 @@ async function collectWaveStatus(deps) {
11229
11243
  }
11230
11244
  const headText = await deps.fetchHeadText?.(wt.branch).catch(() => void 0);
11231
11245
  rows.push({
11232
- path: wt.path,
11246
+ // #3338: emit NATIVE separators (see toNativePath). `-C wt.path` above and the `primary` compare
11247
+ // below keep git's raw form — only the emitted value is converted.
11248
+ path: toNativePath(wt.path),
11233
11249
  branch: wt.branch,
11234
11250
  primary: wt.path === primaryPath,
11235
11251
  dirty,
@@ -11615,6 +11631,25 @@ async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
11615
11631
  ${stdout.trim() || "(empty)"}`);
11616
11632
  return result;
11617
11633
  }
11634
+ function parseParentIssueUrl(apiUrl) {
11635
+ if (typeof apiUrl !== "string" || !apiUrl.trim()) return null;
11636
+ const match = /\/repos\/([^/]+\/[^/]+)\/issues\/(\d+)\/?$/.exec(apiUrl.trim());
11637
+ if (!match) return null;
11638
+ const [, repo, number] = match;
11639
+ return { repo, number: Number(number), url: `https://github.com/${repo}/issues/${number}` };
11640
+ }
11641
+ function resolveParentField(payload) {
11642
+ if (!payload || typeof payload !== "object") {
11643
+ return { parentReadError: "issue payload was empty or not an object" };
11644
+ }
11645
+ if (!("sub_issues_summary" in payload)) {
11646
+ return { parentReadError: "issue payload carries no sub-issue fields \u2014 parent state is unknown, not absent" };
11647
+ }
11648
+ const raw = payload.parent_issue_url;
11649
+ if (raw === void 0 || raw === null) return { parent: null };
11650
+ const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
11651
+ return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
11652
+ }
11618
11653
  function parentLinkFields(result, error) {
11619
11654
  if (result) return { parent: result };
11620
11655
  if (error) return { parentLinkError: error };
@@ -13332,7 +13367,7 @@ function appendPublishDispatch(deploy, publish) {
13332
13367
  deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
13333
13368
  };
13334
13369
  }
13335
- async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
13370
+ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
13336
13371
  const workflowRuns = [];
13337
13372
  for (const target of targets) {
13338
13373
  try {
@@ -13342,8 +13377,68 @@ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
13342
13377
  workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
13343
13378
  }
13344
13379
  }
13380
+ if (!enumerateSha) return workflowRuns;
13381
+ const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
13382
+ workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
13345
13383
  return workflowRuns;
13346
13384
  }
13385
+ var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
13386
+ "pull_request",
13387
+ "pull_request_target",
13388
+ "pull_request_review",
13389
+ "pull_request_review_comment",
13390
+ "schedule",
13391
+ "issues",
13392
+ "issue_comment",
13393
+ "discussion",
13394
+ "discussion_comment",
13395
+ "fork",
13396
+ "watch",
13397
+ "star",
13398
+ "public",
13399
+ "gollum",
13400
+ "create",
13401
+ "delete"
13402
+ ]);
13403
+ var SHA_ENUM_LIMIT = 50;
13404
+ var NON_FAILING_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "skipped", "neutral"]);
13405
+ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
13406
+ const marker = `sha-enumeration(${headSha.slice(0, 7)})`;
13407
+ let rows;
13408
+ try {
13409
+ rows = JSON.parse(await deps.run("gh", [
13410
+ "run",
13411
+ "list",
13412
+ "--repo",
13413
+ repo,
13414
+ "--commit",
13415
+ headSha,
13416
+ "--limit",
13417
+ String(SHA_ENUM_LIMIT),
13418
+ "--json",
13419
+ "databaseId,url,event,status,conclusion,workflowName"
13420
+ ]));
13421
+ if (!Array.isArray(rows)) throw new Error("run list did not return an array");
13422
+ } catch {
13423
+ return [{ workflow: marker, conclusion: "pending" }];
13424
+ }
13425
+ const extra = [];
13426
+ if (rows.length >= SHA_ENUM_LIMIT) extra.push({ workflow: `${marker} truncated`, conclusion: "pending" });
13427
+ for (const row of rows) {
13428
+ if (typeof row.databaseId !== "number" || seenRunIds.has(row.databaseId)) continue;
13429
+ if (NON_DEPLOY_EVENTS.has(row.event ?? "")) continue;
13430
+ extra.push({
13431
+ workflow: row.workflowName ?? `run:${row.databaseId}`,
13432
+ runId: row.databaseId,
13433
+ runUrl: row.url,
13434
+ // An in-flight run is reported UNVERIFIED, never watched. Blocking on an arbitrary discovered run
13435
+ // would let one long push workflow hang the release command and mask the failed rows behind it, and
13436
+ // `gh run watch` has no timeout. The NAMED targets are the ones worth blocking on (also #3382 review).
13437
+ conclusion: row.status === "completed" ? NON_FAILING_CONCLUSIONS.has(row.conclusion ?? "") ? "success" : "failure" : "pending"
13438
+ });
13439
+ }
13440
+ return extra;
13441
+ }
13347
13442
  async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
13348
13443
  if (model === "tenant-container" || model === "solo-container") {
13349
13444
  const since = (deps.now ?? Date.now)();
@@ -13370,7 +13465,8 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
13370
13465
  ctx.repo,
13371
13466
  [{ workflow: "publish.yml", event: "release" }],
13372
13467
  since,
13373
- autoRunHeadSha
13468
+ autoRunHeadSha,
13469
+ true
13374
13470
  );
13375
13471
  const primary = workflowRuns[0];
13376
13472
  return { note, runId: primary?.runId, runUrl: primary?.runUrl, workflowRuns, deployStatus: aggregateWorkflowRuns(workflowRuns) };
@@ -13384,7 +13480,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
13384
13480
  { workflow: "deploy.yml", event: "release" },
13385
13481
  { workflow: "publish.yml", event: "release" }
13386
13482
  ];
13387
- const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha);
13483
+ const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha, ref !== "rc");
13388
13484
  const primary = workflowRuns[0];
13389
13485
  return {
13390
13486
  note,
@@ -15386,7 +15482,7 @@ var LINK_RE = /\]\(([^)#\s]+?\.md)(?:#[^)]*)?\)/g;
15386
15482
  var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
15387
15483
  var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
15388
15484
  var ROOT_DOCS = ["README.md", "architecture.md"];
15389
- var SKIP_WALK = ["docs/Archive/", "docs/incidents/"];
15485
+ var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/"];
15390
15486
  function stripFences(markdown) {
15391
15487
  let inFence = false;
15392
15488
  return markdown.split(/\r?\n/).map((line) => {
@@ -15487,13 +15583,18 @@ function checkRefs(root, deps, docs2) {
15487
15583
  const docDir = import_node_path16.posix.dirname(doc);
15488
15584
  for (const { target, line } of extractLinks(markdown)) {
15489
15585
  const resolved = import_node_path16.posix.normalize(import_node_path16.posix.join(docDir === "." ? "" : docDir, target));
15586
+ if (resolved.startsWith("..")) {
15587
+ candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
15588
+ continue;
15589
+ }
15490
15590
  if (!exists((0, import_node_path16.join)(root, resolved))) {
15491
15591
  candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
15492
15592
  }
15493
15593
  }
15494
15594
  }
15495
- const ignored = candidates.length ? isIgnored(candidates.map((c) => c.kind === "missing-link" ? c.resolved : c.detail)) : /* @__PURE__ */ new Set();
15496
- const findings = candidates.filter((c) => !ignored.has(c.kind === "missing-link" ? c.resolved : c.detail)).map(({ resolved, ...finding }) => finding);
15595
+ const queryable = candidates.filter((c) => !c.escapesRoot);
15596
+ const ignored = queryable.length ? isIgnored(queryable.map((c) => c.kind === "missing-link" ? c.resolved : c.detail)) : /* @__PURE__ */ new Set();
15597
+ const findings = candidates.filter((c) => c.escapesRoot || !ignored.has(c.kind === "missing-link" ? c.resolved : c.detail)).map(({ resolved: _resolved, escapesRoot: _escapesRoot, ...finding }) => finding);
15497
15598
  return { ok: findings.length === 0, findings };
15498
15599
  }
15499
15600
  function checkCommands(docs2, commandPaths) {
@@ -15538,10 +15639,12 @@ function defaultListDocs(root) {
15538
15639
  return [...ROOT_DOCS.filter((rel) => (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, rel))), ...docs2];
15539
15640
  }
15540
15641
  function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
15642
+ const inRepo = relPaths.filter((p) => !p.startsWith(".."));
15643
+ if (inRepo.length === 0) return /* @__PURE__ */ new Set();
15541
15644
  try {
15542
15645
  const out = exec("git", ["check-ignore", "--stdin"], {
15543
15646
  cwd: root,
15544
- input: relPaths.join("\n"),
15647
+ input: inRepo.join("\n"),
15545
15648
  encoding: "utf8"
15546
15649
  });
15547
15650
  return new Set(out.split(/\r?\n/).filter(Boolean));
@@ -17027,6 +17130,9 @@ function checkGithubPools(probe) {
17027
17130
  return lines;
17028
17131
  }
17029
17132
 
17133
+ // src/box-commands.ts
17134
+ var import_node_fs20 = require("node:fs");
17135
+
17030
17136
  // src/box.ts
17031
17137
  var BOX_KEYS = {
17032
17138
  /** Opens every MM box INCLUDING the CI runner (`mmi-runner`). The general-purpose MM key. */
@@ -17128,6 +17234,12 @@ function formatSshRecipe(box) {
17128
17234
  `ssh -i "$kf" -o StrictHostKeyChecking=accept-new root@${box.address} 'hostname; uptime'`
17129
17235
  ].join("\n");
17130
17236
  }
17237
+ var SSH_RECIPE_AGENT_NOTE = "# agent callers: this is multi-line, so running it inline is denied (#1473/#2125).\n# Use `mmi-cli runtime box get <name> --ssh --script <path>` to write it to a file, then `bash <path>`.";
17238
+ function sshRecipeScript(box) {
17239
+ return `#!/usr/bin/env bash
17240
+ ${formatSshRecipe(box)}
17241
+ `;
17242
+ }
17131
17243
 
17132
17244
  // src/box-commands.ts
17133
17245
  var HETZNER_API = "https://api.hetzner.cloud/v1/servers";
@@ -17200,7 +17312,7 @@ function registerBoxCommands(program3) {
17200
17312
  await failGraceful(e.message);
17201
17313
  }
17202
17314
  });
17203
- box.command("get <name>").description("one box by name \u2014 its address and ssh key path; --ssh prints a ready-to-run connect recipe").option("--json", "machine-readable output").option("--ssh", "print a copy-paste ssh recipe (materializes the key to a temp file; never echoes it)").action(async (name, o) => {
17315
+ box.command("get <name>").description("one box by name \u2014 its address and ssh key path; --ssh prints a ready-to-run connect recipe").option("--json", "machine-readable output").option("--ssh", "print a copy-paste ssh recipe (materializes the key to a temp file; never echoes it)").option("--script <path>", "with --ssh: write the recipe to <path> and print the one line to run \u2014 the runnable form for agent callers, whose transport denies a multi-line inline body (#3385)").action(async (name, o) => {
17204
17316
  try {
17205
17317
  const { boxes, incomplete } = await fetchInventory();
17206
17318
  const result = lookupBox(name, boxes, incomplete);
@@ -17216,8 +17328,16 @@ function registerBoxCommands(program3) {
17216
17328
  return;
17217
17329
  }
17218
17330
  const found = result.box;
17331
+ if (o.script && !o.ssh) {
17332
+ await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
17333
+ return;
17334
+ }
17219
17335
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
17220
- else if (o.ssh) console.log(formatSshRecipe(found));
17336
+ else if (o.ssh && o.script) {
17337
+ (0, import_node_fs20.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
17338
+ console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
17339
+ } else if (o.ssh) console.log(`${formatSshRecipe(found)}
17340
+ ${SSH_RECIPE_AGENT_NOTE}`);
17221
17341
  else console.log(formatBoxTable([found]));
17222
17342
  if (!o.json) warnIncomplete(incomplete);
17223
17343
  if (incomplete.length) process.exitCode = 1;
@@ -17237,7 +17357,7 @@ var ORG = "mutmutco";
17237
17357
  var DOC_START_MARKER = "<!-- schedules:inventory:start -->";
17238
17358
  var DOC_END_MARKER = "<!-- schedules:inventory:end -->";
17239
17359
  function isJervResource(name) {
17240
- return /^jerv/i.test(name);
17360
+ return /^jerv-memory(?:-|$)/i.test(name);
17241
17361
  }
17242
17362
  function llmFromHeader(yamlText) {
17243
17363
  const m = /^#\s*llm:\s*(.+)$/im.exec(yamlText);
@@ -17344,6 +17464,11 @@ function awsScheduleRefs(payload) {
17344
17464
  }
17345
17465
  var DECLARED_ENTRIES = [
17346
17466
  { name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli runtime box get <box> --ssh)" },
17467
+ // #3370: the floor under runner-hygiene.yml. That workflow runs ON the runner and needs checkout +
17468
+ // setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
17469
+ // unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
17470
+ // a no-op below 90% disk, which is why 15 minutes is affordable.
17471
+ { name: "mmi-runner-hygiene-oob (mmi-runner)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-hygiene \u2192 /opt/mmi-control/runner-hygiene-oob.sh; acts only at >=90% disk (verify: mmi-cli runtime box get mmi-runner --ssh)" },
17347
17472
  { name: "zuber-bake", cadence: "*:05/30 (every 30 min)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake.timer \u2192 rolling overlay bake, metro tier, next ~6h window (verify over ssh)" },
17348
17473
  { name: "zuber-bake-full", cadence: "00:30 UTC (03:30 TRT, daily)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake-full.timer \u2192 full 24h overlay rebuild, all 81 il (verify over ssh)" }
17349
17474
  ];
@@ -17938,7 +18063,7 @@ function withArmingReport(body) {
17938
18063
  function registerSchedulesLiftCommand(program3, deps = {}) {
17939
18064
  const schedules = program3.commands.find((c) => c.name() === "schedules");
17940
18065
  if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
17941
- schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
18066
+ jsonParity(schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write")).action(async (o) => {
17942
18067
  try {
17943
18068
  const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
17944
18069
  if (o.dryRun) {
@@ -18417,7 +18542,7 @@ function registerQueryCommands(program3) {
18417
18542
  }
18418
18543
 
18419
18544
  // src/bootstrap-commands.ts
18420
- var import_node_fs20 = require("node:fs");
18545
+ var import_node_fs21 = require("node:fs");
18421
18546
 
18422
18547
  // src/bootstrap-verify.ts
18423
18548
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -18965,7 +19090,7 @@ function registerBootstrapCommands(program3) {
18965
19090
  client: defaultGitHubClient(),
18966
19091
  projectMeta: meta,
18967
19092
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
18968
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs20.existsSync)(path2) ? (0, import_node_fs20.readFileSync)(path2, "utf8") : null,
19093
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
18969
19094
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
18970
19095
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
18971
19096
  requiredGcpApis: (() => {
@@ -19016,12 +19141,12 @@ function registerBootstrapCommands(program3) {
19016
19141
  return fail(`bootstrap apply: ${e.message}`);
19017
19142
  }
19018
19143
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
19019
- if (!(0, import_node_fs20.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
19020
- const manifest = loadBootstrapSeeds((0, import_node_fs20.readFileSync)(manifestPath, "utf8"));
19144
+ if (!(0, import_node_fs21.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
19145
+ const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
19021
19146
  const baseBranch = o.class === "content" ? "main" : "development";
19022
19147
  const slug = parsedRepo.slug;
19023
19148
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
19024
- const readFile6 = (p) => (0, import_node_fs20.existsSync)(p) ? (0, import_node_fs20.readFileSync)(p, "utf8") : null;
19149
+ const readFile6 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
19025
19150
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
19026
19151
  const rawVars = {};
19027
19152
  for (const value of cmdOpts.var ?? []) {
@@ -19217,11 +19342,11 @@ LIVE apply to ${repo}:
19217
19342
  }
19218
19343
 
19219
19344
  // src/stage-commands.ts
19220
- var import_node_fs22 = require("node:fs");
19345
+ var import_node_fs23 = require("node:fs");
19221
19346
  var import_node_path20 = require("node:path");
19222
19347
 
19223
19348
  // src/port-registry.ts
19224
- var import_node_fs21 = require("node:fs");
19349
+ var import_node_fs22 = require("node:fs");
19225
19350
  var import_node_path19 = require("node:path");
19226
19351
 
19227
19352
  // ../infra/port-geometry.mjs
@@ -19236,8 +19361,8 @@ function nextPortBlock(registry2) {
19236
19361
  return [base, base + PORT_SPAN];
19237
19362
  }
19238
19363
  function loadPortRegistry(path2) {
19239
- if (!(0, import_node_fs21.existsSync)(path2)) return {};
19240
- const raw = JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8"));
19364
+ if (!(0, import_node_fs22.existsSync)(path2)) return {};
19365
+ const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
19241
19366
  const out = {};
19242
19367
  for (const [key, value] of Object.entries(raw)) {
19243
19368
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -19251,9 +19376,9 @@ function ensurePortRange(repo, path2) {
19251
19376
  const existing = registry2[repo];
19252
19377
  if (existing) return existing;
19253
19378
  const range = nextPortBlock(registry2);
19254
- const raw = (0, import_node_fs21.existsSync)(path2) ? JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8")) : {};
19379
+ const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
19255
19380
  raw[repo] = range;
19256
- (0, import_node_fs21.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
19381
+ (0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
19257
19382
  return range;
19258
19383
  }
19259
19384
  function portCursorSeed(registry2) {
@@ -19277,7 +19402,7 @@ function existingPortRange(repo, registry2) {
19277
19402
  function portRangeInfraAt(root, source) {
19278
19403
  const registryPath = (0, import_node_path19.join)(root, "infra", "port-ranges.json");
19279
19404
  const ddbScriptPath = (0, import_node_path19.join)(root, "infra", "port-ddb.mjs");
19280
- if (!(0, import_node_fs21.existsSync)(registryPath) || !(0, import_node_fs21.existsSync)(ddbScriptPath)) return null;
19405
+ if (!(0, import_node_fs22.existsSync)(registryPath) || !(0, import_node_fs22.existsSync)(ddbScriptPath)) return null;
19281
19406
  return { root, source, registryPath, ddbScriptPath };
19282
19407
  }
19283
19408
  function resolvePortRangeInfra(cwd, packageDir) {
@@ -19466,8 +19591,8 @@ function registerStageCommands(program3) {
19466
19591
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
19467
19592
  return decideStage({
19468
19593
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
19469
- hasCompose: (0, import_node_fs22.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
19470
- hasEnvExample: (0, import_node_fs22.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
19594
+ hasCompose: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
19595
+ hasEnvExample: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
19471
19596
  });
19472
19597
  }
19473
19598
  async function fetchStageVaultEnvMerge() {
@@ -19902,7 +20027,7 @@ function registerBoardCommands(program3) {
19902
20027
  }
19903
20028
 
19904
20029
  // src/merge-cleanup.ts
19905
- var import_node_fs23 = require("node:fs");
20030
+ var import_node_fs24 = require("node:fs");
19906
20031
  var import_promises5 = require("node:fs/promises");
19907
20032
  var import_node_child_process11 = require("node:child_process");
19908
20033
 
@@ -20226,7 +20351,7 @@ async function applyGcPlan(plan, remote) {
20226
20351
  return cleanupPrMergeLocalBranch(branch.branch, {
20227
20352
  beforeWorktrees,
20228
20353
  startingPath: branch.worktreePath,
20229
- pathExists: (p) => (0, import_node_fs23.existsSync)(p),
20354
+ pathExists: (p) => (0, import_node_fs24.existsSync)(p),
20230
20355
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20231
20356
  teardownWorktreeStage,
20232
20357
  deferredStore,
@@ -20252,7 +20377,7 @@ async function applyGcPlan(plan, remote) {
20252
20377
  for (const wt of plan.worktreeDirs) {
20253
20378
  try {
20254
20379
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
20255
- realpath: (path2) => (0, import_node_fs23.realpathSync)(path2)
20380
+ realpath: (path2) => (0, import_node_fs24.realpathSync)(path2)
20256
20381
  });
20257
20382
  if (!cleanupTarget.ok) {
20258
20383
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -20379,13 +20504,13 @@ var realWorktreeDirRemover = {
20379
20504
  probe: (p) => {
20380
20505
  let st;
20381
20506
  try {
20382
- st = (0, import_node_fs23.lstatSync)(p);
20507
+ st = (0, import_node_fs24.lstatSync)(p);
20383
20508
  } catch {
20384
20509
  return null;
20385
20510
  }
20386
20511
  if (st.isSymbolicLink()) return "link";
20387
20512
  try {
20388
- (0, import_node_fs23.readlinkSync)(p);
20513
+ (0, import_node_fs24.readlinkSync)(p);
20389
20514
  return "link";
20390
20515
  } catch {
20391
20516
  }
@@ -20393,7 +20518,7 @@ var realWorktreeDirRemover = {
20393
20518
  },
20394
20519
  readdir: (p) => {
20395
20520
  try {
20396
- return (0, import_node_fs23.readdirSync)(p);
20521
+ return (0, import_node_fs24.readdirSync)(p);
20397
20522
  } catch {
20398
20523
  return [];
20399
20524
  }
@@ -20402,9 +20527,9 @@ var realWorktreeDirRemover = {
20402
20527
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
20403
20528
  detachLink: (p) => {
20404
20529
  try {
20405
- (0, import_node_fs23.rmdirSync)(p);
20530
+ (0, import_node_fs24.rmdirSync)(p);
20406
20531
  } catch {
20407
- (0, import_node_fs23.unlinkSync)(p);
20532
+ (0, import_node_fs24.unlinkSync)(p);
20408
20533
  }
20409
20534
  },
20410
20535
  removeTree: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -20437,9 +20562,9 @@ async function worktreeHasStageState(worktreePath) {
20437
20562
  }
20438
20563
  }
20439
20564
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
20440
- if (!(0, import_node_fs23.existsSync)(statePath)) return false;
20565
+ if (!(0, import_node_fs24.existsSync)(statePath)) return false;
20441
20566
  try {
20442
- const state = JSON.parse((0, import_node_fs23.readFileSync)(statePath, "utf8"));
20567
+ const state = JSON.parse((0, import_node_fs24.readFileSync)(statePath, "utf8"));
20443
20568
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
20444
20569
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
20445
20570
  } catch {
@@ -20528,12 +20653,15 @@ function parseRestPrSnapshot(json) {
20528
20653
  async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
20529
20654
  return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
20530
20655
  }
20656
+ async function fetchHeadCheckRuns(headSha, repo, gh) {
20657
+ const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id}"]);
20658
+ return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
20659
+ }
20531
20660
  async function fetchHeadCheckBuckets(headSha, repo, gh) {
20532
- const [runsOut, statusesOut] = await Promise.all([
20533
- gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id}"]),
20661
+ const [runs, statusesOut] = await Promise.all([
20662
+ fetchHeadCheckRuns(headSha, repo, gh),
20534
20663
  gh(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state}"])
20535
20664
  ]);
20536
- const runs = dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
20537
20665
  const statuses = parseNdjsonLines(statusesOut);
20538
20666
  return [...runs.map(classifyCheckRun), ...statuses.map((s) => classifyCommitStatus(s.state))];
20539
20667
  }
@@ -20555,6 +20683,48 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
20555
20683
  return false;
20556
20684
  }
20557
20685
  }
20686
+ var CI_BUDGET_ANNOTATION_TITLE = "CI budget exceeded";
20687
+ function isErrorAnnotation(a) {
20688
+ const level = a.annotation_level?.toLowerCase();
20689
+ return level === "failure" || level === "error";
20690
+ }
20691
+ function classifyFailedChecks(failing) {
20692
+ const budgetKilled = [];
20693
+ const otherFailures = [];
20694
+ for (const check of failing) {
20695
+ const errors = check.annotations.filter(isErrorAnnotation);
20696
+ const allBudget = errors.length > 0 && errors.every((a) => a.title?.trim() === CI_BUDGET_ANNOTATION_TITLE);
20697
+ (allBudget ? budgetKilled : otherFailures).push(check.name);
20698
+ }
20699
+ if (budgetKilled.length && !otherFailures.length) {
20700
+ return {
20701
+ cause: "ci-budget-kill",
20702
+ budgetKilled,
20703
+ otherFailures,
20704
+ 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.`
20705
+ };
20706
+ }
20707
+ return {
20708
+ cause: "checks-failure",
20709
+ budgetKilled,
20710
+ otherFailures,
20711
+ reason: budgetKilled.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${budgetKilled.join(", ")} was killed for wall-clock, not failed.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
20712
+ };
20713
+ }
20714
+ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
20715
+ try {
20716
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh);
20717
+ const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
20718
+ if (!failing.length) return null;
20719
+ const annotated = await Promise.all(failing.map(async (run) => ({
20720
+ name: run.name ?? `check-run ${run.id}`,
20721
+ annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
20722
+ })));
20723
+ return classifyFailedChecks(annotated);
20724
+ } catch {
20725
+ return null;
20726
+ }
20727
+ }
20558
20728
  async function fetchRestCorePool(gh = defaultGhApi) {
20559
20729
  try {
20560
20730
  const parsed = JSON.parse(await gh(["rate_limit"]));
@@ -20567,7 +20737,7 @@ async function fetchRestCorePool(gh = defaultGhApi) {
20567
20737
  }
20568
20738
 
20569
20739
  // src/worktree-lifecycle-commands.ts
20570
- var import_node_fs24 = require("node:fs");
20740
+ var import_node_fs25 = require("node:fs");
20571
20741
  var import_node_path22 = require("node:path");
20572
20742
  var GH_TIMEOUT_MS = 2e4;
20573
20743
  var DEFAULT_BASE = "origin/development";
@@ -20653,17 +20823,39 @@ function classifyStaleLeaks(input) {
20653
20823
  }
20654
20824
  const knownPaths = new Set(input.worktrees.map((w) => w.path));
20655
20825
  for (const dir of input.orphanDirs) {
20656
- if (!knownPaths.has(dir)) {
20657
- leaks.push({
20658
- kind: "orphan-dir",
20659
- ref: dir,
20660
- detail: `directory under the worktrees root is not a registered worktree`,
20661
- remediation: `mmi-cli worktree gc --apply (or: Remove-Item/rm -rf "${dir}")`
20662
- });
20663
- }
20826
+ if (knownPaths.has(dir.path)) continue;
20827
+ leaks.push({
20828
+ kind: "orphan-dir",
20829
+ ref: dir.path,
20830
+ // Say only what the inspection proved. `orphaned-folder` proves the `.git` entry is gone while this
20831
+ // repo's worktree metadata still points at the directory — NOT that the directory is empty; gc
20832
+ // checks contents separately, at apply time.
20833
+ detail: dir.reason === "dangling-gitdir" ? "this repo's worktree directory whose git metadata is gone" : "this repo's worktree metadata points here, but the .git entry is gone",
20834
+ // gc --apply re-proves ownership before deleting anything; a raw `rm -rf` does not, so it is never
20835
+ // offered here — following it on a mis-classified dir would destroy another repo's live worktree.
20836
+ remediation: "mmi-cli worktree gc --apply"
20837
+ });
20664
20838
  }
20665
20839
  return leaks;
20666
20840
  }
20841
+ var defaultOrphanDirScanDeps = {
20842
+ listDirs: (root) => {
20843
+ try {
20844
+ return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path22.join)(root, e.name));
20845
+ } catch {
20846
+ return [];
20847
+ }
20848
+ },
20849
+ inspect: inspectSiblingWorktreeDir
20850
+ };
20851
+ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirScanDeps) {
20852
+ const candidates = [];
20853
+ for (const dir of deps.listDirs(worktreesRoot)) {
20854
+ const { cleanup } = classifySiblingWorktreeDir(deps.inspect(dir, worktreeGitRoot));
20855
+ if (cleanup) candidates.push({ path: dir, reason: cleanup.reason });
20856
+ }
20857
+ return candidates;
20858
+ }
20667
20859
  function formatStaleLeaks(leaks) {
20668
20860
  if (!leaks.length) return "worktree list --stale: no leaks found";
20669
20861
  const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
@@ -20706,7 +20898,7 @@ function registerWorktreeCommands(program3) {
20706
20898
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
20707
20899
  }
20708
20900
  const gitFile = (0, import_node_path22.join)(wtPath, ".git");
20709
- const isLinked = (0, import_node_fs24.existsSync)(gitFile) && (0, import_node_fs24.statSync)(gitFile).isFile();
20901
+ const isLinked = (0, import_node_fs25.existsSync)(gitFile) && (0, import_node_fs25.statSync)(gitFile).isFile();
20710
20902
  if (apply && !isLinked) {
20711
20903
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
20712
20904
  }
@@ -20795,7 +20987,7 @@ async function gatherWorktreeContext() {
20795
20987
  const porcelain = (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
20796
20988
  const parsed = parseWorktreePorcelain(porcelain);
20797
20989
  const primaryPath = parsed[0]?.path ?? repoRoot2;
20798
- const worktrees = parsed.map((w) => ({ path: w.path, branch: w.branch, primary: w.path === primaryPath }));
20990
+ const worktrees = parsed.map((w) => ({ path: toNativePath(w.path), branch: w.branch, primary: w.path === primaryPath }));
20799
20991
  const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
20800
20992
  const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
20801
20993
  const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
@@ -20821,18 +21013,10 @@ async function gatherWorktreeContext() {
20821
21013
  const s = readStageSummary(wt.path);
20822
21014
  if (s) stages.push({ path: wt.path, port: s.port });
20823
21015
  }
20824
- const orphanDirs = [];
20825
21016
  const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
20826
- if ((0, import_node_fs24.existsSync)(wtRoot)) {
20827
- let entries = [];
20828
- try {
20829
- entries = (0, import_node_fs24.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path22.join)(wtRoot, e.name));
20830
- } catch {
20831
- }
20832
- const known = new Set(worktrees.map((w) => w.path));
20833
- for (const dir of entries) {
20834
- if (!known.has(dir)) orphanDirs.push(dir);
20835
- }
21017
+ let orphanDirs = [];
21018
+ if ((0, import_node_fs25.existsSync)(wtRoot)) {
21019
+ orphanDirs = scanOrphanDirs(wtRoot, await currentRepoWorktreeGitRoot(repoRoot2));
20836
21020
  }
20837
21021
  return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
20838
21022
  }
@@ -20851,7 +21035,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
20851
21035
  }
20852
21036
 
20853
21037
  // src/issue-commands.ts
20854
- var import_node_fs25 = require("node:fs");
21038
+ var import_node_fs26 = require("node:fs");
20855
21039
  var import_node_crypto5 = require("node:crypto");
20856
21040
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
20857
21041
  async function editIssue(client, options, deps = {}) {
@@ -20865,7 +21049,7 @@ async function editIssue(client, options, deps = {}) {
20865
21049
  if (options.body !== void 0 || options.bodyFile !== void 0) {
20866
21050
  let body;
20867
21051
  if (options.bodyFile) {
20868
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs25.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
21052
+ const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs26.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
20869
21053
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
20870
21054
  } else {
20871
21055
  body = options.body ?? "";
@@ -21130,6 +21314,7 @@ function validateBatchSpecs(specs) {
21130
21314
  async function createIssuesBatch(specs, options, deps = {}) {
21131
21315
  const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
21132
21316
  const client = deps.client ?? defaultGitHubClient();
21317
+ const runCreate = deps.runCreate ?? (async (args) => (await execFileP2("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout);
21133
21318
  const validation = validateBatchSpecs(specs);
21134
21319
  if (!validation.ok) {
21135
21320
  const lines = validation.errors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
@@ -21166,20 +21351,33 @@ ${lines}`);
21166
21351
  if (rowKey) {
21167
21352
  body = appendIdempotencyMarker(body, rowKey);
21168
21353
  }
21169
- const args = buildIssueArgs({ type, title: spec.title, body, priority, repo: rowRepo(spec), labels: spec.labels });
21354
+ const rowPriority = spec.priority ? priority : options.priority ?? priority;
21355
+ const args = buildIssueArgs({ type, title: spec.title, body, priority: rowPriority, repo: rowRepo(spec), labels: spec.labels });
21170
21356
  const swapped = await bodyArgsViaFile(args);
21357
+ let row;
21171
21358
  try {
21172
- const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
21359
+ const stdout = await runCreate(swapped.args);
21173
21360
  const result = parseCreatedUrl(stdout);
21174
- created.push({ row: entry.row, number: result.number, url: result.url });
21361
+ row = { row: entry.row, number: result.number, url: result.url };
21362
+ created.push(row);
21175
21363
  } finally {
21176
21364
  await swapped.cleanup();
21177
21365
  }
21178
- if (spec.parent) {
21366
+ if (deps.attach) {
21367
+ const attached = await deps.attach(row.number, rowRepo(spec), rowPriority);
21368
+ row.onBoard = attached.onBoard;
21369
+ if (attached.projectItemId) row.projectItemId = attached.projectItemId;
21370
+ }
21371
+ const parentRef = spec.parent ?? options.parent;
21372
+ if (parentRef) {
21179
21373
  try {
21180
21374
  const run = deps.runGh ?? ghRunner;
21181
- await linkSubIssue(run, spec.parent, `https://github.com/${rowRepo(spec)}/issues/${created[created.length - 1].number}`, rowRepo(spec));
21182
- } catch {
21375
+ row.parent = await linkSubIssue(run, parentRef, row.url, rowRepo(spec));
21376
+ } catch (e) {
21377
+ const err = e;
21378
+ row.parentLinkError = (err.stderr || err.message || String(e)).trim();
21379
+ process.stderr.write(`warning: issue #${row.number} created but NOT linked under ${parentRef}: ${row.parentLinkError}
21380
+ `);
21183
21381
  }
21184
21382
  }
21185
21383
  } catch (e) {
@@ -21212,7 +21410,7 @@ function parseCloseReason(raw) {
21212
21410
  if (trimmed === "duplicateof" || trimmed === "duplicate") return { reason: "duplicate-of" };
21213
21411
  throw new Error(`unknown close reason "${raw}" \u2014 expected completed, not-planned, or duplicate-of`);
21214
21412
  }
21215
- function registerIssueLifecycleCommands(program3) {
21413
+ function registerIssueLifecycleCommands(program3, deps = {}) {
21216
21414
  const issue2 = program3.commands.find((c) => c.name() === "issue");
21217
21415
  if (!issue2) return;
21218
21416
  mutating(
@@ -21335,9 +21533,9 @@ function registerIssueLifecycleCommands(program3) {
21335
21533
  return failGraceful(`issue unlink-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
21336
21534
  }
21337
21535
  });
21338
- extendCreateCommand(issue2);
21536
+ extendCreateCommand(issue2, deps.attach);
21339
21537
  }
21340
- function extendCreateCommand(issue2) {
21538
+ function extendCreateCommand(issue2, batchAttach) {
21341
21539
  const createCmd = issue2.commands.find((c) => c.name() === "create");
21342
21540
  if (!createCmd) return;
21343
21541
  createCmd.option("--batch <file.json>", "create multiple issues from a JSON array file (pre-validates all rows before creating any)").option("--idempotency-key <key>", "create-if-not-exists: skip creation when an issue with this key already exists (retried loops must not duplicate)");
@@ -21348,7 +21546,7 @@ function extendCreateCommand(issue2) {
21348
21546
  if (opts.batch) {
21349
21547
  let specs;
21350
21548
  try {
21351
- const raw = (0, import_node_fs25.readFileSync)(opts.batch, "utf8");
21549
+ const raw = (0, import_node_fs26.readFileSync)(opts.batch, "utf8");
21352
21550
  specs = JSON.parse(raw);
21353
21551
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
21354
21552
  } catch (e) {
@@ -21360,13 +21558,27 @@ function extendCreateCommand(issue2) {
21360
21558
  return fail(`issue create --batch: validation failed (${validation.errors.length} error(s)):
21361
21559
  ${lines}`);
21362
21560
  }
21561
+ const batchParent = opts.parent;
21562
+ const batchPriority = opts.priority ? normalizePriority(opts.priority) : void 0;
21363
21563
  if (opts.dryRun || opts.validateOnly) {
21364
- const planned = validation.validated.map((v) => ({ row: v.row, type: v.type, title: v.spec.title, priority: v.priority, repo: v.spec.repo }));
21564
+ const planned = validation.validated.map((v) => ({
21565
+ row: v.row,
21566
+ type: v.type,
21567
+ title: v.spec.title,
21568
+ priority: v.spec.priority ? v.priority : batchPriority ?? v.priority,
21569
+ repo: v.spec.repo,
21570
+ ...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {}
21571
+ }));
21365
21572
  console.log(JSON.stringify(opts.validateOnly ? { ok: true, planned } : { dry_run: true, planned }));
21366
21573
  return;
21367
21574
  }
21368
21575
  try {
21369
- const result = await createIssuesBatch(specs, { repo: opts.repo, idempotencyKey: opts.idempotencyKey });
21576
+ const result = await createIssuesBatch(specs, {
21577
+ repo: opts.repo,
21578
+ idempotencyKey: opts.idempotencyKey,
21579
+ parent: batchParent,
21580
+ priority: batchPriority
21581
+ }, { attach: batchAttach });
21370
21582
  console.log(JSON.stringify(result));
21371
21583
  if (result.failures.length) process.exitCode = 1;
21372
21584
  } catch (e) {
@@ -21392,11 +21604,11 @@ ${lines}`);
21392
21604
  }
21393
21605
 
21394
21606
  // src/train-commands.ts
21395
- var import_node_fs27 = require("node:fs");
21607
+ var import_node_fs28 = require("node:fs");
21396
21608
  var import_node_path24 = require("node:path");
21397
21609
 
21398
21610
  // src/plugin-guard-io.ts
21399
- var import_node_fs26 = require("node:fs");
21611
+ var import_node_fs27 = require("node:fs");
21400
21612
  var import_node_path23 = require("node:path");
21401
21613
  var import_node_os6 = require("node:os");
21402
21614
 
@@ -21531,7 +21743,7 @@ var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
21531
21743
  };
21532
21744
  function readInstalledPlugins(surface = detectSurface(process.env)) {
21533
21745
  try {
21534
- return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath2(surface), "utf8"));
21746
+ return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath2(surface), "utf8"));
21535
21747
  } catch {
21536
21748
  return null;
21537
21749
  }
@@ -21545,7 +21757,7 @@ function marketplaceCloneCandidates(surface, home) {
21545
21757
  }
21546
21758
  return [(0, import_node_path23.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21547
21759
  }
21548
- function marketplaceClonePresent(surface, home, exists = import_node_fs26.existsSync) {
21760
+ function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
21549
21761
  return marketplaceCloneCandidates(surface, home).some(exists);
21550
21762
  }
21551
21763
  async function fetchNpmReleasedVersion() {
@@ -21572,7 +21784,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
21572
21784
  isOrgRepo,
21573
21785
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
21574
21786
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
21575
- pluginCachePresent: (0, import_node_fs26.existsSync)((0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21787
+ pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21576
21788
  };
21577
21789
  }
21578
21790
  function claudePluginGuardState(isOrgRepo) {
@@ -21707,7 +21919,7 @@ function formatTrainStatus(r) {
21707
21919
  // src/train-commands.ts
21708
21920
  function readRepoVersion() {
21709
21921
  try {
21710
- return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
21922
+ return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
21711
21923
  } catch {
21712
21924
  return void 0;
21713
21925
  }
@@ -21918,12 +22130,14 @@ async function recommendNext(deps) {
21918
22130
  const load = deps?.loadConfig ?? loadConfig;
21919
22131
  const reader = deps?.readBoard ?? readBoard;
21920
22132
  const cfg = await load();
21921
- if (!cfg.sagaApiUrl) return null;
22133
+ if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
21922
22134
  let report;
21923
22135
  try {
21924
22136
  report = await reader({ config: cfg });
21925
- } catch {
21926
- return null;
22137
+ } catch (e) {
22138
+ throw new Error(
22139
+ `board unreachable \u2014 ${e.message}. This is NOT an empty board: confirm the ACTIVE gh account can see this repo (\`gh auth status\`, \`gh repo view <owner/repo>\`; \`gh auth switch --user <other>\` when more than one account is present).`
22140
+ );
21927
22141
  }
21928
22142
  const claimable = report.primary.claimable;
21929
22143
  if (!claimable.length) return null;
@@ -21997,12 +22211,16 @@ async function collectOnboardStatus() {
21997
22211
  } else if (!board.ok) {
21998
22212
  nextCommand = "mmi-cli board read";
21999
22213
  } else {
22000
- const top = await recommendNext();
22001
- if (top) {
22002
- nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
22003
- } else {
22004
- nextCommand = "mmi-cli board read \u2014 no claimable items found";
22214
+ let top = null;
22215
+ let readError;
22216
+ try {
22217
+ top = await recommendNext();
22218
+ } catch (e) {
22219
+ readError = e.message;
22005
22220
  }
22221
+ if (readError) nextCommand = `mmi-cli board read \u2014 ${readError}`;
22222
+ else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
22223
+ else nextCommand = "mmi-cli board read \u2014 no claimable items found";
22006
22224
  }
22007
22225
  return { track, board, registry: registry2, secrets, nextCommand };
22008
22226
  }
@@ -22027,7 +22245,7 @@ function registerDiscoveryCommands(program3) {
22027
22245
  try {
22028
22246
  const item = await recommendNext();
22029
22247
  if (!item) {
22030
- console.log("No claimable board items found.");
22248
+ console.log("No claimable board items found (board read OK).");
22031
22249
  return;
22032
22250
  }
22033
22251
  const priorityNote = item.priority ? ` [${item.priority}]` : "";
@@ -22794,14 +23012,30 @@ function doctorReportExitCode(checks) {
22794
23012
  // src/doctor-clean.ts
22795
23013
  function checkGithubAuth(probe) {
22796
23014
  const login = probe.login?.trim();
22797
- const ok = Boolean(login);
23015
+ const authed = Boolean(login);
23016
+ const reach = probe.reach;
23017
+ if (authed && reach?.repo && reach.reachable === false) {
23018
+ const rescuers = reach.otherAccountsThatCanSee ?? [];
23019
+ return {
23020
+ ok: false,
23021
+ label: "github auth",
23022
+ fix: rescuers.length ? `the active gh account (${login}) cannot see ${reach.repo}; another account is logged in \u2014 try: gh auth switch --user ${rescuers[0]}` : `the active gh account (${login}) cannot see ${reach.repo} \u2014 check org membership, or: gh auth login`,
23023
+ verbose: [
23024
+ `gh installed: ${probe.ghInstalled ? "yes" : "no"}`,
23025
+ `authenticated as: ${login}`,
23026
+ `can resolve ${reach.repo}: NO`,
23027
+ rescuers.length ? `other gh accounts logged in: ${rescuers.join(", ")} \u2014 switching is the usual fix` : "no other gh account is logged in, so this is not a wrong-account mix-up"
23028
+ ]
23029
+ };
23030
+ }
22798
23031
  return {
22799
- ok,
23032
+ ok: authed,
22800
23033
  label: "github auth",
22801
23034
  fix: probe.ghInstalled ? 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"' : "install GitHub CLI (https://cli.github.com), then: gh auth login",
22802
23035
  verbose: [
22803
23036
  `gh installed: ${probe.ghInstalled ? "yes" : "no"}`,
22804
- `authenticated as: ${login || "(none)"}`
23037
+ `authenticated as: ${login || "(none)"}`,
23038
+ ...reach?.repo ? [`can resolve ${reach.repo}: ${reach.reachable === true ? "yes" : "not probed"}`] : []
22805
23039
  ]
22806
23040
  };
22807
23041
  }
@@ -22974,17 +23208,22 @@ function gcReapable(plan) {
22974
23208
  }
22975
23209
  async function runDoctorClean(opts, io, deps) {
22976
23210
  const apply = Boolean(opts.apply);
22977
- const [login, isOrgRepo, released, callerArn] = await Promise.all([
23211
+ const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
22978
23212
  deps.githubLogin(),
22979
23213
  deps.isOrgRepo(),
22980
23214
  opts.fast ? Promise.resolve(void 0) : deps.releasedVersion(),
22981
- opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn()
23215
+ opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn(),
23216
+ // #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
23217
+ // network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
23218
+ // `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
23219
+ // and is exactly where the wrong-active-account trap has to be caught. Fail-soft either way.
23220
+ opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
22982
23221
  ]);
22983
23222
  const ghInstalled2 = login ? true : await deps.ghInstalled();
22984
23223
  const installed = deps.installedPluginVersion();
22985
23224
  const checks = [];
22986
23225
  let restartPending = false;
22987
- checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2 }));
23226
+ checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
22988
23227
  if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
22989
23228
  checks.push(...checkGithubPools(await deps.githubPools()));
22990
23229
  }
@@ -23158,8 +23397,86 @@ async function runDoctorClean(opts, io, deps) {
23158
23397
  return exitCode;
23159
23398
  }
23160
23399
 
23400
+ // src/gh-account-visibility.ts
23401
+ function parseGhAuthAccounts(stdout) {
23402
+ const accounts = [];
23403
+ for (const raw of stdout.split(/\r?\n/)) {
23404
+ const match = /Logged in to \S+ (?:account|as)\s+([A-Za-z0-9-]+)/.exec(raw);
23405
+ if (!match?.[1]) continue;
23406
+ accounts.push({ login: match[1], active: /\(active\)/.test(raw) });
23407
+ }
23408
+ return accounts;
23409
+ }
23410
+ function parseOriginRepo(remoteUrl) {
23411
+ const url = remoteUrl.trim();
23412
+ if (!url) return void 0;
23413
+ const match = /github\.com[/:]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url);
23414
+ if (!match) return void 0;
23415
+ return `${match[1]}/${match[2]}`;
23416
+ }
23417
+ function ghHostsConfigPath(env, platform2) {
23418
+ const sep2 = platform2 === "win32" ? "\\" : "/";
23419
+ const join24 = (...parts) => parts.join(sep2);
23420
+ const explicit = env.GH_CONFIG_DIR?.trim();
23421
+ if (explicit) return join24(explicit, "hosts.yml");
23422
+ if (platform2 === "win32") {
23423
+ const appData = (env.AppData ?? env.APPDATA)?.trim();
23424
+ return appData ? join24(appData, "GitHub CLI", "hosts.yml") : void 0;
23425
+ }
23426
+ const xdg = env.XDG_CONFIG_HOME?.trim();
23427
+ if (xdg) return join24(xdg, "gh", "hosts.yml");
23428
+ const home = env.HOME?.trim();
23429
+ return home ? join24(home, ".config", "gh", "hosts.yml") : void 0;
23430
+ }
23431
+ function parseGhHostsAccounts(yaml, host = "github.com") {
23432
+ let hostIndent = null;
23433
+ let usersIndent = null;
23434
+ let loginIndent = null;
23435
+ let activeLogin;
23436
+ const logins = [];
23437
+ for (const raw of yaml.split(/\r?\n/)) {
23438
+ if (!raw.trim() || raw.trimStart().startsWith("#")) continue;
23439
+ const indent = raw.length - raw.trimStart().length;
23440
+ const keyMatch = /^"?([A-Za-z0-9._-]+)"?\s*:(.*)$/.exec(raw.trim());
23441
+ if (!keyMatch) continue;
23442
+ const key = keyMatch[1];
23443
+ if (hostIndent === null) {
23444
+ if (key === host) hostIndent = indent;
23445
+ continue;
23446
+ }
23447
+ if (indent <= hostIndent) break;
23448
+ if (usersIndent !== null && indent > usersIndent) {
23449
+ if (loginIndent === null) loginIndent = indent;
23450
+ if (indent === loginIndent && !/token/i.test(key)) logins.push(key);
23451
+ continue;
23452
+ }
23453
+ usersIndent = null;
23454
+ loginIndent = null;
23455
+ if (key === "users") {
23456
+ usersIndent = indent;
23457
+ continue;
23458
+ }
23459
+ if (key === "user") {
23460
+ const value = keyMatch[2].trim().replace(/^["']|["']$/g, "");
23461
+ if (value) activeLogin = value;
23462
+ }
23463
+ }
23464
+ const unique = [...new Set(logins)];
23465
+ if (!unique.length && activeLogin) unique.push(activeLogin);
23466
+ return unique.map((login) => ({ login, active: login === activeLogin }));
23467
+ }
23468
+ function ghAccountCaveat(announcedLogin, accounts) {
23469
+ if (accounts.length <= 1) return void 0;
23470
+ const active = accounts.find((a) => a.active)?.login;
23471
+ if (active && active !== announcedLogin) {
23472
+ return `NOTE: gh's active account is ${active}, not ${announcedLogin} \u2014 repo reads and pushes use ${active}. A repo that reads as "not found" is that mismatch, not lost access: gh auth switch --user ${announcedLogin}`;
23473
+ }
23474
+ const others = accounts.filter((a) => a.login !== active).map((a) => a.login);
23475
+ return `NOTE: gh has ${accounts.length} accounts (active: ${active ?? "unknown"}; also ${others.join(", ")}) \u2014 a repo that reads as "not found" is usually the wrong active account, not lost access: gh auth status, then gh auth switch`;
23476
+ }
23477
+
23161
23478
  // src/doctor-io.ts
23162
- var import_node_fs28 = require("node:fs");
23479
+ var import_node_fs29 = require("node:fs");
23163
23480
  var import_node_os7 = require("node:os");
23164
23481
  var import_node_path25 = require("node:path");
23165
23482
  var import_node_child_process12 = require("node:child_process");
@@ -23169,7 +23486,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
23169
23486
  function installedClaudePluginVersion() {
23170
23487
  try {
23171
23488
  const file = JSON.parse(
23172
- (0, import_node_fs28.readFileSync)((0, import_node_path25.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23489
+ (0, import_node_fs29.readFileSync)((0, import_node_path25.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
23173
23490
  );
23174
23491
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
23175
23492
  if (versions.length === 0) return void 0;
@@ -23196,7 +23513,7 @@ function readGitignore() {
23196
23513
  const path2 = gitignorePath();
23197
23514
  if (path2 === null) return null;
23198
23515
  try {
23199
- return (0, import_node_fs28.readFileSync)(path2, "utf8");
23516
+ return (0, import_node_fs29.readFileSync)(path2, "utf8");
23200
23517
  } catch {
23201
23518
  return null;
23202
23519
  }
@@ -23205,7 +23522,7 @@ function writeGitignore(content) {
23205
23522
  const path2 = gitignorePath();
23206
23523
  if (path2 === null) return false;
23207
23524
  try {
23208
- (0, import_node_fs28.writeFileSync)(path2, content, "utf8");
23525
+ (0, import_node_fs29.writeFileSync)(path2, content, "utf8");
23209
23526
  return true;
23210
23527
  } catch {
23211
23528
  return false;
@@ -23229,7 +23546,7 @@ async function repoRoot() {
23229
23546
  }
23230
23547
  function hasRepoLocalWorktrees() {
23231
23548
  const root = worktreeRootSync();
23232
- return root !== null && (0, import_node_fs28.existsSync)((0, import_node_path25.join)(root, ".worktrees"));
23549
+ return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path25.join)(root, ".worktrees"));
23233
23550
  }
23234
23551
 
23235
23552
  // src/index.ts
@@ -23250,10 +23567,31 @@ async function readDocsAuditFetch(repo) {
23250
23567
  verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
23251
23568
  };
23252
23569
  }
23570
+ async function githubRepoReachProbe() {
23571
+ const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
23572
+ const repo = parseOriginRepo(remote);
23573
+ if (!repo) return void 0;
23574
+ const reachable = await execFileP2("gh", ["repo", "view", repo, "--json", "name"], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => true).catch(() => false);
23575
+ if (reachable) return { repo, reachable: true };
23576
+ const status = await execFileP2("gh", ["auth", "status"], { timeout: GH_MUTATION_TIMEOUT_MS }).then((r) => `${r.stdout}
23577
+ ${r.stderr ?? ""}`).catch(() => "");
23578
+ const others = parseGhAuthAccounts(status).filter((a) => !a.active).map((a) => a.login);
23579
+ return { repo, reachable: false, otherAccountsThatCanSee: others };
23580
+ }
23581
+ function ghMultiAccountCaveat(announcedLogin) {
23582
+ try {
23583
+ const hostsPath = ghHostsConfigPath(process.env, process.platform);
23584
+ if (!hostsPath || !(0, import_node_fs30.existsSync)(hostsPath)) return void 0;
23585
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs30.readFileSync)(hostsPath, "utf8")));
23586
+ } catch {
23587
+ return void 0;
23588
+ }
23589
+ }
23253
23590
  function mmiDoctorDeps() {
23254
23591
  return {
23255
23592
  githubLogin,
23256
23593
  ghInstalled,
23594
+ githubRepoReach: githubRepoReachProbe,
23257
23595
  awsCallerArn,
23258
23596
  isOrgRepo: () => isOrgRepoRoot(),
23259
23597
  installedPluginVersion: installedClaudePluginVersion,
@@ -23457,18 +23795,18 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
23457
23795
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
23458
23796
  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) => {
23459
23797
  const path2 = (0, import_node_path26.join)(process.cwd(), ".gitignore");
23460
- const current = (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null;
23798
+ const current = (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null;
23461
23799
  const plan = planManagedGitignore(current);
23462
23800
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
23463
23801
  if (opts.json) {
23464
- if (opts.write && plan.changed) (0, import_node_fs29.writeFileSync)(path2, plan.content, "utf8");
23802
+ if (opts.write && plan.changed) (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
23465
23803
  console.log(JSON.stringify(plan, null, 2));
23466
23804
  if (!opts.write && plan.changed) process.exitCode = 1;
23467
23805
  return;
23468
23806
  }
23469
23807
  if (opts.write) {
23470
23808
  if (plan.changed) {
23471
- (0, import_node_fs29.writeFileSync)(path2, plan.content, "utf8");
23809
+ (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
23472
23810
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
23473
23811
  } else {
23474
23812
  console.log("mmi-cli org rules gitignore: up to date");
@@ -23594,7 +23932,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
23594
23932
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
23595
23933
  try {
23596
23934
  const plan = await gcPlan(o.remote, limit);
23597
- if (o.apply && !o.json) console.log(formatGcPlan(plan, false));
23935
+ if (o.apply && !o.json) console.log(formatGcPlan(plan, true));
23598
23936
  let applyResult;
23599
23937
  if (o.apply) {
23600
23938
  const deferredStore = await createDeferredWorktreeStore();
@@ -23658,26 +23996,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
23658
23996
  function acquireWorktreeSetupLock(worktreeRoot) {
23659
23997
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
23660
23998
  const take = () => {
23661
- const fd = (0, import_node_fs29.openSync)(lockPath, "wx");
23999
+ const fd = (0, import_node_fs30.openSync)(lockPath, "wx");
23662
24000
  try {
23663
- (0, import_node_fs29.writeSync)(fd, String(Date.now()));
24001
+ (0, import_node_fs30.writeSync)(fd, String(Date.now()));
23664
24002
  } finally {
23665
- (0, import_node_fs29.closeSync)(fd);
24003
+ (0, import_node_fs30.closeSync)(fd);
23666
24004
  }
23667
24005
  return () => {
23668
24006
  try {
23669
- (0, import_node_fs29.rmSync)(lockPath, { force: true });
24007
+ (0, import_node_fs30.rmSync)(lockPath, { force: true });
23670
24008
  } catch {
23671
24009
  }
23672
24010
  };
23673
24011
  };
23674
24012
  try {
23675
- (0, import_node_fs29.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
24013
+ (0, import_node_fs30.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
23676
24014
  return take();
23677
24015
  } catch {
23678
24016
  try {
23679
- if (Date.now() - (0, import_node_fs29.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
23680
- (0, import_node_fs29.rmSync)(lockPath, { force: true });
24017
+ if (Date.now() - (0, import_node_fs30.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
24018
+ (0, import_node_fs30.rmSync)(lockPath, { force: true });
23681
24019
  return take();
23682
24020
  }
23683
24021
  } catch {
@@ -23687,7 +24025,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
23687
24025
  }
23688
24026
  var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
23689
24027
  withExamples(mutating(
23690
- 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>)").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"),
24028
+ 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"),
23691
24029
  worktreeCreatePlan
23692
24030
  ).action(async (target, o, cmd) => {
23693
24031
  try {
@@ -24119,7 +24457,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
24119
24457
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
24120
24458
  if (o.secretsFile) {
24121
24459
  try {
24122
- vars.push(`secrets=${(0, import_node_fs29.readFileSync)(o.secretsFile, "utf8")}`);
24460
+ vars.push(`secrets=${(0, import_node_fs30.readFileSync)(o.secretsFile, "utf8")}`);
24123
24461
  } catch (e) {
24124
24462
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
24125
24463
  }
@@ -24385,11 +24723,14 @@ function resolveCreatePriority(raw, command) {
24385
24723
  });
24386
24724
  }
24387
24725
  }
24388
- function resolveCreateType(raw, command) {
24726
+ function resolveCreateType(raw, command, labels) {
24389
24727
  if (raw === void 0 || raw === "") {
24390
- fail(`${command}: --type is required (bug | feature | task) unless --batch supplies it per row`, {
24728
+ const nearMiss = labels?.find((l) => ISSUE_TYPES.includes(l));
24729
+ const hint = nearMiss ? ` (you passed --label ${nearMiss} \u2014 did you mean --type ${nearMiss}?)` : "";
24730
+ fail(`${command}: --type is required (bug | feature | task) unless --batch supplies it per row${hint}`, {
24391
24731
  code: ERROR_CODES.ERR_MISSING_FLAG,
24392
- offending_flag: "--type"
24732
+ offending_flag: "--type",
24733
+ ...nearMiss ? { corrected_command: `${command} --type ${nearMiss} \u2026` } : {}
24393
24734
  });
24394
24735
  }
24395
24736
  if (!ISSUE_TYPES.includes(raw)) {
@@ -24403,12 +24744,12 @@ function resolveCreateType(raw, command) {
24403
24744
  }
24404
24745
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
24405
24746
  withExamples(mutating(
24406
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
24747
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
24407
24748
  // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
24408
24749
  // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
24409
24750
  // (`--parent`) and title-source are validated by the action on a real run.
24410
24751
  (opts) => {
24411
- const type = resolveCreateType(opts.type, "issue create");
24752
+ const type = resolveCreateType(opts.type, "issue create", opts.label);
24412
24753
  const priority = resolveCreatePriority(opts.priority, "issue create");
24413
24754
  return { command: "issue create", type, title: opts.title ?? opts.titleFile, priority, repo: opts.repo };
24414
24755
  }
@@ -24421,7 +24762,7 @@ withExamples(mutating(
24421
24762
  let extraLabels = [];
24422
24763
  let targetRepo2;
24423
24764
  try {
24424
- issueType = resolveCreateType(o.type, "issue create");
24765
+ issueType = resolveCreateType(o.type, "issue create", o.label);
24425
24766
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
24426
24767
  body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
24427
24768
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
@@ -24476,6 +24817,16 @@ withExamples(mutating(
24476
24817
  "--type is one of bug|feature|task.",
24477
24818
  "--dry-run and --validate-only pre-validate the create plan before any issue is written."
24478
24819
  ]);
24820
+ async function readParentField(number, repo) {
24821
+ let payload;
24822
+ try {
24823
+ payload = await ghJson(["api", `repos/${repo}/issues/${number}`]);
24824
+ } catch (e) {
24825
+ const err = e;
24826
+ return { parentReadError: (err.stderr || err.message || String(e)).trim() };
24827
+ }
24828
+ return resolveParentField(payload);
24829
+ }
24479
24830
  issue.command("view <number>").description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
24480
24831
  const n = Number(number);
24481
24832
  if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
@@ -24486,11 +24837,11 @@ issue.command("view <number>").description('read an issue as structured JSON \u2
24486
24837
  if (o.comments || o.context) {
24487
24838
  const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
24488
24839
  const data2 = await runIssueViewContext(queryDeps(), { number: n, repo, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: fields, boardProjectId });
24489
- console.log(JSON.stringify(data2));
24840
+ console.log(JSON.stringify({ ...data2, ...await readParentField(n, repo) }));
24490
24841
  return;
24491
24842
  }
24492
24843
  const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
24493
- console.log(JSON.stringify(data));
24844
+ console.log(JSON.stringify({ ...data, ...await readParentField(n, repo) }));
24494
24845
  } catch (e) {
24495
24846
  const err = e;
24496
24847
  const raw = (err.stderr || err.message || String(e)).trim();
@@ -24536,7 +24887,7 @@ issue.command("discover-related").description("find related issues for an existi
24536
24887
  return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
24537
24888
  }
24538
24889
  });
24539
- issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
24890
+ jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
24540
24891
  const defaultRepo = await resolveRepo(o.repo);
24541
24892
  try {
24542
24893
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
@@ -24547,7 +24898,7 @@ issue.command("link-child <parent> <child>").description("link an existing issue
24547
24898
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
24548
24899
  }
24549
24900
  });
24550
- issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
24901
+ jsonParity(issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
24551
24902
  let parsed;
24552
24903
  try {
24553
24904
  parsed = parseIssueRef(ref);
@@ -24570,7 +24921,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
24570
24921
  return fail(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
24571
24922
  }
24572
24923
  });
24573
- issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
24924
+ jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
24574
24925
  let parsed;
24575
24926
  try {
24576
24927
  parsed = parseIssueRef(ref);
@@ -24755,10 +25106,10 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
24755
25106
  });
24756
25107
  async function listCiWorkflowPaths(cwd = process.cwd()) {
24757
25108
  const wfDir = (0, import_node_path26.join)(cwd, ".github", "workflows");
24758
- if (!(0, import_node_fs29.existsSync)(wfDir)) return [];
24759
- return (0, import_node_fs29.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
25109
+ if (!(0, import_node_fs30.existsSync)(wfDir)) return [];
25110
+ return (0, import_node_fs30.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
24760
25111
  try {
24761
- return workflowReportsPrChecks((0, import_node_fs29.readFileSync)((0, import_node_path26.join)(wfDir, name), "utf8"));
25112
+ return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path26.join)(wfDir, name), "utf8"));
24762
25113
  } catch {
24763
25114
  return true;
24764
25115
  }
@@ -24786,15 +25137,15 @@ function ciAuditDeps() {
24786
25137
  readSeedFile: (path2) => {
24787
25138
  if (!root) return null;
24788
25139
  const fullPath = (0, import_node_path26.join)(root, path2);
24789
- return (0, import_node_fs29.existsSync)(fullPath) ? (0, import_node_fs29.readFileSync)(fullPath, "utf8") : null;
25140
+ return (0, import_node_fs30.existsSync)(fullPath) ? (0, import_node_fs30.readFileSync)(fullPath, "utf8") : null;
24790
25141
  }
24791
25142
  };
24792
25143
  }
24793
25144
  function hubRoot() {
24794
25145
  const fromPkg = (0, import_node_path26.join)(__dirname, "..", "..");
24795
25146
  const marker = "skills/bootstrap/seeds/manifest.json";
24796
- if ((0, import_node_fs29.existsSync)((0, import_node_path26.join)(fromPkg, marker))) return fromPkg;
24797
- if ((0, import_node_fs29.existsSync)((0, import_node_path26.join)(process.cwd(), marker))) return process.cwd();
25147
+ if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(fromPkg, marker))) return fromPkg;
25148
+ if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(process.cwd(), marker))) return process.cwd();
24798
25149
  return null;
24799
25150
  }
24800
25151
  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) => {
@@ -24816,6 +25167,9 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
24816
25167
  pollChecks: () => pollRestPrChecks(number, repo),
24817
25168
  pollMergeable: () => pollRestPrMergeable(number, repo),
24818
25169
  pollRateLimit: () => fetchRestCorePool(),
25170
+ // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
25171
+ // reading as "your tests failed". One call per failing run, only at the verdict.
25172
+ diagnoseFailure: () => diagnoseFailedRestChecks(number, repo),
24819
25173
  baseBranch,
24820
25174
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
24821
25175
  log: (message) => console.warn(message),
@@ -24831,6 +25185,8 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
24831
25185
  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>.`);
24832
25186
  } else if (result.status === "rate-limited") {
24833
25187
  printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
25188
+ } else if (result.detail === "ci-budget-kill") {
25189
+ printLine(`pr checks-wait: failure (wall-clock budget kill, NOT a test failure) \u2014 ${result.reason}`);
24834
25190
  } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
24835
25191
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
24836
25192
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
@@ -24861,6 +25217,9 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
24861
25217
  // other base), so the fast-fail message's base branch is always 'development' here.
24862
25218
  pollMergeable: () => pollRestPrMergeable(prNumber, repo),
24863
25219
  pollRateLimit: () => fetchRestCorePool(),
25220
+ // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
25221
+ // then read its own wall-clock kill as a broken diff.
25222
+ diagnoseFailure: () => diagnoseFailedRestChecks(prNumber, repo),
24864
25223
  baseBranch: "development",
24865
25224
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
24866
25225
  log: (message) => console.warn(message),
@@ -24928,7 +25287,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
24928
25287
  }
24929
25288
  if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
24930
25289
  });
24931
- pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
25290
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)")).action(async (number, o) => {
24932
25291
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
24933
25292
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
24934
25293
  const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
@@ -24947,6 +25306,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24947
25306
  pollChecks: () => pollRestPrChecks(number, repo),
24948
25307
  pollMergeable: () => pollRestPrMergeable(number, repo),
24949
25308
  pollRateLimit: () => fetchRestCorePool(),
25309
+ diagnoseFailure: () => diagnoseFailedRestChecks(number, repo),
24950
25310
  baseBranch,
24951
25311
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
24952
25312
  log: (message) => console.warn(message),
@@ -25033,7 +25393,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
25033
25393
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
25034
25394
  beforeWorktrees,
25035
25395
  startingPath,
25036
- pathExists: (p) => (0, import_node_fs29.existsSync)(p),
25396
+ pathExists: (p) => (0, import_node_fs30.existsSync)(p),
25037
25397
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
25038
25398
  teardownWorktreeStage,
25039
25399
  deferredStore,
@@ -25078,7 +25438,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
25078
25438
  });
25079
25439
  registerQueryCommands(program2);
25080
25440
  registerWorktreeCommands(program2);
25081
- registerIssueLifecycleCommands(program2);
25441
+ registerIssueLifecycleCommands(program2, { attach: attachToProject });
25082
25442
  registerTrainCommands(program2);
25083
25443
  registerDeployCommands(program2);
25084
25444
  registerDiscoveryCommands(program2);
@@ -25166,7 +25526,8 @@ function renderDeployLine(d) {
25166
25526
  else if (d.runUrl) parts.push(`run ${d.runUrl}`);
25167
25527
  if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
25168
25528
  else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
25169
- else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
25529
+ else if (d.runId != null) parts.push(`deploy: UNVERIFIED \u2014 dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
25530
+ else parts.push("deploy: UNVERIFIED \u2014 no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
25170
25531
  return parts.join("; ");
25171
25532
  }
25172
25533
  function renderTrainApply(commandName, r) {
@@ -25336,7 +25697,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
25336
25697
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(cfg));
25337
25698
  if (!verdict.ok) {
25338
25699
  if (o.json) {
25339
- console.log(JSON.stringify({ repo, train: false, error: verdict.error }));
25700
+ console.log(JSON.stringify({ repo, train: false, verified: false, error: verdict.error }));
25340
25701
  process.exitCode = 1;
25341
25702
  return;
25342
25703
  }
@@ -25344,7 +25705,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
25344
25705
  }
25345
25706
  const a = verdict.authority;
25346
25707
  if (o.json) {
25347
- console.log(JSON.stringify(a));
25708
+ console.log(JSON.stringify({ ...a, verified: true }));
25348
25709
  if (!a.train) process.exitCode = 1;
25349
25710
  return;
25350
25711
  }
@@ -25373,10 +25734,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
25373
25734
  targets = resolution.targets;
25374
25735
  }
25375
25736
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
25376
- const fileMatrix = (0, import_node_fs29.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs29.readFileSync)("access-matrix.json", "utf8")) : {};
25737
+ const fileMatrix = (0, import_node_fs30.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs30.readFileSync)("access-matrix.json", "utf8")) : {};
25377
25738
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
25378
25739
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
25379
- const fileContracts = (0, import_node_fs29.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs29.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
25740
+ const fileContracts = (0, import_node_fs30.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs30.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
25380
25741
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
25381
25742
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
25382
25743
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -25396,6 +25757,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
25396
25757
  banner: opts.banner,
25397
25758
  preflight: opts.preflight,
25398
25759
  fast: opts.fast || opts.self,
25760
+ self: opts.self,
25399
25761
  json: opts.json,
25400
25762
  verbose: opts.verbose
25401
25763
  },
@@ -25409,7 +25771,7 @@ function directoryBytes(path2) {
25409
25771
  let total = 0;
25410
25772
  let entries;
25411
25773
  try {
25412
- entries = (0, import_node_fs29.readdirSync)(path2, { withFileTypes: true });
25774
+ entries = (0, import_node_fs30.readdirSync)(path2, { withFileTypes: true });
25413
25775
  } catch {
25414
25776
  return 0;
25415
25777
  }
@@ -25418,7 +25780,7 @@ function directoryBytes(path2) {
25418
25780
  if (entry.isDirectory()) total += directoryBytes(child2);
25419
25781
  else {
25420
25782
  try {
25421
- total += (0, import_node_fs29.statSync)(child2).size;
25783
+ total += (0, import_node_fs30.statSync)(child2).size;
25422
25784
  } catch {
25423
25785
  }
25424
25786
  }
@@ -25426,25 +25788,25 @@ function directoryBytes(path2) {
25426
25788
  return total;
25427
25789
  }
25428
25790
  function listDirEntries(dir) {
25429
- return (0, import_node_fs29.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
25791
+ return (0, import_node_fs30.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
25430
25792
  }
25431
25793
  function readInstalledPluginRefs(home) {
25432
25794
  const p = installedPluginsPath(home);
25433
- if (!(0, import_node_fs29.existsSync)(p)) return [];
25795
+ if (!(0, import_node_fs30.existsSync)(p)) return [];
25434
25796
  try {
25435
- return installedPluginPaths((0, import_node_fs29.readFileSync)(p, "utf8"));
25797
+ return installedPluginPaths((0, import_node_fs30.readFileSync)(p, "utf8"));
25436
25798
  } catch {
25437
25799
  return null;
25438
25800
  }
25439
25801
  }
25440
25802
  function pluginCacheFsDeps(home, dirBytes) {
25441
25803
  return {
25442
- exists: (p) => (0, import_node_fs29.existsSync)(p),
25443
- listVersionDirs: (root) => (0, import_node_fs29.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
25804
+ exists: (p) => (0, import_node_fs30.existsSync)(p),
25805
+ listVersionDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
25444
25806
  dirBytes,
25445
- listStagingDirs: (root) => (0, import_node_fs29.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
25807
+ listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
25446
25808
  try {
25447
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path26.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs29.statSync)(p).mtimeMs) };
25809
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path26.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
25448
25810
  } catch {
25449
25811
  return { name: d.name, mtimeMs: Date.now() };
25450
25812
  }
@@ -25459,9 +25821,9 @@ function stagingApplyFsGuard(home) {
25459
25821
  referencedPaths: () => readInstalledPluginRefs(home),
25460
25822
  mtimeMs: (name) => {
25461
25823
  const p = (0, import_node_path26.join)(stagingRoot, name);
25462
- if (!(0, import_node_fs29.existsSync)(p)) return null;
25824
+ if (!(0, import_node_fs30.existsSync)(p)) return null;
25463
25825
  try {
25464
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs29.statSync)(q).mtimeMs);
25826
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
25465
25827
  } catch {
25466
25828
  return null;
25467
25829
  }
@@ -25477,7 +25839,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
25477
25839
  { withBytes: true }
25478
25840
  );
25479
25841
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
25480
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs29.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
25842
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs30.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
25481
25843
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
25482
25844
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
25483
25845
  else console.log(renderPluginCachePlan(plan, result));
@@ -25502,7 +25864,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
25502
25864
  hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
25503
25865
  ghLogin: githubLogin
25504
25866
  });
25505
- const line = whoamiLine(report);
25867
+ const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
25506
25868
  if (line) io.log(line);
25507
25869
  },
25508
25870
  boardSlice: (io) => runBoardSlice(io, {