@mutmutco/cli 4.3.7 → 4.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +564 -223
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5226,7 +5226,7 @@ function unknownCommandDomainGuide(parentPath, token) {
5226
5226
  }
5227
5227
 
5228
5228
  // src/command-composition.ts
5229
- var import_node_os20 = require("node:os");
5229
+ var import_node_os21 = require("node:os");
5230
5230
  var import_node_path46 = require("node:path");
5231
5231
 
5232
5232
  // src/board-read.ts
@@ -6998,6 +6998,19 @@ function isPromotionBase(base, track) {
6998
6998
  }
6999
6999
 
7000
7000
  // src/readiness-audit.ts
7001
+ async function probeHttpBounded(url, timeoutMs = 5e3) {
7002
+ try {
7003
+ const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
7004
+ return { ok: res.ok, status: res.status, url };
7005
+ } catch (e) {
7006
+ return { ok: false, error: e.message, url };
7007
+ }
7008
+ }
7009
+ async function probePublicHealth(publicUrl) {
7010
+ const root = await probeHttpBounded(publicUrl);
7011
+ if (root.status !== 401 && root.status !== 403) return root;
7012
+ return probeHttpBounded(`${publicUrl.replace(/\/$/, "")}/health`);
7013
+ }
7001
7014
  var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
7002
7015
  function pickTenantDeployRun(rows, slug, stage) {
7003
7016
  const escaped = slug.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
@@ -7016,7 +7029,8 @@ function tenantRuntimeHints(stage, fact, probe) {
7016
7029
  if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
7017
7030
  if (!fact.domain) hints.push(`DEPLOY#${stage} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
7018
7031
  if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
7019
- if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
7032
+ if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root is auth-walled (401/403) and ${probe.url} answered ${probe.status ?? probe.error}; expose a 2xx /health route or fix the app \u2014 the edge is fine.`);
7033
+ else if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
7020
7034
  if (stage === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
7021
7035
  return hints;
7022
7036
  }
@@ -9435,7 +9449,7 @@ function invalidReference(ref) {
9435
9449
  return new Error(`invalid reference "${ref}" \u2014 expected ${ISSUE_REF_SHAPES}`);
9436
9450
  }
9437
9451
  function parseIssueRef(ref, expectedRepo) {
9438
- const trimmed = ref.trim();
9452
+ const trimmed = String(ref).trim();
9439
9453
  const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
9440
9454
  const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
9441
9455
  const bare = trimmed.match(/^#?(\d+)$/);
@@ -11061,14 +11075,7 @@ function writableOrUnknown(writable) {
11061
11075
  function renderBoardSource() {
11062
11076
  return "source: live";
11063
11077
  }
11064
- async function readBoard(options, deps = {}) {
11065
- const cfg = resolveBoardConfig(options.config);
11066
- const client = deps.client ?? defaultGitHubClient();
11067
- let collected;
11068
- let writable;
11069
- let pullRequests;
11070
- let github;
11071
- let snapshotFallback;
11078
+ async function collectBoardPreferringSnapshot(cfg, options, deps) {
11072
11079
  const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
11073
11080
  {
11074
11081
  owner: cfg.projectOwner,
@@ -11094,19 +11101,37 @@ async function readBoard(options, deps = {}) {
11094
11101
  warnings.push(message2);
11095
11102
  partial = true;
11096
11103
  }
11104
+ return {
11105
+ collected: {
11106
+ items: read.items,
11107
+ viewer: snapshot.viewer,
11108
+ repo: currentRepo,
11109
+ projectId: snapshot.project.id,
11110
+ projectTitle: snapshot.project.title,
11111
+ warnings,
11112
+ partial
11113
+ },
11114
+ snapshot
11115
+ };
11116
+ }
11117
+ const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
11118
+ if (attempt?.state === "unavailable") {
11119
+ collected.warnings.push(`Hub board snapshot unavailable (${attempt.reason}) \u2014 served by the direct user-auth read (emergency fallback)`);
11120
+ }
11121
+ return { collected };
11122
+ }
11123
+ async function readBoard(options, deps = {}) {
11124
+ const cfg = resolveBoardConfig(options.config);
11125
+ const client = deps.client ?? defaultGitHubClient();
11126
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
11127
+ let writable;
11128
+ let pullRequests;
11129
+ let github;
11130
+ if (snapshot) {
11097
11131
  for (const unreadable of snapshot.unreadableRepos) {
11098
- warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
11099
- partial = true;
11132
+ collected.warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
11133
+ collected.partial = true;
11100
11134
  }
11101
- collected = {
11102
- items: read.items,
11103
- viewer: snapshot.viewer,
11104
- repo: currentRepo,
11105
- projectId: snapshot.project.id,
11106
- projectTitle: snapshot.project.title,
11107
- warnings,
11108
- partial
11109
- };
11110
11135
  writable = {
11111
11136
  repos: new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase())),
11112
11137
  unknown: new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()))
@@ -11114,11 +11139,6 @@ async function readBoard(options, deps = {}) {
11114
11139
  pullRequests = snapshot.pullRequests;
11115
11140
  github = snapshot.github;
11116
11141
  } else {
11117
- if (attempt?.state === "unavailable") snapshotFallback = attempt.reason;
11118
- collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
11119
- if (snapshotFallback) {
11120
- collected.warnings.push(`Hub board snapshot unavailable (${snapshotFallback}) \u2014 served by the direct user-auth read (emergency fallback)`);
11121
- }
11122
11142
  const probed = await resolveWritableReposForClaimables(collected.items, client);
11123
11143
  collected.warnings.push(...probed.warnings);
11124
11144
  collected.partial = collected.partial || probed.partial;
@@ -12057,21 +12077,45 @@ async function moveBoardItem(options, deps = {}) {
12057
12077
  partial: false
12058
12078
  };
12059
12079
  }
12060
- async function prepareClaimContext(options, selectors, deps, collected) {
12080
+ async function resolveClaimWritable(collected, client, snapshot, unscanned) {
12081
+ const probe = async (items) => {
12082
+ const probed2 = await resolveWritableReposForClaimables(items, client);
12083
+ collected.warnings.push(...probed2.warnings);
12084
+ collected.partial = collected.partial || probed2.partial;
12085
+ return probed2;
12086
+ };
12087
+ if (!snapshot) {
12088
+ const probed2 = await probe(collected.items);
12089
+ return { repos: probed2.repos, unknown: probed2.unknown };
12090
+ }
12091
+ for (const unreadable of snapshot.unreadableRepos) {
12092
+ collected.warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
12093
+ collected.partial = true;
12094
+ }
12095
+ const repos = new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase()));
12096
+ const unknown = new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()));
12097
+ const missed = unscanned.filter((item) => !repos.has(item.repository.toLowerCase()) && !unknown.has(item.repository.toLowerCase()));
12098
+ if (!missed.length) return { repos, unknown };
12099
+ const probed = await probe(missed);
12100
+ return { repos: /* @__PURE__ */ new Set([...repos, ...probed.repos]), unknown: /* @__PURE__ */ new Set([...unknown, ...probed.unknown]) };
12101
+ }
12102
+ async function prepareClaimContext(options, selectors, deps, collected, snapshot) {
12061
12103
  const cfg = resolveBoardConfig(options.config);
12062
12104
  const client = deps.client ?? defaultGitHubClient();
12063
12105
  const board = { owner: cfg.projectOwner, number: cfg.projectNumber };
12106
+ const unscanned = [];
12064
12107
  for (const selector of selectors) {
12065
12108
  try {
12066
12109
  findBoardItem(collected.items, selector, board);
12067
12110
  } catch {
12068
12111
  const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
12069
- if (fallback) collected.items.push(fallback);
12112
+ if (fallback) {
12113
+ collected.items.push(fallback);
12114
+ unscanned.push(fallback);
12115
+ }
12070
12116
  }
12071
12117
  }
12072
- const writable = await resolveWritableReposForClaimables(collected.items, client);
12073
- collected.warnings.push(...writable.warnings);
12074
- collected.partial = collected.partial || writable.partial;
12118
+ const writable = await resolveClaimWritable(collected, client, snapshot, unscanned);
12075
12119
  const report = {
12076
12120
  project: { owner: cfg.projectOwner, number: cfg.projectNumber, id: collected.projectId, title: collected.projectTitle || String(cfg.projectNumber) },
12077
12121
  viewer: collected.viewer,
@@ -12209,14 +12253,15 @@ async function claimOneBoardItem(ctx, selector, options) {
12209
12253
  }
12210
12254
  async function claimBoardIssue(options, deps = {}) {
12211
12255
  const cfg = resolveBoardConfig(options.config);
12212
- const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
12256
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
12213
12257
  const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
12214
- const ctx = await prepareClaimContext(options, [selector], deps, collected);
12215
- return claimOneBoardItem(ctx, selector, options);
12258
+ const ctx = await prepareClaimContext(options, [selector], deps, collected, snapshot);
12259
+ const result = await claimOneBoardItem(ctx, selector, options);
12260
+ return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
12216
12261
  }
12217
12262
  async function claimBoardIssues(options, deps = {}) {
12218
12263
  const cfg = resolveBoardConfig(options.config);
12219
- const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
12264
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
12220
12265
  const selectors = [];
12221
12266
  const seen = /* @__PURE__ */ new Set();
12222
12267
  for (const raw of options.selectors) {
@@ -12226,7 +12271,7 @@ async function claimBoardIssues(options, deps = {}) {
12226
12271
  seen.add(key);
12227
12272
  selectors.push(selector);
12228
12273
  }
12229
- const ctx = await prepareClaimContext(options, selectors, deps, collected);
12274
+ const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
12230
12275
  const results = new Array(selectors.length);
12231
12276
  let next = 0;
12232
12277
  const worker = async () => {
@@ -12247,7 +12292,9 @@ async function claimBoardIssues(options, deps = {}) {
12247
12292
  viewer: ctx.report.viewer,
12248
12293
  repo: ctx.report.repo,
12249
12294
  results,
12250
- failed: results.filter((result) => !result.claimed).length
12295
+ failed: results.filter((result) => !result.claimed).length,
12296
+ // #6162: as in claimBoardIssue — the emergency user-auth fallback is named with the verdict.
12297
+ ...ctx.report.warnings.length ? { warnings: [...ctx.report.warnings] } : {}
12251
12298
  };
12252
12299
  }
12253
12300
  async function moveBoardIssues(options, deps = {}) {
@@ -15403,10 +15450,10 @@ var rollout_plan_default = {
15403
15450
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
15404
15451
  },
15405
15452
  baseline: {
15406
- version: "4.3.7",
15407
- tag: "v4.3.7",
15408
- commit: "d7f9efaf3e66",
15409
- npm: "@mutmutco/cli@4.3.7"
15453
+ version: "4.3.9",
15454
+ tag: "v4.3.9",
15455
+ commit: "64fc1be95e37",
15456
+ npm: "@mutmutco/cli@4.3.9"
15410
15457
  },
15411
15458
  exitCriterion: "fleet-n-of-n",
15412
15459
  hubOnlyShortcut: "forbidden",
@@ -15423,14 +15470,14 @@ var rollout_plan_default = {
15423
15470
  repo: "mutmutco/mmi-hub",
15424
15471
  role: "canary",
15425
15472
  schedule: "train",
15426
- v3Target: "v4.3.7"
15473
+ v3Target: "v4.3.9"
15427
15474
  }
15428
15475
  ],
15429
15476
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
15430
15477
  rollback: {
15431
15478
  independent: true,
15432
- mechanism: "npm dist-tag latest -> 4.3.7 and redeploy the Hub Lambda from tag v4.3.7 (d7f9efaf3e66); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15433
- v3Target: "v4.3.7 (@mutmutco/cli@4.3.7, tag commit d7f9efaf3e66 \u2014 last known-good release carrying the repo-index v4-only contract)"
15479
+ mechanism: "npm dist-tag latest -> 4.3.9 and redeploy the Hub Lambda from tag v4.3.9 (64fc1be95e37); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15480
+ v3Target: "v4.3.9 (@mutmutco/cli@4.3.9, tag commit 64fc1be95e37 \u2014 last known-good release carrying the repo-index v4-only contract)"
15434
15481
  }
15435
15482
  },
15436
15483
  {
@@ -18286,7 +18333,8 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
18286
18333
  if (dispatch.deployStatus !== "success") {
18287
18334
  return {
18288
18335
  ...dispatch,
18289
- note: `${dispatch.note}; Jerv Gateway deploy deferred until package publication is proven green`
18336
+ note: `${dispatch.note}; Jerv Gateway deploy deferred until package publication is proven green`,
18337
+ workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "pending" }]
18290
18338
  };
18291
18339
  }
18292
18340
  try {
@@ -18964,6 +19012,7 @@ Resume it: ${resumeCommand}
18964
19012
 
18965
19013
  // src/train-doctor.ts
18966
19014
  var import_node_fs23 = require("node:fs");
19015
+ var import_node_os13 = require("node:os");
18967
19016
  var import_node_path22 = require("node:path");
18968
19017
 
18969
19018
  // src/doctor-io.ts
@@ -20599,8 +20648,11 @@ function deployPhaseInput(model, dispatch, opts = {}) {
20599
20648
  note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
20600
20649
  };
20601
20650
  }
20602
- case "registry-publish":
20651
+ case "registry-publish": {
20652
+ const hostRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
20653
+ if (hostRun) return { state: runRowState(hostRun, dispatch.deployStatus), ...sha ? { sha } : {}, workflow: hostRun.workflow, note: dispatch.note };
20603
20654
  return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
20655
+ }
20604
20656
  case "tenant-container":
20605
20657
  case "solo-container":
20606
20658
  case "static-cdn":
@@ -20661,6 +20713,7 @@ function releasePhaseInputsFromDispatch(model, dispatch, publishDispatch, publis
20661
20713
  function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
20662
20714
  const deployRun = rows.find((r) => r.workflow === "deploy.yml");
20663
20715
  const publishRun = rows.find((r) => r.workflow === "publish.yml");
20716
+ const hostRun = rows.find((r) => r.workflow === "jerv-gateway");
20664
20717
  switch (model) {
20665
20718
  case "hub-serverless":
20666
20719
  return {
@@ -20679,7 +20732,9 @@ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
20679
20732
  };
20680
20733
  case "registry-publish":
20681
20734
  return {
20682
- deploy: { state: "skipped", sha: tagSha, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" },
20735
+ // #884/#6145: an operator-host deploy leg (Jerv-Hub's Gateway) outranks 'skipped' here exactly as
20736
+ // in deployPhaseInput — it carries no runId, so no run metadata and no resume re-verification.
20737
+ deploy: hostRun ? { state: runRowState(hostRun, "pending"), sha: tagSha, workflow: hostRun.workflow, note: "jerv-gateway operator-host deploy on the release SHA" } : { state: "skipped", sha: tagSha, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" },
20683
20738
  publish: publishRun ? {
20684
20739
  state: runRowState(publishRun, "pending"),
20685
20740
  sha: tagSha,
@@ -21046,12 +21101,14 @@ async function selfConvergeTrainCli(input) {
21046
21101
  var TRAIN_LANES = ["release", "rcand", "hotfix"];
21047
21102
  var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
21048
21103
  var SCRATCH_BRANCH_GLOBS = ["train/check/*", "hotfix-fold/*--port-*"];
21104
+ var COMPOSE_GUARD_HARD_FAIL = /^secrets preflight: .*noEnvFile is (not )?true.*$/m;
21049
21105
  function readLocalWorkflowsDefault(root) {
21050
21106
  const dir = (0, import_node_path22.join)(root, ".github", "workflows");
21051
21107
  let names;
21052
21108
  try {
21053
21109
  names = (0, import_node_fs23.readdirSync)(dir);
21054
- } catch {
21110
+ } catch (e) {
21111
+ if (e.code === "ENOENT") return [];
21055
21112
  return null;
21056
21113
  }
21057
21114
  const files = [];
@@ -21063,6 +21120,25 @@ function readLocalWorkflowsDefault(root) {
21063
21120
  }
21064
21121
  return files;
21065
21122
  }
21123
+ var NPM_REGISTRY = "https://registry.npmjs.org";
21124
+ async function probeRegistryWhoami(train) {
21125
+ const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path22.join)((0, import_node_os13.tmpdir)(), "mmi-npmrc-"));
21126
+ const rc = (0, import_node_path22.join)(dir, "npmrc");
21127
+ try {
21128
+ (0, import_node_fs23.writeFileSync)(rc, "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n");
21129
+ await train.runSelf(["secrets", "use", "npm/NPM_TOKEN", "--slug", "_org", "--", "npm", "whoami", "--registry", NPM_REGISTRY, "--userconfig", rc]);
21130
+ return { kind: "ok", detail: "the registry answered a login" };
21131
+ } catch (e) {
21132
+ const err = e;
21133
+ const stderr = typeof err.stderr === "string" ? err.stderr : "";
21134
+ const first = (clean2(stderr) || message(e)).split("\n")[0] ?? "";
21135
+ if (err.code === SECRETS_USE_WRAPPER_EXIT_CODE) return { kind: "unprobed", detail: first };
21136
+ if (/\bE401\b|401 Unauthorized/.test(stderr)) return { kind: "rejected", detail: "npm whoami answered 401 Unauthorized" };
21137
+ return { kind: "unverified", detail: first };
21138
+ } finally {
21139
+ (0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
21140
+ }
21141
+ }
21066
21142
  var defaultDeps2 = {
21067
21143
  cliVersion: resolveClientVersion,
21068
21144
  fetchReleasedVersion: fetchNpmReleasedVersion,
@@ -21080,6 +21156,14 @@ var defaultDeps2 = {
21080
21156
  }
21081
21157
  },
21082
21158
  healAlignmentLeg: healStaleAlignmentLeg,
21159
+ readSurfacesRaw: (cwd) => {
21160
+ try {
21161
+ return (0, import_node_fs23.readFileSync)((0, import_node_path22.join)(cwd, "surfaces.json"), "utf8");
21162
+ } catch {
21163
+ return void 0;
21164
+ }
21165
+ },
21166
+ registryWhoami: probeRegistryWhoami,
21083
21167
  env: process.env
21084
21168
  };
21085
21169
  function message(e) {
@@ -21281,12 +21365,12 @@ async function runTrainDoctor(input) {
21281
21365
  ...!hints.hasMainBranch ? ["main"] : [],
21282
21366
  ...!hints.hasRcBranch && track === "full" ? ["rc"] : []
21283
21367
  ];
21284
- if (missing.length) add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `train branch(es) missing on origin: ${missing.join(", ")}`, remedy: `bootstrap the repo train: \`mmi-cli bootstrap apply ${repo} --execute\` (from the MMI-Hub root), then rerun` });
21368
+ if (missing.length) add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `train branch(es) missing on origin: ${missing.join(", ")}`, remedy: `bootstrap the repo train: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), then rerun` });
21285
21369
  }
21286
21370
  try {
21287
21371
  const required = await discoverRequiredCheckContexts(train, ctx, stage);
21288
21372
  if (required.length === 0) {
21289
- add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `no ruleset requires a status check on ${stage} \u2014 the train would tag without a gate`, remedy: `activate the product ruleset: \`mmi-cli bootstrap apply ${repo} --execute\` (from the MMI-Hub root), or \`mmi-cli devops ci audit --repo ${repo}\`` });
21373
+ add({ code: "bootstrap-gap", severity: "warning", source: "origin", title: `no ruleset requires a status check on ${stage} \u2014 the train tags without a check wait (the GitHub push gate is the backstop)`, remedy: `activate the product ruleset: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), or \`mmi-cli devops ci audit --repo ${repo}\`` });
21290
21374
  } else {
21291
21375
  try {
21292
21376
  assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
@@ -21357,13 +21441,25 @@ async function runTrainDoctor(input) {
21357
21441
  add({ code: "npm-major-mismatch", severity: "blocker", source: "local", title: "local npm major differs from the npm CI declares (#5666)", remedy: message(e) });
21358
21442
  }
21359
21443
  }
21444
+ if (/"publishVisibility"\s*:\s*"private"/.test(deps.readSurfacesRaw(cwd) ?? "")) {
21445
+ const probe = await deps.registryWhoami(train);
21446
+ if (probe.kind === "rejected") {
21447
+ add({ code: "npm-token-rejected", severity: "warning", source: "origin", title: `the vault npm token (_org npm/NPM_TOKEN) is REJECTED by registry.npmjs.org \u2014 ${probe.detail}; every authenticated \`npm view\` of a restricted package will 404 exactly like an absent one`, remedy: 'rotate the org token: mint a fresh npm automation token, store it with `mmi-cli vault secrets set npm/NPM_TOKEN --slug _org` (master or an exact grant), and rerun. Until whoami answers a login, a restricted-package 404 proves nothing \u2014 never read it as "not published" (#6184)' });
21448
+ } else if (probe.kind === "unprobed") {
21449
+ add({ code: "npm-token-rejected", severity: "info", source: "origin", title: `the vault npm token was not probed (no _org npm/NPM_TOKEN grant, or a vault error): ${probe.detail}`, remedy: "nothing here \u2014 the release does not depend on it (CI publishes through OIDC Trusted Publishing). Before verifying a private publish by hand, run the whoami probe from an account holding the `_org` npm/NPM_TOKEN grant" });
21450
+ } else if (probe.kind === "unverified") {
21451
+ unverified("the vault npm token (`npm whoami` through the `_org` npm/NPM_TOKEN hop)", probe.detail);
21452
+ }
21453
+ }
21360
21454
  try {
21361
- await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo]);
21455
+ await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
21362
21456
  } catch (e) {
21363
21457
  const err = e;
21364
21458
  const text = [typeof err.stdout === "string" ? err.stdout : "", typeof err.stderr === "string" ? err.stderr : "", message(e)].join("\n");
21365
21459
  if (/^missing /m.test(text)) {
21366
- add({ code: "secrets-missing", severity: "blocker", source: "origin", title: `required ${stage} secret name(s) are absent: ${clean2(text.match(/^missing .*$/m)?.[0] ?? "")}`, remedy: `provision them in the vault (\`mmi-cli vault secrets request <KEY> --repo ${repo}\`), then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}\`` });
21460
+ add({ code: "secrets-missing", severity: "blocker", source: "origin", title: `required ${stage} secret name(s) are absent: ${clean2(text.match(/^missing .*$/m)?.[0] ?? "")}`, remedy: `provision them in the vault (\`mmi-cli vault secrets request <KEY> --repo ${repo}\`), then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}${lane === "hotfix" ? " --lane hotfix" : ""}\`` });
21461
+ } else if (COMPOSE_GUARD_HARD_FAIL.test(text)) {
21462
+ add({ code: "compose-guard-mismatch", severity: "blocker", source: "origin", title: clean2(text.match(COMPOSE_GUARD_HARD_FAIL)?.[0] ?? text), remedy: `fix the compose/DEPLOY#${stage}.noEnvFile pair per the fileless-transition guide the preflight printed, then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}${lane === "hotfix" ? " --lane hotfix" : ""}\`; --skip-compose-guard only after independent verification (#2813)` });
21367
21463
  } else {
21368
21464
  unverified(`the ${stage} secrets preflight`, clean2(text) || e);
21369
21465
  }
@@ -21553,7 +21649,7 @@ function enforceGateBudget(deps, repo) {
21553
21649
  );
21554
21650
  }
21555
21651
  }
21556
- async function preflight(deps, ctx, stage, meta) {
21652
+ async function preflight(deps, ctx, stage, meta, lane) {
21557
21653
  const model = requireDeployModel(meta, ctx.repo);
21558
21654
  if (model === "content") {
21559
21655
  throw new Error(`${ctx.repo} is a content repo (deployModel=content) \u2014 the release train does not apply (trunk-based; PR to main)`);
@@ -21561,7 +21657,7 @@ async function preflight(deps, ctx, stage, meta) {
21561
21657
  if (model === "none") {
21562
21658
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
21563
21659
  }
21564
- await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo]);
21660
+ await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
21565
21661
  await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
21566
21662
  await assertActionsJobsCanStart(deps, ctx.repo);
21567
21663
  enforceGateBudget(deps, ctx.repo);
@@ -22207,7 +22303,7 @@ ${recovery.note}`), recoveryInput);
22207
22303
  const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
22208
22304
  const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
22209
22305
  const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
22210
- const deployDispatch = await recoverFailedDispatchPhase(
22306
+ const recoveredDeployDispatch = await recoverFailedDispatchPhase(
22211
22307
  deps,
22212
22308
  ledger,
22213
22309
  ledgerAnchors,
@@ -22220,6 +22316,7 @@ ${recovery.note}`), recoveryInput);
22220
22316
  workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
22221
22317
  }
22222
22318
  );
22319
+ const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
22223
22320
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
22224
22321
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
22225
22322
  });
@@ -22244,7 +22341,6 @@ ${recovery.note}`), recoveryInput);
22244
22341
  landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
22245
22342
  });
22246
22343
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
22247
- dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
22248
22344
  if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
22249
22345
  return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
22250
22346
  }
@@ -22520,17 +22616,18 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
22520
22616
  historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
22521
22617
  historicalNote = recovered.note;
22522
22618
  }
22619
+ const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
22620
+ note: historicalNote,
22621
+ deployStatus: historicalDeploy,
22622
+ workflowRuns: historicalRows
22623
+ });
22523
22624
  if (persisted) {
22524
- const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
22625
+ const phaseInputs = phaseInputsFromRunRows(deployModel, dispatch2.workflowRuns ?? historicalRows, tagSha, { repo: ctx.repo });
22525
22626
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
22526
22627
  await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
22527
22628
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
22528
22629
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
22529
22630
  }
22530
- const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
22531
- note: historicalNote,
22532
- deployStatus: historicalDeploy
22533
- });
22534
22631
  if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
22535
22632
  if (persisted) {
22536
22633
  await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { strict: true, landed: "the released, verified, deploy-resolved release (alignment PRs reused/landed)" });
@@ -22612,7 +22709,7 @@ ${recovery.note}`), recoveryInput);
22612
22709
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
22613
22710
  const autoRunSince = (deps.now ?? Date.now)();
22614
22711
  const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
22615
- const deployDispatch = await recoverFailedDispatchPhase(
22712
+ const recoveredDeployDispatch = await recoverFailedDispatchPhase(
22616
22713
  deps,
22617
22714
  ledger,
22618
22715
  anchors,
@@ -22625,6 +22722,7 @@ ${recovery.note}`), recoveryInput);
22625
22722
  workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
22626
22723
  }
22627
22724
  );
22725
+ const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
22628
22726
  steps.push(`dispatched the ${deployModel} deploy path`);
22629
22727
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
22630
22728
  strict: ledgerMode === "strict",
@@ -22643,8 +22741,7 @@ ${recovery.note}`), recoveryInput);
22643
22741
  strict: ledgerMode === "strict",
22644
22742
  landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
22645
22743
  });
22646
- let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
22647
- dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
22744
+ const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
22648
22745
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
22649
22746
  steps.push(`development roll-forward: ${devRollForward.status}`);
22650
22747
  const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
@@ -25269,6 +25366,13 @@ function registerBoardCommands(program3) {
25269
25366
  const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
25270
25367
  return `@${holder.login}${lane}`;
25271
25368
  }
25369
+ function printClaimWarnings(warnings, toStderr = false) {
25370
+ for (const warning of warnings ?? []) {
25371
+ if (toStderr) process.stderr.write(`Warning: ${warning}
25372
+ `);
25373
+ else console.log(`Warning: ${warning}`);
25374
+ }
25375
+ }
25272
25376
  function claimVerdict(ref, result) {
25273
25377
  const holder = formatClaimHolder(result.holder);
25274
25378
  const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
@@ -25291,24 +25395,26 @@ function registerBoardCommands(program3) {
25291
25395
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
25292
25396
  board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
25293
25397
  withExamples(mutating(
25294
- board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
25398
+ board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
25295
25399
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
25296
25400
  ).action(async (issueRefs, o) => {
25297
25401
  if (issueRefs.length === 1) {
25298
25402
  const issueRef = issueRefs[0];
25299
25403
  try {
25404
+ const config = await loadConfigForBoardSelector2(issueRef, o.repo);
25300
25405
  const result = await claimBoardIssue({
25301
- config: await loadConfigForBoardSelector2(issueRef, o.repo),
25406
+ config,
25302
25407
  selector: issueRef,
25303
25408
  repo: o.repo,
25304
25409
  assignee: o.for,
25305
25410
  force: o.force,
25306
25411
  check: o.check,
25307
25412
  allowPartial: o.allowPartial
25308
- });
25413
+ }, { snapshot: registryClientDeps(config) });
25309
25414
  if (!result.checked) invalidateStatuslineBoardCache();
25310
25415
  if (o.json) return console.log(JSON.stringify(result));
25311
25416
  console.log(claimVerdict(result.item.ref, result));
25417
+ printClaimWarnings(result.warnings);
25312
25418
  } catch (e) {
25313
25419
  if (refuseRateLimited(e, o.json)) return;
25314
25420
  return failGraceful(`board claim failed: ${e.message}`);
@@ -25316,22 +25422,25 @@ function registerBoardCommands(program3) {
25316
25422
  return;
25317
25423
  }
25318
25424
  try {
25425
+ const config = await loadConfigForBoardSelector2(issueRefs[0], o.repo);
25319
25426
  const bulk = await claimBoardIssues({
25320
- config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
25427
+ config,
25321
25428
  selectors: issueRefs,
25322
25429
  repo: o.repo,
25323
25430
  assignee: o.for,
25324
25431
  force: o.force,
25325
25432
  check: o.check,
25326
25433
  allowPartial: o.allowPartial
25327
- });
25434
+ }, { snapshot: registryClientDeps(config) });
25328
25435
  if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
25329
25436
  if (o.json) {
25330
25437
  console.log(JSON.stringify(bulk.results));
25438
+ printClaimWarnings(bulk.warnings, true);
25331
25439
  } else {
25332
25440
  for (const result of bulk.results) {
25333
25441
  console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
25334
25442
  }
25443
+ printClaimWarnings(bulk.warnings);
25335
25444
  }
25336
25445
  if (bulk.failed > 0) process.exitCode = 1;
25337
25446
  } catch (e) {
@@ -25505,7 +25614,7 @@ function registerBoardCommands(program3) {
25505
25614
 
25506
25615
  // src/bootstrap-commands.ts
25507
25616
  var import_node_fs26 = require("node:fs");
25508
- var import_node_os13 = require("node:os");
25617
+ var import_node_os14 = require("node:os");
25509
25618
  var import_node_path25 = require("node:path");
25510
25619
 
25511
25620
  // src/command-plans.ts
@@ -25644,7 +25753,7 @@ function trainPlan(command, options = {}) {
25644
25753
  { label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
25645
25754
  // #6068: hotfix start/release run the SAME shared preflight as release/rcand, before any git mutation.
25646
25755
  { label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
25647
- { label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
25756
+ { label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo> --lane hotfix", gated: true },
25648
25757
  { label: "preflight local npm major vs the CI-declared npm, GitHub Actions hosted job start (billing/spending), and the gate wall-clock budget", command: "shared train preflight (#5666/#5604/#3178) \u2014 runs inside hotfix start and hotfix release", gated: true },
25649
25758
  { label: "refuse on a prior pending/failed train ledger leg (release, rcand or hotfix) before mutating anything", command: "shared release ledger (#5987/#6068) \u2014 finish that run first", gated: true },
25650
25759
  { label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
@@ -27357,7 +27466,7 @@ function registerBootstrapCommands(program3) {
27357
27466
  const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
27358
27467
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
27359
27468
  const putSeed = async (target, content, ref, sha) => {
27360
- const tmp = (0, import_node_path25.join)((0, import_node_os13.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
27469
+ const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
27361
27470
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
27362
27471
  try {
27363
27472
  await gh(contentPutInputArgs(repo, target, tmp));
@@ -27956,7 +28065,7 @@ LIVE apply to ${repo}:
27956
28065
  } catch {
27957
28066
  existingSha = void 0;
27958
28067
  }
27959
- const tmp = (0, import_node_path25.join)((0, import_node_os13.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
28068
+ const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
27960
28069
  const desiredContent = desiredByRepo.get(rec.repo);
27961
28070
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
27962
28071
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
@@ -28131,7 +28240,7 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
28131
28240
  } catch {
28132
28241
  existingSha = void 0;
28133
28242
  }
28134
- const tmp = (0, import_node_path25.join)((0, import_node_os13.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
28243
+ const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
28135
28244
  (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
28136
28245
  try {
28137
28246
  await gh(contentPutInputArgs(repo, seed.target, tmp));
@@ -28252,7 +28361,7 @@ function formatDeployStatus(r) {
28252
28361
  `running version: ${r.runningVersion ?? "none stamped"}`,
28253
28362
  `last deploy run: ${formatLastRun(r)}`,
28254
28363
  `public URL: ${r.publicUrl ?? "none"}`,
28255
- `health probe: ${r.health ? `${r.health.ok ? "ok" : "failed"}${r.health.status ? ` (HTTP ${r.health.status})` : ""}${r.health.error ? ` \u2014 ${r.health.error}` : ""}` : "not probed"}`,
28364
+ `health probe: ${r.health ? `${r.health.ok ? "ok" : "failed"}${r.health.status ? ` (HTTP ${r.health.status})` : ""}${r.health.error ? ` \u2014 ${r.health.error}` : ""}${r.health.url && r.health.url !== r.publicUrl ? ` at ${r.health.url}` : ""}` : "not probed"}`,
28256
28365
  `deploy state: ${r.deployOk === void 0 ? "not stamped" : r.deployOk ? "ok" : "failed"}`
28257
28366
  ];
28258
28367
  if (r.hints.length) {
@@ -28263,22 +28372,9 @@ function formatDeployStatus(r) {
28263
28372
 
28264
28373
  // src/deploy-commands.ts
28265
28374
  var STAGES2 = ["dev", "rc", "main"];
28266
- async function probeHttpBounded(url, timeoutMs = 5e3) {
28267
- const controller = new AbortController();
28268
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
28269
- timeout.unref?.();
28270
- try {
28271
- const res = await fetch(url, { method: "GET", signal: controller.signal });
28272
- return { ok: res.ok, status: res.status };
28273
- } catch (e) {
28274
- return { ok: false, error: e.message };
28275
- } finally {
28276
- clearTimeout(timeout);
28277
- }
28278
- }
28279
28375
  function registerDeployCommands(program3) {
28280
28376
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
28281
- deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
28377
+ deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
28282
28378
  if (!STAGES2.includes(stage)) {
28283
28379
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
28284
28380
  }
@@ -28290,7 +28386,7 @@ function registerDeployCommands(program3) {
28290
28386
  const deployFacts = await fetchDeployFactsBySlug(slug, reg);
28291
28387
  const publicUrl = publicUrlFromDeployFact(deployFacts?.stages[stage] ?? null);
28292
28388
  const [health, lastRun] = await Promise.all([
28293
- publicUrl ? probeHttpBounded(publicUrl) : Promise.resolve(void 0),
28389
+ publicUrl ? probePublicHealth(publicUrl) : Promise.resolve(void 0),
28294
28390
  fetchLastTenantDeployRun(slug, stage)
28295
28391
  ]);
28296
28392
  const report = buildDeployStatusReport({
@@ -29021,7 +29117,8 @@ function composeMarksRuntimeEnvTrue(composeText) {
29021
29117
  return false;
29022
29118
  });
29023
29119
  }
29024
- function promotionSourceBranch(stage, releaseTrack) {
29120
+ function promotionSourceBranch(stage, releaseTrack, lane) {
29121
+ if (lane === "hotfix") return "main";
29025
29122
  if (stage === "main") return releaseTrack === "direct" ? "development" : "rc";
29026
29123
  return "development";
29027
29124
  }
@@ -29887,7 +29984,7 @@ async function collectWaveStatus(deps) {
29887
29984
 
29888
29985
  // src/discovery-commands.ts
29889
29986
  var import_node_fs30 = require("node:fs");
29890
- var import_node_os14 = require("node:os");
29987
+ var import_node_os15 = require("node:os");
29891
29988
  var import_node_path28 = require("node:path");
29892
29989
  var GC_GH_TIMEOUT_MS = 2e4;
29893
29990
  async function collectStatus() {
@@ -29922,7 +30019,7 @@ async function collectStatus() {
29922
30019
  try {
29923
30020
  const cfg = await loadConfigOrDiscover();
29924
30021
  if (cfg.sagaApiUrl) {
29925
- const report = await readBoard({ config: cfg });
30022
+ const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
29926
30023
  claimedItems = report.primary.userOwned.map((item) => ({
29927
30024
  number: item.number,
29928
30025
  title: item.title,
@@ -29969,8 +30066,8 @@ var PRIORITY_RANK = {
29969
30066
  };
29970
30067
  async function recommendNext(repo, deps) {
29971
30068
  const load = deps?.loadConfig ?? loadConfigForRepo;
29972
- const reader = deps?.readBoard ?? readBoard;
29973
30069
  const cfg = await load(repo);
30070
+ const reader = deps?.readBoard ?? ((opts) => readBoard(opts, { snapshot: registryClientDeps(cfg) }));
29974
30071
  if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
29975
30072
  let report;
29976
30073
  try {
@@ -30016,7 +30113,7 @@ async function collectOnboardStatus(opts = {}) {
30016
30113
  let board = { ok: false, detail: "no config" };
30017
30114
  try {
30018
30115
  if (cfg.sagaApiUrl) {
30019
- const report = await readBoard({ config: cfg });
30116
+ const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
30020
30117
  const total = report.primary.claimable.length + report.primary.userOwned.length + report.primary.taken.length;
30021
30118
  board = { ok: true, detail: `board has ${total} active items (${report.primary.claimable.length} claimable, ${report.primary.userOwned.length} yours)` };
30022
30119
  } else {
@@ -30076,7 +30173,7 @@ async function collectOnboardStatus(opts = {}) {
30076
30173
  else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
30077
30174
  else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
30078
30175
  }
30079
- const home = (0, import_node_os14.homedir)();
30176
+ const home = (0, import_node_os15.homedir)();
30080
30177
  const plugin = onboardPluginGate({
30081
30178
  readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
30082
30179
  readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
@@ -32025,7 +32122,7 @@ var import_node_fs31 = require("node:fs");
32025
32122
  var import_node_crypto9 = require("node:crypto");
32026
32123
 
32027
32124
  // src/issue-body.ts
32028
- var import_node_os15 = require("node:os");
32125
+ var import_node_os16 = require("node:os");
32029
32126
  var TextArgError = class extends Error {
32030
32127
  constructor(message2, code, offendingFlag) {
32031
32128
  super(message2);
@@ -32037,7 +32134,7 @@ var TextArgError = class extends Error {
32037
32134
  offendingFlag;
32038
32135
  };
32039
32136
  function emptyStdinMessage(fileFlag) {
32040
- if ((0, import_node_os15.platform)() === "win32") {
32137
+ if ((0, import_node_os16.platform)() === "win32") {
32041
32138
  return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
32042
32139
  }
32043
32140
  return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
@@ -32577,6 +32674,10 @@ function validateBatchSpecs(specs) {
32577
32674
  spec.labels = [...spec.labels ?? [], ...alias];
32578
32675
  delete spec.label;
32579
32676
  }
32677
+ if (spec.labels !== void 0 && (!Array.isArray(spec.labels) || spec.labels.some((l) => typeof l !== "string" || !l.trim()))) {
32678
+ errors.push({ row, error: "labels must be an array of non-empty strings" });
32679
+ continue;
32680
+ }
32580
32681
  if (spec.repo !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(spec.repo)) {
32581
32682
  errors.push({ row, error: `bad repo "${spec.repo}" \u2014 expected owner/repo` });
32582
32683
  continue;
@@ -32585,13 +32686,17 @@ function validateBatchSpecs(specs) {
32585
32686
  errors.push({ row, error: `unknown type "${spec.type}" \u2014 expected one of: ${validTypes.join(", ")}` });
32586
32687
  continue;
32587
32688
  }
32588
- if (!spec.title || !spec.title.trim()) {
32689
+ if (typeof spec.title !== "string" || !spec.title.trim()) {
32589
32690
  errors.push({ row, error: "missing or empty title" });
32590
32691
  continue;
32591
32692
  }
32693
+ if (spec.body !== void 0 && typeof spec.body !== "string") {
32694
+ errors.push({ row, error: "body must be a string" });
32695
+ continue;
32696
+ }
32592
32697
  let priority;
32593
32698
  try {
32594
- priority = spec.priority ? normalizePriority(spec.priority) : "medium";
32699
+ priority = spec.priority ? normalizePriority(String(spec.priority)) : "medium";
32595
32700
  } catch (e) {
32596
32701
  errors.push({ row, error: e.message });
32597
32702
  continue;
@@ -32604,6 +32709,14 @@ function validateBatchSpecs(specs) {
32604
32709
  if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
32605
32710
  delete spec.surface;
32606
32711
  }
32712
+ if (spec.parent !== void 0) {
32713
+ try {
32714
+ parseIssueRef(spec.parent);
32715
+ } catch (e) {
32716
+ errors.push({ row, error: e.message });
32717
+ continue;
32718
+ }
32719
+ }
32607
32720
  validated.push({ row, spec, priority, type: spec.type });
32608
32721
  }
32609
32722
  return { ok: errors.length === 0, errors, validated };
@@ -34030,6 +34143,8 @@ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!,
34030
34143
  projectV2 { id }
34031
34144
  }
34032
34145
  }`;
34146
+ var ProjectInfoReadUnavailableError = class extends Error {
34147
+ };
34033
34148
  function shortDescriptionFromReadme(markdown) {
34034
34149
  const lines2 = markdown.replace(/\r/g, "").split("\n");
34035
34150
  const h1 = lines2.findIndex((line) => /^#\s+\S/.test(line.trim()));
@@ -34516,7 +34631,7 @@ function registerSchedulesCommands(program3) {
34516
34631
  // src/secrets-commands.ts
34517
34632
  var import_node_fs33 = require("node:fs");
34518
34633
  var import_node_path30 = require("node:path");
34519
- var import_node_os16 = require("node:os");
34634
+ var import_node_os17 = require("node:os");
34520
34635
 
34521
34636
  // src/secrets-diff.ts
34522
34637
  var TIMEOUT_MS2 = 8e3;
@@ -34639,7 +34754,7 @@ async function decryptRailsCredentials(input) {
34639
34754
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
34640
34755
  "puts JSON.generate(config.config)"
34641
34756
  ].join("\n");
34642
- const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path30.join)((0, import_node_os16.tmpdir)(), "mmi-rails-decrypt-"));
34757
+ const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path30.join)((0, import_node_os17.tmpdir)(), "mmi-rails-decrypt-"));
34643
34758
  const scriptPath = (0, import_node_path30.join)(scriptDir, "decrypt.rb");
34644
34759
  (0, import_node_fs33.writeFileSync)(scriptPath, script, "utf8");
34645
34760
  try {
@@ -34751,10 +34866,13 @@ function registerSecretsCommands(program3) {
34751
34866
  const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
34752
34867
  if (!ok) process.exitCode = 1;
34753
34868
  }));
34754
- secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--json", "machine-readable output").action(async (o) => {
34869
+ secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--lane <hotfix>", "only hotfix is accepted: the hotfix lane deploys main, so the #2813 guard reads main, not rc (#6136); release/rcand need no flag").option("--json", "machine-readable output").action(async (o) => {
34755
34870
  if (!["dev", "rc", "main"].includes(o.stage)) {
34756
34871
  return fail("secrets preflight: --stage must be dev, rc, or main");
34757
34872
  }
34873
+ if (o.lane !== void 0 && o.lane !== "hotfix") {
34874
+ return fail("secrets preflight: --lane must be hotfix (the only lane that changes the compose source)");
34875
+ }
34758
34876
  const cfg = await loadConfig();
34759
34877
  if (!cfg.sagaApiUrl) {
34760
34878
  fail("secrets: Hub API URL not configured");
@@ -34775,7 +34893,7 @@ function registerSecretsCommands(program3) {
34775
34893
  let filelessOk = true;
34776
34894
  if (meta && centralContainer && !o.skipComposeGuard) {
34777
34895
  const stage = o.stage;
34778
- const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
34896
+ const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo), o.lane);
34779
34897
  const cwdRepo = repoFromRemoteUrl((await execFileP("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
34780
34898
  const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
34781
34899
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
@@ -34981,10 +35099,11 @@ async function resolveBoardConfig2(repoOption) {
34981
35099
  const floor = await loadConfig();
34982
35100
  if (!floor.sagaApiUrl) return null;
34983
35101
  const slug = repoOption ? (repoOption.replace(/\.git$/, "").split("/").pop() ?? repoOption).toLowerCase() : await repoSlug();
34984
- const read = await fetchProjectBySlugChecked(slug, registryClientDeps(floor));
35102
+ const registry2 = registryClientDeps(floor);
35103
+ const read = await fetchProjectBySlugChecked(slug, registry2);
34985
35104
  if (!read.ok || !read.project) return null;
34986
35105
  const cfg = boardConfigFromProject(read.project, floor);
34987
- return readBoard({ config: cfg, repo: repoOption, allowPartial: true });
35106
+ return readBoard({ config: cfg, repo: repoOption, allowPartial: true }, { snapshot: registry2 });
34988
35107
  }
34989
35108
  function registerSessionReport(program3) {
34990
35109
  const report = program3.commands.find((c) => c.name() === "report");
@@ -35926,7 +36045,7 @@ async function runPrLand(prNumber, options, deps) {
35926
36045
  // src/merge-cleanup.ts
35927
36046
  var import_node_fs37 = require("node:fs");
35928
36047
  var import_node_path34 = require("node:path");
35929
- var import_node_os17 = require("node:os");
36048
+ var import_node_os18 = require("node:os");
35930
36049
 
35931
36050
  // src/board-advance.ts
35932
36051
  function repoOf2(ref) {
@@ -36753,24 +36872,31 @@ function runTestPolicy(root, deps = {}) {
36753
36872
  ` + (deps.base === HOTFIX_DIFF_BASE ? ` Hotfix lane (--base ${HOTFIX_DIFF_BASE}): pass --policy-ref <exact-${HOTFIX_POLICY_REF}-commit> so the current policy is read from ${HOTFIX_POLICY_REF}.` : ` Restore ${POLICY_FILE} on this branch; the policy file belongs on every non-hotfix lane.`)
36754
36873
  }] : [];
36755
36874
  const present = (path2) => exists((0, import_node_path33.join)(root, path2));
36875
+ const policyTree = policySource.sha ? new Set(git2(["ls-tree", "-r", "-z", "--name-only", policySource.sha], root).split("\0").filter(Boolean).map((p) => (0, import_node_path33.join)(root, p))) : null;
36876
+ const policyTreeExists = policyTree ? (abs) => policyTree.has(abs) : exists;
36877
+ const where = policySource.sha ? `${policySource.ref}@${policySource.sha}` : "this worktree";
36756
36878
  const removedByThisDiff = removedPaths(changed);
36757
36879
  const staleFindings = [];
36758
- const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
36880
+ const unresolved = unresolvedProtectedEntries(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
36759
36881
  if (unresolved.length > 0) {
36760
36882
  staleFindings.push({
36761
36883
  kind: "stale-protected-entry",
36762
36884
  paths: unresolved,
36763
- detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
36764
- ` + unresolved.map((p) => ` ${p}`).join("\n") + "\n An entry naming a missing file reads as protection while protecting nothing.\n Restore the file, or remove its entry."
36885
+ detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist on ${where}:
36886
+ ` + unresolved.map((p) => ` ${p}`).join("\n") + `
36887
+ An entry naming a missing file reads as protection while protecting nothing.
36888
+ Restore the file on ${where}, or remove its entry.`
36765
36889
  });
36766
36890
  }
36767
- const staleSatisfiers = unresolvedSatisfiers(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
36891
+ const staleSatisfiers = unresolvedSatisfiers(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
36768
36892
  if (staleSatisfiers.length > 0) {
36769
36893
  staleFindings.push({
36770
36894
  kind: "stale-satisfied-by",
36771
36895
  paths: staleSatisfiers,
36772
- detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
36773
- ` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + "\n A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.\n Restore the file, or drop it from satisfiedBy so the glob asks for a test again."
36896
+ detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist on ${where}:
36897
+ ` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + `
36898
+ A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.
36899
+ Restore the file on ${where}, or drop it from satisfiedBy so the glob asks for a test again.`
36774
36900
  });
36775
36901
  }
36776
36902
  const override = lookup.override;
@@ -36957,7 +37083,7 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
36957
37083
  return rewritten === base ? null : rewritten;
36958
37084
  }
36959
37085
  function writeSquashBodyFile(body) {
36960
- const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path34.join)((0, import_node_os17.tmpdir)(), "mmi-squash-body-"));
37086
+ const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path34.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
36961
37087
  const path2 = (0, import_node_path34.join)(dir, "body.txt");
36962
37088
  (0, import_node_fs37.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
36963
37089
  `, "utf8");
@@ -37085,15 +37211,17 @@ function cleanupGitArgs(cwd, args) {
37085
37211
  }
37086
37212
  async function remoteBranchExists2(branch, options = {}) {
37087
37213
  if (!branch) return void 0;
37214
+ const remote = options.remote ?? "origin";
37088
37215
  try {
37089
- if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch", "origin", "--prune"]), { timeout: GIT_TIMEOUT_MS });
37090
- return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", "origin", branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
37216
+ if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch", remote, "--prune"]), { timeout: GIT_TIMEOUT_MS });
37217
+ return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", remote, branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
37091
37218
  } catch {
37092
37219
  return void 0;
37093
37220
  }
37094
37221
  }
37095
37222
  async function deleteMergedRemoteBranch(options) {
37096
- const remediation = `git push origin --delete ${options.branch}`;
37223
+ const remote = options.remote ?? "origin";
37224
+ const remediation = `git push ${remote} --delete ${options.branch}`;
37097
37225
  if (!options.branch) {
37098
37226
  return {
37099
37227
  branch: options.branch,
@@ -37122,13 +37250,13 @@ async function deleteMergedRemoteBranch(options) {
37122
37250
  existedBefore: false,
37123
37251
  attempted: false,
37124
37252
  status: "failed",
37125
- error: `could not verify absence of origin/${options.branch}`,
37253
+ error: `could not verify absence of ${remote}/${options.branch}`,
37126
37254
  remediation
37127
37255
  };
37128
37256
  }
37129
37257
  }
37130
37258
  try {
37131
- await options.execGit(["push", "origin", "--delete", options.branch]);
37259
+ await options.execGit(["push", remote, "--delete", options.branch]);
37132
37260
  } catch (e) {
37133
37261
  const exists2 = await options.branchExists(options.branch);
37134
37262
  if (exists2 === false) {
@@ -37162,7 +37290,7 @@ async function deleteMergedRemoteBranch(options) {
37162
37290
  existedBefore: options.existedBefore,
37163
37291
  attempted: true,
37164
37292
  status: "failed",
37165
- error: exists ? `origin still reports ${options.branch} after deletion` : `could not verify deletion of origin/${options.branch}`,
37293
+ error: exists ? `${remote} still reports ${options.branch} after deletion` : `could not verify deletion of ${remote}/${options.branch}`,
37166
37294
  remediation
37167
37295
  };
37168
37296
  }
@@ -37504,11 +37632,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
37504
37632
  const actor = deps.actor ?? describeSessionIdentity();
37505
37633
  const checkContest = deps.checkContest ?? checkLaneContest;
37506
37634
  for (const number of issues) {
37635
+ const state = await client.rest("GET", `repos/${repo}/issues/${number}`).then((issue) => issue?.state, () => void 0);
37636
+ if (state?.toLowerCase() === "closed") continue;
37507
37637
  const contest = await checkContest(client, { repository: repo, number }, actor);
37508
37638
  if (!contest.contested) continue;
37509
37639
  const ref = `${repo}#${number}`;
37510
37640
  const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
37511
- return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` before creating this PR`;
37641
+ return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` (move it to In Progress first if it sits In Review) or drop the closing line before creating this PR`;
37512
37642
  }
37513
37643
  return void 0;
37514
37644
  }
@@ -37519,13 +37649,13 @@ var import_node_path38 = require("node:path");
37519
37649
 
37520
37650
  // src/jervcode-node-modules-cleanup.ts
37521
37651
  var import_node_fs39 = require("node:fs");
37522
- var import_node_os18 = require("node:os");
37652
+ var import_node_os19 = require("node:os");
37523
37653
  var import_node_path36 = require("node:path");
37524
37654
  var JERVCODE_PACKAGE_ENTRY = (0, import_node_path36.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
37525
37655
  var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
37526
37656
  var POSIX_NAMES2 = ["jervcode"];
37527
37657
  var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
37528
- function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform) {
37658
+ function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os19.homedir)(), platform2 = process.platform) {
37529
37659
  const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
37530
37660
  const out = [];
37531
37661
  for (const dir of jervCliCandidateDirs(env, home, platform2)) {
@@ -37533,7 +37663,7 @@ function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os18.h
37533
37663
  }
37534
37664
  return out;
37535
37665
  }
37536
- function resolveJervcodePath(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform, exists = import_node_fs39.existsSync) {
37666
+ function resolveJervcodePath(env = process.env, home = (0, import_node_os19.homedir)(), platform2 = process.platform, exists = import_node_fs39.existsSync) {
37537
37667
  for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
37538
37668
  if (exists(candidate)) return candidate;
37539
37669
  }
@@ -37542,7 +37672,7 @@ function resolveJervcodePath(env = process.env, home = (0, import_node_os18.home
37542
37672
  function jervcodeExecFileArgs(args, opts = {}) {
37543
37673
  const platform2 = opts.platform ?? process.platform;
37544
37674
  const exists = opts.exists ?? import_node_fs39.existsSync;
37545
- const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os18.homedir)(), platform2, exists);
37675
+ const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os19.homedir)(), platform2, exists);
37546
37676
  if (resolved) {
37547
37677
  const entry = (0, import_node_path36.join)((0, import_node_path36.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
37548
37678
  if (exists(entry)) {
@@ -37908,6 +38038,69 @@ function safeRemoveTree(path2) {
37908
38038
  }
37909
38039
  (0, import_node_fs41.unlinkSync)(path2);
37910
38040
  }
38041
+ function errorMessage(e) {
38042
+ return e instanceof Error ? e.message : String(e);
38043
+ }
38044
+ function resolvedOrRaw(path2) {
38045
+ try {
38046
+ return normPath2((0, import_node_fs41.realpathSync)(path2));
38047
+ } catch {
38048
+ return normPath2(path2);
38049
+ }
38050
+ }
38051
+ function unlinkEscapingReparsePoints(root, primaryRoot) {
38052
+ let realRoot;
38053
+ try {
38054
+ if ((0, import_node_fs41.lstatSync)(root).isSymbolicLink()) {
38055
+ return { ok: false, error: `delete root ${normPath2(root)} is a reparse point resolving to ${resolvedOrRaw(root)}` };
38056
+ }
38057
+ realRoot = normPath2((0, import_node_fs41.realpathSync)(root));
38058
+ } catch (e) {
38059
+ if (e.code === "ENOENT") return { ok: true, unlinked: [] };
38060
+ return { ok: false, error: `cannot resolve delete root ${normPath2(root)}: ${errorMessage(e)}` };
38061
+ }
38062
+ const realPrimary = resolvedOrRaw(primaryRoot);
38063
+ const forbidden = isPathAtOrWithin(realPrimary, realRoot) ? realPrimary : [`${realPrimary}/node_modules`, `${realPrimary}/packages`].find((p) => isPathAtOrWithin(realRoot, p));
38064
+ if (forbidden) {
38065
+ return { ok: false, error: `delete root ${normPath2(root)} (resolved ${realRoot}) is or contains the primary checkout path ${forbidden}` };
38066
+ }
38067
+ const unlinked = [];
38068
+ const stack = [realRoot];
38069
+ while (stack.length) {
38070
+ const dir = stack.pop();
38071
+ let entries;
38072
+ try {
38073
+ entries = (0, import_node_fs41.readdirSync)(dir, { withFileTypes: true });
38074
+ } catch (e) {
38075
+ return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
38076
+ }
38077
+ for (const entry of entries) {
38078
+ const child2 = (0, import_node_path38.join)(dir, entry.name);
38079
+ if (entry.isSymbolicLink()) {
38080
+ let target = "";
38081
+ try {
38082
+ target = normPath2((0, import_node_fs41.realpathSync)(child2));
38083
+ } catch {
38084
+ target = "";
38085
+ }
38086
+ if (target && isPathAtOrWithin(target, realRoot)) continue;
38087
+ try {
38088
+ safeRemoveTree(child2);
38089
+ } catch (e) {
38090
+ return { ok: false, error: `cannot unlink reparse point ${normPath2(child2)} -> ${target || "unresolvable"}: ${errorMessage(e)}` };
38091
+ }
38092
+ unlinked.push(normPath2(child2));
38093
+ continue;
38094
+ }
38095
+ if (entry.isDirectory()) stack.push(child2);
38096
+ }
38097
+ }
38098
+ return { ok: true, unlinked };
38099
+ }
38100
+ function reparseEscapeRemediation(wtPath) {
38101
+ const quote = (value) => value.replace(/'/g, "''");
38102
+ return `inspect '${quote(wtPath)}' manually \u2014 a reparse point resolves outside the worktree; never remove it recursively`;
38103
+ }
37911
38104
  async function describePreCleanFailure(wtPath, execGit, error) {
37912
38105
  const dryRun = await execGit(["-C", wtPath, "clean", "-ndX"]).catch(() => "");
37913
38106
  const remaining = dryRun.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("Would remove ")).map((line) => line.slice("Would remove ".length));
@@ -37942,6 +38135,25 @@ async function verifyBranchHead(git3, branch, expectedHeadOid) {
37942
38135
  }
37943
38136
  return { ok: true };
37944
38137
  }
38138
+ async function switchPrimaryCheckoutOffMergedBranch(wtPath, branch, baseRef, execGit, expectedHeadOid) {
38139
+ const git3 = (args) => execGit(["-C", wtPath, ...args]);
38140
+ const headCheck = await verifyBranchHead(git3, branch, expectedHeadOid);
38141
+ if (!headCheck.ok) return { ok: false, error: headCheck.error ? `${headCheck.reason}: ${headCheck.error}` : headCheck.reason };
38142
+ const porcelain = await git3(["status", "--porcelain"]).catch(() => void 0);
38143
+ if (porcelain === void 0) return { ok: false, error: "could not read the primary checkout status" };
38144
+ if (porcelainHasBlockingChanges(porcelain)) return { ok: false, error: "dirty-worktree" };
38145
+ try {
38146
+ await git3(["switch", baseRef]);
38147
+ } catch (e) {
38148
+ return { ok: false, error: `switch to ${baseRef} failed: ${formatGitCommandError(e)}` };
38149
+ }
38150
+ try {
38151
+ await git3(["branch", "-D", branch]);
38152
+ } catch (e) {
38153
+ return { ok: false, switchedTo: baseRef, error: `branch -D ${branch} failed after switching to ${baseRef}: ${formatGitCommandError(e)}` };
38154
+ }
38155
+ return { ok: true, switchedTo: baseRef };
38156
+ }
37945
38157
  async function teardownWorktreeStage(worktreePath) {
37946
38158
  try {
37947
38159
  const result = await stopStage({ cwd: worktreePath, requiredIdentityCwd: worktreePath, globalStatePath: false });
@@ -38035,7 +38247,7 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
38035
38247
  }
38036
38248
  function isPrMergeWorktreePartial(cleanup) {
38037
38249
  const worktree = cleanup?.worktree;
38038
- return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked");
38250
+ return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked" && worktree.status !== "switched-primary");
38039
38251
  }
38040
38252
  function prMergeLocalCleanupExitCode(cleanup) {
38041
38253
  return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
@@ -38075,10 +38287,29 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38075
38287
  const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
38076
38288
  if (!wtPath || mainWorktreeTarget) {
38077
38289
  if (wtPath && mainWorktreeTarget) {
38290
+ const switched = await switchPrimaryCheckoutOffMergedBranch(wtPath, branch, options.baseRef, execGit, options.expectedHeadOid);
38291
+ if (switched.ok) {
38292
+ report.worktree = { path: wtPath, status: "switched-primary", reason: "main-worktree", switchedTo: switched.switchedTo };
38293
+ report.localBranch = { name: branch, status: "deleted" };
38294
+ return report;
38295
+ }
38296
+ if (switched.switchedTo) {
38297
+ report.worktree = {
38298
+ path: wtPath,
38299
+ status: "switched-primary",
38300
+ reason: "main-worktree",
38301
+ switchedTo: switched.switchedTo,
38302
+ error: switched.error,
38303
+ remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
38304
+ };
38305
+ report.localBranch = { name: branch, status: "failed", error: switched.error };
38306
+ return report;
38307
+ }
38078
38308
  report.worktree = {
38079
38309
  path: wtPath,
38080
38310
  status: "not-attempted",
38081
38311
  reason: "main-worktree",
38312
+ error: switched.error,
38082
38313
  remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
38083
38314
  };
38084
38315
  }
@@ -38098,7 +38329,12 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38098
38329
  return report;
38099
38330
  }
38100
38331
  const porcelain = await execGit(["-C", wtPath, "status", "--porcelain"]).catch(() => void 0);
38101
- if (porcelain?.trim()) {
38332
+ if (porcelain === void 0) {
38333
+ report.worktree = { path: wtPath, status: "refused", reason: "status-unreadable", error: "could not read the worktree status" };
38334
+ report.localBranch = { name: branch, status: "not-attempted", reason: "status-unreadable" };
38335
+ return report;
38336
+ }
38337
+ if (porcelain.trim()) {
38102
38338
  report.worktree = { path: wtPath, status: "refused", reason: "dirty-worktree" };
38103
38339
  report.localBranch = { name: branch, status: "not-attempted", reason: "dirty-worktree" };
38104
38340
  return report;
@@ -38148,6 +38384,25 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38148
38384
  report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
38149
38385
  return report;
38150
38386
  }
38387
+ const unlinkedReparsePoints = [];
38388
+ const refuseReparseEscape = (error) => {
38389
+ report.worktree = {
38390
+ path: wtPath,
38391
+ status: "refused",
38392
+ reason: "reparse-escape",
38393
+ error,
38394
+ artifactsArchive,
38395
+ stageTeardown,
38396
+ tmpEvidenceCount: tmpEvidence.length,
38397
+ ...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
38398
+ remediation: reparseEscapeRemediation(wtPath)
38399
+ };
38400
+ report.localBranch = { name: branch, status: "not-attempted", reason: "reparse-escape" };
38401
+ return report;
38402
+ };
38403
+ const preHelperGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
38404
+ if (!preHelperGuard.ok) return refuseReparseEscape(preHelperGuard.error);
38405
+ unlinkedReparsePoints.push(...preHelperGuard.unlinked);
38151
38406
  if (pathExists((0, import_node_path38.join)(wtPath, "node_modules"))) {
38152
38407
  const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
38153
38408
  if (!nmRemoved.ok) {
@@ -38159,6 +38414,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38159
38414
  artifactsArchive,
38160
38415
  stageTeardown,
38161
38416
  tmpEvidenceCount: tmpEvidence.length,
38417
+ ...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
38162
38418
  remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
38163
38419
  };
38164
38420
  report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
@@ -38175,12 +38431,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38175
38431
  artifactsArchive,
38176
38432
  stageTeardown,
38177
38433
  tmpEvidenceCount: tmpEvidence.length,
38434
+ ...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
38178
38435
  // #6076: name the surviving node_modules remover when the dry run found one, else the clean.
38179
38436
  remediation: preClean.remediation ?? `git -C '${wtPath.replace(/'/g, "''")}' clean -ffdX`
38180
38437
  };
38181
38438
  report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
38182
38439
  return report;
38183
38440
  }
38441
+ const preRemoveGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
38442
+ if (!preRemoveGuard.ok) return refuseReparseEscape(preRemoveGuard.error);
38443
+ unlinkedReparsePoints.push(...preRemoveGuard.unlinked);
38184
38444
  moveCwdToSafeWorktree(wtPath, safeCwd);
38185
38445
  const removal = await removeWorktreeWithReconcile(
38186
38446
  wtPath,
@@ -38195,7 +38455,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38195
38455
  error: removal.error,
38196
38456
  artifactsArchive,
38197
38457
  stageTeardown,
38198
- tmpEvidenceCount: tmpEvidence.length
38458
+ tmpEvidenceCount: tmpEvidence.length,
38459
+ ...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
38199
38460
  };
38200
38461
  report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
38201
38462
  return report;
@@ -38206,7 +38467,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
38206
38467
  ...removal.reason ? { reason: removal.reason } : {},
38207
38468
  artifactsArchive,
38208
38469
  stageTeardown,
38209
- tmpEvidenceCount: tmpEvidence.length
38470
+ tmpEvidenceCount: tmpEvidence.length,
38471
+ ...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
38210
38472
  };
38211
38473
  if (pathExists(wtPath)) {
38212
38474
  const residue = await (options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath));
@@ -38254,12 +38516,18 @@ function renderPrMergeCleanupLines(cleanup) {
38254
38516
  lines2.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
38255
38517
  } else if (wt.status === "retained-locked") {
38256
38518
  lines2.push(`pr merge: worktree ${wt.path} cleanup deferred \u2014 its Windows parent shell still holds the cwd; ${wt.remediation ?? "exit the shell and remove the residue from the primary checkout"}`);
38519
+ } else if (wt.status === "switched-primary") {
38520
+ const now = wt.switchedTo ? `to ${wt.switchedTo}` : "to the base branch";
38521
+ lines2.push(wt.error ? `pr merge: primary checkout ${wt.path} switched ${now} but deleting the merged branch failed \u2014 ${wt.error}; remediate: ${wt.remediation ?? "delete the merged branch manually"}` : `pr merge: primary checkout ${wt.path} held the merged branch \u2014 switched it back ${now} and deleted the branch`);
38257
38522
  } else if (wt.status === "not-attempted" && wt.reason === "main-worktree") {
38258
- lines2.push(`pr merge: merged branch is still checked out in the primary checkout ${wt.path} \u2014 not removed; remediate: ${wt.remediation ?? "switch it back to the base branch and delete the merged branch"}`);
38523
+ lines2.push(`pr merge: merged branch is still checked out in the primary checkout ${wt.path} \u2014 not removed${wt.error ? ` (${wt.error})` : ""}; remediate: ${wt.remediation ?? "switch it back to the base branch and delete the merged branch"}`);
38259
38524
  }
38260
38525
  if (wt.residue === "left" && wt.status !== "retained-locked") {
38261
38526
  lines2.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
38262
38527
  }
38528
+ if (wt.unlinkedReparsePoints?.length) {
38529
+ lines2.push(`pr merge: unlinked ${wt.unlinkedReparsePoints.length} reparse point(s) resolving outside ${wt.path}: ${wt.unlinkedReparsePoints.join(", ")}`);
38530
+ }
38263
38531
  if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
38264
38532
  lines2.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
38265
38533
  } else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
@@ -39321,7 +39589,7 @@ ${list}`);
39321
39589
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
39322
39590
  if (result.status === "failed") process.exitCode = 1;
39323
39591
  });
39324
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").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)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
39592
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").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); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
39325
39593
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
39326
39594
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
39327
39595
  if (o.disableAuto) {
@@ -39381,7 +39649,23 @@ ${list}`);
39381
39649
  const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
39382
39650
  const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
39383
39651
  const beforeWorktreesRead = await execFileP("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => ({ state: "ok", stdout: r.stdout })).catch((e) => ({ state: "failed", error: e.message || "git worktree list failed" }));
39384
- const beforeWorktrees = beforeWorktreesRead.state === "ok" ? parseGitWorktreePorcelain(beforeWorktreesRead.stdout) : [];
39652
+ let beforeWorktrees = beforeWorktreesRead.state === "ok" ? parseGitWorktreePorcelain(beforeWorktreesRead.stdout) : [];
39653
+ const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
39654
+ const targetRepo2 = repoForPostCleanup ? repoForPostCleanup.split("/").slice(-2).join("/") : void 0;
39655
+ const foreignCwd = Boolean(o.repo) && Boolean(targetRepo2) && cwdRepo?.toLowerCase() !== targetRepo2.toLowerCase();
39656
+ const remote = foreignCwd ? `https://github.com/${targetRepo2}.git` : "origin";
39657
+ let foreignCheckout;
39658
+ if (foreignCwd) {
39659
+ const sibling = (0, import_node_path39.join)((0, import_node_path39.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
39660
+ const siblingRepo = repoFromRemoteUrl(await gitOut(["-C", sibling, "remote", "get-url", "origin"]).catch(() => ""));
39661
+ if (siblingRepo?.toLowerCase() === targetRepo2.toLowerCase()) {
39662
+ const siblingWorktrees = await execFileP("git", ["-C", sibling, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseGitWorktreePorcelain(r.stdout)).catch(() => void 0);
39663
+ if (siblingWorktrees?.length) {
39664
+ foreignCheckout = sibling;
39665
+ beforeWorktrees = siblingWorktrees;
39666
+ }
39667
+ }
39668
+ }
39385
39669
  const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
39386
39670
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
39387
39671
  if (o.wait) {
@@ -39439,7 +39723,7 @@ ${list}`);
39439
39723
  if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
39440
39724
  if (guard.note) console.warn(`pr merge: ${guard.note}`);
39441
39725
  }
39442
- const remoteBefore = await remoteBranchExists2(headRef);
39726
+ const remoteBefore = await remoteBranchExists2(headRef, { remote });
39443
39727
  let upgradedToAuto = false;
39444
39728
  let remoteNotAttemptedReason = "preserved-delayed-cleanup";
39445
39729
  const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
@@ -39558,30 +39842,34 @@ ${list}`);
39558
39842
  }
39559
39843
  const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
39560
39844
  let localCleanup;
39561
- try {
39562
- localCleanup = await cleanupPrMergeLocalBranch(headRef, {
39563
- beforeWorktrees,
39564
- startingPath,
39565
- baseRef,
39566
- primaryRoot,
39567
- preserveWorktree: o.preserveWorktree,
39568
- gcAcknowledged: o.gc,
39569
- expectedHeadOid: headRefOid,
39570
- pathExists: (p) => (0, import_node_fs42.existsSync)(p),
39571
- // #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
39572
- // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
39573
- // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
39574
- execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
39575
- });
39576
- } catch (e) {
39577
- localCleanup = {
39578
- branch: headRef,
39579
- localBranch: {
39580
- name: headRef,
39581
- status: "failed",
39582
- error: e instanceof Error ? e.message : String(e)
39583
- }
39584
- };
39845
+ if (foreignCwd && !foreignCheckout) {
39846
+ localCleanup = { branch: headRef, localBranch: { name: headRef, status: "not-attempted", reason: "skipped-foreign-cwd" } };
39847
+ } else {
39848
+ try {
39849
+ localCleanup = await cleanupPrMergeLocalBranch(headRef, {
39850
+ beforeWorktrees,
39851
+ startingPath,
39852
+ baseRef,
39853
+ primaryRoot,
39854
+ preserveWorktree: o.preserveWorktree,
39855
+ gcAcknowledged: o.gc,
39856
+ expectedHeadOid: headRefOid,
39857
+ pathExists: (p) => (0, import_node_fs42.existsSync)(p),
39858
+ // #5899: pin cleanup git calls to the main checkout the task worktree this process may be
39859
+ // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
39860
+ // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
39861
+ execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
39862
+ });
39863
+ } catch (e) {
39864
+ localCleanup = {
39865
+ branch: headRef,
39866
+ localBranch: {
39867
+ name: headRef,
39868
+ status: "failed",
39869
+ error: e instanceof Error ? e.message : String(e)
39870
+ }
39871
+ };
39872
+ }
39585
39873
  }
39586
39874
  const remoteBranch = await deleteMergedRemoteBranch({
39587
39875
  branch: headRef,
@@ -39592,7 +39880,9 @@ ${list}`);
39592
39880
  // #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
39593
39881
  // the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
39594
39882
  execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
39595
- branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
39883
+ branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot, remote }),
39884
+ // #6148: aim the delete and its proof at `--repo`, not the cwd checkout's origin.
39885
+ remote
39596
39886
  });
39597
39887
  const worktree = localCleanup?.worktree;
39598
39888
  const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
@@ -39655,6 +39945,7 @@ ${list}`);
39655
39945
  ...methodField ? { method: methodField } : {},
39656
39946
  remoteBranch,
39657
39947
  housekeeping,
39948
+ ...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
39658
39949
  ...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
39659
39950
  ...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
39660
39951
  ...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
@@ -39970,7 +40261,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
39970
40261
  var import_node_child_process16 = require("node:child_process");
39971
40262
  var import_node_crypto12 = require("node:crypto");
39972
40263
  var import_node_fs45 = require("node:fs");
39973
- var import_node_os19 = require("node:os");
40264
+ var import_node_os20 = require("node:os");
39974
40265
  var import_node_path41 = require("node:path");
39975
40266
 
39976
40267
  // ../scripts/distribution-digest.mjs
@@ -40121,7 +40412,7 @@ function rebuildTo(packageRoot, outDir) {
40121
40412
  });
40122
40413
  }
40123
40414
  function runDistStatus(root) {
40124
- const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path41.join)((0, import_node_os19.tmpdir)(), "mmi-dist-drift-"));
40415
+ const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path41.join)((0, import_node_os20.tmpdir)(), "mmi-dist-drift-"));
40125
40416
  let overlayCount = 0;
40126
40417
  try {
40127
40418
  const cliOut = (0, import_node_path41.join)(stage, "cli-dist");
@@ -40564,8 +40855,12 @@ function registerDeveloperCommands(program3) {
40564
40855
  registerSchedulesCommands(program3);
40565
40856
  registerSchedulesLiftCommand(program3);
40566
40857
  const docs = program3.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
40567
- docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
40858
+ docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs index reads the current checkout").hideHelp()).action(async (o) => {
40568
40859
  try {
40860
+ if (o.repo) {
40861
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs index ${o.check ? "--check" : "--write"}`, "docs index");
40862
+ if (!guard.ok) return failGraceful(guard.message);
40863
+ }
40569
40864
  const root = await repoRoot();
40570
40865
  const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
40571
40866
  if (o.check) {
@@ -40580,8 +40875,12 @@ function registerDeveloperCommands(program3) {
40580
40875
  await failGraceful(e.message);
40581
40876
  }
40582
40877
  });
40583
- docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by \u2014 -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (#3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").action(async (o) => {
40878
+ docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by \u2014 -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (#3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).action(async (o) => {
40584
40879
  try {
40880
+ if (o.repo) {
40881
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
40882
+ if (!guard.ok) return failGraceful(guard.message);
40883
+ }
40585
40884
  const root = await repoRoot();
40586
40885
  const commandPaths = new Set(
40587
40886
  buildCommandManifest(program3).index.map((entry) => entry.path)
@@ -40607,8 +40906,12 @@ function registerDeveloperCommands(program3) {
40607
40906
  }
40608
40907
  });
40609
40908
  const spawnCmd = program3.command("spawn").description("this repo's process-spawn contract \u2014 every child process must be unable to pop a console window");
40610
- spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").action(async (o) => {
40909
+ spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 spawn policy reads the current checkout").hideHelp()).action(async (o) => {
40611
40910
  try {
40911
+ if (o.repo) {
40912
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli spawn policy${o.json ? " --json" : ""}`, "spawn policy");
40913
+ if (!guard.ok) return failGraceful(guard.message);
40914
+ }
40612
40915
  const root = await repoRoot();
40613
40916
  const result = runSpawnPolicy(root);
40614
40917
  if (o.json) {
@@ -40630,8 +40933,13 @@ function registerDeveloperCommands(program3) {
40630
40933
  }
40631
40934
  });
40632
40935
  const tests = program3.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
40633
- tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load only test-policy.json from the exact fetched origin/development commit; valid only with --base origin/main").action(async (o) => {
40936
+ tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load test-policy.json from the exact fetched origin/development commit and audit its protected/satisfiedBy paths against that commit's tree, not this checkout; valid only with --base origin/main").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 tests policy reads the current checkout").hideHelp()).action(async (o) => {
40634
40937
  try {
40938
+ if (o.repo) {
40939
+ const rerun = `mmi-cli tests policy${o.base ? ` --base ${o.base}` : ""}${o.policyRef ? ` --policy-ref ${o.policyRef}` : ""}${o.json ? " --json" : ""}`;
40940
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun, "tests policy");
40941
+ if (!guard.ok) return failGraceful(guard.message);
40942
+ }
40635
40943
  const root = await repoRoot();
40636
40944
  const result = runTestPolicy(root, { base: o.base, policyRef: o.policyRef });
40637
40945
  if (o.json) {
@@ -40661,8 +40969,12 @@ function registerDeveloperCommands(program3) {
40661
40969
  }
40662
40970
  });
40663
40971
  const distCmd = program3.command("dist").description("this repo's committed dist/BOM drift receipt \u2014 whether cli/dist, updater/dist and distribution-bom.json still match a fresh build of source");
40664
- distCmd.command("status").description("rebuild every committed dist artifact to a temp dir and report committed vs rebuilt-expected sha256 plus the BOM's recorded dist identities \u2014 a visible, non-blocking receipt. Development checkouts may lag source until the release fold; drift NEVER fails the run, and nothing is refreshed for you (#5576)").option("--json", "machine-readable receipt: { ok, staleCount, artifacts[], bom, summary } (full hashes; drift still exits 0)").action(async (o) => {
40972
+ distCmd.command("status").description("rebuild every committed dist artifact to a temp dir and report committed vs rebuilt-expected sha256 plus the BOM's recorded dist identities \u2014 a visible, non-blocking receipt. Development checkouts may lag source until the release fold; drift NEVER fails the run, and nothing is refreshed for you (#5576)").option("--json", "machine-readable receipt: { ok, staleCount, artifacts[], bom, summary } (full hashes; drift still exits 0)").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 dist status reads the current checkout").hideHelp()).action(async (o) => {
40665
40973
  try {
40974
+ if (o.repo) {
40975
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli dist status${o.json ? " --json" : ""}`, "dist status");
40976
+ if (!guard.ok) return failGraceful(guard.message);
40977
+ }
40666
40978
  const root = await repoRoot();
40667
40979
  const receipt = runDistStatus(root);
40668
40980
  if (o.json) {
@@ -40957,7 +41269,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
40957
41269
  }
40958
41270
  async function hotfixPreflight(deps, ctx, verb, targetTag) {
40959
41271
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
40960
- const deployModel = await preflight(deps, ctx, "main", meta);
41272
+ const deployModel = await preflight(deps, ctx, "main", meta, "hotfix");
40961
41273
  const root = await hotfixCheckoutRoot(deps);
40962
41274
  const begin = await beginReleaseLedger(deps, root, targetTag);
40963
41275
  if (!begin.ok) throw new Error(`hotfix ${verb} refused before any mutation: ${begin.error}`);
@@ -41316,6 +41628,16 @@ function hotfixDispatchFromRuns(runs, note) {
41316
41628
  const tenantDeploy2 = runs.find((r) => r.workflow === "tenant-deploy.yml");
41317
41629
  return { note, deployStatus, workflowRuns, ...tenantDeploy2?.runId != null ? { runId: tenantDeploy2.runId } : {}, ...tenantDeploy2?.url ? { runUrl: tenantDeploy2.url } : {} };
41318
41630
  }
41631
+ async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
41632
+ const input = hotfixDispatchFromRuns(runs, note);
41633
+ const gateway = await appendJervGatewayReleaseDeploy(deps, repo, tag, sha, input);
41634
+ if (gateway === input) return note;
41635
+ const row = gateway.workflowRuns?.at(-1);
41636
+ if (row?.workflow === "jerv-gateway") {
41637
+ runs.push({ workflow: row.workflow, ...row.runUrlNote ? { runUrlNote: row.runUrlNote } : {}, conclusion: row.conclusion });
41638
+ }
41639
+ return gateway.note;
41640
+ }
41319
41641
  function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
41320
41642
  const dispatch = hotfixDispatchFromRuns(runs, note);
41321
41643
  return {
@@ -41559,6 +41881,7 @@ ${decision.note}`);
41559
41881
  } else {
41560
41882
  deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
41561
41883
  }
41884
+ deployNote = await appendHotfixGatewayRun(deps, ctx.repo, tag, mergedSha, runs, deployNote);
41562
41885
  const ledgerInputs = hotfixPhaseInputsFromRuns(
41563
41886
  deployModel,
41564
41887
  runs,
@@ -41751,6 +42074,9 @@ function hotfixStatusRunsFromLedger(ledger) {
41751
42074
  workflow: record.workflow ?? phase,
41752
42075
  ...record.runId != null ? { runId: record.runId } : {},
41753
42076
  ...record.runUrl ? { url: record.runUrl } : {},
42077
+ // #912: the operator-host Gateway leg carries no run id by design — without its own note the
42078
+ // renderer's missing-URL fallback reads it as 'run absent or unreadable'.
42079
+ ...record.workflow === "jerv-gateway" ? { runUrlNote: JERV_GATEWAY_RUN_URL_NOTE } : {},
41754
42080
  conclusion: record.state === "complete" ? "success" : record.state === "failed" ? "failure" : "pending"
41755
42081
  }];
41756
42082
  });
@@ -42231,6 +42557,7 @@ function renderReleaseResume(r) {
42231
42557
  if (r.devRollForward) lines2.push(` development: ${r.devRollForward.note}`);
42232
42558
  if (r.rcAlignment) lines2.push(` rc: ${r.rcAlignment.note}`);
42233
42559
  if (r.checkout) lines2.push(` checkout: ${r.checkout.note}`);
42560
+ if (r.projectInfoSync) lines2.push(` project info: ${r.projectInfoSync.note}`);
42234
42561
  if (r.ledger) lines2.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
42235
42562
  return lines2.join("\n");
42236
42563
  }
@@ -42258,7 +42585,8 @@ function releaseFollowUpLegs(result, projectInfoSync) {
42258
42585
  const legs = [
42259
42586
  {
42260
42587
  leg: "project-info",
42261
- status: projectInfoSync && "error" in projectInfoSync ? "failed" : "success",
42588
+ // #6180: a transient Hub read outage is PENDING (rerun the manual verb), never a failed follow-up.
42589
+ status: projectInfoSync && "error" in projectInfoSync ? projectInfoSync.pending ? "pending" : "failed" : "success",
42262
42590
  ...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
42263
42591
  }
42264
42592
  ];
@@ -42297,6 +42625,24 @@ function releaseFollowUpLegs(result, projectInfoSync) {
42297
42625
  }
42298
42626
  return legs;
42299
42627
  }
42628
+ function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote, deployNote) {
42629
+ const legs = runs.map((run) => ({
42630
+ leg: run.workflow,
42631
+ status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
42632
+ // #912 lane parity with releaseFollowUpLegs: the operator-host Gateway leg has no workflow run to
42633
+ // read, so its deploy note IS the error — a flat "workflow reported failure" names nothing.
42634
+ ...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" && deployNote ? deployNote : "workflow reported failure" } : {}
42635
+ }));
42636
+ if (foldPort === "failure") {
42637
+ legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
42638
+ }
42639
+ if (alignment === "unresolved") {
42640
+ legs.push({ leg: "development-fold-alignment", status: "pending" });
42641
+ } else if (alignment === "failure" && foldPort !== "failure") {
42642
+ legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
42643
+ }
42644
+ return legs;
42645
+ }
42300
42646
  var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
42301
42647
  async function runPostReleaseJervDoctor(repo) {
42302
42648
  if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
@@ -42312,10 +42658,35 @@ async function runPostReleaseJervDoctor(repo) {
42312
42658
  };
42313
42659
  }
42314
42660
  }
42661
+ async function runProjectInfoSyncLeg(cb, repo, sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))) {
42662
+ for (let attempt = 0; ; attempt++) {
42663
+ try {
42664
+ return await cb(repo, true);
42665
+ } catch (e) {
42666
+ const error = e.message;
42667
+ if (!(e instanceof ProjectInfoReadUnavailableError)) return { applied: false, note: `FAILED \u2014 ${error}`, error };
42668
+ if (attempt === 0) {
42669
+ await sleep2(PROJECT_INFO_RETRY_MS);
42670
+ continue;
42671
+ }
42672
+ return {
42673
+ applied: false,
42674
+ pending: true,
42675
+ note: `PENDING \u2014 ${error}; rerun \`mmi-cli oracle org project sync-info ${repo} --apply\``,
42676
+ error
42677
+ };
42678
+ }
42679
+ }
42680
+ }
42681
+ var PROJECT_INFO_RETRY_MS = 3e3;
42682
+ function projectInfoOutcome(projectInfoSync) {
42683
+ if (!projectInfoSync || !("error" in projectInfoSync)) return "ok";
42684
+ return projectInfoSync.pending ? "unresolved" : "failure";
42685
+ }
42315
42686
  function buildReleaseVerdict(commandName, result, projectInfoSync) {
42316
42687
  const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
42317
42688
  const followUpStatus = deriveTrainFollowUpStatus({
42318
- projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
42689
+ projectInfo: projectInfoOutcome(projectInfoSync),
42319
42690
  deploy: deployFollowUpOutcome(result.deployStatus),
42320
42691
  rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
42321
42692
  alignment: alignmentPending ? "unresolved" : "ok",
@@ -42496,8 +42867,17 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
42496
42867
  }
42497
42868
  const raw = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
42498
42869
  const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
42499
- emitTrainResult("release --resume", o.json ? JSON.stringify(result, null, 2) : renderReleaseResume(result), o.out);
42500
- applyTrainFollowUpExit(resumeFollowUpOf(result.state));
42870
+ const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
42871
+ const resumed = { ...result, projectInfoSync };
42872
+ emitTrainResult("release --resume", o.json ? JSON.stringify(resumed, null, 2) : renderReleaseResume(resumed), o.out);
42873
+ const resumeStatus = resumeFollowUpOf(result.state);
42874
+ applyTrainFollowUpExit(deriveTrainFollowUpStatus({
42875
+ projectInfo: projectInfoOutcome(projectInfoSync),
42876
+ deploy: "ok",
42877
+ rcRetirement: "ok",
42878
+ alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
42879
+ foldPort: "ok"
42880
+ }));
42501
42881
  return;
42502
42882
  } catch (e) {
42503
42883
  applyTrainFollowUpExit("failed");
@@ -42535,12 +42915,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
42535
42915
  let projectInfoSync;
42536
42916
  const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
42537
42917
  if (commandName === "release") {
42538
- try {
42539
- projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
42540
- } catch (e) {
42541
- const error = e.message;
42542
- projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
42543
- }
42918
+ projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
42544
42919
  }
42545
42920
  const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
42546
42921
  const reported = {
@@ -42637,22 +43012,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
42637
43012
  if (conclusion === "failure") return "failure";
42638
43013
  return "unresolved";
42639
43014
  }
42640
- function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote) {
42641
- const legs = runs.map((run) => ({
42642
- leg: run.workflow,
42643
- status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
42644
- ...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
42645
- }));
42646
- if (foldPort === "failure") {
42647
- legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
42648
- }
42649
- if (alignment === "unresolved") {
42650
- legs.push({ leg: "development-fold-alignment", status: "pending" });
42651
- } else if (alignment === "failure" && foldPort !== "failure") {
42652
- legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
42653
- }
42654
- return legs;
42655
- }
42656
43015
  async function runHotfixSub(sub, body, o, render) {
42657
43016
  try {
42658
43017
  await requireFreshTrainCli("hotfix");
@@ -42664,11 +43023,12 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
42664
43023
  const ledger = result.ledger;
42665
43024
  const alignment = result.alignmentStatus ?? ledgerAlignmentFollowUpOutcome(ledger);
42666
43025
  const projectInfoSync = result.projectInfoSync;
42667
- const legs = runs ? hotfixFollowUpLegs(runs, foldPort, alignment, result.foldNote) : void 0;
43026
+ const deployNote = result.deployNote ?? ledger?.phases.deploy.error ?? ledger?.phases.deploy.note;
43027
+ const legs = runs ? hotfixFollowUpLegs(runs, foldPort, alignment, result.foldNote, deployNote) : void 0;
42668
43028
  const json = o.json || Boolean(hotfixCmd.opts().json);
42669
43029
  emitTrainResult(`hotfix ${sub}`, json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result), o.out);
42670
43030
  if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
42671
- projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
43031
+ projectInfo: projectInfoOutcome(projectInfoSync),
42672
43032
  deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
42673
43033
  rcRetirement: "ok",
42674
43034
  alignment,
@@ -42692,13 +43052,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
42692
43052
  hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line Hub Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => runHotfixSub("release", async () => {
42693
43053
  const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
42694
43054
  const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
42695
- let projectInfoSync;
42696
- try {
42697
- projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
42698
- } catch (e) {
42699
- const error = e.message;
42700
- projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
42701
- }
43055
+ const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
42702
43056
  return { ...result, projectInfoSync, ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {} };
42703
43057
  }, o, renderHotfixRelease));
42704
43058
  function hotfixStatusDeps() {
@@ -42742,7 +43096,7 @@ function envHealLockPath(home) {
42742
43096
  async function withEnvHealLock(what, run) {
42743
43097
  try {
42744
43098
  return await withFileLock(
42745
- envHealLockPath((0, import_node_os20.homedir)()),
43099
+ envHealLockPath((0, import_node_os21.homedir)()),
42746
43100
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
42747
43101
  run
42748
43102
  );
@@ -42838,7 +43192,7 @@ function mmiDoctorDeps(opts = {}) {
42838
43192
  const configRoot = surfaceConfigRoot(surface);
42839
43193
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
42840
43194
  const plan = buildPluginCachePlan(
42841
- (0, import_node_os20.homedir)(),
43195
+ (0, import_node_os21.homedir)(),
42842
43196
  running,
42843
43197
  pluginCacheFsDeps(configRoot, () => 0),
42844
43198
  { configRoot, includeStaging: surface !== "codex" }
@@ -42866,7 +43220,7 @@ function mmiDoctorDeps(opts = {}) {
42866
43220
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
42867
43221
  const installed = installedActivePluginVersion(surface);
42868
43222
  const plan = buildPluginCachePlan(
42869
- (0, import_node_os20.homedir)(),
43223
+ (0, import_node_os21.homedir)(),
42870
43224
  running,
42871
43225
  pluginCacheFsDeps(configRoot, () => 0),
42872
43226
  { configRoot, includeStaging: surface !== "codex", installedVersion: installed }
@@ -43279,7 +43633,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
43279
43633
  return failGraceful(`runtime tenant reconcile: ${e.message}`);
43280
43634
  }
43281
43635
  });
43282
- tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").action(async (repo, stage) => {
43636
+ tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
43283
43637
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
43284
43638
  const cfg = await loadConfig();
43285
43639
  const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
@@ -43321,26 +43675,13 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
43321
43675
  return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
43322
43676
  }
43323
43677
  });
43324
- async function probeHttpBounded2(url, timeoutMs = 5e3) {
43325
- const controller = new AbortController();
43326
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
43327
- timeout.unref?.();
43328
- try {
43329
- const res = await fetch(url, { method: "GET", signal: controller.signal });
43330
- return { ok: res.ok, status: res.status };
43331
- } catch (e) {
43332
- return { ok: false, error: e.message };
43333
- } finally {
43334
- clearTimeout(timeout);
43335
- }
43336
- }
43337
43678
  async function buildTenantRuntimeStatusFor(target, stage, cfg) {
43338
43679
  const slug = slugOf(target);
43339
43680
  const reg = registryClientDeps(cfg);
43340
43681
  const facts = await fetchDeployFactsBySlug(slug, reg);
43341
43682
  const deploy = facts?.stages[stage] ?? null;
43342
43683
  const publicUrl = publicUrlFromDeployFact(deploy);
43343
- const publicProbe = publicUrl ? await probeHttpBounded2(publicUrl) : void 0;
43684
+ const publicProbe = publicUrl ? await probePublicHealth(publicUrl) : void 0;
43344
43685
  return buildTenantRuntimeStatus({
43345
43686
  repo: target,
43346
43687
  slug,
@@ -43368,9 +43709,9 @@ async function runProjectInfoSync(target, apply) {
43368
43709
  fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
43369
43710
  fetchProjectsList(registry2)
43370
43711
  ]);
43371
- if (!read.ok) throw new Error(`org project sync-info: Hub registry read failed (${read.error})`);
43712
+ if (!read.ok) throw new ProjectInfoReadUnavailableError(`org project sync-info: Hub registry read failed (${read.error})`);
43372
43713
  if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
43373
- if (!projects) throw new Error("org project sync-info: Hub project list unavailable");
43714
+ if (!projects) throw new ProjectInfoReadUnavailableError("org project sync-info: Hub project list unavailable");
43374
43715
  if (apply) {
43375
43716
  const authority = await fetchTrainAuthority(targetRepo2, registry2);
43376
43717
  if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
@@ -44017,7 +44358,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
44017
44358
  return;
44018
44359
  }
44019
44360
  const plan = buildPluginCachePlan(
44020
- (0, import_node_os20.homedir)(),
44361
+ (0, import_node_os21.homedir)(),
44021
44362
  running,
44022
44363
  pluginCacheFsDeps(configRoot, directoryBytes),
44023
44364
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }