@mutmutco/cli 4.3.1 → 4.3.3

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 +2016 -570
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3416,7 +3416,7 @@ function useColor() {
3416
3416
  var program = new Command();
3417
3417
 
3418
3418
  // src/command-composition.ts
3419
- var import_node_fs46 = require("node:fs");
3419
+ var import_node_fs48 = require("node:fs");
3420
3420
 
3421
3421
  // src/clean-exit.ts
3422
3422
  var UNDICI_GLOBAL_DISPATCHER_SYMBOL = Object.getOwnPropertySymbols(globalThis).find(
@@ -3465,19 +3465,19 @@ function hardExit(code) {
3465
3465
  }
3466
3466
  var STDIO_FLUSH_TIMEOUT_MS = 2e3;
3467
3467
  function flushStream(stream) {
3468
- return new Promise((resolve6) => {
3468
+ return new Promise((resolve7) => {
3469
3469
  try {
3470
- stream.write("", () => resolve6());
3470
+ stream.write("", () => resolve7());
3471
3471
  } catch {
3472
- resolve6();
3472
+ resolve7();
3473
3473
  }
3474
3474
  });
3475
3475
  }
3476
3476
  async function flushStdio(timeoutMs = STDIO_FLUSH_TIMEOUT_MS) {
3477
3477
  await Promise.race([
3478
3478
  Promise.all([flushStream(process.stdout), flushStream(process.stderr)]),
3479
- new Promise((resolve6) => {
3480
- setTimeout(resolve6, timeoutMs).unref?.();
3479
+ new Promise((resolve7) => {
3480
+ setTimeout(resolve7, timeoutMs).unref?.();
3481
3481
  })
3482
3482
  ]);
3483
3483
  }
@@ -3485,7 +3485,7 @@ async function cleanExit(code) {
3485
3485
  process.exitCode = code;
3486
3486
  await closeHttpPool();
3487
3487
  await flushStdio();
3488
- await new Promise((resolve6) => setImmediate(resolve6));
3488
+ await new Promise((resolve7) => setImmediate(resolve7));
3489
3489
  return void 0;
3490
3490
  }
3491
3491
  var CLI_EXIT_WATCHDOG_MS = (() => {
@@ -3930,7 +3930,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3930
3930
  const attempts = opts.attempts ?? 3;
3931
3931
  const baseDelayMs = opts.baseDelayMs ?? 250;
3932
3932
  const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3933
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
3933
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
3934
3934
  let lastErr;
3935
3935
  for (let i = 0; i < attempts; i++) {
3936
3936
  const isLast = i === attempts - 1;
@@ -4198,8 +4198,8 @@ async function readStdin(opts = {}) {
4198
4198
  })().catch(() => {
4199
4199
  });
4200
4200
  let timer;
4201
- const timeout = new Promise((resolve6) => {
4202
- timer = setTimeout(resolve6, timeoutMs);
4201
+ const timeout = new Promise((resolve7) => {
4202
+ timer = setTimeout(resolve7, timeoutMs);
4203
4203
  });
4204
4204
  try {
4205
4205
  await Promise.race([drain, timeout]);
@@ -4253,12 +4253,12 @@ function killProcessTree(pid) {
4253
4253
  function execFileHard(file, args, options) {
4254
4254
  const { timeout, step, ...rest } = options;
4255
4255
  const started = Date.now();
4256
- return new Promise((resolve6, reject) => {
4256
+ return new Promise((resolve7, reject) => {
4257
4257
  const child2 = (0, import_node_child_process2.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...rest, timeout: 0 }, (error, stdout, stderr) => {
4258
4258
  clearTimeout(timer);
4259
4259
  if (expired) return;
4260
4260
  if (error) reject(error);
4261
- else resolve6({ stdout: String(stdout), stderr: String(stderr) });
4261
+ else resolve7({ stdout: String(stdout), stderr: String(stderr) });
4262
4262
  });
4263
4263
  let expired = false;
4264
4264
  const timer = setTimeout(() => {
@@ -5219,8 +5219,8 @@ function unknownCommandDomainGuide(parentPath, token) {
5219
5219
  }
5220
5220
 
5221
5221
  // src/command-composition.ts
5222
- var import_node_os19 = require("node:os");
5223
- var import_node_path43 = require("node:path");
5222
+ var import_node_os20 = require("node:os");
5223
+ var import_node_path45 = require("node:path");
5224
5224
 
5225
5225
  // src/board-read.ts
5226
5226
  var import_node_fs13 = require("node:fs");
@@ -5330,7 +5330,7 @@ function rateLimitedReceipt(opts) {
5330
5330
  };
5331
5331
  }
5332
5332
  async function runWithRateLimitBackoff(operation, opts) {
5333
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
5333
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
5334
5334
  const now = opts.now ?? Date.now;
5335
5335
  const log = opts.log ?? ((message) => console.warn(message));
5336
5336
  const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
@@ -5555,6 +5555,31 @@ function rewriteNegatedClosingPhrases(text) {
5555
5555
  (seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, plain, linked) => `leaves #${plain ?? linked} open`)
5556
5556
  ).join("");
5557
5557
  }
5558
+ var LINE_START_CLOSING_RE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?\s*(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:#\d+|\[#\d+\]\()/i;
5559
+ var FENCE_LINE_RE = /^\s*(?:```|~~~)/;
5560
+ function rewriteCloseKeywordProse(text) {
5561
+ if (!text.includes("#")) return { text, rewritten: [] };
5562
+ const rewritten = /* @__PURE__ */ new Set();
5563
+ let inFence = false;
5564
+ const lines = text.split("\n").map((line) => {
5565
+ if (FENCE_LINE_RE.test(line)) {
5566
+ inFence = !inFence;
5567
+ return line;
5568
+ }
5569
+ if (inFence || LINE_START_CLOSING_RE.test(line)) return line;
5570
+ CLOSING_MENTION_RE.lastIndex = 0;
5571
+ return line.split(/(`[^`\n]*`)/g).map((seg, i) => {
5572
+ if (i % 2 === 1) return seg;
5573
+ return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
5574
+ const n = Number(plain ?? linked);
5575
+ if (!Number.isInteger(n) || n <= 0) return match;
5576
+ rewritten.add(n);
5577
+ return `part of #${n}`;
5578
+ });
5579
+ }).join("");
5580
+ });
5581
+ return rewritten.size ? { text: lines.join("\n"), rewritten: [...rewritten] } : { text, rewritten: [] };
5582
+ }
5558
5583
  function findClosingMentions(text) {
5559
5584
  const mentions = [];
5560
5585
  for (const match of text.matchAll(CLOSING_MENTION_RE)) {
@@ -5588,11 +5613,12 @@ var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
5588
5613
  function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
5589
5614
  if (!closingNumbers.length) return [];
5590
5615
  const closing = new Set(closingNumbers);
5616
+ const bareClosing = new Set(findClosingMentions(text).map((m) => m.issue));
5591
5617
  const flagged = /* @__PURE__ */ new Set();
5592
5618
  const repoLower = repo.toLowerCase();
5593
5619
  for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
5594
5620
  const n = Number(match[2]);
5595
- if (!closing.has(n)) continue;
5621
+ if (!closing.has(n) || !bareClosing.has(n)) continue;
5596
5622
  if ((match[1] ?? "").toLowerCase() === repoLower) continue;
5597
5623
  flagged.add(n);
5598
5624
  }
@@ -8016,13 +8042,47 @@ async function defaultCompareRefs(exec, repo, base, head) {
8016
8042
  return "unknown";
8017
8043
  }
8018
8044
  }
8045
+ async function publishAbsentPrHeadIfOwned(repo, head, exec) {
8046
+ if (isForkStyleHead(head)) return false;
8047
+ let originRepo;
8048
+ try {
8049
+ const { stdout } = await exec("git", ["remote", "get-url", "--push", "origin"], { timeout: GIT_TIMEOUT_MS });
8050
+ originRepo = repoFromRemoteUrl(stdout.trim());
8051
+ } catch {
8052
+ return false;
8053
+ }
8054
+ if (!originRepo || originRepo.toLowerCase() !== repo.toLowerCase()) return false;
8055
+ let currentBranch2;
8056
+ try {
8057
+ currentBranch2 = (await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
8058
+ } catch {
8059
+ return false;
8060
+ }
8061
+ if (!currentBranch2 || currentBranch2 !== head) return false;
8062
+ let commit;
8063
+ try {
8064
+ commit = (await exec("git", ["rev-parse", "--verify", "HEAD^{commit}"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
8065
+ } catch {
8066
+ return false;
8067
+ }
8068
+ if (!commit) return false;
8069
+ try {
8070
+ await exec("git", ["push", "origin", `${commit}:refs/heads/${head}`], { timeout: GH_MUTATION_TIMEOUT_MS });
8071
+ return true;
8072
+ } catch {
8073
+ return false;
8074
+ }
8075
+ }
8019
8076
  async function preflightPrCreateRemoteHead(args, deps) {
8020
8077
  const repo = flagValue(args, "--repo");
8021
8078
  const head = flagValue(args, "--head");
8022
8079
  if (!repo || !head || isForkStyleHead(head)) return void 0;
8023
8080
  const readRemoteHead = deps.readRemoteHead ?? ((input) => defaultReadRemoteHead(deps.exec, input.repo, input.head));
8024
- const presence = await readRemoteHead({ repo, head });
8025
- if (presence === "absent") return unpushedHeadMessage(head);
8081
+ let presence = await readRemoteHead({ repo, head });
8082
+ if (presence === "absent") {
8083
+ if (!await publishAbsentPrHeadIfOwned(repo, head, deps.exec)) return unpushedHeadMessage(head);
8084
+ presence = "present";
8085
+ }
8026
8086
  if (presence !== "present") return void 0;
8027
8087
  const base = flagValue(args, "--base");
8028
8088
  if (!base) return void 0;
@@ -8438,7 +8498,7 @@ async function createPrViaRestFallback(args, swappedArgs, deps, knownPools) {
8438
8498
  }
8439
8499
  async function ghCreate(args, deps = {}) {
8440
8500
  const exec = deps.exec ?? execFileP;
8441
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
8501
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
8442
8502
  const now = deps.now ?? Date.now;
8443
8503
  const read = deps.readFile ?? import_promises.readFile;
8444
8504
  const restCreatePr = deps.restCreatePr ?? ((input) => defaultRestCreatePr(exec, input));
@@ -9151,8 +9211,8 @@ var PRE_SPAWN_DRAIN_TIMEOUT_MS = 2e3;
9151
9211
  async function drainHttpPoolBeforeSpawn() {
9152
9212
  const drained = await Promise.race([
9153
9213
  closeHttpPool().then(() => true),
9154
- new Promise((resolve6) => {
9155
- setTimeout(() => resolve6(false), PRE_SPAWN_DRAIN_TIMEOUT_MS).unref?.();
9214
+ new Promise((resolve7) => {
9215
+ setTimeout(() => resolve7(false), PRE_SPAWN_DRAIN_TIMEOUT_MS).unref?.();
9156
9216
  })
9157
9217
  ]);
9158
9218
  if (!drained) destroyHttpPool();
@@ -9400,8 +9460,22 @@ function repoFromSelector(selector) {
9400
9460
  return void 0;
9401
9461
  }
9402
9462
  }
9463
+ var REPO_OPTION_SHAPES = "owner/repo or host/owner/repo";
9464
+ function invalidRepoOption(repo, hint) {
9465
+ const base = `invalid repo "${repo}" (expected ${REPO_OPTION_SHAPES})`;
9466
+ return new Error(hint ? `${base} \u2014 ${hint}` : base);
9467
+ }
9468
+ async function validateRepoOption(repo) {
9469
+ const value = repo.trim();
9470
+ const parts = value.split("/");
9471
+ if ((parts.length === 2 || parts.length === 3) && parts.every((p) => p !== "")) return value;
9472
+ const origin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
9473
+ const originName = origin?.split("/")[1];
9474
+ const hint = originName !== void 0 && value.toLowerCase() === originName.toLowerCase() ? `did you mean ${origin}?` : void 0;
9475
+ throw invalidRepoOption(repo, hint);
9476
+ }
9403
9477
  async function resolveRepo(repo) {
9404
- if (repo) return repo;
9478
+ if (repo) return validateRepoOption(repo);
9405
9479
  const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
9406
9480
  if (fromOrigin) return fromOrigin;
9407
9481
  try {
@@ -11660,7 +11734,16 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
11660
11734
  const out = { failed: [] };
11661
11735
  try {
11662
11736
  const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
11663
- out.marker = latestClaimMarker(comments.map((comment) => ({ body: comment.body ?? "" })));
11737
+ const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
11738
+ out.marker = latestClaimMarker(rows);
11739
+ if (out.marker) {
11740
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
11741
+ if (latestClaimMarker([rows[i]])) {
11742
+ out.markerBy = rows[i].login;
11743
+ break;
11744
+ }
11745
+ }
11746
+ }
11664
11747
  out.markerAgeMs = out.marker ? Date.now() - Date.parse(out.marker.ts) : void 0;
11665
11748
  if (out.marker) {
11666
11749
  const state = probeLocalClaimSession(out.marker);
@@ -11727,17 +11810,26 @@ function laneOwnership(marker, current) {
11727
11810
  }
11728
11811
  return "unknown";
11729
11812
  }
11730
- async function checkLaneContest(client, item, actor = describeSessionIdentity()) {
11813
+ function laneResumeEvidence(evidence) {
11814
+ if (!evidence.marker || evidence.sessionState !== "dead" || evidence.failed.length > 0) return void 0;
11815
+ if (!isHostSessionId(evidence.marker.session)) return void 0;
11816
+ const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
11817
+ return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
11818
+ }
11819
+ async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin) {
11731
11820
  const evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
11732
11821
  const ownership = laneOwnership(evidence.marker, actor);
11822
+ const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
11823
+ const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
11733
11824
  const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
11734
11825
  const unverifiable = ownership === "mine" ? [] : evidence.failed;
11735
11826
  return {
11736
- contested: ownership !== "mine" && (live.length > 0 || unverifiable.length > 0),
11827
+ contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
11737
11828
  live,
11738
11829
  unverifiable,
11739
11830
  ownership,
11740
- marker: evidence.marker
11831
+ marker: evidence.marker,
11832
+ resume
11741
11833
  };
11742
11834
  }
11743
11835
  function laneContestMessage(ref, contest, verb) {
@@ -12036,8 +12128,9 @@ async function claimOneBoardItem(ctx, selector, options) {
12036
12128
  host: ctx.session.host
12037
12129
  };
12038
12130
  let previousHolder;
12039
- const claimedReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
12040
- const heldReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
12131
+ let resumeEvidence;
12132
+ const claimedReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
12133
+ const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
12041
12134
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
12042
12135
  const pre = evaluateClaim(flatItem, assignedLogin);
12043
12136
  if (!pre.ok) throw new Error(pre.reason);
@@ -12048,17 +12141,25 @@ async function claimOneBoardItem(ctx, selector, options) {
12048
12141
  if (!verdict.ok) throw new Error(verdict.reason);
12049
12142
  item = fresh;
12050
12143
  const refuseIfContested = async () => {
12051
- const contest = await checkLaneContest(client, item, ctx.session);
12052
- if (!contest.contested) return;
12053
- if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
12054
- previousHolder = {
12144
+ const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
12145
+ resumeEvidence = void 0;
12146
+ const displaced = () => ({
12055
12147
  login: assignedLogin,
12056
12148
  ...contest.marker ? {
12057
12149
  session: contest.marker.session,
12058
12150
  surface: contest.marker.surface,
12059
12151
  host: contest.marker.host
12060
12152
  } : {}
12061
- };
12153
+ });
12154
+ if (!contest.contested) {
12155
+ if (contest.resume && contest.marker) {
12156
+ resumeEvidence = contest.resume;
12157
+ previousHolder = displaced();
12158
+ }
12159
+ return;
12160
+ }
12161
+ if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
12162
+ previousHolder = displaced();
12062
12163
  };
12063
12164
  await refuseIfContested();
12064
12165
  if (verdict.alreadyClaimed) {
@@ -12128,7 +12229,7 @@ async function claimBoardIssues(options, deps = {}) {
12128
12229
  const ref = `${selector.repo}#${selector.number}`;
12129
12230
  try {
12130
12231
  const result = await claimOneBoardItem(ctx, selector, options);
12131
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
12232
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
12132
12233
  } catch (e) {
12133
12234
  results[index] = { ref, claimed: false, reason: e.message };
12134
12235
  }
@@ -12271,7 +12372,7 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
12271
12372
  await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
12272
12373
  return cliPriorityToFieldName(priority);
12273
12374
  }
12274
- var defaultRetrySleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
12375
+ var defaultRetrySleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
12275
12376
  async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
12276
12377
  const attempts = Math.max(1, opts.attempts ?? 5);
12277
12378
  const delayMs = opts.delayMs ?? 300;
@@ -15274,10 +15375,10 @@ var rollout_plan_default = {
15274
15375
  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)."
15275
15376
  },
15276
15377
  baseline: {
15277
- version: "4.3.1",
15278
- tag: "v4.3.1",
15279
- commit: "c3366772907f",
15280
- npm: "@mutmutco/cli@4.3.1"
15378
+ version: "4.3.3",
15379
+ tag: "v4.3.3",
15380
+ commit: "851ec4c56e34",
15381
+ npm: "@mutmutco/cli@4.3.3"
15281
15382
  },
15282
15383
  exitCriterion: "fleet-n-of-n",
15283
15384
  hubOnlyShortcut: "forbidden",
@@ -15294,14 +15395,14 @@ var rollout_plan_default = {
15294
15395
  repo: "mutmutco/mmi-hub",
15295
15396
  role: "canary",
15296
15397
  schedule: "train",
15297
- v3Target: "v4.3.1"
15398
+ v3Target: "v4.3.3"
15298
15399
  }
15299
15400
  ],
15300
15401
  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.",
15301
15402
  rollback: {
15302
15403
  independent: true,
15303
- mechanism: "npm dist-tag latest -> 4.3.1 and redeploy the Hub Lambda from tag v4.3.1 (c3366772907f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15304
- v3Target: "v4.3.1 (@mutmutco/cli@4.3.1, tag commit c3366772907f \u2014 last known-good release carrying the repo-index v4-only contract)"
15404
+ mechanism: "npm dist-tag latest -> 4.3.3 and redeploy the Hub Lambda from tag v4.3.3 (851ec4c56e34); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15405
+ v3Target: "v4.3.3 (@mutmutco/cli@4.3.3, tag commit 851ec4c56e34 \u2014 last known-good release carrying the repo-index v4-only contract)"
15305
15406
  }
15306
15407
  },
15307
15408
  {
@@ -16169,6 +16270,7 @@ function validateEnum(flag, allowed, value, command) {
16169
16270
  var ISSUE_STATES = ["open", "closed", "all"];
16170
16271
  var PR_STATES = ["open", "all"];
16171
16272
  async function resolveBoardProjectId(repoOption) {
16273
+ if (repoOption !== void 0) await validateRepoOption(repoOption);
16172
16274
  try {
16173
16275
  const floor = await loadConfig();
16174
16276
  if (!floor.sagaApiUrl) return void 0;
@@ -17270,7 +17372,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
17270
17372
  async function predictMergeConflicts(deps, ours, theirs) {
17271
17373
  return runMergeTreePreflight(deps, ours, theirs);
17272
17374
  }
17273
- async function mergeWithToleratedResolution(deps, sourceRef, label, resolve6, extraTolerated = []) {
17375
+ async function mergeWithToleratedResolution(deps, sourceRef, label, resolve7, extraTolerated = []) {
17274
17376
  try {
17275
17377
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
17276
17378
  return;
@@ -17284,7 +17386,7 @@ async function mergeWithToleratedResolution(deps, sourceRef, label, resolve6, ex
17284
17386
  unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on untolerated path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
17285
17387
  );
17286
17388
  }
17287
- await deps.run("git", ["checkout", `--${resolve6}`, "--", ...unmerged]);
17389
+ await deps.run("git", ["checkout", `--${resolve7}`, "--", ...unmerged]);
17288
17390
  await deps.run("git", ["add", "--", ...unmerged]);
17289
17391
  await deps.run("git", ["commit", "--no-edit"]);
17290
17392
  }
@@ -17463,7 +17565,7 @@ var CORRELATE_SKEW_SLACK_MS = 1e4;
17463
17565
  var CORRELATE_PAGE_LIMIT = 50;
17464
17566
  var RUN_CONFIRM_ATTEMPTS = 3;
17465
17567
  var RUN_CONFIRM_DELAY_MS = 1e3;
17466
- var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
17568
+ var defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
17467
17569
  function resolveSleep(deps) {
17468
17570
  return deps.sleep ?? defaultSleep;
17469
17571
  }
@@ -18292,12 +18394,6 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
18292
18394
  }
18293
18395
  return extra;
18294
18396
  }
18295
- async function deriveHistoricalDeployStatus(deps, ctx, model, releaseSha) {
18296
- const targets = prodReleaseTargets(model);
18297
- if (!targets.length) return "pending";
18298
- const repo = model === "hub-serverless" ? HUB_REPO2 : ctx.repo;
18299
- return aggregateWorkflowRuns(await listOwnWorkflowRuns(deps, repo, targets, releaseSha));
18300
- }
18301
18397
  async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
18302
18398
  if (isCentralDispatchModel(model)) {
18303
18399
  const since = (deps.now ?? Date.now)();
@@ -18592,9 +18688,9 @@ function renderTenantControl(r) {
18592
18688
  }
18593
18689
 
18594
18690
  // src/train-apply-phases.ts
18595
- var import_node_fs19 = require("node:fs");
18596
- var import_promises2 = require("node:fs/promises");
18597
- var import_node_path17 = require("node:path");
18691
+ var import_node_fs20 = require("node:fs");
18692
+ var import_promises3 = require("node:fs/promises");
18693
+ var import_node_path19 = require("node:path");
18598
18694
 
18599
18695
  // src/actions-billing-preflight.ts
18600
18696
  var CANARY_WORKFLOW = "actions-job-start-canary.yml";
@@ -18818,37 +18914,1077 @@ Resume it: ${resumeCommand}
18818
18914
  );
18819
18915
  }
18820
18916
 
18917
+ // src/release-ledger.ts
18918
+ var import_node_fs19 = require("node:fs");
18919
+ var import_node_path18 = require("node:path");
18920
+ var import_node_crypto5 = require("node:crypto");
18921
+
18922
+ // src/file-lock.ts
18923
+ var import_promises2 = require("node:fs/promises");
18924
+ var import_node_path17 = require("node:path");
18925
+ var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
18926
+ var IMMEDIATE_RETRY_BUDGET = 3;
18927
+ var FileLockBusyError = class extends Error {
18928
+ lockPath;
18929
+ constructor(label, lockPath, maxWaitMs) {
18930
+ super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
18931
+ this.name = "FileLockBusyError";
18932
+ this.lockPath = lockPath;
18933
+ }
18934
+ };
18935
+ function resolveFileLockOpts(opts = {}) {
18936
+ return {
18937
+ staleMs: opts.staleMs ?? 3e4,
18938
+ retryMs: opts.retryMs ?? 50,
18939
+ maxWaitMs: opts.maxWaitMs ?? 5e3,
18940
+ label: opts.label ?? "file lock"
18941
+ };
18942
+ }
18943
+ async function acquireFileLock(lockPath, opts, deadline) {
18944
+ let immediateRetries = 0;
18945
+ for (; ; ) {
18946
+ let handle;
18947
+ try {
18948
+ handle = await (0, import_promises2.open)(lockPath, "wx");
18949
+ } catch (e) {
18950
+ const code = e.code;
18951
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
18952
+ const retryNow = async () => {
18953
+ if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18954
+ if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
18955
+ };
18956
+ try {
18957
+ const age = Date.now() - (await (0, import_promises2.stat)(lockPath)).mtimeMs;
18958
+ if (age > opts.staleMs) {
18959
+ try {
18960
+ await (0, import_promises2.unlink)(lockPath);
18961
+ await retryNow();
18962
+ continue;
18963
+ } catch (unlinkError) {
18964
+ if (unlinkError instanceof FileLockBusyError) throw unlinkError;
18965
+ const unlinkCode = unlinkError.code;
18966
+ if (unlinkCode === "ENOENT") {
18967
+ await retryNow();
18968
+ continue;
18969
+ }
18970
+ }
18971
+ }
18972
+ } catch (statError) {
18973
+ if (statError instanceof FileLockBusyError) throw statError;
18974
+ if (statError.code !== "ENOENT") throw statError;
18975
+ await retryNow();
18976
+ continue;
18977
+ }
18978
+ if (Date.now() >= deadline) {
18979
+ throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
18980
+ }
18981
+ await sleep(opts.retryMs);
18982
+ continue;
18983
+ }
18984
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18985
+ try {
18986
+ await handle.writeFile(token);
18987
+ } catch (e) {
18988
+ await handle.close().catch(() => void 0);
18989
+ throw e;
18990
+ }
18991
+ return { token, handle };
18992
+ }
18993
+ }
18994
+ async function fileLockHeldBy(lockPath, token) {
18995
+ try {
18996
+ return await (0, import_promises2.readFile)(lockPath, "utf8") === token;
18997
+ } catch {
18998
+ return false;
18999
+ }
19000
+ }
19001
+ async function releaseFileLock(lockPath, guard) {
19002
+ await guard.handle.close().catch(() => void 0);
19003
+ if (await fileLockHeldBy(lockPath, guard.token)) {
19004
+ await (0, import_promises2.unlink)(lockPath).catch(() => void 0);
19005
+ }
19006
+ }
19007
+ async function withFileLock(lockPath, opts, fn) {
19008
+ const resolved = resolveFileLockOpts(opts);
19009
+ await (0, import_promises2.mkdir)((0, import_node_path17.dirname)(lockPath), { recursive: true }).catch(() => void 0);
19010
+ const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
19011
+ try {
19012
+ return await fn();
19013
+ } finally {
19014
+ await releaseFileLock(lockPath, guard);
19015
+ }
19016
+ }
19017
+
19018
+ // src/release-ledger.ts
19019
+ var RELEASE_LEDGER_SCHEMA = "mmi-release-ledger/v1";
19020
+ var RELEASE_LEDGER_PHASES = [
19021
+ "promotion",
19022
+ "ordinaryCi",
19023
+ "githubRelease",
19024
+ "publish",
19025
+ "deploy",
19026
+ "alignment"
19027
+ ];
19028
+ var PHASE_SET = new Set(RELEASE_LEDGER_PHASES);
19029
+ var STATE_SET = /* @__PURE__ */ new Set(["pending", "complete", "failed", "skipped"]);
19030
+ var ReleaseLedgerError = class extends Error {
19031
+ kind;
19032
+ constructor(kind, message) {
19033
+ super(message);
19034
+ this.name = "ReleaseLedgerError";
19035
+ this.kind = kind;
19036
+ }
19037
+ };
19038
+ var SHA_RE = /^[0-9a-f]{40}$/;
19039
+ var MAX_TEXT = 500;
19040
+ function truncate(text, max) {
19041
+ return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
19042
+ }
19043
+ function normalizeLedgerAnchors(anchors) {
19044
+ const repo = anchors.repo?.trim();
19045
+ const tag = anchors.tag?.trim();
19046
+ const tagSha = anchors.tagSha?.trim().toLowerCase();
19047
+ if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) {
19048
+ throw new ReleaseLedgerError("invalid-record", `release ledger: invalid repo anchor ${JSON.stringify(anchors.repo ?? "")}`);
19049
+ }
19050
+ if (!tag || !/^v\d+\.\d+\.\d+(-rc\.\d+)?$/.test(tag)) {
19051
+ throw new ReleaseLedgerError("invalid-record", `release ledger: invalid tag anchor ${JSON.stringify(anchors.tag ?? "")}`);
19052
+ }
19053
+ if (!tagSha || !SHA_RE.test(tagSha)) {
19054
+ throw new ReleaseLedgerError("invalid-record", `release ledger: invalid tagSha anchor ${JSON.stringify(anchors.tagSha ?? "")}`);
19055
+ }
19056
+ return { repo, tag, tagSha };
19057
+ }
19058
+ function emptyPhases() {
19059
+ return Object.fromEntries(RELEASE_LEDGER_PHASES.map((p) => [p, { state: "pending", updatedAt: "never" }]));
19060
+ }
19061
+ function validatePhaseRecord(phase, raw) {
19062
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
19063
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} is not an object`);
19064
+ }
19065
+ const rec = raw;
19066
+ const state = rec.state;
19067
+ if (typeof state !== "string" || !STATE_SET.has(state)) {
19068
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has invalid state ${JSON.stringify(state)}`);
19069
+ }
19070
+ if (rec.sha !== void 0 && (typeof rec.sha !== "string" || !SHA_RE.test(rec.sha))) {
19071
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed sha`);
19072
+ }
19073
+ if (rec.runId !== void 0 && (typeof rec.runId !== "number" || !Number.isSafeInteger(rec.runId) || rec.runId <= 0)) {
19074
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runId`);
19075
+ }
19076
+ if (rec.runRepo !== void 0 && (typeof rec.runRepo !== "string" || !/^[\w.-]+\/[\w.-]+$/.test(rec.runRepo))) {
19077
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runRepo`);
19078
+ }
19079
+ if (rec.runProvenance !== void 0 && rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
19080
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed runProvenance ${JSON.stringify(rec.runProvenance)}`);
19081
+ }
19082
+ for (const key of ["runUrl", "note", "error", "workflow", "event"]) {
19083
+ if (rec[key] !== void 0 && (typeof rec[key] !== "string" || rec[key].length > MAX_TEXT * 4)) {
19084
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed ${key}`);
19085
+ }
19086
+ }
19087
+ if (rec.retries !== void 0 && (typeof rec.retries !== "number" || !Number.isSafeInteger(rec.retries) || rec.retries < 0 || rec.retries > 9)) {
19088
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} has a malformed retries value`);
19089
+ }
19090
+ if (typeof rec.updatedAt !== "string" || !rec.updatedAt) {
19091
+ throw new ReleaseLedgerError("invalid-record", `release ledger: phase ${phase} is missing updatedAt`);
19092
+ }
19093
+ return rec;
19094
+ }
19095
+ function parseReleaseLedger(raw) {
19096
+ let parsed;
19097
+ try {
19098
+ parsed = JSON.parse(raw);
19099
+ } catch (e) {
19100
+ throw new ReleaseLedgerError("unreadable", `release ledger is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to resume against unreadable state (#5987)`);
19101
+ }
19102
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
19103
+ throw new ReleaseLedgerError("unreadable", "release ledger is not a JSON object");
19104
+ }
19105
+ const obj = parsed;
19106
+ if (obj.schema !== RELEASE_LEDGER_SCHEMA) {
19107
+ throw new ReleaseLedgerError(
19108
+ "unknown-schema",
19109
+ `release ledger schema ${JSON.stringify(obj.schema ?? "(missing)")} is not ${JSON.stringify(RELEASE_LEDGER_SCHEMA)} \u2014 a NEWER ledger this CLI cannot interpret must fail closed; do not delete or hand-edit the file, use a CLI that understands it`
19110
+ );
19111
+ }
19112
+ const repo = obj.repo;
19113
+ const tag = obj.tag;
19114
+ const tagSha = obj.tagSha;
19115
+ if (typeof repo !== "string" || !repo || typeof tag !== "string" || !tag || typeof tagSha !== "string" || !SHA_RE.test(tagSha)) {
19116
+ throw new ReleaseLedgerError("unreadable", `release ledger has malformed identity anchors (repo/tag/tagSha)`);
19117
+ }
19118
+ if (typeof obj.updatedAt !== "string" || !obj.updatedAt) {
19119
+ throw new ReleaseLedgerError("unreadable", "release ledger is missing updatedAt");
19120
+ }
19121
+ const phasesRaw = obj.phases;
19122
+ if (typeof phasesRaw !== "object" || phasesRaw === null || Array.isArray(phasesRaw)) {
19123
+ throw new ReleaseLedgerError("unreadable", "release ledger phases block is not an object");
19124
+ }
19125
+ const phases = { ...emptyPhases() };
19126
+ for (const [phase, rec] of Object.entries(phasesRaw)) {
19127
+ if (!PHASE_SET.has(phase)) continue;
19128
+ phases[phase] = validatePhaseRecord(phase, rec);
19129
+ }
19130
+ return { schema: RELEASE_LEDGER_SCHEMA, repo, tag, tagSha, phases, updatedAt: obj.updatedAt };
19131
+ }
19132
+ function validateLedgerAnchors(ledger, expected) {
19133
+ const anchors = normalizeAnchorsForCompare(expected);
19134
+ if (ledger.repo.toLowerCase() !== anchors.repo.toLowerCase()) {
19135
+ throw new ReleaseLedgerError(
19136
+ "repo-mismatch",
19137
+ `release ledger belongs to ${ledger.repo}, not ${anchors.repo} \u2014 refusing to mix releases across repos (#5987)`
19138
+ );
19139
+ }
19140
+ if (ledger.tag !== anchors.tag) {
19141
+ throw new ReleaseLedgerError(
19142
+ "tag-mismatch",
19143
+ `release ledger belongs to ${ledger.tag}, not ${anchors.tag} \u2014 refusing to resume a different tag against it (#5987)`
19144
+ );
19145
+ }
19146
+ if (ledger.tagSha.toLowerCase() !== anchors.tagSha.toLowerCase()) {
19147
+ throw new ReleaseLedgerError(
19148
+ "tag-sha-mismatch",
19149
+ `release ledger anchors ${ledger.tag} at ${ledger.tagSha.slice(0, 12)} but live origin reports ${anchors.tagSha.slice(0, 12)} \u2014 the immutable tag moved or the ledger is stale; failing closed instead of trusting either (#5987)`
19150
+ );
19151
+ }
19152
+ }
19153
+ function normalizeAnchorsForCompare(expected) {
19154
+ const repo = expected.repo?.trim() ?? "";
19155
+ const tag = expected.tag?.trim() ?? "";
19156
+ const tagSha = expected.tagSha?.trim().toLowerCase() ?? "";
19157
+ if (!repo || !tag || !SHA_RE.test(tagSha)) {
19158
+ throw new ReleaseLedgerError("invalid-record", `release ledger: invalid live anchors (repo=${JSON.stringify(repo)}, tag=${JSON.stringify(tag)})`);
19159
+ }
19160
+ return { repo, tag, tagSha };
19161
+ }
19162
+ function resolvePrimaryGitDir(cwd) {
19163
+ const gitDir = resolveGitDir(cwd);
19164
+ if (!gitDir) {
19165
+ throw new ReleaseLedgerError("no-checkout", `release ledger: no git checkout found above ${cwd}`);
19166
+ }
19167
+ let common;
19168
+ try {
19169
+ common = (0, import_node_fs19.readFileSync)((0, import_node_path18.join)(gitDir, "commondir"), "utf8").trim();
19170
+ } catch (e) {
19171
+ if (e.code !== "ENOENT") {
19172
+ throw new ReleaseLedgerError("unreadable", `release ledger: the worktree gitdir's commondir pointer could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not primary (#5987/#4713)`);
19173
+ }
19174
+ }
19175
+ if (!common) return gitDir;
19176
+ const commonDir = (0, import_node_path18.isAbsolute)(common) ? common : (0, import_node_path18.resolve)(gitDir, common);
19177
+ try {
19178
+ (0, import_node_fs19.readFileSync)((0, import_node_path18.join)(commonDir, "HEAD"), "utf8");
19179
+ } catch (e) {
19180
+ throw new ReleaseLedgerError("no-checkout", `release ledger: the worktree's commondir pointer names ${commonDir}, which has no readable HEAD (${e instanceof Error ? e.message : String(e)}) \u2014 the primary checkout may have been removed; refusing to write the ledger into a doomed location`);
19181
+ }
19182
+ return commonDir;
19183
+ }
19184
+ function releaseLedgerPath(cwd) {
19185
+ return (0, import_node_path18.join)(resolvePrimaryGitDir(cwd), "mmi-runtime", "release", "phases.json");
19186
+ }
19187
+ function atomicWriteJson(path2, ledger) {
19188
+ (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path2), { recursive: true });
19189
+ const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`;
19190
+ (0, import_node_fs19.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
19191
+ `, "utf8");
19192
+ (0, import_node_fs19.renameSync)(tmp, path2);
19193
+ }
19194
+ async function withReleaseLedger(path2, expected, fn, opts = {}) {
19195
+ return withFileLock(`${path2}.lock`, { label: "release ledger", maxWaitMs: opts.maxWaitMs ?? 5e3 }, async () => {
19196
+ let current;
19197
+ let raw;
19198
+ try {
19199
+ raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
19200
+ } catch (e) {
19201
+ if (e.code !== "ENOENT") {
19202
+ throw new ReleaseLedgerError("unreadable", `release ledger at ${path2} could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not absent (#5987)`);
19203
+ }
19204
+ }
19205
+ if (raw !== void 0) {
19206
+ current = parseReleaseLedger(raw);
19207
+ if (expected) validateLedgerAnchors(current, expected);
19208
+ }
19209
+ const { ledger, value } = await fn(current);
19210
+ if (ledger) atomicWriteJson(path2, ledger);
19211
+ return value;
19212
+ });
19213
+ }
19214
+ function openReleaseLedgerWriter(cwd, opts = {}) {
19215
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
19216
+ let path2;
19217
+ try {
19218
+ path2 = releaseLedgerPath(cwd);
19219
+ } catch (e) {
19220
+ const failure = e;
19221
+ return {
19222
+ path: "(unresolved)",
19223
+ record: async () => ({ ok: false, error: failure instanceof Error ? failure.message : String(failure) }),
19224
+ load: async () => {
19225
+ throw failure;
19226
+ }
19227
+ };
19228
+ }
19229
+ return {
19230
+ path: path2,
19231
+ record: async (anchors, phase, entry) => {
19232
+ try {
19233
+ return await withReleaseLedger(
19234
+ path2,
19235
+ anchors,
19236
+ (current) => {
19237
+ const normalized = normalizeLedgerAnchors(anchors);
19238
+ const base = current ?? {
19239
+ schema: RELEASE_LEDGER_SCHEMA,
19240
+ repo: normalized.repo,
19241
+ tag: normalized.tag,
19242
+ tagSha: normalized.tagSha,
19243
+ phases: emptyPhases(),
19244
+ updatedAt: now().toISOString()
19245
+ };
19246
+ const previousRetries = base.phases[phase].retries ?? 0;
19247
+ const record = {
19248
+ ...entry,
19249
+ note: entry.note !== void 0 ? truncate(entry.note, MAX_TEXT) : void 0,
19250
+ error: entry.error !== void 0 ? truncate(entry.error, MAX_TEXT * 2) : void 0,
19251
+ retries: entry.retries ?? previousRetries,
19252
+ updatedAt: now().toISOString()
19253
+ };
19254
+ const updated = {
19255
+ ...base,
19256
+ phases: { ...base.phases, [phase]: record },
19257
+ updatedAt: now().toISOString()
19258
+ };
19259
+ return { ledger: updated, value: { ok: true } };
19260
+ },
19261
+ { maxWaitMs: opts.maxWaitMs }
19262
+ );
19263
+ } catch (e) {
19264
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
19265
+ }
19266
+ },
19267
+ load: async (anchors) => {
19268
+ try {
19269
+ return await withReleaseLedger(
19270
+ path2,
19271
+ anchors,
19272
+ (current) => ({ value: current }),
19273
+ { maxWaitMs: opts.maxWaitMs }
19274
+ );
19275
+ } catch (e) {
19276
+ if (e instanceof ReleaseLedgerError) throw e;
19277
+ throw new ReleaseLedgerError("unreadable", e instanceof Error ? e.message : String(e));
19278
+ }
19279
+ }
19280
+ };
19281
+ }
19282
+ function compactEntry(entry) {
19283
+ return Object.fromEntries(Object.entries(entry).filter(([, v]) => v !== void 0));
19284
+ }
19285
+ function phaseEntry(entry) {
19286
+ return compactEntry(entry);
19287
+ }
19288
+ var PHASE_RANK = { failed: 0, pending: 1, complete: 2, skipped: 2 };
19289
+ function deriveReleasePhaseStatus(ledger) {
19290
+ const legs = RELEASE_LEDGER_PHASES.map((phase) => {
19291
+ const rec = ledger.phases[phase];
19292
+ return {
19293
+ phase,
19294
+ state: rec.state,
19295
+ ...rec.runId !== void 0 ? { runId: rec.runId } : {},
19296
+ ...rec.runRepo !== void 0 ? { runRepo: rec.runRepo } : {},
19297
+ ...rec.workflow !== void 0 ? { workflow: rec.workflow } : {},
19298
+ ...rec.event !== void 0 ? { event: rec.event } : {},
19299
+ ...rec.note !== void 0 ? { note: rec.note } : {},
19300
+ ...rec.error !== void 0 ? { error: rec.error } : {}
19301
+ };
19302
+ });
19303
+ const worst = legs.reduce((acc, leg) => Math.min(acc, PHASE_RANK[leg.state]), 2);
19304
+ return { phaseStatus: worst === 0 ? "phases-failed" : worst === 1 ? "phases-pending" : "phases-green", legs };
19305
+ }
19306
+ function safeRunAction(runId, runRepo, state) {
19307
+ return state === "failed" ? `next: diagnose with gh run view ${runId} --repo ${runRepo} --log-failed; rerun by hand only when the cause is fixed: gh run rerun ${runId} --repo ${runRepo} --failed (never re-tag)` : `next: gh run watch ${runId} --repo ${runRepo} --exit-status`;
19308
+ }
19309
+ function formatReleaseLedgerReport(report) {
19310
+ const lines = [`release phase ledger: ${report.phaseStatus.toUpperCase()} (${report.path}) \u2014 phase status of the recorded boundaries, NOT the release verdict`];
19311
+ for (const leg of report.legs) {
19312
+ const detail = leg.error ?? leg.note;
19313
+ const run = [leg.workflow, leg.event ? `(${leg.event})` : void 0, leg.runId != null ? `run ${leg.runId}` : void 0, leg.runRepo ? `in ${leg.runRepo}` : void 0].filter(Boolean).join(" ");
19314
+ lines.push(` - ${leg.phase}: ${leg.state.toUpperCase()}${run ? ` \u2014 ${run}` : ""}${detail ? ` \u2014 ${detail}` : ""}`);
19315
+ if ((leg.state === "failed" || leg.state === "pending") && leg.runId != null && leg.runRepo) {
19316
+ lines.push(` ${safeRunAction(leg.runId, leg.runRepo, leg.state === "failed" ? "failed" : "pending")}`);
19317
+ }
19318
+ }
19319
+ return lines;
19320
+ }
19321
+ function releaseLedgerReport(ledger, path2) {
19322
+ if (!ledger) return void 0;
19323
+ const derived = deriveReleasePhaseStatus(ledger);
19324
+ return { path: path2, phases: ledger.phases, phaseStatus: derived.phaseStatus, legs: derived.legs };
19325
+ }
19326
+ var TRANSIENT_TRANSPORT_MARKERS = [
19327
+ /connection (?:reset|refused|aborted|timed? ?out)/i,
19328
+ /\bECONN(?:RESET|REFUSED|ABORTED|TIMEDOUT)\b/,
19329
+ /\bE(?:HOST|NET)UNREACH\b/,
19330
+ /\bEAI_AGAIN\b/,
19331
+ /\bEPIPE\b/,
19332
+ /lost communication with (?:the )?github actions runner service/i,
19333
+ /runner lost communication/i,
19334
+ /\b(?:502|503|504)\b[^0-9]{0,40}(?:bad gateway|service unavailable|gateway time-?out|server error)/i,
19335
+ /(?:bad gateway|service unavailable|gateway time-?out)/i,
19336
+ /fetch failed/i,
19337
+ /network (?:error|failure|unreachable|timeout|timed? ?out)/i,
19338
+ /socket hang ?up/i,
19339
+ /remote end hung ?up unexpectedly/i,
19340
+ /early EOF/i,
19341
+ /tls handshake timeout/i
19342
+ ];
19343
+ function classifyTransientTransportFailure(evidence) {
19344
+ if (!evidence) return false;
19345
+ return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence));
19346
+ }
19347
+ var TRANSIENT_EVIDENCE_MAX_CHARS = 16e3;
19348
+ var AUTO_RERUN_MAX_RETRIES = 1;
19349
+ function failedRunIdentity(run, repo) {
19350
+ return `${run.workflow}${run.event ? ` (${run.event})` : ""} run ${run.runId} in ${repo}`;
19351
+ }
19352
+ function manualRunAction(run, repo) {
19353
+ return `safe next action: diagnose with gh run view ${run.runId} --repo ${repo} --log-failed; rerun by hand only when the cause is fixed: gh run rerun ${run.runId} --repo ${repo} --failed (never re-tag)`;
19354
+ }
19355
+ async function readFailedRunsOnSha(deps, repo, sha) {
19356
+ try {
19357
+ const rows = JSON.parse(await deps.run("gh", [
19358
+ "run",
19359
+ "list",
19360
+ "--repo",
19361
+ repo,
19362
+ "--commit",
19363
+ sha,
19364
+ "--limit",
19365
+ "20",
19366
+ "--json",
19367
+ "databaseId,url,workflowName,event,status,conclusion"
19368
+ ]));
19369
+ if (!Array.isArray(rows)) return [];
19370
+ return rows.filter((r) => typeof r.databaseId === "number" && r.status === "completed" && (r.conclusion === "failure" || r.conclusion === "cancelled")).map((r) => ({
19371
+ workflow: r.workflowName ?? `run:${r.databaseId}`,
19372
+ runId: r.databaseId,
19373
+ runUrl: r.url,
19374
+ repo,
19375
+ ...r.event ? { event: r.event } : {},
19376
+ conclusion: r.conclusion ?? "failure"
19377
+ }));
19378
+ } catch {
19379
+ return [];
19380
+ }
19381
+ }
19382
+ async function transientEvidenceForRun(deps, repo, runId) {
19383
+ const parts = [];
19384
+ try {
19385
+ parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "jobs", "--jq", '[.jobs[] | select(.conclusion=="failure") | {name:.name, steps:[.steps[] | select(.conclusion=="failure") | .name]}] | tostring']));
19386
+ } catch {
19387
+ }
19388
+ try {
19389
+ parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--log-failed"]));
19390
+ } catch {
19391
+ }
19392
+ const evidence = parts.filter(Boolean).join("\n");
19393
+ return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
19394
+ }
19395
+ function extractFailedRequiredContexts(failure) {
19396
+ const match = /required train check failed:\s*([^\n(]+)/.exec(failure);
19397
+ if (!match) return [];
19398
+ return match[1].split(",").map((s) => s.trim()).filter(Boolean);
19399
+ }
19400
+ async function readFailedCheckRunsOnSha(deps, repo, sha) {
19401
+ try {
19402
+ const rows = JSON.parse(await deps.run("gh", [
19403
+ "api",
19404
+ `repos/${repo}/commits/${sha}/check-runs`,
19405
+ "--jq",
19406
+ '[.check_runs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != null) | {name: .name, url: .details_url}]'
19407
+ ]));
19408
+ if (!Array.isArray(rows)) return [];
19409
+ return rows.flatMap((r) => {
19410
+ if (typeof r.name !== "string" || typeof r.url !== "string") return [];
19411
+ const id = /\/actions\/runs\/(\d+)/.exec(r.url);
19412
+ return [{ name: r.name, ...id ? { runId: Number(id[1]) } : {} }];
19413
+ });
19414
+ } catch {
19415
+ return [];
19416
+ }
19417
+ }
19418
+ async function correlateCandidateRunsToRequiredContexts(deps, repo, sha, candidates, contexts) {
19419
+ if (contexts.length === 0) {
19420
+ return {
19421
+ unique: false,
19422
+ note: `the failure names no failed required context \u2014 rerunning the first failed run could rerun an unrelated workflow; manual intervention required; safe next action: gh run list --repo ${repo} --commit ${sha.slice(0, 12)} to identify the run`
19423
+ };
19424
+ }
19425
+ const failedChecks = await readFailedCheckRunsOnSha(deps, repo, sha);
19426
+ const contextRunIds = /* @__PURE__ */ new Map();
19427
+ for (const check of failedChecks) {
19428
+ if (check.runId != null && contexts.includes(check.name)) contextRunIds.set(check.runId, check.name);
19429
+ }
19430
+ const proven = candidates.filter((c) => c.runId != null && contextRunIds.has(c.runId));
19431
+ if (proven.length === 1) return { unique: true, candidate: proven[0] };
19432
+ if (proven.length > 1) {
19433
+ return {
19434
+ unique: false,
19435
+ note: `${proven.length} failed runs correspond to the failed required contexts (${contexts.join(", ")}): ${proven.map((c) => failedRunIdentity(c, c.repo ?? repo)).join("; ")} \u2014 ambiguous which to rerun; manual intervention required; safe next action: gh run view <run-id> --repo ${repo} --log-failed`
19436
+ };
19437
+ }
19438
+ return {
19439
+ unique: false,
19440
+ note: `no enumerated failed run corresponds to the failed required contexts (${contexts.join(", ")}) \u2014 the failed run(s) are unrelated to the required gate; manual intervention required; safe next action: gh run view <run-id> --repo ${repo} --log-failed`
19441
+ };
19442
+ }
19443
+ async function transientRecoveryForFailedPhase(deps, input) {
19444
+ const { ledger, anchors, phase, sha, failure } = input;
19445
+ const enumerated = !input.runs;
19446
+ const provenance = input.runsProvenance ?? "sha";
19447
+ const recordProvenance = provenance === "watched" ? "dispatch" : "sha";
19448
+ let candidates = (input.runs ?? await transientRecoveryEnumerate(deps, input.repo, sha)).filter((r) => typeof r.runId === "number" && (r.conclusion === "failure" || r.conclusion === "cancelled"));
19449
+ const first = candidates[0];
19450
+ await ledger.record(anchors, phase, phaseEntry({
19451
+ state: "failed",
19452
+ sha,
19453
+ error: failure,
19454
+ ...first ? {
19455
+ runId: first.runId,
19456
+ ...first.runUrl ? { runUrl: first.runUrl } : {},
19457
+ runRepo: first.repo ?? input.repo,
19458
+ runProvenance: recordProvenance,
19459
+ workflow: first.workflow,
19460
+ ...first.event ? { event: first.event } : {}
19461
+ } : {}
19462
+ }));
19463
+ const recorded = await ledger.load(anchors).catch(() => void 0);
19464
+ const retriesSoFar = recorded?.phases[phase].retries ?? 0;
19465
+ if (retriesSoFar >= AUTO_RERUN_MAX_RETRIES) {
19466
+ const spent = first ? ` for ${failedRunIdentity(first, first.repo ?? input.repo)}` : "";
19467
+ return {
19468
+ reran: false,
19469
+ recovered: false,
19470
+ ...first?.runId != null ? { runId: first.runId, runRepo: first.repo ?? input.repo } : {},
19471
+ note: `automatic rerun already spent for ${phase}${spent} \u2014 manual intervention required; ${first?.runId != null ? manualRunAction(first, first.repo ?? input.repo) : "safe next action: identify the failed run on the Actions tab"}`
19472
+ };
19473
+ }
19474
+ if (candidates.length === 0) {
19475
+ return { reran: false, recovered: false, note: `no failed run with an id could be classified on ${sha.slice(0, 12)} \u2014 manual intervention required (no marker proof, no automatic rerun); safe next action: gh run list --repo ${input.repo} --commit ${sha.slice(0, 12)} to identify the run` };
19476
+ }
19477
+ if (enumerated && phase === "ordinaryCi") {
19478
+ const correlation = await correlateCandidateRunsToRequiredContexts(deps, input.repo, sha, candidates, extractFailedRequiredContexts(failure));
19479
+ if (!correlation.unique) return { reran: false, recovered: false, note: correlation.note };
19480
+ candidates = [correlation.candidate];
19481
+ }
19482
+ const candidate = candidates[0];
19483
+ const runRepo = candidate.repo ?? input.repo;
19484
+ let meta;
19485
+ try {
19486
+ meta = JSON.parse(await deps.run("gh", [
19487
+ "run",
19488
+ "view",
19489
+ String(candidate.runId),
19490
+ "--repo",
19491
+ runRepo,
19492
+ "--json",
19493
+ "headSha,status,conclusion,url"
19494
+ ]));
19495
+ } catch (e) {
19496
+ return { reran: false, recovered: false, note: `could not READ failed run ${failedRunIdentity(candidate, runRepo)} to prove its identity (${e instanceof Error ? e.message : String(e)}) \u2014 no automatic rerun without proof; ${manualRunAction(candidate, runRepo)}` };
19497
+ }
19498
+ const shaProven = provenance === "watched" || (meta.headSha ?? "").toLowerCase() === sha.toLowerCase();
19499
+ if (!shaProven || meta.conclusion !== "failure") {
19500
+ return { reran: false, recovered: false, note: `run ${failedRunIdentity(candidate, runRepo)} does not prove a failed run on ${sha.slice(0, 12)} \u2014 no automatic rerun; ${manualRunAction(candidate, runRepo)}` };
19501
+ }
19502
+ const evidence = await transientEvidenceForRun(deps, runRepo, candidate.runId);
19503
+ if (!classifyTransientTransportFailure(evidence)) {
19504
+ return { reran: false, recovered: false, note: `run ${failedRunIdentity(candidate, runRepo)} failed on evidence that is NOT a proven transient transport/network failure \u2014 manual intervention required; ${manualRunAction(candidate, runRepo)}` };
19505
+ }
19506
+ const reservation = await ledger.record(anchors, phase, phaseEntry({
19507
+ state: "pending",
19508
+ sha,
19509
+ runId: candidate.runId,
19510
+ ...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
19511
+ runRepo,
19512
+ runProvenance: recordProvenance,
19513
+ workflow: candidate.workflow,
19514
+ ...candidate.event ? { event: candidate.event } : {},
19515
+ note: "ONE automatic transient rerun RESERVED (cap spent durably) \u2014 dispatch pending",
19516
+ retries: retriesSoFar + 1
19517
+ }));
19518
+ if (!reservation.ok) {
19519
+ return { reran: false, recovered: false, note: `the rerun reservation could not be durably recorded (${reservation.error}) \u2014 no automatic rerun without a durable cap; manual intervention required; ${manualRunAction(candidate, runRepo)}` };
19520
+ }
19521
+ try {
19522
+ await deps.run("gh", ["run", "rerun", String(candidate.runId), "--repo", runRepo, "--failed"]);
19523
+ } catch (e) {
19524
+ await ledger.record(anchors, phase, phaseEntry({
19525
+ state: "failed",
19526
+ sha,
19527
+ runId: candidate.runId,
19528
+ ...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
19529
+ runRepo,
19530
+ runProvenance: recordProvenance,
19531
+ workflow: candidate.workflow,
19532
+ ...candidate.event ? { event: candidate.event } : {},
19533
+ error: `automatic rerun dispatch failed: ${e instanceof Error ? e.message : String(e)}`,
19534
+ retries: retriesSoFar + 1
19535
+ }));
19536
+ return {
19537
+ reran: true,
19538
+ recovered: false,
19539
+ runId: candidate.runId,
19540
+ runRepo,
19541
+ note: `automatic rerun of ${failedRunIdentity(candidate, runRepo)} FAILED to dispatch (${e instanceof Error ? e.message : String(e)}) \u2014 the one automatic rerun remains spent; manual intervention required; ${manualRunAction(candidate, runRepo)}`
19542
+ };
19543
+ }
19544
+ const watched = await (input.watchRun ?? defaultWatchRun)(deps, candidate.runId, runRepo);
19545
+ const runFields = {
19546
+ sha,
19547
+ runId: candidate.runId,
19548
+ ...meta.url ?? candidate.runUrl ? { runUrl: meta.url ?? candidate.runUrl } : {},
19549
+ runRepo,
19550
+ runProvenance: recordProvenance,
19551
+ workflow: candidate.workflow,
19552
+ ...candidate.event ? { event: candidate.event } : {}
19553
+ };
19554
+ if (watched === "success") {
19555
+ await ledger.record(anchors, phase, phaseEntry({
19556
+ state: "complete",
19557
+ ...runFields,
19558
+ note: `recovered: the ONE automatic rerun was watched green in this invocation`,
19559
+ retries: retriesSoFar + 1
19560
+ }));
19561
+ return {
19562
+ reran: true,
19563
+ recovered: true,
19564
+ runId: candidate.runId,
19565
+ runRepo,
19566
+ note: `${failedRunIdentity(candidate, runRepo)} was a proven transient transport failure \u2014 the ONE automatic rerun was dispatched and watched GREEN; continuing in this invocation`
19567
+ };
19568
+ }
19569
+ if (watched === "failure") {
19570
+ await ledger.record(anchors, phase, phaseEntry({
19571
+ state: "failed",
19572
+ ...runFields,
19573
+ error: "the automatic rerun was watched FAILURE \u2014 not transient after all",
19574
+ retries: retriesSoFar + 1
19575
+ }));
19576
+ return {
19577
+ reran: true,
19578
+ recovered: false,
19579
+ runId: candidate.runId,
19580
+ runRepo,
19581
+ note: `the automatic rerun of ${failedRunIdentity(candidate, runRepo)} FAILED \u2014 the one automatic rerun is spent; manual intervention required; ${manualRunAction(candidate, runRepo)}`
19582
+ };
19583
+ }
19584
+ return {
19585
+ reran: true,
19586
+ recovered: false,
19587
+ runId: candidate.runId,
19588
+ runRepo,
19589
+ note: `the automatic rerun of ${failedRunIdentity(candidate, runRepo)} did not resolve within the bounded watch \u2014 typed PENDING (the one automatic rerun is spent); safe next action: gh run watch ${candidate.runId} --repo ${runRepo} --exit-status`
19590
+ };
19591
+ }
19592
+ async function transientRecoveryEnumerate(deps, repo, sha) {
19593
+ return readFailedRunsOnSha(deps, repo, sha);
19594
+ }
19595
+ var RUN_CONFIRM_ATTEMPTS2 = 3;
19596
+ var RUN_CONFIRM_DELAY_MS2 = 1e3;
19597
+ async function defaultWatchRun(deps, runId, repo) {
19598
+ let watchSaidSuccess = true;
19599
+ try {
19600
+ await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
19601
+ } catch {
19602
+ watchSaidSuccess = false;
19603
+ }
19604
+ let confirmError;
19605
+ for (let attempt = 0; attempt < RUN_CONFIRM_ATTEMPTS2; attempt++) {
19606
+ if (attempt > 0) await new Promise((resolve7) => setTimeout(resolve7, RUN_CONFIRM_DELAY_MS2));
19607
+ try {
19608
+ const parsed = JSON.parse(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]));
19609
+ if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
19610
+ confirmError = void 0;
19611
+ } catch (e) {
19612
+ confirmError = e;
19613
+ }
19614
+ }
19615
+ if (!watchSaidSuccess) return "failure";
19616
+ if (confirmError) return "pending";
19617
+ return "success";
19618
+ }
19619
+ function alignmentPhaseEntry(dev, rc) {
19620
+ if (!dev && !rc) return { state: "skipped", note: "no protected-branch alignment leg on this release" };
19621
+ const pending = dev?.status === "pr-pending" || rc?.status === "pr-pending";
19622
+ const note = [dev ? `development: ${dev.note}` : void 0, rc ? `rc: ${rc.note}` : void 0].filter(Boolean).join("; ");
19623
+ return { state: pending ? "pending" : "complete", note };
19624
+ }
19625
+ function runRowState(run, fallback) {
19626
+ if (!run) return fallback === "success" ? "complete" : fallback === "failure" ? "failed" : "pending";
19627
+ return run.conclusion === "success" ? "complete" : run.conclusion === "failure" ? "failed" : "pending";
19628
+ }
19629
+ function runMetadata(run, runRepo, provenance) {
19630
+ return {
19631
+ ...run.runId != null ? { runId: run.runId } : {},
19632
+ ...run.runUrl != null ? { runUrl: run.runUrl } : {},
19633
+ ...run.runId != null ? { runRepo, runProvenance: provenance } : {},
19634
+ ...run.workflow ? { workflow: run.workflow } : {},
19635
+ ...run.event ? { event: run.event } : {}
19636
+ };
19637
+ }
19638
+ function releaseRunRepoFor(model, productRepo) {
19639
+ return model === "hub-serverless" || isCentralDispatchModel(model) ? HUB_REPO2 : productRepo;
19640
+ }
19641
+ function deployPhaseInput(model, dispatch, opts = {}) {
19642
+ const sha = opts.releaseSha;
19643
+ const runRepo = releaseRunRepoFor(model, "");
19644
+ switch (model) {
19645
+ case "hub-serverless": {
19646
+ const deployRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "deploy.yml");
19647
+ return {
19648
+ state: runRowState(deployRun, dispatch.deployStatus),
19649
+ ...sha ? { sha } : {},
19650
+ ...runMetadata(deployRun ?? {}, runRepo, "sha"),
19651
+ note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
19652
+ };
19653
+ }
19654
+ case "registry-publish":
19655
+ return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
19656
+ case "tenant-container":
19657
+ case "solo-container":
19658
+ case "static-cdn":
19659
+ return {
19660
+ state: dispatch.deployStatus === "success" ? "complete" : dispatch.deployStatus === "failure" ? "failed" : "pending",
19661
+ ...sha ? { sha } : {},
19662
+ ...runMetadata({ runId: dispatch.runId, runUrl: dispatch.runUrl, workflow: "tenant-deploy.yml", event: "workflow_dispatch" }, runRepo, "dispatch"),
19663
+ note: dispatch.note
19664
+ };
19665
+ default:
19666
+ return {
19667
+ state: dispatch.deployStatus === "success" ? "complete" : dispatch.deployStatus === "failure" ? "failed" : "pending",
19668
+ ...sha ? { sha } : {},
19669
+ note: dispatch.note
19670
+ };
19671
+ }
19672
+ }
19673
+ function publishPhaseInput(model, dispatch, publishDispatch, publishRequired, opts = {}) {
19674
+ const sha = opts.releaseSha;
19675
+ const publishRow = publishDispatch ? {
19676
+ state: publishDispatch.deployStatus === "success" ? "complete" : publishDispatch.deployStatus === "failure" ? "failed" : "pending",
19677
+ ...runMetadata({ runId: publishDispatch.runId, runUrl: publishDispatch.runUrl, workflow: "tenant-publish.yml", event: "workflow_dispatch" }, HUB_REPO2, "dispatch"),
19678
+ note: publishDispatch.note
19679
+ } : void 0;
19680
+ switch (model) {
19681
+ case "hub-serverless": {
19682
+ const publishRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "publish.yml");
19683
+ return publishRow ?? {
19684
+ state: runRowState(publishRun, dispatch.deployStatus),
19685
+ ...sha ? { sha } : {},
19686
+ ...runMetadata(publishRun ?? {}, HUB_REPO2, "sha"),
19687
+ note: publishRun ? `${publishRun.workflow} release run` : dispatch.note
19688
+ };
19689
+ }
19690
+ case "registry-publish": {
19691
+ const publishRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "publish.yml");
19692
+ return publishRow ?? {
19693
+ state: runRowState(publishRun, dispatch.deployStatus),
19694
+ ...sha ? { sha } : {},
19695
+ ...runMetadata(publishRun ?? {}, opts.repo ?? "", "sha"),
19696
+ note: publishRun ? `${publishRun.workflow} release run` : dispatch.note
19697
+ };
19698
+ }
19699
+ case "tenant-container":
19700
+ case "solo-container":
19701
+ case "static-cdn":
19702
+ return publishRow ?? (publishRequired ? { state: "skipped", ...sha ? { sha } : {}, note: opts.skipPublishNote ?? "publish skipped \u2014 deploy not confirmed" } : { state: "skipped", ...sha ? { sha } : {}, note: "publishRequired=false \u2014 no publish leg" });
19703
+ default:
19704
+ return { state: "skipped", ...sha ? { sha } : {}, note: `${model} has no publish leg` };
19705
+ }
19706
+ }
19707
+ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
19708
+ const deployRun = rows.find((r) => r.workflow === "deploy.yml");
19709
+ const publishRun = rows.find((r) => r.workflow === "publish.yml");
19710
+ switch (model) {
19711
+ case "hub-serverless":
19712
+ return {
19713
+ deploy: {
19714
+ state: runRowState(deployRun, "pending"),
19715
+ sha: tagSha,
19716
+ ...runMetadata(deployRun ?? {}, HUB_REPO2, "sha"),
19717
+ note: deployRun ? `${deployRun.workflow} run on the release SHA` : "deploy.yml release run not yet correlated"
19718
+ },
19719
+ publish: publishRun ? {
19720
+ state: runRowState(publishRun, "pending"),
19721
+ sha: tagSha,
19722
+ ...runMetadata(publishRun, HUB_REPO2, "sha"),
19723
+ note: `${publishRun.workflow} run on the release SHA`
19724
+ } : { state: "pending", sha: tagSha, note: "publish.yml release run not yet correlated" }
19725
+ };
19726
+ case "registry-publish":
19727
+ return {
19728
+ deploy: { state: "skipped", sha: tagSha, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" },
19729
+ publish: publishRun ? {
19730
+ state: runRowState(publishRun, "pending"),
19731
+ sha: tagSha,
19732
+ ...runMetadata(publishRun, opts.repo ?? "", "sha"),
19733
+ note: `${publishRun.workflow} run on the release SHA`
19734
+ } : { state: "pending", sha: tagSha, note: "publish.yml release run not yet correlated" }
19735
+ };
19736
+ default:
19737
+ return {
19738
+ deploy: { state: "pending", sha: tagSha, note: `${model} deploys outside the release SHA's run set \u2014 historical outcome uncorrelatable` },
19739
+ publish: { state: "skipped", sha: tagSha, note: "no historical publish leg correlated for this model" }
19740
+ };
19741
+ }
19742
+ }
19743
+ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
19744
+ const now = input.now ?? (() => /* @__PURE__ */ new Date());
19745
+ const notes = [];
19746
+ const result = await withReleaseLedger(
19747
+ path2,
19748
+ anchors,
19749
+ async (current) => {
19750
+ if (!current) return { value: { ledger: void 0, notes } };
19751
+ const phases = { ...current.phases };
19752
+ for (const phase of ["deploy", "publish"]) {
19753
+ const rec = phases[phase];
19754
+ if (rec.updatedAt === "never") continue;
19755
+ if (rec.runId == null) {
19756
+ if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 the live deploy/publish re-derivation below decides the leg`);
19757
+ continue;
19758
+ }
19759
+ if (rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
19760
+ throw new ReleaseLedgerError(
19761
+ "invalid-record",
19762
+ `the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'|'dispatch' required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
19763
+ );
19764
+ }
19765
+ const runRepo = rec.runRepo ?? input.repo;
19766
+ let meta;
19767
+ try {
19768
+ meta = JSON.parse(await deps.run("gh", [
19769
+ "run",
19770
+ "view",
19771
+ String(rec.runId),
19772
+ "--repo",
19773
+ runRepo,
19774
+ "--json",
19775
+ "status,conclusion,headSha,url"
19776
+ ]));
19777
+ } catch (e) {
19778
+ throw new ReleaseLedgerError(
19779
+ "unreadable",
19780
+ `resume could not re-verify the persisted ${phase} boundary (run ${rec.runId} in ${runRepo}: ${e instanceof Error ? e.message : String(e)}) \u2014 a failed live read is unverified, not passable; retry when GitHub answers (#5987/#4713)`
19781
+ );
19782
+ }
19783
+ if (rec.runProvenance === "sha" && (meta.headSha ?? "").toLowerCase() !== input.sha.toLowerCase()) {
19784
+ throw new ReleaseLedgerError(
19785
+ "tag-sha-mismatch",
19786
+ `the persisted ${phase} boundary names run ${rec.runId} at ${(meta.headSha ?? "").slice(0, 12)} but the release tag is ${input.sha.slice(0, 12)} \u2014 ledger contradicted by live proof; failing closed (#5987)`
19787
+ );
19788
+ }
19789
+ const liveLabel = `${rec.workflow ? `${rec.workflow}${rec.event ? ` (${rec.event})` : ""} ` : ""}run ${rec.runId} in ${runRepo}`;
19790
+ if (meta.status !== "completed") {
19791
+ phases[phase] = { ...rec, state: "pending", note: `re-verified live: ${liveLabel} still in flight`, updatedAt: now().toISOString() };
19792
+ notes.push(`${phase}: ${liveLabel} still in flight \u2014 leg re-verified as pending`);
19793
+ continue;
19794
+ }
19795
+ if (meta.conclusion === "success") {
19796
+ if (rec.state !== "complete") {
19797
+ phases[phase] = { ...rec, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
19798
+ notes.push(`${phase}: ${liveLabel} re-verified green live`);
19799
+ }
19800
+ continue;
19801
+ }
19802
+ if (rec.state === "complete") {
19803
+ throw new ReleaseLedgerError(
19804
+ "contradicted",
19805
+ `the persisted ${phase} boundary says COMPLETE but live ${liveLabel} concluded ${meta.conclusion ?? "not-success"} \u2014 ledger contradicted by live GitHub proof; failing closed instead of trusting either (#5987)`
19806
+ );
19807
+ }
19808
+ phases[phase] = { ...rec, state: "failed", error: `live re-verification: ${liveLabel} concluded ${meta.conclusion}`, updatedAt: now().toISOString() };
19809
+ notes.push(`${phase}: ${liveLabel} failed live \u2014 leg re-verified as failed`);
19810
+ }
19811
+ const ledger = { ...current, phases, updatedAt: now().toISOString() };
19812
+ return { ledger, value: { ledger, notes } };
19813
+ }
19814
+ );
19815
+ return result;
19816
+ }
19817
+ async function clearLedgerFile(path2, anchors, maxWaitMs) {
19818
+ return withFileLock(`${path2}.lock`, { label: "release ledger", maxWaitMs }, async () => {
19819
+ let raw;
19820
+ try {
19821
+ raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
19822
+ } catch (e) {
19823
+ if (e.code === "ENOENT") return { cleared: false };
19824
+ return { cleared: false, note: `release ledger at ${path2} could not be read for clearing (${e instanceof Error ? e.message : String(e)}) \u2014 left in place` };
19825
+ }
19826
+ let ledger;
19827
+ try {
19828
+ ledger = parseReleaseLedger(raw);
19829
+ } catch (e) {
19830
+ return { cleared: false, note: `release ledger at ${path2} is unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 left in place; inspect it by hand` };
19831
+ }
19832
+ try {
19833
+ validateLedgerAnchors(ledger, anchors);
19834
+ } catch (e) {
19835
+ return { cleared: false, note: `release ledger at ${path2} belongs to ${ledger.tag} @ ${ledger.tagSha.slice(0, 12)} \u2014 not this release; left in place (${e instanceof Error ? e.message : String(e)})` };
19836
+ }
19837
+ (0, import_node_fs19.unlinkSync)(path2);
19838
+ return { cleared: true, note: `cleared the release ledger at ${path2} for the live-proven aborted ${anchors.tag}` };
19839
+ });
19840
+ }
19841
+ async function clearReleaseLedger(cwd, anchors, opts = {}) {
19842
+ let path2;
19843
+ try {
19844
+ path2 = releaseLedgerPath(cwd);
19845
+ } catch {
19846
+ return void 0;
19847
+ }
19848
+ return (await clearLedgerFile(path2, anchors, opts.maxWaitMs)).note;
19849
+ }
19850
+ async function reconcileLedgerAfterFailedTagPush(deps, ledger, anchors) {
19851
+ let remoteOut;
19852
+ try {
19853
+ remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${anchors.tag}`]);
19854
+ } catch (e) {
19855
+ return {
19856
+ action: "kept-unproven",
19857
+ note: `the tag-existence read for ${anchors.tag} FAILED (${e instanceof Error ? e.message : String(e)}) \u2014 the just-started all-pending ledger is LEFT in place (never cleared on a failed read); it will block the next release until this release is resolved by hand (#5987)`
19858
+ };
19859
+ }
19860
+ const remoteSha = remoteOut.trim().split(/\s+/)[0] ?? "";
19861
+ if (remoteSha.toLowerCase() === anchors.tagSha.toLowerCase()) {
19862
+ return {
19863
+ action: "kept-present",
19864
+ note: `origin shows ${anchors.tag} at this release SHA ${remoteSha.slice(0, 12)} \u2014 the partial release state stands and the ledger is kept; resume with mmi-cli devops release --resume (do not re-tag) (#5987)`
19865
+ };
19866
+ }
19867
+ let clear;
19868
+ try {
19869
+ clear = await clearLedgerFile(ledger.path, anchors);
19870
+ } catch (e) {
19871
+ clear = { cleared: false, note: `clear failed: ${e instanceof Error ? e.message : String(e)}` };
19872
+ }
19873
+ if (!clear.cleared && clear.note) {
19874
+ return { action: "kept-unproven", note: `the just-started ledger could not be cleared after origin proved this run has no resumable tag (${clear.note}) \u2014 resolve it by hand (#5987)` };
19875
+ }
19876
+ return {
19877
+ action: "cleared",
19878
+ note: remoteSha ? `origin shows ${anchors.tag} at foreign SHA ${remoteSha.slice(0, 12)}, not this run's ${anchors.tagSha.slice(0, 12)} \u2014 this run's just-started ledger was cleared; let the owning release finish before retrying (#5987)` : `origin proves ${anchors.tag} was never pushed (no tag on origin) \u2014 the just-started all-pending ledger was cleared, so the next release is not blocked; rerun the release once the failure above is fixed (#5987)`
19879
+ };
19880
+ }
19881
+ async function startReleaseLedger(ledger, anchors, opts = {}) {
19882
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
19883
+ try {
19884
+ return await withReleaseLedger(
19885
+ ledger.path,
19886
+ anchors,
19887
+ (current) => {
19888
+ if (current) return { value: { ok: true } };
19889
+ const normalized = normalizeLedgerAnchors(anchors);
19890
+ const fresh = {
19891
+ schema: RELEASE_LEDGER_SCHEMA,
19892
+ repo: normalized.repo,
19893
+ tag: normalized.tag,
19894
+ tagSha: normalized.tagSha,
19895
+ phases: emptyPhases(),
19896
+ updatedAt: now().toISOString()
19897
+ };
19898
+ return { ledger: fresh, value: { ok: true } };
19899
+ }
19900
+ );
19901
+ } catch (e) {
19902
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
19903
+ }
19904
+ }
19905
+ async function beginReleaseLedger(deps, cwd, targetTag) {
19906
+ let path2;
19907
+ try {
19908
+ path2 = releaseLedgerPath(cwd);
19909
+ } catch {
19910
+ return { ok: true };
19911
+ }
19912
+ return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
19913
+ let raw;
19914
+ try {
19915
+ raw = (0, import_node_fs19.readFileSync)(path2, "utf8");
19916
+ } catch (e) {
19917
+ if (e.code === "ENOENT") return { ok: true };
19918
+ return { ok: false, error: `the release ledger at ${path2} could not be READ (${e instanceof Error ? e.message : String(e)}) \u2014 a failed read is unverified, not absent (#5987/#4713)` };
19919
+ }
19920
+ let prior;
19921
+ try {
19922
+ prior = parseReleaseLedger(raw);
19923
+ } catch (e) {
19924
+ return { ok: false, error: `the active release ledger at ${path2} is unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before starting a release (#5987)` };
19925
+ }
19926
+ if (targetTag && prior.tag === targetTag) return { ok: true };
19927
+ const derived = deriveReleasePhaseStatus(prior);
19928
+ if (derived.phaseStatus !== "phases-green") {
19929
+ const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
19930
+ return {
19931
+ ok: false,
19932
+ error: `a prior release ledger for ${prior.tag} is ${derived.phaseStatus} (${open2.map((l) => `${l.phase}=${l.state}`).join(", ")}) \u2014 a new release must not stack on an unresolved one. Finish it first: mmi-cli devops release --resume (or, only when proven unpublished, mmi-cli devops release --abort --apply) (#5987)`
19933
+ };
19934
+ }
19935
+ let liveSha = "";
19936
+ try {
19937
+ liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
19938
+ } catch (e) {
19939
+ return { ok: false, error: `could not prove the completed ${prior.tag} live (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to archive the ledger; retry when origin answers (#5987/#4713)` };
19940
+ }
19941
+ if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
19942
+ return { ok: false, error: `the completed ${prior.tag} ledger says phases-green but origin reports ${liveSha.slice(0, 12) || "no tag"} \u2260 ${prior.tagSha.slice(0, 12)} \u2014 refusing to archive on unproven state; resolve by hand (#5987)` };
19943
+ }
19944
+ const archivePath = (0, import_node_path18.join)((0, import_node_path18.dirname)(path2), `phases.archive-${prior.tag}.json`);
19945
+ try {
19946
+ (0, import_node_fs19.unlinkSync)(archivePath);
19947
+ } catch (e) {
19948
+ if (e.code !== "ENOENT") {
19949
+ return { ok: false, error: `could not clear the stale archive at ${archivePath} (${e instanceof Error ? e.message : String(e)}) \u2014 the ledger for ${prior.tag} stays active (#5987)` };
19950
+ }
19951
+ }
19952
+ (0, import_node_fs19.renameSync)(path2, archivePath);
19953
+ return { ok: true, note: `archived the completed release ledger ${prior.tag} to ${archivePath} (live-proven at ${prior.tagSha.slice(0, 12)})` };
19954
+ });
19955
+ }
19956
+
18821
19957
  // src/train-apply-phases.ts
18822
19958
  var TRAIN_BUMP_INTENTS = ["major", "minor", "patch"];
18823
19959
  function readLocalGateWorkflows() {
18824
- const dir = (0, import_node_path17.join)(".github", "workflows");
19960
+ const dir = (0, import_node_path19.join)(".github", "workflows");
18825
19961
  let names;
18826
19962
  try {
18827
- names = (0, import_node_fs19.readdirSync)(dir);
19963
+ names = (0, import_node_fs20.readdirSync)(dir);
18828
19964
  } catch {
18829
19965
  return null;
18830
19966
  }
18831
19967
  const files = [];
18832
19968
  for (const name of names.filter(isGateWorkflowPath)) {
18833
19969
  try {
18834
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(dir, name), "utf8") });
19970
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(dir, name), "utf8") });
18835
19971
  } catch {
18836
19972
  }
18837
19973
  }
18838
19974
  return files;
18839
19975
  }
18840
19976
  function readLocalWorkflows() {
18841
- const dir = (0, import_node_path17.join)(".github", "workflows");
19977
+ const dir = (0, import_node_path19.join)(".github", "workflows");
18842
19978
  let names;
18843
19979
  try {
18844
- names = (0, import_node_fs19.readdirSync)(dir);
19980
+ names = (0, import_node_fs20.readdirSync)(dir);
18845
19981
  } catch {
18846
19982
  return null;
18847
19983
  }
18848
19984
  const files = [];
18849
19985
  for (const name of names.filter((n) => /\.ya?ml$/i.test(n))) {
18850
19986
  try {
18851
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(dir, name), "utf8") });
19987
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(dir, name), "utf8") });
18852
19988
  } catch {
18853
19989
  }
18854
19990
  }
@@ -18986,7 +20122,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
18986
20122
  const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
18987
20123
  for (; ; ) {
18988
20124
  try {
18989
- await (0, import_promises2.stat)(lockPath);
20125
+ await (0, import_promises3.stat)(lockPath);
18990
20126
  } catch (e) {
18991
20127
  if (e.code === "ENOENT") return;
18992
20128
  throw foldFailureGuidance(
@@ -19316,8 +20452,35 @@ async function gateTrueMergeCommit(deps, ctx, tag, releaseSha, requiredChecks) {
19316
20452
  }
19317
20453
  }
19318
20454
  }
19319
- async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints) {
20455
+ async function recordPhase(ledger, deps, anchors, phase, entry, opts = {}) {
20456
+ if (!ledger) return;
20457
+ const res = await ledger.record(anchors, phase, entry);
20458
+ if (!res.ok) {
20459
+ const message = `release ledger write FAILED for phase '${phase}' (${res.error}) - already landed and immutable: ${opts.landed ?? "no boundary recorded yet"}. Nothing is rolled back, but this run stops here rather than mutating untracked: resolve the ledger at the recorded path and rerun mmi-cli devops release --resume (#5987)`;
20460
+ if (opts.strict !== false) throw new Error(message);
20461
+ deps.warn?.(message);
20462
+ }
20463
+ }
20464
+ async function recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, phase, input) {
20465
+ if (!ledger) return { reran: false, recovered: false, note: "" };
20466
+ try {
20467
+ return await transientRecoveryForFailedPhase(deps, {
20468
+ ledger,
20469
+ anchors,
20470
+ phase,
20471
+ repo: input.repo,
20472
+ sha: input.sha,
20473
+ failure: input.failure,
20474
+ ...input.runs ? { runs: input.runs } : {},
20475
+ runsProvenance: input.watched ? "watched" : "sha"
20476
+ });
20477
+ } catch (e) {
20478
+ return { reran: false, recovered: false, note: `release ledger: the transient recovery read failed (${e instanceof Error ? e.message : String(e)}) - manual intervention required` };
20479
+ }
20480
+ }
20481
+ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger) {
19320
20482
  const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
20483
+ const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
19321
20484
  try {
19322
20485
  await verifyPublishDryRun(deps, ctx, meta, deployModel);
19323
20486
  } catch (e) {
@@ -19344,45 +20507,128 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
19344
20507
  }
19345
20508
  }
19346
20509
  let tagPush;
20510
+ let tagLedgerStarted = false;
20511
+ const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
19347
20512
  try {
19348
20513
  await assertComputedReleaseTagStillNext(deps, tag);
19349
20514
  if (deployModel === "hub-serverless") {
19350
20515
  await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", tag.replace(/^v/, ""), "--release-commit", releaseSha]);
19351
20516
  }
20517
+ if (ledger) {
20518
+ const start = await startReleaseLedger(ledger, ledgerAnchors);
20519
+ if (!start.ok) {
20520
+ throw new Error(
20521
+ `release refused before the tag was pushed: the durable phase ledger could not be started at ${ledger.path} (${start.error}) - nothing was tagged or pushed; repair the ledger location and rerun the release (#5987)`
20522
+ );
20523
+ }
20524
+ tagLedgerStarted = true;
20525
+ }
19352
20526
  tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
19353
20527
  } catch (e) {
20528
+ if (ledger && tagLedgerStarted) {
20529
+ const decision = await reconcileLedgerAfterFailedTagPush(deps, ledger, ledgerAnchors);
20530
+ const annotated = new Error(`${e instanceof Error ? e.message : String(e)}
20531
+
20532
+ ${decision.note}`);
20533
+ if (decision.action === "kept-present") throw partialTrainRecoveryError(annotated, recoveryInput);
20534
+ if (decision.action === "kept-unproven") throw annotated;
20535
+ throw await recoverFailedFold(deps, annotated, startBranch, preFold.mainSha, resumeCommand);
20536
+ }
19354
20537
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
19355
20538
  }
19356
20539
  const tagPushSince = (deps.now ?? Date.now)();
19357
20540
  let checks;
19358
- const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
19359
20541
  try {
19360
20542
  checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
19361
20543
  } catch (e) {
19362
- throw partialTrainRecoveryError(e, recoveryInput);
20544
+ const failureText = e instanceof Error ? e.message : String(e);
20545
+ const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, "ordinaryCi", {
20546
+ repo: ctx.repo,
20547
+ sha: releaseSha,
20548
+ failure: failureText,
20549
+ watched: false
20550
+ });
20551
+ if (recovery.reran && recovery.recovered) {
20552
+ try {
20553
+ checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, void 0);
20554
+ } catch (e2) {
20555
+ const spent = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, "ordinaryCi", {
20556
+ repo: ctx.repo,
20557
+ sha: releaseSha,
20558
+ failure: e2 instanceof Error ? e2.message : String(e2),
20559
+ watched: false
20560
+ });
20561
+ throw partialTrainRecoveryError(new Error(`${e2 instanceof Error ? e2.message : String(e2)}
20562
+
20563
+ ${spent.note}`), recoveryInput);
20564
+ }
20565
+ } else {
20566
+ throw partialTrainRecoveryError(new Error(`${failureText}
20567
+
20568
+ ${recovery.note}`), recoveryInput);
20569
+ }
19363
20570
  }
20571
+ await recordPhase(ledger, deps, ledgerAnchors, "ordinaryCi", phaseEntry({ state: "complete", sha: releaseSha, note: checks }), {
20572
+ landed: "the immutable tag pushed at " + releaseSha.slice(0, 12)
20573
+ });
19364
20574
  if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
19365
20575
  try {
19366
20576
  await runGitPush(deps, ["push", "origin", "main"]);
19367
20577
  } catch (e) {
20578
+ await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "failed", sha: releaseSha, error: `origin/main push failed after ${tag} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable tag pushed at " + releaseSha.slice(0, 12) });
19368
20579
  throw partialTrainRecoveryError(e, recoveryInput);
19369
20580
  }
20581
+ await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "complete", sha: releaseSha, note: `immutable ${tag} pushed at ${releaseSha.slice(0, 12)}; origin/main fast-forwarded to it` }), { landed: "the immutable tag and the green required-check wall" });
19370
20582
  const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
19371
20583
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
20584
+ await recordPhase(ledger, deps, ledgerAnchors, "githubRelease", phaseEntry({ state: "complete", sha: releaseSha, runUrl: releaseUrl, note: `verified GitHub Release ${tag} against ${releaseSha.slice(0, 12)}` }), { landed: "the immutable tag, the green required-check wall, and the origin/main fast-forward" });
19372
20585
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
19373
20586
  const autoRunSince = (deps.now ?? Date.now)();
19374
- const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
20587
+ const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
20588
+ let deployDispatch = deployDispatch0;
20589
+ let deployRecoveryNote;
20590
+ if (deployDispatch0.deployStatus === "failure" && ledger) {
20591
+ const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
20592
+ const failedRow = (deployDispatch0.workflowRuns ?? []).find((r) => r.runId != null && r.conclusion === "failure");
20593
+ const dispatchRun = deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : [];
20594
+ const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, ledgerAnchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
20595
+ repo: deployRunRepo,
20596
+ sha: releaseSha,
20597
+ failure: `watched deploy run(s) failed on the release SHA: ${deployDispatch0.note}`,
20598
+ runs: deployDispatch0.workflowRuns?.filter((r) => r.runId != null).map((r) => ({ ...r, repo: deployRunRepo })) ?? (deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : []),
20599
+ watched: isCentralDispatchModel(deployModel)
20600
+ });
20601
+ if (recovery.reran && recovery.recovered) {
20602
+ deployDispatch = {
20603
+ ...deployDispatch0,
20604
+ deployStatus: "success",
20605
+ workflowRuns: deployDispatch0.workflowRuns?.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r),
20606
+ note: `${deployDispatch0.note}; ${recovery.note}`
20607
+ };
20608
+ } else if (recovery.note) {
20609
+ deployRecoveryNote = recovery.note;
20610
+ }
20611
+ }
20612
+ await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
20613
+ landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
20614
+ });
19375
20615
  const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
19376
- let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
19377
- dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
20616
+ let publishSkipNote;
19378
20617
  if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
19379
- const reason = deployDispatch.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
19380
- dispatch = {
19381
- ...dispatch,
19382
- note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
19383
- };
20618
+ publishSkipNote = deployDispatch.deployStatus === "failure" ? "box deploy failed - redeploy the box before publishing" : "box deploy not confirmed (run with --watch) - publish after the box deploy lands";
19384
20619
  }
19385
- return { checks, releaseUrl, announceNote, dispatch };
20620
+ await recordPhase(ledger, deps, ledgerAnchors, "publish", phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, {
20621
+ releaseSha,
20622
+ repo: ctx.repo,
20623
+ ...publishSkipNote !== void 0 ? { skipPublishNote: publishSkipNote } : {}
20624
+ })), {
20625
+ landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
20626
+ });
20627
+ let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
20628
+ dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
20629
+ if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
20630
+ if (deployRecoveryNote) dispatch = { ...dispatch, note: `${dispatch.note}; ${deployRecoveryNote}` };
20631
+ return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
19386
20632
  }
19387
20633
  function trainResumeExitCode(state) {
19388
20634
  if (state === "failed") return 1;
@@ -19492,6 +20738,35 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19492
20738
  );
19493
20739
  }
19494
20740
  const base = { command: "release-resume", repo: ctx.repo, tag, tagSha, deployModel };
20741
+ const ledger = openReleaseLedgerWriter(process.cwd());
20742
+ const anchors = { repo: ctx.repo, tag, tagSha };
20743
+ const begin = await beginReleaseLedger(deps, process.cwd(), tag);
20744
+ if (!begin.ok) throw new Error(`release --resume refused: ${begin.error}`);
20745
+ else if (begin.note) deps.warn?.(`release ledger: ${begin.note}`);
20746
+ let persisted;
20747
+ try {
20748
+ persisted = await ledger.load(anchors);
20749
+ } catch (e) {
20750
+ throw new Error(
20751
+ `release --resume: ${e instanceof Error ? e.message : String(e)}
20752
+ Live origin/tag/Release proof stays authoritative; nothing was written. Inspect ${ledger.path} before retrying.`
20753
+ );
20754
+ }
20755
+ if (persisted) {
20756
+ let reverifyNotes = [];
20757
+ try {
20758
+ const reverified = await reverifyPersistedRunPhases(deps, ledger.path, anchors, { sha: tagSha, repo: ctx.repo });
20759
+ persisted = reverified.ledger;
20760
+ reverifyNotes = reverified.notes;
20761
+ } catch (e) {
20762
+ throw new Error(
20763
+ `release --resume: ${e instanceof Error ? e.message : String(e)}
20764
+ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the release state by hand).`
20765
+ );
20766
+ }
20767
+ for (const note of reverifyNotes) deps.warn?.(`release ledger: ${note}`);
20768
+ }
20769
+ const ledgerMode = persisted ? "strict" : "migration";
19495
20770
  const partialRelease = await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo);
19496
20771
  if (!partialRelease) {
19497
20772
  try {
@@ -19506,12 +20781,44 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19506
20781
  steps2.push(`development alignment: ${devRollForward2.status}`);
19507
20782
  const rcAlignment2 = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
19508
20783
  if (rcAlignment2) steps2.push(`rc alignment: ${rcAlignment2.status}`);
19509
- const historicalDeploy = await deriveHistoricalDeployStatus(deps, ctx, deployModel, tagSha);
20784
+ const historicalTargets = prodReleaseTargets(deployModel);
20785
+ const historicalRepo = releaseRunRepoFor(deployModel, ctx.repo);
20786
+ let historicalRows = historicalTargets.length ? await listOwnWorkflowRuns(deps, historicalRepo, historicalTargets, tagSha) : [];
20787
+ let historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : "pending";
20788
+ let historicalNote = `historical ${deployModel} deploy/publish status: ${historicalDeploy}`;
20789
+ if (historicalDeploy === "failure" && ledger) {
20790
+ const failedRow = historicalRows.find((r) => r.runId != null && r.conclusion === "failure");
20791
+ const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
20792
+ repo: historicalRepo,
20793
+ sha: tagSha,
20794
+ failure: `historical deploy/publish run(s) failed on the release SHA: ${historicalRows.map((r) => `${r.workflow}=${r.conclusion}`).join(", ")}`,
20795
+ runs: historicalRows.filter((r) => r.runId != null).map((r) => ({ ...r, repo: historicalRepo })),
20796
+ watched: false
20797
+ });
20798
+ if (recovery.reran && recovery.recovered) {
20799
+ historicalRows = historicalRows.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r);
20800
+ historicalDeploy = aggregateWorkflowRuns(historicalRows);
20801
+ historicalNote = `historical ${deployModel} deploy/publish status: ${historicalDeploy}; ${recovery.note}`;
20802
+ } else if (recovery.note) {
20803
+ historicalNote = `${historicalNote}; ${recovery.note}`;
20804
+ }
20805
+ }
20806
+ if (persisted) {
20807
+ const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
20808
+ await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
20809
+ await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
20810
+ 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)" });
20811
+ 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" });
20812
+ }
19510
20813
  const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
19511
- note: `historical ${deployModel} deploy/publish status: ${historicalDeploy}`,
20814
+ note: historicalNote,
19512
20815
  deployStatus: historicalDeploy
19513
20816
  });
19514
20817
  if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
20818
+ if (persisted) {
20819
+ await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { strict: true, landed: "the released, verified, deploy-resolved release (alignment PRs reused/landed)" });
20820
+ }
20821
+ const ledgerReport2 = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
19515
20822
  const state2 = resumeStateOf(deriveTrainFollowUpStatus({
19516
20823
  projectInfo: "ok",
19517
20824
  deploy: deployFollowUpOutcome(dispatch2.deployStatus),
@@ -19528,6 +20835,7 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19528
20835
  devRollForward: devRollForward2,
19529
20836
  rcAlignment: rcAlignment2,
19530
20837
  dispatch: dispatch2,
20838
+ ...ledgerReport2 ? { ledger: ledgerReport2 } : {},
19531
20839
  note: state2 === "failed" ? `release ${tag} was already published, but its deploy/publish on the release SHA FAILED \u2014 re-run the failed job (do NOT re-tag); the protected alignment PRs were reused` : state2 === "pending" ? `release ${tag} was already published; reused its protected alignment PRs, but a deploy/publish or alignment follow-up is unresolved \u2014 watch it to completion` : `release ${tag} was already published; alignment is complete and the deploy on the release SHA is proven green`
19532
20840
  };
19533
20841
  }
@@ -19548,21 +20856,81 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19548
20856
  try {
19549
20857
  steps.push(await waitForRequiredTrainChecks(deps, ctx, tagSha, requiredChecks, void 0));
19550
20858
  } catch (e) {
19551
- throw partialTrainRecoveryError(e, recoveryInput);
20859
+ const failureText = e instanceof Error ? e.message : String(e);
20860
+ const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, "ordinaryCi", {
20861
+ repo: ctx.repo,
20862
+ sha: tagSha,
20863
+ failure: failureText,
20864
+ watched: false
20865
+ });
20866
+ if (recovery.reran && recovery.recovered) {
20867
+ try {
20868
+ steps.push(await waitForRequiredTrainChecks(deps, ctx, tagSha, requiredChecks, void 0));
20869
+ } catch (e2) {
20870
+ const spent = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, "ordinaryCi", {
20871
+ repo: ctx.repo,
20872
+ sha: tagSha,
20873
+ failure: e2 instanceof Error ? e2.message : String(e2),
20874
+ watched: false
20875
+ });
20876
+ throw partialTrainRecoveryError(new Error(`${e2 instanceof Error ? e2.message : String(e2)}
20877
+
20878
+ ${spent.note}`), recoveryInput);
20879
+ }
20880
+ } else {
20881
+ throw partialTrainRecoveryError(new Error(`${failureText}
20882
+
20883
+ ${recovery.note}`), recoveryInput);
20884
+ }
19552
20885
  }
20886
+ await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict: ledgerMode === "strict", landed: "the immutable tag on origin at the resumed SHA" });
19553
20887
  await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
19554
20888
  steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
20889
+ await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/main (tag preserved, never re-cut)` }), { strict: ledgerMode === "strict", landed: "the immutable tag and the re-proven required-check wall" });
19555
20890
  const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
19556
20891
  steps.push(`created the GitHub Release for ${tag}`);
19557
20892
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
19558
20893
  steps.push("verified the Release published against the tagged commit");
20894
+ await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, runUrl: releaseUrl, note: "verified the Release published against the tagged commit" }), { strict: ledgerMode === "strict", landed: "the immutable tag, the re-proven wall, and the origin/main push" });
19559
20895
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
19560
20896
  const autoRunSince = (deps.now ?? Date.now)();
19561
- const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
20897
+ const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
20898
+ let deployDispatch = deployDispatch0;
20899
+ let deployRecoveryNote;
20900
+ if (deployDispatch0.deployStatus === "failure" && ledger) {
20901
+ const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
20902
+ const failedRow = (deployDispatch0.workflowRuns ?? []).find((r) => r.runId != null && r.conclusion === "failure");
20903
+ const recovery = await recoverPhaseWithMaybeTransientRerun(deps, ledger, anchors, failedRow?.workflow === "publish.yml" ? "publish" : "deploy", {
20904
+ repo: deployRunRepo,
20905
+ sha: tagSha,
20906
+ failure: `watched deploy run(s) failed on the release SHA: ${deployDispatch0.note}`,
20907
+ runs: deployDispatch0.workflowRuns?.filter((r) => r.runId != null).map((r) => ({ ...r, repo: deployRunRepo })) ?? (deployDispatch0.runId != null ? [{ workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow", runId: deployDispatch0.runId, runUrl: deployDispatch0.runUrl, conclusion: "failure", repo: deployRunRepo, ...isCentralDispatchModel(deployModel) ? { event: "workflow_dispatch" } : {} }] : []),
20908
+ watched: isCentralDispatchModel(deployModel)
20909
+ });
20910
+ if (recovery.reran && recovery.recovered) {
20911
+ deployDispatch = {
20912
+ ...deployDispatch0,
20913
+ deployStatus: "success",
20914
+ workflowRuns: deployDispatch0.workflowRuns?.map((r) => recovery.runId != null && r.runId === recovery.runId ? { ...r, conclusion: "success" } : r),
20915
+ note: `${deployDispatch0.note}; ${recovery.note}`
20916
+ };
20917
+ } else if (recovery.note) {
20918
+ deployRecoveryNote = recovery.note;
20919
+ }
20920
+ }
20921
+ steps.push(`dispatched the ${deployModel} deploy path`);
20922
+ await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
20923
+ strict: ledgerMode === "strict",
20924
+ landed: "the immutable tag, the re-proven wall, the origin/main push, and the verified GitHub Release"
20925
+ });
19562
20926
  const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
20927
+ await recordPhase(ledger, deps, anchors, "publish", phaseEntry(publishPhaseInput(deployModel, deployDispatch, publishDispatch, meta.publishRequired === true, { releaseSha: tagSha, repo: ctx.repo })), {
20928
+ strict: ledgerMode === "strict",
20929
+ landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
20930
+ });
19563
20931
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
19564
20932
  dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
19565
- steps.push(`dispatched the ${deployModel} deploy path`);
20933
+ if (deployRecoveryNote) dispatch = { ...dispatch, note: `${dispatch.note}; ${deployRecoveryNote}` };
19566
20934
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
19567
20935
  steps.push(`development roll-forward: ${devRollForward.status}`);
19568
20936
  const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
@@ -19575,6 +20943,8 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19575
20943
  foldPort: "ok"
19576
20944
  });
19577
20945
  const state = resumeStateOf(followUp);
20946
+ await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward, rcAlignment)), { strict: ledgerMode === "strict", landed: "the promotion, verified Release, and deploy path of the resumed release" });
20947
+ const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
19578
20948
  return {
19579
20949
  ...base,
19580
20950
  mode: "partial-release",
@@ -19586,6 +20956,7 @@ async function runReleaseResumePipeline(deps, options, prepared) {
19586
20956
  dispatch,
19587
20957
  devRollForward,
19588
20958
  rcAlignment,
20959
+ ...ledgerReport ? { ledger: ledgerReport } : {},
19589
20960
  note: followUp === "failed" ? `resumed ${tag} (version preserved, not re-cut) \u2014 but a watched deploy/publish run FAILED; the Release stands, re-run the failed job (do NOT re-tag)` : followUp === "pending" ? `resumed ${tag} (version preserved, not re-cut) \u2014 a deploy/publish or alignment follow-up is unresolved; watch it to completion` : `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
19590
20961
  };
19591
20962
  }
@@ -19679,13 +21050,14 @@ async function runReleaseAbort(deps, options = {}) {
19679
21050
  if (restoredMainSha !== originMainSha) {
19680
21051
  throw new Error(`release --abort removed ${tag}, but local main did not restore to origin/main; inspect before retrying`);
19681
21052
  }
21053
+ const clearedLedgerNote = await clearReleaseLedger(process.cwd(), { repo: ctx.repo, tag, tagSha });
19682
21054
  return {
19683
21055
  command: "release-abort",
19684
21056
  repo: ctx.repo,
19685
21057
  tag,
19686
21058
  tagSha,
19687
21059
  restoredMainSha,
19688
- note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
21060
+ note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting` + (clearedLedgerNote ? `; ${clearedLedgerNote}` : "")
19689
21061
  };
19690
21062
  }
19691
21063
  async function runReleasePublishRetry(deps, runId, options = {}) {
@@ -19732,6 +21104,25 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
19732
21104
  if (options.watch && await watchTenantRun(deps, runId, ctx.repo) !== "success") {
19733
21105
  throw new Error(`publish run ${runId} did not finish successfully after retry`);
19734
21106
  }
21107
+ const ledger = openReleaseLedgerWriter(process.cwd());
21108
+ const existing = await ledger.load({ repo: ctx.repo, tag, tagSha }).catch(() => void 0);
21109
+ let ledgerNote;
21110
+ if (existing) {
21111
+ const record = await ledger.record({ repo: ctx.repo, tag, tagSha }, "publish", phaseEntry({
21112
+ state: options.watch ? "complete" : "pending",
21113
+ sha: tagSha,
21114
+ runId,
21115
+ runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
21116
+ runRepo: ctx.repo,
21117
+ runProvenance: "sha",
21118
+ workflow: "publish.yml",
21119
+ event: "release",
21120
+ note: options.watch ? `publish run ${runId} retried via release --retry-publish and passed (live-proven)` : `publish run ${runId} queued for retry via release --retry-publish`
21121
+ }));
21122
+ ledgerNote = record.ok ? options.watch ? "publish boundary updated to complete in the release ledger" : "publish boundary updated to pending in the release ledger" : `release ledger not updated: ${record.error}`;
21123
+ } else {
21124
+ ledgerNote = "no release ledger on disk for this release \u2014 nothing to update";
21125
+ }
19735
21126
  return {
19736
21127
  command: "release-retry-publish",
19737
21128
  repo: ctx.repo,
@@ -19740,12 +21131,18 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
19740
21131
  runId,
19741
21132
  runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
19742
21133
  status: options.watch ? "success" : "pending",
19743
- note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} queued for retry`
21134
+ note: options.watch ? `publish run ${runId} retried and passed; ${ledgerNote}` : `publish run ${runId} queued for retry; ${ledgerNote}`
19744
21135
  };
19745
21136
  }
19746
21137
  async function runTrainApplyPipeline(mode, input) {
19747
21138
  const { deps, ctx, command, meta, branchHints, watch, options, bumpIntent } = input;
19748
21139
  const directTrack = input.directTrack ?? false;
21140
+ const ledger = mode === "rcand" ? void 0 : openReleaseLedgerWriter(process.cwd());
21141
+ if (ledger) {
21142
+ const begin = await beginReleaseLedger(deps, process.cwd());
21143
+ if (!begin.ok) throw new Error(`release refused before any mutation: ${begin.error}`);
21144
+ else if (begin.note) deps.warn?.(`release ledger: ${begin.note}`);
21145
+ }
19749
21146
  if (mode === "rcand") {
19750
21147
  await requireBranch(deps, "development");
19751
21148
  if (directTrack) {
@@ -19832,8 +21229,10 @@ async function runTrainApplyPipeline(mode, input) {
19832
21229
  const releaseSha3 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
19833
21230
  return { versionFold: versionFold3, releaseSha: releaseSha3 };
19834
21231
  });
19835
- const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2, branchHints);
21232
+ const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2, ledgerAnchors: ledgerAnchors2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2, branchHints, ledger);
19836
21233
  const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
21234
+ await recordPhase(ledger, deps, ledgerAnchors2, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
21235
+ const ledgerReport2 = releaseLedgerReport(await ledger?.load(ledgerAnchors2).catch(() => void 0), ledger?.path ?? "(no ledger)");
19837
21236
  if (directTrack) {
19838
21237
  return {
19839
21238
  ...ctx,
@@ -19856,11 +21255,14 @@ async function runTrainApplyPipeline(mode, input) {
19856
21255
  devRollForward: devRollForward2,
19857
21256
  announceNote: announceNote2,
19858
21257
  devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
19859
- release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
21258
+ release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 },
21259
+ ...ledgerReport2 ? { ledger: ledgerReport2 } : {}
19860
21260
  };
19861
21261
  }
19862
21262
  const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
19863
21263
  const rcAlignment2 = hasRcBranch ? await alignRcForward(deps, ctx, tag2) : void 0;
21264
+ await recordPhase(ledger, deps, ledgerAnchors2, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
21265
+ const fullLedgerReport = releaseLedgerReport(await ledger?.load(ledgerAnchors2).catch(() => void 0), ledger?.path ?? "(no ledger)");
19864
21266
  const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
19865
21267
  return {
19866
21268
  ...ctx,
@@ -19886,7 +21288,8 @@ async function runTrainApplyPipeline(mode, input) {
19886
21288
  announceNote: announceNote2,
19887
21289
  devNote: "released development -> main (--dev), skipping the rc candidate",
19888
21290
  release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 },
19889
- environments: environments2
21291
+ environments: environments2,
21292
+ ...fullLedgerReport ? { ledger: fullLedgerReport } : {}
19890
21293
  };
19891
21294
  }
19892
21295
  await requireBranch(deps, "rc");
@@ -19919,10 +21322,12 @@ async function runTrainApplyPipeline(mode, input) {
19919
21322
  const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
19920
21323
  return { versionFold: versionFold2, releaseSha: releaseSha2 };
19921
21324
  });
19922
- const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe, branchHints);
21325
+ const { checks, releaseUrl, announceNote, dispatch: d, ledgerAnchors } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe, branchHints, ledger);
19923
21326
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
19924
21327
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
19925
21328
  const rcAlignment = await alignRcForward(deps, ctx, tag);
21329
+ await recordPhase(ledger, deps, ledgerAnchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward, rcAlignment)), { landed: "the immutable tag, the green wall, origin/main, the verified Release, and the deploy/publish path" });
21330
+ const ledgerReport = releaseLedgerReport(await ledger?.load(ledgerAnchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
19926
21331
  const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
19927
21332
  return {
19928
21333
  ...ctx,
@@ -19947,7 +21352,8 @@ async function runTrainApplyPipeline(mode, input) {
19947
21352
  devRollForward,
19948
21353
  announceNote,
19949
21354
  release: { tag, url: releaseUrl, targetSha: releaseSha },
19950
- environments
21355
+ environments,
21356
+ ...ledgerReport ? { ledger: ledgerReport } : {}
19951
21357
  };
19952
21358
  }
19953
21359
  async function latestLocalReleaseTag(deps) {
@@ -21015,20 +22421,20 @@ async function applyCiReconcileRepo(repo, deps) {
21015
22421
  }
21016
22422
 
21017
22423
  // src/claude-binary-doctor.ts
21018
- var import_node_fs21 = require("node:fs");
22424
+ var import_node_fs22 = require("node:fs");
21019
22425
  var import_node_os11 = require("node:os");
21020
- var import_node_path19 = require("node:path");
22426
+ var import_node_path21 = require("node:path");
21021
22427
 
21022
22428
  // src/jerv-cli-spawn.ts
21023
- var import_node_fs20 = require("node:fs");
22429
+ var import_node_fs21 = require("node:fs");
21024
22430
  var import_node_os10 = require("node:os");
21025
- var import_node_path18 = require("node:path");
22431
+ var import_node_path20 = require("node:path");
21026
22432
  var WIN_NAMES = ["jerv-cli.cmd", "jerv-cli.exe", "jerv-cli"];
21027
22433
  var POSIX_NAMES = ["jerv-cli"];
21028
- var JERV_CLI_ENTRY = (0, import_node_path18.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
22434
+ var JERV_CLI_ENTRY = (0, import_node_path20.join)("node_modules", "@jervaise", "jerv-cli", "dist", "index.cjs");
21029
22435
  function pathEnvEntries(pathEnv, platform2 = process.platform) {
21030
22436
  if (platform2 !== "win32") {
21031
- return pathEnv.split(import_node_path18.delimiter).map((e) => e.trim()).filter(Boolean);
22437
+ return pathEnv.split(import_node_path20.delimiter).map((e) => e.trim()).filter(Boolean);
21032
22438
  }
21033
22439
  if (pathEnv.includes(";")) {
21034
22440
  return pathEnv.split(";").map((e) => e.trim()).filter(Boolean);
@@ -21061,10 +22467,10 @@ function jervCliCandidateDirs(env = process.env, home = (0, import_node_os10.hom
21061
22467
  push(normalizeSpawnPathEntry(entry, platform2));
21062
22468
  }
21063
22469
  if (platform2 === "win32") {
21064
- if (env.APPDATA) push((0, import_node_path18.join)(env.APPDATA, "npm"));
21065
- if (env.LOCALAPPDATA) push((0, import_node_path18.join)(env.LOCALAPPDATA, "npm"));
22470
+ if (env.APPDATA) push((0, import_node_path20.join)(env.APPDATA, "npm"));
22471
+ if (env.LOCALAPPDATA) push((0, import_node_path20.join)(env.LOCALAPPDATA, "npm"));
21066
22472
  } else {
21067
- push((0, import_node_path18.join)(home, ".local", "bin"));
22473
+ push((0, import_node_path20.join)(home, ".local", "bin"));
21068
22474
  }
21069
22475
  return out;
21070
22476
  }
@@ -21072,23 +22478,23 @@ function jervCliCandidatePaths(env = process.env, home = (0, import_node_os10.ho
21072
22478
  const names = platform2 === "win32" ? WIN_NAMES : POSIX_NAMES;
21073
22479
  const out = [];
21074
22480
  for (const dir of jervCliCandidateDirs(env, home, platform2)) {
21075
- for (const name of names) out.push((0, import_node_path18.join)(dir, name));
22481
+ for (const name of names) out.push((0, import_node_path20.join)(dir, name));
21076
22482
  }
21077
22483
  return out;
21078
22484
  }
21079
- function resolveJervCliPath(env = process.env, home = (0, import_node_os10.homedir)(), platform2 = process.platform, exists = import_node_fs20.existsSync) {
22485
+ function resolveJervCliPath(env = process.env, home = (0, import_node_os10.homedir)(), platform2 = process.platform, exists = import_node_fs21.existsSync) {
21080
22486
  for (const candidate of jervCliCandidatePaths(env, home, platform2)) {
21081
22487
  if (exists(candidate)) return candidate;
21082
22488
  }
21083
22489
  return void 0;
21084
22490
  }
21085
- function resolveJervCliNodeEntry(shimPath, exists = import_node_fs20.existsSync) {
21086
- const entry = (0, import_node_path18.join)((0, import_node_path18.dirname)(shimPath), JERV_CLI_ENTRY);
22491
+ function resolveJervCliNodeEntry(shimPath, exists = import_node_fs21.existsSync) {
22492
+ const entry = (0, import_node_path20.join)((0, import_node_path20.dirname)(shimPath), JERV_CLI_ENTRY);
21087
22493
  return exists(entry) ? entry : void 0;
21088
22494
  }
21089
22495
  function jervCliExecFileArgs(args, opts = {}) {
21090
22496
  const platform2 = opts.platform ?? process.platform;
21091
- const exists = opts.exists ?? import_node_fs20.existsSync;
22497
+ const exists = opts.exists ?? import_node_fs21.existsSync;
21092
22498
  const resolved = resolveJervCliPath(opts.env ?? process.env, opts.home ?? (0, import_node_os10.homedir)(), platform2, exists);
21093
22499
  if (resolved) {
21094
22500
  const entry = resolveJervCliNodeEntry(resolved, exists);
@@ -21178,26 +22584,26 @@ function globalNodeModulesRoots(host) {
21178
22584
  out.push(dir);
21179
22585
  };
21180
22586
  const prefix = env.npm_config_prefix?.trim();
21181
- if (prefix) push(platform2 === "win32" ? (0, import_node_path19.join)(prefix, "node_modules") : (0, import_node_path19.join)(prefix, "lib", "node_modules"));
22587
+ if (prefix) push(platform2 === "win32" ? (0, import_node_path21.join)(prefix, "node_modules") : (0, import_node_path21.join)(prefix, "lib", "node_modules"));
21182
22588
  for (const dir of jervCliCandidateDirs(env, host.home ?? (0, import_node_os11.homedir)(), platform2)) {
21183
- push((0, import_node_path19.join)(dir, "node_modules"));
21184
- push((0, import_node_path19.join)((0, import_node_path19.dirname)(dir), "lib", "node_modules"));
22589
+ push((0, import_node_path21.join)(dir, "node_modules"));
22590
+ push((0, import_node_path21.join)((0, import_node_path21.dirname)(dir), "lib", "node_modules"));
21185
22591
  }
21186
22592
  return out;
21187
22593
  }
21188
22594
  function readHead(path2) {
21189
22595
  let fd;
21190
22596
  try {
21191
- fd = (0, import_node_fs21.openSync)(path2, "r");
22597
+ fd = (0, import_node_fs22.openSync)(path2, "r");
21192
22598
  const buffer = new Uint8Array(MAGIC_HEAD_BYTES);
21193
- const read = (0, import_node_fs21.readSync)(fd, buffer, 0, MAGIC_HEAD_BYTES, 0);
22599
+ const read = (0, import_node_fs22.readSync)(fd, buffer, 0, MAGIC_HEAD_BYTES, 0);
21194
22600
  return buffer.subarray(0, read);
21195
22601
  } catch {
21196
22602
  return void 0;
21197
22603
  } finally {
21198
22604
  if (fd !== void 0) {
21199
22605
  try {
21200
- (0, import_node_fs21.closeSync)(fd);
22606
+ (0, import_node_fs22.closeSync)(fd);
21201
22607
  } catch {
21202
22608
  }
21203
22609
  }
@@ -21205,7 +22611,7 @@ function readHead(path2) {
21205
22611
  }
21206
22612
  function fileBytes(path2) {
21207
22613
  try {
21208
- return (0, import_node_fs21.statSync)(path2).size;
22614
+ return (0, import_node_fs22.statSync)(path2).size;
21209
22615
  } catch {
21210
22616
  return void 0;
21211
22617
  }
@@ -21219,17 +22625,17 @@ function readClaudeBinaryState(host = {}) {
21219
22625
  const arch = host.arch ?? process.arch;
21220
22626
  const magic = EXECUTABLE_MAGIC[platform2];
21221
22627
  if (!magic) return void 0;
21222
- const packageRoot = globalNodeModulesRoots(host).map((root) => (0, import_node_path19.join)(root, ...PACKAGE.split("/"))).find((dir) => (0, import_node_fs21.existsSync)((0, import_node_path19.join)(dir, "package.json")));
22628
+ const packageRoot = globalNodeModulesRoots(host).map((root) => (0, import_node_path21.join)(root, ...PACKAGE.split("/"))).find((dir) => (0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, "package.json")));
21223
22629
  if (!packageRoot) return void 0;
21224
22630
  const keys = platformPackageKeys(platform2, arch);
21225
22631
  const fallbackPackage = `${PACKAGE}-${keys[0]}`;
21226
22632
  let manifest;
21227
22633
  try {
21228
- manifest = JSON.parse((0, import_node_fs21.readFileSync)((0, import_node_path19.join)(packageRoot, "package.json"), "utf8"));
22634
+ manifest = JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path21.join)(packageRoot, "package.json"), "utf8"));
21229
22635
  } catch (e) {
21230
22636
  return {
21231
22637
  state: "unreadable",
21232
- binPath: (0, import_node_path19.join)(packageRoot, "package.json"),
22638
+ binPath: (0, import_node_path21.join)(packageRoot, "package.json"),
21233
22639
  expectedMagic: magic.name,
21234
22640
  platformPackage: fallbackPackage,
21235
22641
  error: `package.json could not be read \u2014 ${e.message}`
@@ -21246,17 +22652,17 @@ function readClaudeBinaryState(host = {}) {
21246
22652
  error: "package.json declares no `bin` entry \u2014 the executed file cannot be resolved"
21247
22653
  };
21248
22654
  }
21249
- const binPath = (0, import_node_path19.join)(packageRoot, binRelative);
21250
- const binName = (0, import_node_path19.basename)(binRelative);
22655
+ const binPath = (0, import_node_path21.join)(packageRoot, binRelative);
22656
+ const binName = (0, import_node_path21.basename)(binRelative);
21251
22657
  const optional = manifest.optionalDependencies;
21252
22658
  const publishes = (name) => typeof optional === "object" && optional !== null && Object.prototype.hasOwnProperty.call(optional, name);
21253
22659
  const published = keys.map((key) => `${PACKAGE}-${key}`).filter(publishes);
21254
22660
  if (published.length === 0) return void 0;
21255
22661
  const binIn = (name) => [
21256
- (0, import_node_path19.join)(packageRoot, "node_modules", ...name.split("/"), binName),
21257
- (0, import_node_path19.join)((0, import_node_path19.dirname)((0, import_node_path19.dirname)(packageRoot)), ...name.split("/"), binName)
22662
+ (0, import_node_path21.join)(packageRoot, "node_modules", ...name.split("/"), binName),
22663
+ (0, import_node_path21.join)((0, import_node_path21.dirname)((0, import_node_path21.dirname)(packageRoot)), ...name.split("/"), binName)
21258
22664
  ];
21259
- const found = published.map((name) => ({ name, path: binIn(name).find((file) => (0, import_node_fs21.existsSync)(file)) })).find((c) => c.path);
22665
+ const found = published.map((name) => ({ name, path: binIn(name).find((file) => (0, import_node_fs22.existsSync)(file)) })).find((c) => c.path);
21260
22666
  const platformPackage = found?.name ?? published[0];
21261
22667
  let source;
21262
22668
  let sourceProblem;
@@ -21267,7 +22673,7 @@ function readClaudeBinaryState(host = {}) {
21267
22673
  } else {
21268
22674
  source = { path: found.path, bytes: fileBytes(found.path) ?? 0 };
21269
22675
  }
21270
- if (!(0, import_node_fs21.existsSync)(binPath)) {
22676
+ if (!(0, import_node_fs22.existsSync)(binPath)) {
21271
22677
  return { state: "missing", binPath, expectedMagic: magic.name, platformPackage, ...source ? { source } : {}, ...sourceProblem ? { sourceProblem } : {} };
21272
22678
  }
21273
22679
  const head = readHead(binPath);
@@ -21310,9 +22716,9 @@ function healClaudeBinary(host = {}, onStep) {
21310
22716
  const platform2 = host.platform ?? process.platform;
21311
22717
  const aside = `${probe.binPath}.stub-${Date.now()}`;
21312
22718
  let renamed = false;
21313
- if ((0, import_node_fs21.existsSync)(probe.binPath)) {
22719
+ if ((0, import_node_fs22.existsSync)(probe.binPath)) {
21314
22720
  try {
21315
- (0, import_node_fs21.renameSync)(probe.binPath, aside);
22721
+ (0, import_node_fs22.renameSync)(probe.binPath, aside);
21316
22722
  renamed = true;
21317
22723
  onStep?.(`renamed the stub aside: ${aside}`);
21318
22724
  } catch (e) {
@@ -21321,12 +22727,12 @@ function healClaudeBinary(host = {}, onStep) {
21321
22727
  }
21322
22728
  try {
21323
22729
  onStep?.(`copying ${probe.source.path} \u2192 ${probe.binPath} (${(probe.source.bytes / 1e6).toFixed(0)} MB)`);
21324
- (0, import_node_fs21.copyFileSync)(probe.source.path, probe.binPath);
21325
- if (platform2 !== "win32") (0, import_node_fs21.chmodSync)(probe.binPath, 493);
22730
+ (0, import_node_fs22.copyFileSync)(probe.source.path, probe.binPath);
22731
+ if (platform2 !== "win32") (0, import_node_fs22.chmodSync)(probe.binPath, 493);
21326
22732
  } catch (e) {
21327
22733
  if (renamed) {
21328
22734
  try {
21329
- (0, import_node_fs21.renameSync)(aside, probe.binPath);
22735
+ (0, import_node_fs22.renameSync)(aside, probe.binPath);
21330
22736
  } catch {
21331
22737
  return { ok: false, detail: `copy failed (${e.message}) and the stub could not be restored \u2014 the original is at ${aside}` };
21332
22738
  }
@@ -21340,7 +22746,7 @@ function healClaudeBinary(host = {}, onStep) {
21340
22746
  let kept = false;
21341
22747
  if (renamed) {
21342
22748
  try {
21343
- (0, import_node_fs21.rmSync)(aside);
22749
+ (0, import_node_fs22.rmSync)(aside);
21344
22750
  } catch {
21345
22751
  kept = true;
21346
22752
  }
@@ -22192,7 +23598,7 @@ function renderPluginCachePlan(plan, applied) {
22192
23598
  }
22193
23599
 
22194
23600
  // src/app-actor.ts
22195
- var import_node_crypto5 = require("node:crypto");
23601
+ var import_node_crypto6 = require("node:crypto");
22196
23602
  var APP_ACTOR_ENV = "MMI_ACTOR";
22197
23603
  var APP_VAULT_REPO = "mutmutco/MMI-Hub";
22198
23604
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
@@ -22236,7 +23642,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
22236
23642
  exp: now + APP_JWT_TTL_S,
22237
23643
  iss: appId
22238
23644
  }));
22239
- const signer = (0, import_node_crypto5.createSign)("RSA-SHA256");
23645
+ const signer = (0, import_node_crypto6.createSign)("RSA-SHA256");
22240
23646
  signer.update(`${header}.${payload}`);
22241
23647
  return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
22242
23648
  }
@@ -22412,19 +23818,23 @@ function registerBoardCommands(program3) {
22412
23818
  if (result.checked) {
22413
23819
  if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
22414
23820
  if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
23821
+ if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
22415
23822
  return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
22416
23823
  }
22417
23824
  if (result.partial) {
22418
- return result.outcome === "took-over" ? `Partially took over ${ref} from ${previousHolder}: ${result.warning}` : `Partially claimed ${ref} for ${holder}: ${result.warning}`;
23825
+ if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
23826
+ if (result.outcome === "resumed") return `Partially resumed ${ref} from ${previousHolder}: ${result.warning}`;
23827
+ return `Partially claimed ${ref} for ${holder}: ${result.warning}`;
22419
23828
  }
22420
23829
  if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
23830
+ if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
22421
23831
  if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
22422
23832
  return `Claimed ${ref} for ${holder} - In Progress`;
22423
23833
  }
22424
23834
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
22425
23835
  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));
22426
23836
  withExamples(mutating(
22427
- 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").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"),
23837
+ 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"),
22428
23838
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
22429
23839
  ).action(async (issueRefs, o) => {
22430
23840
  if (issueRefs.length === 1) {
@@ -22637,9 +24047,9 @@ function registerBoardCommands(program3) {
22637
24047
  }
22638
24048
 
22639
24049
  // src/bootstrap-commands.ts
22640
- var import_node_fs23 = require("node:fs");
24050
+ var import_node_fs24 = require("node:fs");
22641
24051
  var import_node_os12 = require("node:os");
22642
- var import_node_path21 = require("node:path");
24052
+ var import_node_path23 = require("node:path");
22643
24053
 
22644
24054
  // src/command-plans.ts
22645
24055
  function parseTrainTag(tag) {
@@ -22799,8 +24209,8 @@ function renderSteps(title, steps) {
22799
24209
  }
22800
24210
 
22801
24211
  // src/port-registry.ts
22802
- var import_node_fs22 = require("node:fs");
22803
- var import_node_path20 = require("node:path");
24212
+ var import_node_fs23 = require("node:fs");
24213
+ var import_node_path22 = require("node:path");
22804
24214
 
22805
24215
  // ../infra/port-geometry.mjs
22806
24216
  var PORT_BLOCK = 100;
@@ -22814,8 +24224,8 @@ function nextPortBlock(registry2) {
22814
24224
  return [base, base + PORT_SPAN];
22815
24225
  }
22816
24226
  function loadPortRegistry(path2) {
22817
- if (!(0, import_node_fs22.existsSync)(path2)) return {};
22818
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
24227
+ if (!(0, import_node_fs23.existsSync)(path2)) return {};
24228
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
22819
24229
  const out = {};
22820
24230
  for (const [key, value] of Object.entries(raw)) {
22821
24231
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -22829,9 +24239,9 @@ function ensurePortRange(repo, path2) {
22829
24239
  const existing = registry2[repo];
22830
24240
  if (existing) return existing;
22831
24241
  const range = nextPortBlock(registry2);
22832
- const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
24242
+ const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
22833
24243
  raw[repo] = range;
22834
- (0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
24244
+ (0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
22835
24245
  return range;
22836
24246
  }
22837
24247
  function portCursorSeed(registry2) {
@@ -22853,22 +24263,22 @@ function existingPortRange(repo, registry2) {
22853
24263
  return registry2[repo] ?? null;
22854
24264
  }
22855
24265
  function portRangeInfraAt(root, source) {
22856
- const registryPath = (0, import_node_path20.join)(root, "infra", "port-ranges.json");
22857
- const ddbScriptPath = (0, import_node_path20.join)(root, "infra", "port-ddb.mjs");
22858
- if (!(0, import_node_fs22.existsSync)(registryPath) || !(0, import_node_fs22.existsSync)(ddbScriptPath)) return null;
24266
+ const registryPath = (0, import_node_path22.join)(root, "infra", "port-ranges.json");
24267
+ const ddbScriptPath = (0, import_node_path22.join)(root, "infra", "port-ddb.mjs");
24268
+ if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
22859
24269
  return { root, source, registryPath, ddbScriptPath };
22860
24270
  }
22861
24271
  function resolvePortRangeInfra(cwd, packageDir) {
22862
24272
  const direct = portRangeInfraAt(cwd, "cwd");
22863
24273
  if (direct) return direct;
22864
- for (let dir = cwd; ; dir = (0, import_node_path20.dirname)(dir)) {
22865
- const sibling = portRangeInfraAt((0, import_node_path20.join)(dir, "MMI-Hub"), "sibling-hub");
24274
+ for (let dir = cwd; ; dir = (0, import_node_path22.dirname)(dir)) {
24275
+ const sibling = portRangeInfraAt((0, import_node_path22.join)(dir, "MMI-Hub"), "sibling-hub");
22866
24276
  if (sibling) return sibling;
22867
- const parent = (0, import_node_path20.dirname)(dir);
24277
+ const parent = (0, import_node_path22.dirname)(dir);
22868
24278
  if (parent === dir) break;
22869
24279
  }
22870
24280
  if (packageDir) {
22871
- const pkgRoot = (0, import_node_path20.join)(packageDir, "..", "..");
24281
+ const pkgRoot = (0, import_node_path22.join)(packageDir, "..", "..");
22872
24282
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
22873
24283
  if (pkgFrom) return pkgFrom;
22874
24284
  }
@@ -22930,7 +24340,7 @@ async function assignPersistedPortRange(repo, slug, reg, opts) {
22930
24340
  }
22931
24341
 
22932
24342
  // src/bootstrap-drift.ts
22933
- var import_node_crypto6 = require("node:crypto");
24343
+ var import_node_crypto7 = require("node:crypto");
22934
24344
  function byteComparableSeeds(manifest, cls) {
22935
24345
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
22936
24346
  }
@@ -22950,7 +24360,7 @@ function compareSeedBytes(hubContent, repoContent) {
22950
24360
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
22951
24361
  }
22952
24362
  function seedContentHash(content) {
22953
- return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
24363
+ return (0, import_node_crypto7.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
22954
24364
  }
22955
24365
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
22956
24366
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
@@ -23112,7 +24522,7 @@ function renderPropagationReport(plan) {
23112
24522
  }
23113
24523
 
23114
24524
  // src/bootstrap-propagation-identity.ts
23115
- var import_node_crypto7 = require("node:crypto");
24525
+ var import_node_crypto8 = require("node:crypto");
23116
24526
  var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
23117
24527
  var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
23118
24528
  function safeBranchPart(value, maxLength, fallback) {
@@ -23125,7 +24535,7 @@ function repoSlug2(repo) {
23125
24535
  function propagationBranch(repo, target) {
23126
24536
  const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
23127
24537
  const targetPart = safeBranchPart(target, 48, "target");
23128
- const hash = (0, import_node_crypto7.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
24538
+ const hash = (0, import_node_crypto8.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
23129
24539
  return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
23130
24540
  }
23131
24541
  function legacyPropagationBranch(repo) {
@@ -23932,13 +25342,13 @@ function registerBootstrapCommands(program3) {
23932
25342
  client: defaultGitHubClient(),
23933
25343
  projectMeta: meta,
23934
25344
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
23935
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs23.existsSync)(path2) ? (0, import_node_fs23.readFileSync)(path2, "utf8") : null,
25345
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null,
23936
25346
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
23937
25347
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
23938
25348
  // #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
23939
25349
  // permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
23940
25350
  // sanction, which is the pre-#3664 behaviour.
23941
- sanctionedAdmins: (0, import_node_fs23.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs23.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
25351
+ sanctionedAdmins: (0, import_node_fs24.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs24.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
23942
25352
  requiredGcpApis: (() => {
23943
25353
  const v = meta?.requiredGcpApis;
23944
25354
  if (Array.isArray(v)) return v;
@@ -23991,14 +25401,14 @@ function registerBootstrapCommands(program3) {
23991
25401
  bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
23992
25402
  const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
23993
25403
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
23994
- if (!(0, import_node_fs23.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
25404
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
23995
25405
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
23996
25406
  if (!seedSource.ok) return fail(`bootstrap drift: ${seedSource.reason}`);
23997
- const manifest = loadBootstrapSeeds((0, import_node_fs23.readFileSync)(manifestPath, "utf8"));
25407
+ const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
23998
25408
  const hubContents = /* @__PURE__ */ new Map();
23999
25409
  for (const s of manifest.seeds) {
24000
25410
  if (s.ownership !== "org" || s.source !== "self") continue;
24001
- hubContents.set(s.target, (0, import_node_fs23.existsSync)(s.target) ? (0, import_node_fs23.readFileSync)(s.target, "utf8") : null);
25411
+ hubContents.set(s.target, (0, import_node_fs24.existsSync)(s.target) ? (0, import_node_fs24.readFileSync)(s.target, "utf8") : null);
24002
25412
  }
24003
25413
  let targets;
24004
25414
  let classOf = (_repo) => "deployable";
@@ -24116,10 +25526,10 @@ function registerBootstrapCommands(program3) {
24116
25526
  return;
24117
25527
  }
24118
25528
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
24119
- if (!(0, import_node_fs23.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
25529
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
24120
25530
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
24121
25531
  if (!seedSource.ok) return fail(`bootstrap apply: ${seedSource.reason}`);
24122
- const manifest = loadBootstrapSeeds((0, import_node_fs23.readFileSync)(manifestPath, "utf8"));
25532
+ const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
24123
25533
  const baseBranch = o.class === "content" ? "main" : "development";
24124
25534
  const slug = parsedRepo.slug;
24125
25535
  const onlyTarget = o.only.trim();
@@ -24131,16 +25541,16 @@ function registerBootstrapCommands(program3) {
24131
25541
  }
24132
25542
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
24133
25543
  const gh = async (args) => execFileP("gh", args, { timeout: 2e4 });
24134
- const readFile8 = (p) => (0, import_node_fs23.existsSync)(p) ? (0, import_node_fs23.readFileSync)(p, "utf8") : null;
25544
+ const readFile8 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
24135
25545
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
24136
25546
  const putSeed = async (target, content, ref, sha) => {
24137
- const tmp = (0, import_node_path21.join)((0, import_node_os12.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
24138
- (0, import_node_fs23.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
25547
+ const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
25548
+ (0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
24139
25549
  try {
24140
25550
  await gh(contentPutInputArgs(repo, target, tmp));
24141
25551
  } finally {
24142
25552
  try {
24143
- (0, import_node_fs23.unlinkSync)(tmp);
25553
+ (0, import_node_fs24.unlinkSync)(tmp);
24144
25554
  } catch {
24145
25555
  }
24146
25556
  }
@@ -24455,10 +25865,10 @@ LIVE apply to ${repo}:
24455
25865
  bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an org-owned whole file or declared Hub-managed block)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
24456
25866
  const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
24457
25867
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
24458
- if (!(0, import_node_fs23.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
25868
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
24459
25869
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
24460
25870
  if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
24461
- const manifest = loadBootstrapSeeds((0, import_node_fs23.readFileSync)(manifestPath, "utf8"));
25871
+ const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
24462
25872
  const propagatable = manifest.seeds.filter(isPropagatableSeed);
24463
25873
  if (!o.target) {
24464
25874
  return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
@@ -24467,9 +25877,9 @@ LIVE apply to ${repo}:
24467
25877
  const seed = propagatable.find((s) => s.target === o.target);
24468
25878
  if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable targets:
24469
25879
  ${propagatable.map((s) => s.target).join("\n ")}`);
24470
- if (!seed.managedBlock && !(0, import_node_fs23.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
24471
- const hubContent = seed.managedBlock ? null : (0, import_node_fs23.readFileSync)(seed.target, "utf8");
24472
- const readSeedFile = (path2) => (0, import_node_fs23.existsSync)(path2) ? (0, import_node_fs23.readFileSync)(path2, "utf8") : null;
25880
+ if (!seed.managedBlock && !(0, import_node_fs24.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
25881
+ const hubContent = seed.managedBlock ? null : (0, import_node_fs24.readFileSync)(seed.target, "utf8");
25882
+ const readSeedFile = (path2) => (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null;
24473
25883
  const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
24474
25884
  const cfg = await loadConfig();
24475
25885
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -24478,9 +25888,9 @@ LIVE apply to ${repo}:
24478
25888
  }
24479
25889
  const rosterRepos = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
24480
25890
  let independentCount = rosterRepos.length;
24481
- if ((0, import_node_fs23.existsSync)("projects.json")) {
25891
+ if ((0, import_node_fs24.existsSync)("projects.json")) {
24482
25892
  try {
24483
- const local = JSON.parse((0, import_node_fs23.readFileSync)("projects.json", "utf8"));
25893
+ const local = JSON.parse((0, import_node_fs24.readFileSync)("projects.json", "utf8"));
24484
25894
  const localRepos = /* @__PURE__ */ new Set();
24485
25895
  for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
24486
25896
  const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
@@ -24640,15 +26050,15 @@ LIVE apply to ${repo}:
24640
26050
  } catch {
24641
26051
  existingSha = void 0;
24642
26052
  }
24643
- const tmp = (0, import_node_path21.join)((0, import_node_os12.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
26053
+ const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
24644
26054
  const desiredContent = desiredByRepo.get(rec.repo);
24645
26055
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
24646
- (0, import_node_fs23.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
26056
+ (0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
24647
26057
  try {
24648
26058
  await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
24649
26059
  } finally {
24650
26060
  try {
24651
- (0, import_node_fs23.unlinkSync)(tmp);
26061
+ (0, import_node_fs24.unlinkSync)(tmp);
24652
26062
  } catch {
24653
26063
  }
24654
26064
  }
@@ -24715,10 +26125,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
24715
26125
  return fail(`bootstrap rollback: ${e.message}`);
24716
26126
  }
24717
26127
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
24718
- if (!(0, import_node_fs23.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
26128
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
24719
26129
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
24720
26130
  if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
24721
- const manifest = loadBootstrapSeeds((0, import_node_fs23.readFileSync)(manifestPath, "utf8"));
26131
+ const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
24722
26132
  const propagatable = manifest.seeds.filter(isPropagatableSeed);
24723
26133
  if (!o.target) {
24724
26134
  return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
@@ -24735,10 +26145,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
24735
26145
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
24736
26146
  let candidates;
24737
26147
  if (o.record) {
24738
- if (!(0, import_node_fs23.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
26148
+ if (!(0, import_node_fs24.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
24739
26149
  let parsed;
24740
26150
  try {
24741
- parsed = JSON.parse((0, import_node_fs23.readFileSync)(o.record, "utf8"));
26151
+ parsed = JSON.parse((0, import_node_fs24.readFileSync)(o.record, "utf8"));
24742
26152
  } catch (e) {
24743
26153
  return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
24744
26154
  }
@@ -24815,13 +26225,13 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
24815
26225
  } catch {
24816
26226
  existingSha = void 0;
24817
26227
  }
24818
- const tmp = (0, import_node_path21.join)((0, import_node_os12.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
24819
- (0, import_node_fs23.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
26228
+ const tmp = (0, import_node_path23.join)((0, import_node_os12.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
26229
+ (0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
24820
26230
  try {
24821
26231
  await gh(contentPutInputArgs(repo, seed.target, tmp));
24822
26232
  } finally {
24823
26233
  try {
24824
- (0, import_node_fs23.unlinkSync)(tmp);
26234
+ (0, import_node_fs24.unlinkSync)(tmp);
24825
26235
  } catch {
24826
26236
  }
24827
26237
  }
@@ -25814,7 +27224,7 @@ function renderDeployPortDoctor(report) {
25814
27224
  }
25815
27225
 
25816
27226
  // src/wave-status.ts
25817
- var import_node_fs26 = require("node:fs");
27227
+ var import_node_fs27 = require("node:fs");
25818
27228
 
25819
27229
  // src/git-worktree-observation.ts
25820
27230
  function parseGitWorktreePorcelain(text) {
@@ -25845,8 +27255,8 @@ function deriveComposeProjectName(worktreePath) {
25845
27255
 
25846
27256
  // src/stage-runner.ts
25847
27257
  var import_node_child_process8 = require("node:child_process");
25848
- var import_node_fs25 = require("node:fs");
25849
- var import_node_path23 = require("node:path");
27258
+ var import_node_fs26 = require("node:fs");
27259
+ var import_node_path25 = require("node:path");
25850
27260
  var import_node_net = require("node:net");
25851
27261
  var import_node_util4 = require("node:util");
25852
27262
 
@@ -25856,8 +27266,8 @@ function normalizeEol2(s) {
25856
27266
  }
25857
27267
 
25858
27268
  // src/env-detection.ts
25859
- var import_node_fs24 = require("node:fs");
25860
- var import_node_path22 = require("node:path");
27269
+ var import_node_fs25 = require("node:fs");
27270
+ var import_node_path24 = require("node:path");
25861
27271
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".jerv", ".pi", ".venv", "venv"]);
25862
27272
  function isEnvBaseName(name) {
25863
27273
  return name.toLowerCase() === ".env";
@@ -25867,7 +27277,7 @@ function findEnvFiles(root) {
25867
27277
  const walk2 = (dir, rel) => {
25868
27278
  let entries;
25869
27279
  try {
25870
- entries = (0, import_node_fs24.readdirSync)(dir, { withFileTypes: true });
27280
+ entries = (0, import_node_fs25.readdirSync)(dir, { withFileTypes: true });
25871
27281
  } catch {
25872
27282
  return;
25873
27283
  }
@@ -25875,13 +27285,13 @@ function findEnvFiles(root) {
25875
27285
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
25876
27286
  if (entry.isDirectory()) {
25877
27287
  if (SKIPPED_DIRS.has(entry.name)) continue;
25878
- walk2((0, import_node_path22.join)(dir, entry.name), relPath);
27288
+ walk2((0, import_node_path24.join)(dir, entry.name), relPath);
25879
27289
  continue;
25880
27290
  }
25881
27291
  if (isEnvBaseName(entry.name)) {
25882
27292
  let isDir = false;
25883
27293
  try {
25884
- isDir = (0, import_node_fs24.lstatSync)((0, import_node_path22.join)(dir, entry.name)).isDirectory();
27294
+ isDir = (0, import_node_fs25.lstatSync)((0, import_node_path24.join)(dir, entry.name)).isDirectory();
25885
27295
  } catch {
25886
27296
  }
25887
27297
  if (!isDir) found.push(relPath);
@@ -25912,7 +27322,7 @@ function healthPollIntervalMs() {
25912
27322
  return HEALTH_POLL_INTERVAL_MS;
25913
27323
  }
25914
27324
  function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
25915
- return new Promise((resolve6, reject) => {
27325
+ return new Promise((resolve7, reject) => {
25916
27326
  let settled = false;
25917
27327
  const finish = (fn) => {
25918
27328
  if (settled) return;
@@ -25922,7 +27332,7 @@ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
25922
27332
  child2.removeAllListeners("exit");
25923
27333
  fn();
25924
27334
  };
25925
- const timer = setTimeout(() => finish(resolve6), graceMs);
27335
+ const timer = setTimeout(() => finish(resolve7), graceMs);
25926
27336
  child2.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
25927
27337
  child2.on("exit", (code, signal) => {
25928
27338
  const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
@@ -26022,7 +27432,7 @@ function appendForceRecreate(up) {
26022
27432
  return `${up.trimEnd()} --force-recreate`;
26023
27433
  }
26024
27434
  function stageStatePath(cwd = process.cwd()) {
26025
- return (0, import_node_path23.join)(cwd, "tmp", "stage", "state.json");
27435
+ return (0, import_node_path25.join)(cwd, "tmp", "stage", "state.json");
26026
27436
  }
26027
27437
  function normPath(path2) {
26028
27438
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -26128,10 +27538,10 @@ function pickStagePort(range, isFree) {
26128
27538
  throw new Error(`no free stage port in range ${start}-${end} \u2014 every port is in use`);
26129
27539
  }
26130
27540
  function isPortFree(port) {
26131
- return new Promise((resolve6) => {
27541
+ return new Promise((resolve7) => {
26132
27542
  const srv = (0, import_node_net.createServer)();
26133
- srv.once("error", () => resolve6(false));
26134
- srv.once("listening", () => srv.close(() => resolve6(true)));
27543
+ srv.once("error", () => resolve7(false));
27544
+ srv.once("listening", () => srv.close(() => resolve7(true)));
26135
27545
  srv.listen(port, "127.0.0.1");
26136
27546
  });
26137
27547
  }
@@ -26246,14 +27656,14 @@ function stageProcessEnv(stagePort, extraEnv) {
26246
27656
  }
26247
27657
  async function ensureStageRuntimeEnv(config, opts, cwd) {
26248
27658
  if (!config.ensureEnv) return;
26249
- const target = (0, import_node_path23.join)(cwd, config.ensureEnv.target);
26250
- const example = (0, import_node_path23.join)(cwd, config.ensureEnv.example);
26251
- if (!(0, import_node_fs25.existsSync)(target) && (0, import_node_fs25.existsSync)(example)) {
26252
- (0, import_node_fs25.copyFileSync)(example, target);
26253
- } else if ((0, import_node_fs25.existsSync)(target) && (0, import_node_fs25.existsSync)(example)) {
26254
- const stale = detectStaleEnvFile((0, import_node_fs25.readFileSync)(example, "utf8"), (0, import_node_fs25.readFileSync)(target, "utf8"), {
26255
- exampleMtimeMs: (0, import_node_fs25.statSync)(example).mtimeMs,
26256
- targetMtimeMs: (0, import_node_fs25.statSync)(target).mtimeMs
27659
+ const target = (0, import_node_path25.join)(cwd, config.ensureEnv.target);
27660
+ const example = (0, import_node_path25.join)(cwd, config.ensureEnv.example);
27661
+ if (!(0, import_node_fs26.existsSync)(target) && (0, import_node_fs26.existsSync)(example)) {
27662
+ (0, import_node_fs26.copyFileSync)(example, target);
27663
+ } else if ((0, import_node_fs26.existsSync)(target) && (0, import_node_fs26.existsSync)(example)) {
27664
+ const stale = detectStaleEnvFile((0, import_node_fs26.readFileSync)(example, "utf8"), (0, import_node_fs26.readFileSync)(target, "utf8"), {
27665
+ exampleMtimeMs: (0, import_node_fs26.statSync)(example).mtimeMs,
27666
+ targetMtimeMs: (0, import_node_fs26.statSync)(target).mtimeMs
26257
27667
  });
26258
27668
  if (stale) {
26259
27669
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -26261,8 +27671,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
26261
27671
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
26262
27672
  }
26263
27673
  }
26264
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs25.existsSync)(target)) {
26265
- (0, import_node_fs25.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs25.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
27674
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs26.existsSync)(target)) {
27675
+ (0, import_node_fs26.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs26.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
26266
27676
  }
26267
27677
  }
26268
27678
  async function gitText(cwd, args) {
@@ -26292,20 +27702,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
26292
27702
  return void 0;
26293
27703
  }
26294
27704
  function readState(path2) {
26295
- if (!(0, import_node_fs25.existsSync)(path2)) return null;
27705
+ if (!(0, import_node_fs26.existsSync)(path2)) return null;
26296
27706
  try {
26297
- return JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8"));
27707
+ return JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
26298
27708
  } catch {
26299
27709
  return null;
26300
27710
  }
26301
27711
  }
26302
27712
  function mkdirFor(path2) {
26303
27713
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
26304
- (0, import_node_fs25.mkdirSync)(dir, { recursive: true });
27714
+ (0, import_node_fs26.mkdirSync)(dir, { recursive: true });
26305
27715
  }
26306
27716
  function writeState(path2, state) {
26307
27717
  mkdirFor(path2);
26308
- (0, import_node_fs25.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
27718
+ (0, import_node_fs26.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
26309
27719
  }
26310
27720
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
26311
27721
  const reservation = {
@@ -26325,7 +27735,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
26325
27735
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
26326
27736
  }
26327
27737
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
26328
- (0, import_node_fs25.rmSync)(path2, { force: true });
27738
+ (0, import_node_fs26.rmSync)(path2, { force: true });
26329
27739
  }
26330
27740
  }
26331
27741
  async function killTree(pid) {
@@ -26342,7 +27752,7 @@ async function killTree(pid) {
26342
27752
  } catch {
26343
27753
  }
26344
27754
  }
26345
- await new Promise((resolve6) => setTimeout(resolve6, 500));
27755
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
26346
27756
  try {
26347
27757
  process.kill(-pid, "SIGKILL");
26348
27758
  } catch {
@@ -26363,7 +27773,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
26363
27773
  } catch (e) {
26364
27774
  last = e.message;
26365
27775
  }
26366
- await new Promise((resolve6) => setTimeout(resolve6, healthPollIntervalMs()));
27776
+ await new Promise((resolve7) => setTimeout(resolve7, healthPollIntervalMs()));
26367
27777
  }
26368
27778
  throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
26369
27779
  }
@@ -26495,8 +27905,8 @@ async function runStage(config = {}, opts = {}) {
26495
27905
  });
26496
27906
  }
26497
27907
  } catch (e) {
26498
- (0, import_node_fs25.rmSync)(statePath, { force: true });
26499
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs25.rmSync)(globalStatePath, { force: true });
27908
+ (0, import_node_fs26.rmSync)(statePath, { force: true });
27909
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs26.rmSync)(globalStatePath, { force: true });
26500
27910
  throw e;
26501
27911
  }
26502
27912
  const started = await startStage(config, {
@@ -26513,9 +27923,9 @@ async function runStage(config = {}, opts = {}) {
26513
27923
  // src/wave-status.ts
26514
27924
  function readStageSummary(worktreePath) {
26515
27925
  const statePath = stageStatePath(worktreePath);
26516
- if (!(0, import_node_fs26.existsSync)(statePath)) return void 0;
27926
+ if (!(0, import_node_fs27.existsSync)(statePath)) return void 0;
26517
27927
  try {
26518
- const state = JSON.parse((0, import_node_fs26.readFileSync)(statePath, "utf8"));
27928
+ const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
26519
27929
  const port = typeof state.port === "number" ? state.port : void 0;
26520
27930
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
26521
27931
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -26570,9 +27980,9 @@ async function collectWaveStatus(deps) {
26570
27980
  }
26571
27981
 
26572
27982
  // src/discovery-commands.ts
26573
- var import_node_fs27 = require("node:fs");
27983
+ var import_node_fs28 = require("node:fs");
26574
27984
  var import_node_os13 = require("node:os");
26575
- var import_node_path24 = require("node:path");
27985
+ var import_node_path26 = require("node:path");
26576
27986
  var GC_GH_TIMEOUT_MS = 2e4;
26577
27987
  async function collectStatus() {
26578
27988
  const repo = await resolveRepo();
@@ -26762,8 +28172,8 @@ async function collectOnboardStatus(opts = {}) {
26762
28172
  }
26763
28173
  const home = (0, import_node_os13.homedir)();
26764
28174
  const plugin = onboardPluginGate({
26765
- readKnown: () => readFileSyncSafe((0, import_node_path24.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs27.readFileSync),
26766
- readSettings: () => readFileSyncSafe((0, import_node_path24.join)(home, ".claude", "settings.json"), import_node_fs27.readFileSync)
28175
+ readKnown: () => readFileSyncSafe((0, import_node_path26.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs28.readFileSync),
28176
+ readSettings: () => readFileSyncSafe((0, import_node_path26.join)(home, ".claude", "settings.json"), import_node_fs28.readFileSync)
26767
28177
  });
26768
28178
  return { track, board, registry: registry2, secrets, plugin, doors: opts.doors ?? [], nextCommand };
26769
28179
  }
@@ -28794,9 +30204,9 @@ async function runDoctorClean(opts, io, deps) {
28794
30204
  }
28795
30205
 
28796
30206
  // src/doctor-io.ts
28797
- var import_node_fs28 = require("node:fs");
30207
+ var import_node_fs29 = require("node:fs");
28798
30208
  var import_node_os14 = require("node:os");
28799
- var import_node_path25 = require("node:path");
30209
+ var import_node_path27 = require("node:path");
28800
30210
  var import_node_child_process9 = require("node:child_process");
28801
30211
  var import_node_util5 = require("node:util");
28802
30212
 
@@ -28809,8 +30219,8 @@ function nodeDiscardSinkPath(platform2 = process.platform) {
28809
30219
  var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process9.execFile);
28810
30220
  function execFileCapture(file, args, opts = {}) {
28811
30221
  const sink = nodeDiscardSinkPath();
28812
- const inFd = (0, import_node_fs28.openSync)(sink, "r");
28813
- const errFd = (0, import_node_fs28.openSync)(sink, "w");
30222
+ const inFd = (0, import_node_fs29.openSync)(sink, "r");
30223
+ const errFd = (0, import_node_fs29.openSync)(sink, "w");
28814
30224
  try {
28815
30225
  return (0, import_node_child_process9.execFileSync)(file, args, {
28816
30226
  ...opts,
@@ -28818,17 +30228,17 @@ function execFileCapture(file, args, opts = {}) {
28818
30228
  stdio: [inFd, "pipe", errFd]
28819
30229
  });
28820
30230
  } finally {
28821
- (0, import_node_fs28.closeSync)(inFd);
28822
- (0, import_node_fs28.closeSync)(errFd);
30231
+ (0, import_node_fs29.closeSync)(inFd);
30232
+ (0, import_node_fs29.closeSync)(errFd);
28823
30233
  }
28824
30234
  }
28825
30235
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
28826
30236
  function stagedClaudePluginVersion() {
28827
30237
  try {
28828
- const stagingRoot = (0, import_node_path25.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "staging");
28829
- const record = JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(stagingRoot, "launch-target.json"), "utf8"));
30238
+ const stagingRoot = (0, import_node_path27.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "staging");
30239
+ const record = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(stagingRoot, "launch-target.json"), "utf8"));
28830
30240
  if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
28831
- const manifest = JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
30241
+ const manifest = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
28832
30242
  if (typeof manifest.version !== "string") return void 0;
28833
30243
  return manifest.version === record.target ? record.target : void 0;
28834
30244
  } catch {
@@ -28838,7 +30248,7 @@ function stagedClaudePluginVersion() {
28838
30248
  function installedClaudePluginVersion() {
28839
30249
  try {
28840
30250
  const file = JSON.parse(
28841
- (0, import_node_fs28.readFileSync)((0, import_node_path25.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
30251
+ (0, import_node_fs29.readFileSync)((0, import_node_path27.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
28842
30252
  );
28843
30253
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
28844
30254
  if (versions.length === 0) return void 0;
@@ -28849,7 +30259,7 @@ function installedClaudePluginVersion() {
28849
30259
  }
28850
30260
  function manifestVersion(path2) {
28851
30261
  try {
28852
- const manifest = JSON.parse((0, import_node_fs28.readFileSync)(path2, "utf8"));
30262
+ const manifest = JSON.parse((0, import_node_fs29.readFileSync)(path2, "utf8"));
28853
30263
  return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
28854
30264
  } catch {
28855
30265
  return void 0;
@@ -28858,12 +30268,12 @@ function manifestVersion(path2) {
28858
30268
  function readHermesPluginEvidence(env = process.env) {
28859
30269
  const host = hermesConfigRoot(env);
28860
30270
  const root = hermesPluginRoot(env);
28861
- const installRecordPresent = (0, import_node_fs28.existsSync)(root);
28862
- const manifestPath = (0, import_node_path25.join)(root, "plugin.yaml");
30271
+ const installRecordPresent = (0, import_node_fs29.existsSync)(root);
30272
+ const manifestPath = (0, import_node_path27.join)(root, "plugin.yaml");
28863
30273
  let installedVersion;
28864
30274
  let manifest = "missing";
28865
30275
  try {
28866
- const text = (0, import_node_fs28.readFileSync)(manifestPath, "utf8");
30276
+ const text = (0, import_node_fs29.readFileSync)(manifestPath, "utf8");
28867
30277
  let version;
28868
30278
  try {
28869
30279
  const parsed = JSON.parse(text).version;
@@ -28877,20 +30287,20 @@ function readHermesPluginEvidence(env = process.env) {
28877
30287
  if (version) {
28878
30288
  installedVersion = version;
28879
30289
  manifest = "valid";
28880
- } else if ((0, import_node_fs28.existsSync)(manifestPath)) manifest = "invalid";
30290
+ } else if ((0, import_node_fs29.existsSync)(manifestPath)) manifest = "invalid";
28881
30291
  } catch {
28882
- if ((0, import_node_fs28.existsSync)(manifestPath)) manifest = "invalid";
30292
+ if ((0, import_node_fs29.existsSync)(manifestPath)) manifest = "invalid";
28883
30293
  }
28884
30294
  let skills = false;
28885
30295
  try {
28886
- skills = (0, import_node_fs28.existsSync)((0, import_node_path25.join)(root, "skills")) && (0, import_node_fs28.statSync)((0, import_node_path25.join)(root, "skills")).isDirectory();
30296
+ skills = (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, "skills")) && (0, import_node_fs29.statSync)((0, import_node_path27.join)(root, "skills")).isDirectory();
28887
30297
  } catch {
28888
30298
  }
28889
30299
  return {
28890
- hostPresent: (0, import_node_fs28.existsSync)(host),
30300
+ hostPresent: (0, import_node_fs29.existsSync)(host),
28891
30301
  installRecordPresent,
28892
30302
  manifest,
28893
- payloadPresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(root, "__init__.py")) && skills && manifest === "valid",
30303
+ payloadPresent: (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, "__init__.py")) && skills && manifest === "valid",
28894
30304
  ...installedVersion ? { installedVersion } : {}
28895
30305
  };
28896
30306
  }
@@ -28898,7 +30308,7 @@ function installedSurfacePluginVersion(surface) {
28898
30308
  const token = surfaceToken(surface);
28899
30309
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
28900
30310
  if (token === "cursor") {
28901
- return manifestVersion((0, import_node_path25.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
30311
+ return manifestVersion((0, import_node_path27.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
28902
30312
  }
28903
30313
  if (token === "jervcode") {
28904
30314
  return installedJervCodePackageVersion();
@@ -28935,13 +30345,13 @@ function worktreeRootSync() {
28935
30345
  }
28936
30346
  var gitignorePath = () => {
28937
30347
  const root = worktreeRootSync();
28938
- return root === null ? null : (0, import_node_path25.join)(root, ".gitignore");
30348
+ return root === null ? null : (0, import_node_path27.join)(root, ".gitignore");
28939
30349
  };
28940
30350
  function readGitignore() {
28941
30351
  const path2 = gitignorePath();
28942
30352
  if (path2 === null) return null;
28943
30353
  try {
28944
- return (0, import_node_fs28.readFileSync)(path2, "utf8");
30354
+ return (0, import_node_fs29.readFileSync)(path2, "utf8");
28945
30355
  } catch {
28946
30356
  return null;
28947
30357
  }
@@ -28950,14 +30360,14 @@ function writeGitignore(content) {
28950
30360
  const path2 = gitignorePath();
28951
30361
  if (path2 === null) return false;
28952
30362
  try {
28953
- (0, import_node_fs28.writeFileSync)(path2, content, "utf8");
30363
+ (0, import_node_fs29.writeFileSync)(path2, content, "utf8");
28954
30364
  return true;
28955
30365
  } catch {
28956
30366
  return false;
28957
30367
  }
28958
30368
  }
28959
30369
  function lineEndingState(root) {
28960
- const attributesPresent = (0, import_node_fs28.existsSync)((0, import_node_path25.join)(root, ".gitattributes"));
30370
+ const attributesPresent = (0, import_node_fs29.existsSync)((0, import_node_path27.join)(root, ".gitattributes"));
28961
30371
  try {
28962
30372
  const output = execFileCapture("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
28963
30373
  windowsHide: true
@@ -29277,102 +30687,6 @@ function registerExplainCommand(program3) {
29277
30687
  });
29278
30688
  }
29279
30689
 
29280
- // src/file-lock.ts
29281
- var import_promises3 = require("node:fs/promises");
29282
- var import_node_path26 = require("node:path");
29283
- var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
29284
- var IMMEDIATE_RETRY_BUDGET = 3;
29285
- var FileLockBusyError = class extends Error {
29286
- lockPath;
29287
- constructor(label, lockPath, maxWaitMs) {
29288
- super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
29289
- this.name = "FileLockBusyError";
29290
- this.lockPath = lockPath;
29291
- }
29292
- };
29293
- function resolveFileLockOpts(opts = {}) {
29294
- return {
29295
- staleMs: opts.staleMs ?? 3e4,
29296
- retryMs: opts.retryMs ?? 50,
29297
- maxWaitMs: opts.maxWaitMs ?? 5e3,
29298
- label: opts.label ?? "file lock"
29299
- };
29300
- }
29301
- async function acquireFileLock(lockPath, opts, deadline) {
29302
- let immediateRetries = 0;
29303
- for (; ; ) {
29304
- let handle;
29305
- try {
29306
- handle = await (0, import_promises3.open)(lockPath, "wx");
29307
- } catch (e) {
29308
- const code = e.code;
29309
- if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
29310
- const retryNow = async () => {
29311
- if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
29312
- if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
29313
- };
29314
- try {
29315
- const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
29316
- if (age > opts.staleMs) {
29317
- try {
29318
- await (0, import_promises3.unlink)(lockPath);
29319
- await retryNow();
29320
- continue;
29321
- } catch (unlinkError) {
29322
- if (unlinkError instanceof FileLockBusyError) throw unlinkError;
29323
- const unlinkCode = unlinkError.code;
29324
- if (unlinkCode === "ENOENT") {
29325
- await retryNow();
29326
- continue;
29327
- }
29328
- }
29329
- }
29330
- } catch (statError) {
29331
- if (statError instanceof FileLockBusyError) throw statError;
29332
- if (statError.code !== "ENOENT") throw statError;
29333
- await retryNow();
29334
- continue;
29335
- }
29336
- if (Date.now() >= deadline) {
29337
- throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
29338
- }
29339
- await sleep(opts.retryMs);
29340
- continue;
29341
- }
29342
- const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
29343
- try {
29344
- await handle.writeFile(token);
29345
- } catch (e) {
29346
- await handle.close().catch(() => void 0);
29347
- throw e;
29348
- }
29349
- return { token, handle };
29350
- }
29351
- }
29352
- async function fileLockHeldBy(lockPath, token) {
29353
- try {
29354
- return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
29355
- } catch {
29356
- return false;
29357
- }
29358
- }
29359
- async function releaseFileLock(lockPath, guard) {
29360
- await guard.handle.close().catch(() => void 0);
29361
- if (await fileLockHeldBy(lockPath, guard.token)) {
29362
- await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
29363
- }
29364
- }
29365
- async function withFileLock(lockPath, opts, fn) {
29366
- const resolved = resolveFileLockOpts(opts);
29367
- await (0, import_promises3.mkdir)((0, import_node_path26.dirname)(lockPath), { recursive: true }).catch(() => void 0);
29368
- const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
29369
- try {
29370
- return await fn();
29371
- } finally {
29372
- await releaseFileLock(lockPath, guard);
29373
- }
29374
- }
29375
-
29376
30690
  // src/gh-account-visibility.ts
29377
30691
  function parseGhAuthAccounts(stdout) {
29378
30692
  const accounts = [];
@@ -29392,8 +30706,8 @@ function parseOriginRepo(remoteUrl) {
29392
30706
  }
29393
30707
 
29394
30708
  // src/issue-commands.ts
29395
- var import_node_fs29 = require("node:fs");
29396
- var import_node_crypto8 = require("node:crypto");
30709
+ var import_node_fs30 = require("node:fs");
30710
+ var import_node_crypto9 = require("node:crypto");
29397
30711
 
29398
30712
  // src/issue-body.ts
29399
30713
  var import_node_os15 = require("node:os");
@@ -29642,7 +30956,7 @@ async function editIssue(client, options, deps = {}) {
29642
30956
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
29643
30957
  const patch = {};
29644
30958
  let bodyChanged = false;
29645
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs29.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
30959
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs30.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
29646
30960
  if (options.titleFile !== void 0) {
29647
30961
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
29648
30962
  } else if (options.title !== void 0) {
@@ -29923,7 +31237,7 @@ function rowIdempotencyKey(batchKey, spec) {
29923
31237
  const identity = `${spec.type}
29924
31238
  ${spec.title.trim()}
29925
31239
  ${spec.body ?? ""}`;
29926
- const hash = (0, import_node_crypto8.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
31240
+ const hash = (0, import_node_crypto9.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
29927
31241
  return `${batchKey}:${hash}`;
29928
31242
  }
29929
31243
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -30243,8 +31557,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
30243
31557
  (_opts, args) => ({ command: "issue unlink-child", parent: args[0], child: args[1] })
30244
31558
  ).action(async (parentRef, childRef, o) => {
30245
31559
  try {
30246
- parseIssueRef(parentRef, o.repo);
30247
- parseIssueRef(childRef, o.repo);
31560
+ parseIssueRef(parentRef);
31561
+ parseIssueRef(childRef);
30248
31562
  const defaultRepo = await resolveRepo(o.repo);
30249
31563
  const result = await unlinkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
30250
31564
  console.log(JSON.stringify(result));
@@ -30267,7 +31581,7 @@ function extendCreateCommand(issue, batchAttach) {
30267
31581
  if (opts.batch) {
30268
31582
  let specs;
30269
31583
  try {
30270
- const raw = (0, import_node_fs29.readFileSync)(opts.batch, "utf8");
31584
+ const raw = (0, import_node_fs30.readFileSync)(opts.batch, "utf8");
30271
31585
  specs = JSON.parse(raw);
30272
31586
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
30273
31587
  } catch (e) {
@@ -30739,7 +32053,7 @@ async function readRestPrSnapshotWithRetry(prNumber, repo, gh = defaultGhApi, op
30739
32053
  const retries = options?.retries ?? PR_SNAPSHOT_READ_RETRIES;
30740
32054
  const untilMs = options?.retryTransientUntilMs;
30741
32055
  const delayMs = options?.delayMs ?? (untilMs !== void 0 ? PR_CHECKS_POLL_MS : PR_SNAPSHOT_READ_DELAY_MS);
30742
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
32056
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
30743
32057
  const now = options?.now ?? (() => Date.now());
30744
32058
  let lastError = "no attempt completed";
30745
32059
  let attempt = 0;
@@ -31394,8 +32708,8 @@ function registerPrLifecycleCommands(program3) {
31394
32708
  }
31395
32709
 
31396
32710
  // src/project-info-sync.ts
31397
- var import_node_fs30 = require("node:fs");
31398
- var import_node_path27 = require("node:path");
32711
+ var import_node_fs31 = require("node:fs");
32712
+ var import_node_path28 = require("node:path");
31399
32713
  var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
31400
32714
  updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
31401
32715
  projectV2 { id }
@@ -31440,14 +32754,14 @@ function sharedName(entries, fallback) {
31440
32754
  }
31441
32755
  function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
31442
32756
  if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
31443
- const readmePath = (0, import_node_path27.join)(repoRoot2, "README.md");
31444
- if (!(0, import_node_fs30.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
32757
+ const readmePath = (0, import_node_path28.join)(repoRoot2, "README.md");
32758
+ if (!(0, import_node_fs31.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
31445
32759
  const entries = entriesFor(project2, projects);
31446
32760
  const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
31447
32761
  const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
31448
32762
  if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
31449
32763
  const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
31450
- const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs30.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
32764
+ const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs31.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
31451
32765
  const lines = [
31452
32766
  `# ${projectName}`,
31453
32767
  "",
@@ -31466,8 +32780,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
31466
32780
  const targetBase = `https://github.com/${targetRepo2}`;
31467
32781
  const targetBranch = branchFor(targetRepo2, projects);
31468
32782
  const orgDocs = [
31469
- (0, import_node_fs30.existsSync)((0, import_node_path27.join)(repoRoot2, "architecture.md")) ? `- [Hub architecture](${targetBase}/blob/${targetBranch}/architecture.md)` : "",
31470
- (0, import_node_fs30.existsSync)((0, import_node_path27.join)(repoRoot2, "docs", "Architecture", "agentic-dev-environment.md")) ? `- [Agentic development environment](${targetBase}/blob/${targetBranch}/docs/Architecture/agentic-dev-environment.md)` : ""
32783
+ (0, import_node_fs31.existsSync)((0, import_node_path28.join)(repoRoot2, "architecture.md")) ? `- [Hub architecture](${targetBase}/blob/${targetBranch}/architecture.md)` : "",
32784
+ (0, import_node_fs31.existsSync)((0, import_node_path28.join)(repoRoot2, "docs", "Architecture", "agentic-dev-environment.md")) ? `- [Agentic development environment](${targetBase}/blob/${targetBranch}/docs/Architecture/agentic-dev-environment.md)` : ""
31471
32785
  ].filter(Boolean);
31472
32786
  if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
31473
32787
  return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
@@ -31682,7 +32996,7 @@ async function awsJson(args) {
31682
32996
  try {
31683
32997
  return await run();
31684
32998
  } catch {
31685
- await new Promise((resolve6) => setTimeout(resolve6, AWS_RETRY_DELAY_MS));
32999
+ await new Promise((resolve7) => setTimeout(resolve7, AWS_RETRY_DELAY_MS));
31686
33000
  return run();
31687
33001
  }
31688
33002
  }
@@ -31885,8 +33199,8 @@ function registerSchedulesCommands(program3) {
31885
33199
  }
31886
33200
 
31887
33201
  // src/secrets-commands.ts
31888
- var import_node_fs31 = require("node:fs");
31889
- var import_node_path28 = require("node:path");
33202
+ var import_node_fs32 = require("node:fs");
33203
+ var import_node_path29 = require("node:path");
31890
33204
  var import_node_os16 = require("node:os");
31891
33205
 
31892
33206
  // src/secrets-diff.ts
@@ -31989,18 +33303,18 @@ function collectMap(value, previous = []) {
31989
33303
  return [...previous, value];
31990
33304
  }
31991
33305
  async function decryptRailsCredentials(input) {
31992
- const appDir = (0, import_node_path28.resolve)(input.appDir ?? process.cwd());
33306
+ const appDir = (0, import_node_path29.resolve)(input.appDir ?? process.cwd());
31993
33307
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
31994
33308
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
31995
- const credentialsPath = (0, import_node_path28.resolve)(appDir, credentialsFile);
31996
- const masterKeyPath = (0, import_node_path28.resolve)(appDir, masterKeyFile);
33309
+ const credentialsPath = (0, import_node_path29.resolve)(appDir, credentialsFile);
33310
+ const masterKeyPath = (0, import_node_path29.resolve)(appDir, masterKeyFile);
31997
33311
  const env = {
31998
33312
  ...process.env,
31999
33313
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
32000
33314
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
32001
33315
  };
32002
- if ((0, import_node_fs31.existsSync)(masterKeyPath)) {
32003
- env.RAILS_MASTER_KEY = (0, import_node_fs31.readFileSync)(masterKeyPath, "utf8").trim();
33316
+ if ((0, import_node_fs32.existsSync)(masterKeyPath)) {
33317
+ env.RAILS_MASTER_KEY = (0, import_node_fs32.readFileSync)(masterKeyPath, "utf8").trim();
32004
33318
  }
32005
33319
  const script = [
32006
33320
  'require "json"',
@@ -32010,9 +33324,9 @@ async function decryptRailsCredentials(input) {
32010
33324
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
32011
33325
  "puts JSON.generate(config.config)"
32012
33326
  ].join("\n");
32013
- const scriptDir = (0, import_node_fs31.mkdtempSync)((0, import_node_path28.join)((0, import_node_os16.tmpdir)(), "mmi-rails-decrypt-"));
32014
- const scriptPath = (0, import_node_path28.join)(scriptDir, "decrypt.rb");
32015
- (0, import_node_fs31.writeFileSync)(scriptPath, script, "utf8");
33327
+ const scriptDir = (0, import_node_fs32.mkdtempSync)((0, import_node_path29.join)((0, import_node_os16.tmpdir)(), "mmi-rails-decrypt-"));
33328
+ const scriptPath = (0, import_node_path29.join)(scriptDir, "decrypt.rb");
33329
+ (0, import_node_fs32.writeFileSync)(scriptPath, script, "utf8");
32016
33330
  try {
32017
33331
  const args = ["exec", "ruby", scriptPath];
32018
33332
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -32024,7 +33338,7 @@ async function decryptRailsCredentials(input) {
32024
33338
  });
32025
33339
  return JSON.parse(stdout);
32026
33340
  } finally {
32027
- (0, import_node_fs31.rmSync)(scriptDir, { recursive: true, force: true });
33341
+ (0, import_node_fs32.rmSync)(scriptDir, { recursive: true, force: true });
32028
33342
  }
32029
33343
  }
32030
33344
  async function readSecretStdin() {
@@ -32114,7 +33428,7 @@ function registerSecretsCommands(program3) {
32114
33428
  let body;
32115
33429
  if (o.file) {
32116
33430
  try {
32117
- body = (0, import_node_fs31.readFileSync)((0, import_node_path28.resolve)(o.file), "utf8");
33431
+ body = (0, import_node_fs32.readFileSync)((0, import_node_path29.resolve)(o.file), "utf8");
32118
33432
  } catch (e) {
32119
33433
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
32120
33434
  }
@@ -32219,7 +33533,7 @@ function registerSecretsCommands(program3) {
32219
33533
  {
32220
33534
  ...d,
32221
33535
  decryptRailsCredentials,
32222
- removeFile: (path2) => (0, import_node_fs31.unlinkSync)((0, import_node_path28.resolve)(o.appDir ?? process.cwd(), path2))
33536
+ removeFile: (path2) => (0, import_node_fs32.unlinkSync)((0, import_node_path29.resolve)(o.appDir ?? process.cwd(), path2))
32223
33537
  },
32224
33538
  {
32225
33539
  repo: o.repo,
@@ -32380,8 +33694,8 @@ function registerSessionReport(program3) {
32380
33694
  }
32381
33695
 
32382
33696
  // src/stage-commands.ts
32383
- var import_node_fs32 = require("node:fs");
32384
- var import_node_path29 = require("node:path");
33697
+ var import_node_fs33 = require("node:fs");
33698
+ var import_node_path30 = require("node:path");
32385
33699
 
32386
33700
  // src/stage-default.ts
32387
33701
  function shellFor(platform2 = process.platform) {
@@ -32665,8 +33979,8 @@ function registerStageCommands(program3) {
32665
33979
  return {
32666
33980
  resolution: decideStage({
32667
33981
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
32668
- hasCompose: (0, import_node_fs32.existsSync)((0, import_node_path29.join)(process.cwd(), "docker-compose.yml")),
32669
- hasEnvExample: (0, import_node_fs32.existsSync)((0, import_node_path29.join)(process.cwd(), ".env.example")),
33982
+ hasCompose: (0, import_node_fs33.existsSync)((0, import_node_path30.join)(process.cwd(), "docker-compose.yml")),
33983
+ hasEnvExample: (0, import_node_fs33.existsSync)((0, import_node_path30.join)(process.cwd(), ".env.example")),
32670
33984
  repo
32671
33985
  }),
32672
33986
  project: project2,
@@ -32916,20 +34230,20 @@ function registerStageCommands(program3) {
32916
34230
  }
32917
34231
 
32918
34232
  // src/tenant-artifact.ts
32919
- var import_node_crypto9 = require("node:crypto");
32920
- var import_node_fs33 = require("node:fs");
34233
+ var import_node_crypto10 = require("node:crypto");
34234
+ var import_node_fs34 = require("node:fs");
32921
34235
  var import_promises6 = require("node:fs/promises");
32922
- var import_node_path30 = require("node:path");
34236
+ var import_node_path31 = require("node:path");
32923
34237
  var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
32924
34238
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
32925
34239
  async function sha256File(path2) {
32926
- const hash = (0, import_node_crypto9.createHash)("sha256");
32927
- for await (const chunk of (0, import_node_fs33.createReadStream)(path2)) hash.update(chunk);
34240
+ const hash = (0, import_node_crypto10.createHash)("sha256");
34241
+ for await (const chunk of (0, import_node_fs34.createReadStream)(path2)) hash.update(chunk);
32928
34242
  return hash.digest("hex");
32929
34243
  }
32930
34244
  async function putTenantArtifact(repo, stage, inputPath, deps) {
32931
34245
  if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
32932
- const path2 = (0, import_node_path30.resolve)(inputPath);
34246
+ const path2 = (0, import_node_path31.resolve)(inputPath);
32933
34247
  const info = await (0, import_promises6.stat)(path2);
32934
34248
  if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
32935
34249
  if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES} bytes`);
@@ -32947,7 +34261,7 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
32947
34261
  return [key, value];
32948
34262
  }));
32949
34263
  headers["content-length"] = String(info.size);
32950
- const stream = (0, import_node_fs33.createReadStream)(path2);
34264
+ const stream = (0, import_node_fs34.createReadStream)(path2);
32951
34265
  let uploaded;
32952
34266
  try {
32953
34267
  uploaded = await fetch(body.uploadUrl, {
@@ -33087,7 +34401,7 @@ function renderVerifySecrets(body) {
33087
34401
 
33088
34402
  // src/command-register-collaboration.ts
33089
34403
  var import_node_child_process14 = require("node:child_process");
33090
- var import_node_fs39 = require("node:fs");
34404
+ var import_node_fs41 = require("node:fs");
33091
34405
  var import_promises7 = require("node:fs/promises");
33092
34406
 
33093
34407
  // src/session-runtime.ts
@@ -33107,7 +34421,7 @@ function spawnDetachedSelf(args, deps, opts = {}) {
33107
34421
  }
33108
34422
 
33109
34423
  // src/command-register-collaboration.ts
33110
- var import_node_path36 = require("node:path");
34424
+ var import_node_path38 = require("node:path");
33111
34425
 
33112
34426
  // src/attach-to-project.ts
33113
34427
  function boardAttachRateLimitedReceipt(resetEpochSeconds) {
@@ -33194,7 +34508,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
33194
34508
  if (first.mergeStatus !== "failed") return first;
33195
34509
  const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
33196
34510
  if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
33197
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
34511
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
33198
34512
  await sleep2(PR_LAND_MERGE_RETRY_DELAY_MS);
33199
34513
  const retried = await deps.mergeAuto(prNumber, repo);
33200
34514
  if (retried.mergeStatus !== "failed") return retried;
@@ -33205,7 +34519,7 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
33205
34519
  async function confirmAutoMergeEnqueued(deps, options) {
33206
34520
  const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
33207
34521
  const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
33208
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
34522
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
33209
34523
  for (let attempt = 0; attempt < retries; attempt++) {
33210
34524
  if (await deps.readMerged().catch(() => false)) return "merged";
33211
34525
  const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
@@ -33222,7 +34536,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
33222
34536
  async function readGhPrStateWithRetry(fetchState, options) {
33223
34537
  const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
33224
34538
  const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
33225
- const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
34539
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
33226
34540
  let lastError = "empty state";
33227
34541
  for (let attempt = 0; attempt < retries; attempt++) {
33228
34542
  try {
@@ -33295,8 +34609,8 @@ async function runPrLand(prNumber, options, deps) {
33295
34609
  }
33296
34610
 
33297
34611
  // src/merge-cleanup.ts
33298
- var import_node_fs35 = require("node:fs");
33299
- var import_node_path32 = require("node:path");
34612
+ var import_node_fs36 = require("node:fs");
34613
+ var import_node_path33 = require("node:path");
33300
34614
  var import_node_os17 = require("node:os");
33301
34615
 
33302
34616
  // src/board-advance.ts
@@ -33399,8 +34713,8 @@ function boardAdvanceFailureMessage(result) {
33399
34713
 
33400
34714
  // src/test-policy-core.ts
33401
34715
  var import_node_child_process12 = require("node:child_process");
33402
- var import_node_fs34 = require("node:fs");
33403
- var import_node_path31 = require("node:path");
34716
+ var import_node_fs35 = require("node:fs");
34717
+ var import_node_path32 = require("node:path");
33404
34718
 
33405
34719
  // src/test-command-policy-shared.mjs
33406
34720
  var import_node_child_process11 = require("node:child_process");
@@ -33855,7 +35169,7 @@ function isMeaningfulRow(file) {
33855
35169
  return file.meaningful !== false;
33856
35170
  }
33857
35171
  function loadPolicy(root, readFile8 = readFileOrNull2) {
33858
- const raw = readFile8((0, import_node_path31.join)(root, POLICY_FILE));
35172
+ const raw = readFile8((0, import_node_path32.join)(root, POLICY_FILE));
33859
35173
  if (raw == null) return { mandatory: [], declared: false };
33860
35174
  try {
33861
35175
  return { ...JSON.parse(raw), declared: true };
@@ -33865,7 +35179,7 @@ function loadPolicy(root, readFile8 = readFileOrNull2) {
33865
35179
  }
33866
35180
  function readFileOrNull2(path2) {
33867
35181
  try {
33868
- return (0, import_node_fs34.readFileSync)(path2, "utf8");
35182
+ return (0, import_node_fs35.readFileSync)(path2, "utf8");
33869
35183
  } catch {
33870
35184
  return null;
33871
35185
  }
@@ -33893,12 +35207,12 @@ function classify(changed, policy, present = () => false) {
33893
35207
  const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
33894
35208
  return { mandatoryHits, untestedHits, testChanges, meaningfulTestChanges, addedTests, removedProtected };
33895
35209
  }
33896
- function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs34.existsSync)(path2)) {
33897
- return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path31.join)(root, p)));
35210
+ function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs35.existsSync)(path2)) {
35211
+ return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path32.join)(root, p)));
33898
35212
  }
33899
- function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs34.existsSync)(path2)) {
35213
+ function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs35.existsSync)(path2)) {
33900
35214
  const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
33901
- return [...new Set(declared)].filter((p) => !exists((0, import_node_path31.join)(root, p)));
35215
+ return [...new Set(declared)].filter((p) => !exists((0, import_node_path32.join)(root, p)));
33902
35216
  }
33903
35217
  function evaluate(changed, policy, present = () => false) {
33904
35218
  const { mandatoryHits, untestedHits, meaningfulTestChanges, addedTests, removedProtected } = classify(changed, policy, present);
@@ -34026,17 +35340,17 @@ function blobAt(base, path2, cwd) {
34026
35340
  }
34027
35341
  function runTestPolicy(root, deps = {}) {
34028
35342
  const policy = deps.policy ?? loadPolicy(root);
34029
- const exists = deps.exists ?? ((path2) => (0, import_node_fs34.existsSync)(path2));
35343
+ const exists = deps.exists ?? ((path2) => (0, import_node_fs35.existsSync)(path2));
34030
35344
  const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
34031
35345
  const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
34032
35346
  const refusal = deps.changed ? null : untrustworthyRange(root, base);
34033
35347
  const raw = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
34034
35348
  const changed = deps.changed ? raw : annotateChangeMeaning(raw, policy, (path2) => ({
34035
35349
  before: blobAt(base, path2, root),
34036
- after: readFileOrNull2((0, import_node_path31.join)(root, path2))
35350
+ after: readFileOrNull2((0, import_node_path32.join)(root, path2))
34037
35351
  }));
34038
35352
  const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride2(base, root) : { override: null, refusals: [] };
34039
- const present = (path2) => exists((0, import_node_path31.join)(root, path2));
35353
+ const present = (path2) => exists((0, import_node_path32.join)(root, path2));
34040
35354
  const removedByThisDiff = removedPaths(changed);
34041
35355
  const staleFindings = [];
34042
35356
  const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
@@ -34236,13 +35550,13 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
34236
35550
  return rewritten === base ? null : rewritten;
34237
35551
  }
34238
35552
  function writeSquashBodyFile(body) {
34239
- const dir = (0, import_node_fs35.mkdtempSync)((0, import_node_path32.join)((0, import_node_os17.tmpdir)(), "mmi-squash-body-"));
34240
- const path2 = (0, import_node_path32.join)(dir, "body.txt");
34241
- (0, import_node_fs35.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
35553
+ const dir = (0, import_node_fs36.mkdtempSync)((0, import_node_path33.join)((0, import_node_os17.tmpdir)(), "mmi-squash-body-"));
35554
+ const path2 = (0, import_node_path33.join)(dir, "body.txt");
35555
+ (0, import_node_fs36.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
34242
35556
  `, "utf8");
34243
35557
  return { path: path2, cleanup: () => {
34244
35558
  try {
34245
- (0, import_node_fs35.rmSync)(dir, { recursive: true, force: true });
35559
+ (0, import_node_fs36.rmSync)(dir, { recursive: true, force: true });
34246
35560
  } catch {
34247
35561
  }
34248
35562
  } };
@@ -34359,7 +35673,8 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
34359
35673
  return confirmEnqueueOutcome();
34360
35674
  }
34361
35675
  function cleanupGitArgs(cwd, args) {
34362
- return cwd ? ["-C", cwd, ...args] : args;
35676
+ if (!cwd || args[0] === "-C") return args;
35677
+ return ["-C", cwd, ...args];
34363
35678
  }
34364
35679
  async function remoteBranchExists2(branch, options = {}) {
34365
35680
  if (!branch) return void 0;
@@ -34444,8 +35759,8 @@ async function deleteMergedRemoteBranch(options) {
34444
35759
  }
34445
35760
 
34446
35761
  // src/post-merge-recon.ts
34447
- var import_node_fs36 = require("node:fs");
34448
- var import_node_path33 = require("node:path");
35762
+ var import_node_fs37 = require("node:fs");
35763
+ var import_node_path34 = require("node:path");
34449
35764
 
34450
35765
  // src/cross-repo-filing-issue.ts
34451
35766
  function crossRepoFilingRetryCommand(prRepo, prNumber) {
@@ -34611,16 +35926,16 @@ function buildPostMergeReconRecovery(input) {
34611
35926
  }
34612
35927
  function writePostMergeReconRecovery(cwd, recovery) {
34613
35928
  const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
34614
- (0, import_node_fs36.mkdirSync)((0, import_node_path33.dirname)(path2), { recursive: true });
34615
- (0, import_node_fs36.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
35929
+ (0, import_node_fs37.mkdirSync)((0, import_node_path34.dirname)(path2), { recursive: true });
35930
+ (0, import_node_fs37.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
34616
35931
  `, "utf8");
34617
35932
  return path2;
34618
35933
  }
34619
35934
  function clearPostMergeReconRecovery(cwd, repo, pr) {
34620
35935
  const path2 = postMergeReconStatePath(cwd, repo, pr);
34621
- if (!(0, import_node_fs36.existsSync)(path2)) return;
35936
+ if (!(0, import_node_fs37.existsSync)(path2)) return;
34622
35937
  try {
34623
- (0, import_node_fs36.unlinkSync)(path2);
35938
+ (0, import_node_fs37.unlinkSync)(path2);
34624
35939
  } catch {
34625
35940
  }
34626
35941
  }
@@ -34655,7 +35970,7 @@ var import_node_child_process13 = require("node:child_process");
34655
35970
  var GIT_TIMEOUT_MS2 = 15e3;
34656
35971
  function catFileBatch(root, ref, paths) {
34657
35972
  if (paths.length === 0) return Promise.resolve([]);
34658
- return new Promise((resolve6) => {
35973
+ return new Promise((resolve7) => {
34659
35974
  const chunks = [];
34660
35975
  let settled = false;
34661
35976
  const child2 = (0, import_node_child_process13.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
@@ -34663,7 +35978,7 @@ function catFileBatch(root, ref, paths) {
34663
35978
  if (settled) return;
34664
35979
  settled = true;
34665
35980
  clearTimeout(timer);
34666
- resolve6(parseCatFileBatch(Buffer.concat(chunks), paths.length));
35981
+ resolve7(parseCatFileBatch(Buffer.concat(chunks), paths.length));
34667
35982
  };
34668
35983
  const timer = setTimeout(() => {
34669
35984
  child2.kill("SIGKILL");
@@ -34771,13 +36086,88 @@ async function checkDocsIndexAtHead(opts, deps) {
34771
36086
  };
34772
36087
  }
34773
36088
 
36089
+ // src/pr-create-claim-guard.ts
36090
+ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
36091
+ const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
36092
+ if (!issues.length) return void 0;
36093
+ const repo = await requireRepo(repoOption);
36094
+ const client = deps.client ?? defaultGitHubClient();
36095
+ const actor = deps.actor ?? describeSessionIdentity();
36096
+ const checkContest = deps.checkContest ?? checkLaneContest;
36097
+ for (const number of issues) {
36098
+ const contest = await checkContest(client, { repository: repo, number }, actor);
36099
+ if (!contest.contested) continue;
36100
+ const ref = `${repo}#${number}`;
36101
+ const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
36102
+ 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`;
36103
+ }
36104
+ return void 0;
36105
+ }
36106
+
34774
36107
  // src/worktree-merge-cleanup.ts
36108
+ var import_node_fs40 = require("node:fs");
36109
+ var import_node_path37 = require("node:path");
36110
+
36111
+ // src/jervcode-node-modules-cleanup.ts
34775
36112
  var import_node_fs38 = require("node:fs");
36113
+ var import_node_os18 = require("node:os");
34776
36114
  var import_node_path35 = require("node:path");
36115
+ var JERVCODE_PACKAGE_ENTRY = (0, import_node_path35.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
36116
+ var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
36117
+ var POSIX_NAMES2 = ["jervcode"];
36118
+ var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
36119
+ function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform) {
36120
+ const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
36121
+ const out = [];
36122
+ for (const dir of jervCliCandidateDirs(env, home, platform2)) {
36123
+ for (const name of names) out.push((0, import_node_path35.join)(dir, name));
36124
+ }
36125
+ return out;
36126
+ }
36127
+ function resolveJervcodePath(env = process.env, home = (0, import_node_os18.homedir)(), platform2 = process.platform, exists = import_node_fs38.existsSync) {
36128
+ for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
36129
+ if (exists(candidate)) return candidate;
36130
+ }
36131
+ return void 0;
36132
+ }
36133
+ function jervcodeExecFileArgs(args, opts = {}) {
36134
+ const platform2 = opts.platform ?? process.platform;
36135
+ const exists = opts.exists ?? import_node_fs38.existsSync;
36136
+ const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os18.homedir)(), platform2, exists);
36137
+ if (resolved) {
36138
+ const entry = (0, import_node_path35.join)((0, import_node_path35.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
36139
+ if (exists(entry)) {
36140
+ return { file: opts.execPath ?? process.execPath, args: [entry, ...args], via: "node-entry" };
36141
+ }
36142
+ }
36143
+ const bin = resolved ?? "jervcode";
36144
+ if (platform2 === "win32") {
36145
+ return { file: "cmd.exe", args: ["/c", bin, ...args], via: resolved ? "cmd-shim" : "bare" };
36146
+ }
36147
+ return { file: bin, args: [...args], via: resolved ? "posix" : "bare" };
36148
+ }
36149
+ async function removeWorktreeNodeModulesViaHelper(wtPath, opts = {}) {
36150
+ const { cwd, timeoutMs = NODE_MODULES_CLEANUP_TIMEOUT_MS } = opts;
36151
+ const env = opts.env ?? process.env;
36152
+ const exec = opts.exec ?? execFileP;
36153
+ const candidates = jervcodeCandidatePaths(env, opts.home, opts.platform ?? process.platform);
36154
+ const plan = jervcodeExecFileArgs(["worktree-node-modules-cleanup", "--worktree", wtPath], opts);
36155
+ const execOptions = { timeout: timeoutMs, ...cwd ? { cwd } : {} };
36156
+ try {
36157
+ await exec(plan.file, plan.args, execOptions);
36158
+ return { ok: true };
36159
+ } catch (e) {
36160
+ const err = e;
36161
+ const base = formatJervCliSpawnFailure(err, plan, candidates);
36162
+ const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
36163
+ const detail = stderr.split("\n")[0] || base;
36164
+ return { ok: false, error: `jervcode worktree-node-modules-cleanup failed: ${detail}` };
36165
+ }
36166
+ }
34777
36167
 
34778
36168
  // src/worktree-evidence-archive.ts
34779
- var import_node_fs37 = require("node:fs");
34780
- var import_node_path34 = require("node:path");
36169
+ var import_node_fs39 = require("node:fs");
36170
+ var import_node_path36 = require("node:path");
34781
36171
  var JERV_ARTIFACT_RUN_SCOPE_ENV_VARS = ["JERV_RUN_ID", ...SESSION_ID_ENV_VARS];
34782
36172
  function sanitizeArchiveSegment(value, max = 80) {
34783
36173
  const scrubbed = value.replace(/[^A-Za-z0-9._@+-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -34792,24 +36182,24 @@ function resolveArtifactRunScope(env = process.env) {
34792
36182
  }
34793
36183
  function defaultIsDirectory(path2) {
34794
36184
  try {
34795
- return (0, import_node_fs37.lstatSync)(path2).isDirectory();
36185
+ return (0, import_node_fs39.lstatSync)(path2).isDirectory();
34796
36186
  } catch {
34797
36187
  return false;
34798
36188
  }
34799
36189
  }
34800
36190
  function archiveWorktreeJervArtifacts(args, deps = {}) {
34801
- const exists = deps.exists ?? import_node_fs37.existsSync;
36191
+ const exists = deps.exists ?? import_node_fs39.existsSync;
34802
36192
  const isDirectory = deps.isDirectory ?? defaultIsDirectory;
34803
- const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs37.cpSync)(from, to, { recursive: true, force: true }));
36193
+ const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
34804
36194
  const mkdirp = deps.mkdirp ?? ((path2) => {
34805
- (0, import_node_fs37.mkdirSync)(path2, { recursive: true });
36195
+ (0, import_node_fs39.mkdirSync)(path2, { recursive: true });
34806
36196
  });
34807
36197
  const env = deps.env ?? process.env;
34808
36198
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
34809
36199
  const resolveRoot = deps.resolveArchiveRoot ?? repoRuntimeStatePath;
34810
36200
  if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
34811
36201
  if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
34812
- const source = (0, import_node_path34.join)(args.worktreePath, ".jerv");
36202
+ const source = (0, import_node_path36.join)(args.worktreePath, ".jerv");
34813
36203
  if (!exists(source)) return { status: "absent" };
34814
36204
  if (!isDirectory(source)) return { status: "skipped", reason: "jerv-not-a-directory" };
34815
36205
  const runScope = resolveArtifactRunScope(env);
@@ -34817,7 +36207,7 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
34817
36207
  const stamp = now().toISOString().replace(/[:.]/g, "-");
34818
36208
  const dest = resolveRoot(args.primaryRoot, "jerv-artifacts", runScope, branchSlug, stamp, ".jerv");
34819
36209
  try {
34820
- mkdirp((0, import_node_path34.dirname)(dest));
36210
+ mkdirp((0, import_node_path36.dirname)(dest));
34821
36211
  copyDir(source, dest);
34822
36212
  if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
34823
36213
  return { status: "archived", path: dest, runScope };
@@ -34826,14 +36216,14 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
34826
36216
  }
34827
36217
  }
34828
36218
  function defaultStat(path2) {
34829
- const st = (0, import_node_fs37.statSync)(path2);
36219
+ const st = (0, import_node_fs39.statSync)(path2);
34830
36220
  return { mtimeMs: st.mtimeMs, size: st.size, isDirectory: () => st.isDirectory() };
34831
36221
  }
34832
36222
  function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
34833
- const exists = deps.exists ?? import_node_fs37.existsSync;
36223
+ const exists = deps.exists ?? import_node_fs39.existsSync;
34834
36224
  const stat4 = deps.stat ?? defaultStat;
34835
- const readdir2 = deps.readdir ?? import_node_fs37.readdirSync;
34836
- const tmpRoot = (0, import_node_path34.join)(worktreePath, "tmp");
36225
+ const readdir2 = deps.readdir ?? import_node_fs39.readdirSync;
36226
+ const tmpRoot = (0, import_node_path36.join)(worktreePath, "tmp");
34837
36227
  if (!exists(tmpRoot)) return [];
34838
36228
  const entries = [];
34839
36229
  const walk2 = (dir) => {
@@ -34844,14 +36234,14 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
34844
36234
  return;
34845
36235
  }
34846
36236
  for (const name of names) {
34847
- const full = (0, import_node_path34.join)(dir, name);
36237
+ const full = (0, import_node_path36.join)(dir, name);
34848
36238
  let st;
34849
36239
  try {
34850
36240
  st = stat4(full);
34851
36241
  } catch {
34852
36242
  continue;
34853
36243
  }
34854
- const relPath = (0, import_node_path34.relative)(worktreePath, full).replace(/\\/g, "/");
36244
+ const relPath = (0, import_node_path36.relative)(worktreePath, full).replace(/\\/g, "/");
34855
36245
  if (st.isDirectory()) {
34856
36246
  if (st.mtimeMs > newerThanMs) entries.push({ relPath, bytes: 0, mtimeMs: st.mtimeMs });
34857
36247
  walk2(full);
@@ -34865,10 +36255,10 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
34865
36255
  return entries;
34866
36256
  }
34867
36257
  function archiveWorktreeTmpArtifacts(args, deps = {}) {
34868
- const exists = deps.exists ?? import_node_fs37.existsSync;
34869
- const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs37.cpSync)(from, to, { recursive: true, force: true }));
36258
+ const exists = deps.exists ?? import_node_fs39.existsSync;
36259
+ const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
34870
36260
  const mkdirp = deps.mkdirp ?? ((path2) => {
34871
- (0, import_node_fs37.mkdirSync)(path2, { recursive: true });
36261
+ (0, import_node_fs39.mkdirSync)(path2, { recursive: true });
34872
36262
  });
34873
36263
  const env = deps.env ?? process.env;
34874
36264
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -34876,7 +36266,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
34876
36266
  if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
34877
36267
  if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
34878
36268
  const scanned = scanWorktreeTmpEvidence(args.worktreePath, args.newerThanMs, deps);
34879
- const source = (0, import_node_path34.join)(args.worktreePath, "tmp");
36269
+ const source = (0, import_node_path36.join)(args.worktreePath, "tmp");
34880
36270
  if (!scanned.length) return { status: "absent" };
34881
36271
  if (!exists(source)) return { status: "absent" };
34882
36272
  const runScope = resolveArtifactRunScope(env);
@@ -34884,7 +36274,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
34884
36274
  const stamp = now().toISOString().replace(/[:.]/g, "-");
34885
36275
  const dest = resolveRoot(args.primaryRoot, "worktree-artifacts", runScope, branchSlug, stamp, "tmp");
34886
36276
  try {
34887
- mkdirp((0, import_node_path34.dirname)(dest));
36277
+ mkdirp((0, import_node_path36.dirname)(dest));
34888
36278
  copyDir(source, dest);
34889
36279
  if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
34890
36280
  const bytes = scanned.reduce((sum, e) => sum + e.bytes, 0);
@@ -34953,9 +36343,9 @@ function normPath2(p) {
34953
36343
  return p.replace(/\\/g, "/").replace(/\/+$/, "");
34954
36344
  }
34955
36345
  function unlinkNodeModulesJunction(wtPath) {
34956
- const nm = (0, import_node_path35.join)(wtPath, "node_modules");
36346
+ const nm = (0, import_node_path37.join)(wtPath, "node_modules");
34957
36347
  try {
34958
- if ((0, import_node_fs38.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs38.rmdirSync)(nm);
36348
+ if ((0, import_node_fs40.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs40.rmdirSync)(nm);
34959
36349
  return { ok: true };
34960
36350
  } catch (e) {
34961
36351
  if (e.code === "ENOENT") return { ok: true };
@@ -34963,11 +36353,11 @@ function unlinkNodeModulesJunction(wtPath) {
34963
36353
  }
34964
36354
  }
34965
36355
  function defaultSleep2(ms) {
34966
- return new Promise((resolve6) => setTimeout(resolve6, ms));
36356
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
34967
36357
  }
34968
36358
  async function removeResidueDirectory(wtPath, options = {}) {
34969
- const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs38.rmdirSync, exists = import_node_fs38.existsSync } = options;
34970
- const removeRecursive = options.removeRecursive ?? ((path2) => (0, import_node_fs38.rmSync)(path2, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }));
36359
+ const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs40.rmdirSync, exists = import_node_fs40.existsSync } = options;
36360
+ const removeRecursive = options.removeRecursive ?? ((path2) => (0, import_node_fs40.rmSync)(path2, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }));
34971
36361
  const junction = unlinkNodeModulesJunction(wtPath);
34972
36362
  if (!junction.ok) return junction;
34973
36363
  try {
@@ -35167,6 +36557,10 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
35167
36557
  })
35168
36558
  };
35169
36559
  }
36560
+ function isPrMergeWorktreePartial(cleanup) {
36561
+ const worktree = cleanup?.worktree;
36562
+ return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved");
36563
+ }
35170
36564
  function prMergeLocalCleanupExitCode(cleanup) {
35171
36565
  return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
35172
36566
  }
@@ -35181,7 +36575,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
35181
36575
  return report;
35182
36576
  }
35183
36577
  const execGit = options.execGit ?? (async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
35184
- const pathExists = options.pathExists ?? import_node_fs38.existsSync;
36578
+ const pathExists = options.pathExists ?? import_node_fs40.existsSync;
35185
36579
  let afterWorktrees = [];
35186
36580
  try {
35187
36581
  afterWorktrees = parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"]));
@@ -35273,6 +36667,23 @@ async function cleanupPrMergeLocalBranch(branch, options) {
35273
36667
  report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
35274
36668
  return report;
35275
36669
  }
36670
+ if (pathExists((0, import_node_path37.join)(wtPath, "node_modules"))) {
36671
+ const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
36672
+ if (!nmRemoved.ok) {
36673
+ report.worktree = {
36674
+ path: wtPath,
36675
+ status: "failed",
36676
+ reason: "pre-clean-failed",
36677
+ error: `real-node_modules cleanup failed before worktree removal: ${nmRemoved.error}`,
36678
+ artifactsArchive,
36679
+ stageTeardown,
36680
+ tmpEvidenceCount: tmpEvidence.length,
36681
+ remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
36682
+ };
36683
+ report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
36684
+ return report;
36685
+ }
36686
+ }
35276
36687
  const preClean = await preCleanWorktreeForRemoval(wtPath, execGit);
35277
36688
  if (!preClean.ok) {
35278
36689
  report.worktree = {
@@ -35399,10 +36810,10 @@ function argvWantsJson2() {
35399
36810
  return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
35400
36811
  }
35401
36812
  function hubRoot() {
35402
- const fromPkg = (0, import_node_path36.join)(__dirname, "..", "..");
36813
+ const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
35403
36814
  const marker = "skills/bootstrap/seeds/manifest.json";
35404
- if ((0, import_node_fs39.existsSync)((0, import_node_path36.join)(fromPkg, marker))) return fromPkg;
35405
- if ((0, import_node_fs39.existsSync)((0, import_node_path36.join)(process.cwd(), marker))) return process.cwd();
36815
+ if ((0, import_node_fs41.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
36816
+ if ((0, import_node_fs41.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
35406
36817
  return null;
35407
36818
  }
35408
36819
  function ciAuditDeps() {
@@ -35414,8 +36825,8 @@ function ciAuditDeps() {
35414
36825
  getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
35415
36826
  readSeedFile: (path2) => {
35416
36827
  if (!root) return null;
35417
- const fullPath = (0, import_node_path36.join)(root, path2);
35418
- return (0, import_node_fs39.existsSync)(fullPath) ? (0, import_node_fs39.readFileSync)(fullPath, "utf8") : null;
36828
+ const fullPath = (0, import_node_path38.join)(root, path2);
36829
+ return (0, import_node_fs41.existsSync)(fullPath) ? (0, import_node_fs41.readFileSync)(fullPath, "utf8") : null;
35419
36830
  }
35420
36831
  };
35421
36832
  }
@@ -35631,7 +37042,7 @@ function registerCollaborationCommands(program3) {
35631
37042
  repo: targetRepo2,
35632
37043
  labels: extraLabels.length ? extraLabels : void 0
35633
37044
  });
35634
- if (o.parent !== void 0) parseIssueRef(o.parent, o.repo);
37045
+ if (o.parent !== void 0) parseIssueRef(o.parent);
35635
37046
  } catch (e) {
35636
37047
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
35637
37048
  }
@@ -35788,14 +37199,14 @@ function registerCollaborationCommands(program3) {
35788
37199
  jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
35789
37200
  const defaultRepo = await resolveRepo(o.repo);
35790
37201
  try {
35791
- parseIssueRef(parentRef, o.repo);
35792
- parseIssueRef(childRef, o.repo);
37202
+ parseIssueRef(parentRef);
37203
+ parseIssueRef(childRef);
35793
37204
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
35794
37205
  console.log(JSON.stringify(result));
35795
37206
  } catch (e) {
35796
37207
  let conflict;
35797
37208
  try {
35798
- const child2 = parseIssueRef(childRef, o.repo);
37209
+ const child2 = parseIssueRef(childRef);
35799
37210
  const childRepo = child2.repo ?? defaultRepo;
35800
37211
  if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
35801
37212
  } catch {
@@ -36000,10 +37411,20 @@ ${list}`);
36000
37411
  }
36001
37412
  body = normalizeClosingDirectives(body);
36002
37413
  body = rewriteNegatedClosingPhrases(body);
37414
+ const prose = rewriteCloseKeywordProse(body);
37415
+ if (prose.rewritten.length) {
37416
+ body = prose.text;
37417
+ process.stderr.write(
37418
+ `pr create: WARNING \u2014 rewrote close-keyword prose to "part of #N" for ${prose.rewritten.map((n) => `#${n}`).join(", ")}: GitHub parses close/fix/resolve + #N even mid-sentence (#6039). Keep closing keywords on a dedicated "Closes #N" line.
37419
+ `
37420
+ );
37421
+ }
36003
37422
  const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
36004
37423
  if (docsCheck && !docsCheck.ok) {
36005
37424
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
36006
37425
  }
37426
+ const claimRefusal = await prCreateClaimRefusal(body, o.repo);
37427
+ if (claimRefusal) return fail(claimRefusal);
36007
37428
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
36008
37429
  if (isGhCreateRateLimited(created)) {
36009
37430
  console.log(JSON.stringify(created));
@@ -36018,7 +37439,8 @@ ${list}`);
36018
37439
  ], [
36019
37440
  "--head and --base default to the current branch and the repo default; only pass them to override.",
36020
37441
  "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
36021
- "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
37442
+ "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files.",
37443
+ 'Keep closing keywords on a dedicated line ("Closes #N") \u2014 GitHub parses close/fix/resolve + #N even mid-sentence, so pr create rewrites such prose to "part of #N" (#6039).'
36022
37444
  ]);
36023
37445
  pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (ref, o) => {
36024
37446
  let parsed;
@@ -36046,11 +37468,11 @@ ${list}`);
36046
37468
  }
36047
37469
  });
36048
37470
  async function listCiWorkflowPaths(cwd = process.cwd()) {
36049
- const wfDir = (0, import_node_path36.join)(cwd, ".github", "workflows");
36050
- if (!(0, import_node_fs39.existsSync)(wfDir)) return [];
36051
- return (0, import_node_fs39.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
37471
+ const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
37472
+ if (!(0, import_node_fs41.existsSync)(wfDir)) return [];
37473
+ return (0, import_node_fs41.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
36052
37474
  try {
36053
- return workflowReportsPrChecks((0, import_node_fs39.readFileSync)((0, import_node_path36.join)(wfDir, name), "utf8"));
37475
+ return workflowReportsPrChecks((0, import_node_fs41.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
36054
37476
  } catch {
36055
37477
  return true;
36056
37478
  }
@@ -36208,7 +37630,7 @@ ${list}`);
36208
37630
  // #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
36209
37631
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
36210
37632
  baseBranch,
36211
- sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
37633
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
36212
37634
  log: (message) => console.warn(message),
36213
37635
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
36214
37636
  // Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
@@ -36312,7 +37734,7 @@ ${list}`);
36312
37734
  // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
36313
37735
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
36314
37736
  baseBranch: "development",
36315
- sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
37737
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
36316
37738
  log: (message) => console.warn(message),
36317
37739
  // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
36318
37740
  // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
@@ -36358,7 +37780,7 @@ ${list}`);
36358
37780
  } else {
36359
37781
  lastFailure = void 0;
36360
37782
  }
36361
- await new Promise((resolve6) => setTimeout(resolve6, PR_LAND_POLL_MS));
37783
+ await new Promise((resolve7) => setTimeout(resolve7, PR_LAND_POLL_MS));
36362
37784
  }
36363
37785
  if (lastFailure) {
36364
37786
  throw new Error(
@@ -36407,7 +37829,7 @@ ${list}`);
36407
37829
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
36408
37830
  if (result.status === "failed") process.exitCode = 1;
36409
37831
  });
36410
- 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)`).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) => {
37832
+ 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) => {
36411
37833
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
36412
37834
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
36413
37835
  if (o.disableAuto) {
@@ -36445,7 +37867,7 @@ ${list}`);
36445
37867
  const mergeSquashBody = squashBodyTextForMerge(
36446
37868
  closingGuardInput,
36447
37869
  method === "--squash",
36448
- o.squashBodyFile ? (0, import_node_fs39.readFileSync)(o.squashBodyFile, "utf8") : void 0
37870
+ o.squashBodyFile ? (0, import_node_fs41.readFileSync)(o.squashBodyFile, "utf8") : void 0
36449
37871
  );
36450
37872
  if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
36451
37873
  const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
@@ -36471,6 +37893,7 @@ ${list}`);
36471
37893
  const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
36472
37894
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
36473
37895
  if (o.wait) {
37896
+ console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
36474
37897
  const repo = await requireRepo(o.repo);
36475
37898
  const budgetMs = PR_CHECKS_TIMEOUT_MS;
36476
37899
  const waitStarted = Date.now();
@@ -36495,7 +37918,7 @@ ${list}`);
36495
37918
  checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
36496
37919
  pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
36497
37920
  baseBranch,
36498
- sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
37921
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
36499
37922
  log: (message) => console.warn(message),
36500
37923
  timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
36501
37924
  progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
@@ -36549,7 +37972,7 @@ ${list}`);
36549
37972
  }
36550
37973
  if (!repoForPostCleanup) throw e;
36551
37974
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
36552
- const commitMessage = bodyFile ? (0, import_node_fs39.readFileSync)(bodyFile, "utf8") : void 0;
37975
+ const commitMessage = bodyFile ? (0, import_node_fs41.readFileSync)(bodyFile, "utf8") : void 0;
36553
37976
  await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
36554
37977
  body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
36555
37978
  timeoutMs: GH_MUTATION_TIMEOUT_MS
@@ -36637,7 +38060,7 @@ ${list}`);
36637
38060
  preserveWorktree: o.preserveWorktree,
36638
38061
  gcAcknowledged: o.gc,
36639
38062
  expectedHeadOid: headRefOid,
36640
- pathExists: (p) => (0, import_node_fs39.existsSync)(p),
38063
+ pathExists: (p) => (0, import_node_fs41.existsSync)(p),
36641
38064
  // #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
36642
38065
  // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
36643
38066
  // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
@@ -36664,12 +38087,13 @@ ${list}`);
36664
38087
  execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
36665
38088
  branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
36666
38089
  });
36667
- const worktreePartial = localCleanup?.worktree?.path && localCleanup.worktree.status !== "removed" ? {
38090
+ const worktree = localCleanup?.worktree;
38091
+ const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
36668
38092
  kind: "worktree-directory",
36669
- path: localCleanup.worktree.path,
36670
- reason: localCleanup.worktree.reason ?? localCleanup.worktree.status,
36671
- error: localCleanup.worktree.error ?? localCleanup.worktree.residueError,
36672
- remediation: localCleanup.worktree.remediation ?? `Remove-Item -LiteralPath '${localCleanup.worktree.path?.replace(/'/g, "''")}' -Recurse -Force`
38093
+ path: worktree.path,
38094
+ reason: worktree.reason ?? worktree.status,
38095
+ error: worktree.error ?? worktree.residueError,
38096
+ remediation: worktree.remediation ?? `Remove-Item -LiteralPath '${worktree.path.replace(/'/g, "''")}' -Recurse -Force`
36673
38097
  } : void 0;
36674
38098
  const partialCleanup = [
36675
38099
  ...worktreePartial ? [worktreePartial] : [],
@@ -36752,7 +38176,7 @@ ${list}`);
36752
38176
  }
36753
38177
 
36754
38178
  // src/command-register-developer.ts
36755
- var import_node_fs44 = require("node:fs");
38179
+ var import_node_fs46 = require("node:fs");
36756
38180
 
36757
38181
  // src/whoami.ts
36758
38182
  async function resolveWhoami(deps) {
@@ -36782,7 +38206,7 @@ async function resolveWhoami(deps) {
36782
38206
  }
36783
38207
 
36784
38208
  // src/command-register-developer.ts
36785
- var import_node_path41 = require("node:path");
38209
+ var import_node_path43 = require("node:path");
36786
38210
 
36787
38211
  // src/wave-land.ts
36788
38212
  function planWaveLand(prs) {
@@ -36813,7 +38237,7 @@ async function executeWaveLand(plan, deps) {
36813
38237
  }
36814
38238
 
36815
38239
  // src/box-commands.ts
36816
- var import_node_fs40 = require("node:fs");
38240
+ var import_node_fs42 = require("node:fs");
36817
38241
 
36818
38242
  // src/box.ts
36819
38243
  var BOX_KEYS = {
@@ -37014,11 +38438,16 @@ function registerBoxCommands(program3) {
37014
38438
  await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
37015
38439
  return;
37016
38440
  }
37017
- if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
37018
- else if (o.ssh && o.script) {
37019
- (0, import_node_fs40.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
37020
- console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
37021
- } else if (o.ssh) console.log(`${formatSshRecipe(found)}
38441
+ const wroteScript = o.ssh && o.script ? o.script : null;
38442
+ if (wroteScript) (0, import_node_fs42.writeFileSync)(wroteScript, sshRecipeScript(found), "utf8");
38443
+ if (o.json) {
38444
+ console.log(JSON.stringify({
38445
+ box: found,
38446
+ incomplete,
38447
+ ...wroteScript ? { script: { path: wroteScript, run: `bash "${wroteScript}"` } } : {}
38448
+ }, null, 2));
38449
+ } else if (wroteScript) console.log(`wrote ${wroteScript} \u2014 run: bash "${wroteScript}"`);
38450
+ else if (o.ssh) console.log(`${formatSshRecipe(found)}
37022
38451
  ${SSH_RECIPE_AGENT_NOTE}`);
37023
38452
  else console.log(formatBoxTable([found]));
37024
38453
  if (!o.json) warnIncomplete2(incomplete);
@@ -37031,28 +38460,28 @@ ${SSH_RECIPE_AGENT_NOTE}`);
37031
38460
 
37032
38461
  // src/dist-drift.ts
37033
38462
  var import_node_child_process15 = require("node:child_process");
37034
- var import_node_crypto11 = require("node:crypto");
37035
- var import_node_fs42 = require("node:fs");
37036
- var import_node_os18 = require("node:os");
37037
- var import_node_path38 = require("node:path");
38463
+ var import_node_crypto12 = require("node:crypto");
38464
+ var import_node_fs44 = require("node:fs");
38465
+ var import_node_os19 = require("node:os");
38466
+ var import_node_path40 = require("node:path");
37038
38467
 
37039
38468
  // ../scripts/distribution-digest.mjs
37040
- var import_node_crypto10 = require("node:crypto");
37041
- var import_node_fs41 = require("node:fs");
37042
- var import_node_path37 = require("node:path");
38469
+ var import_node_crypto11 = require("node:crypto");
38470
+ var import_node_fs43 = require("node:fs");
38471
+ var import_node_path39 = require("node:path");
37043
38472
  var slash = (value) => value.replaceAll("\\", "/");
37044
38473
  function repoPath(root, declaredPath, label) {
37045
- const absoluteRoot = (0, import_node_path37.resolve)(root);
37046
- const target = (0, import_node_path37.resolve)(root, declaredPath);
37047
- if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path37.sep}`)) {
38474
+ const absoluteRoot = (0, import_node_path39.resolve)(root);
38475
+ const target = (0, import_node_path39.resolve)(root, declaredPath);
38476
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path39.sep}`)) {
37048
38477
  throw new Error(`${label} ${declaredPath} escapes the repository root`);
37049
38478
  }
37050
38479
  return target;
37051
38480
  }
37052
38481
  function digestFiles(files) {
37053
- const hash = (0, import_node_crypto10.createHash)("sha256");
38482
+ const hash = (0, import_node_crypto11.createHash)("sha256");
37054
38483
  for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
37055
- const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs41.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs41.readFileSync)(file.absolute);
38484
+ const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs43.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs43.readFileSync)(file.absolute);
37056
38485
  hash.update(file.relative, "utf8");
37057
38486
  hash.update("\0");
37058
38487
  hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
@@ -37067,8 +38496,8 @@ function digestFiles(files) {
37067
38496
  function digestPackedFiles(packageRoot, packedFiles) {
37068
38497
  return digestFiles(packedFiles.map((path2) => {
37069
38498
  const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
37070
- if (!(0, import_node_fs41.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
37071
- return { absolute, relative: slash(path2), stat: (0, import_node_fs41.lstatSync)(absolute) };
38499
+ if (!(0, import_node_fs43.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
38500
+ return { absolute, relative: slash(path2), stat: (0, import_node_fs43.lstatSync)(absolute) };
37072
38501
  }));
37073
38502
  }
37074
38503
 
@@ -37080,7 +38509,7 @@ var DIST_ARTIFACTS = [
37080
38509
  ];
37081
38510
  var BOM_DIST_TREE_ID = "mmi-cli-dist";
37082
38511
  var ABSENT = "absent";
37083
- var sha256 = (bytes) => `sha256:${(0, import_node_crypto11.createHash)("sha256").update(bytes).digest("hex")}`;
38512
+ var sha256 = (bytes) => `sha256:${(0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex")}`;
37084
38513
  function artifactDrift(path2, committedBytes, rebuiltBytes) {
37085
38514
  const committed = committedBytes ? sha256(committedBytes) : ABSENT;
37086
38515
  const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
@@ -37152,13 +38581,13 @@ function renderDistDriftReceipt(receipt) {
37152
38581
  return lines;
37153
38582
  }
37154
38583
  function readOrNull(path2) {
37155
- return (0, import_node_fs42.existsSync)(path2) ? (0, import_node_fs42.readFileSync)(path2) : null;
38584
+ return (0, import_node_fs44.existsSync)(path2) ? (0, import_node_fs44.readFileSync)(path2) : null;
37156
38585
  }
37157
38586
  function walkFiles(root) {
37158
38587
  const files = [];
37159
38588
  const walk2 = (directory) => {
37160
- for (const entry of (0, import_node_fs42.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
37161
- const child2 = (0, import_node_path38.join)(directory, entry.name);
38589
+ for (const entry of (0, import_node_fs44.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
38590
+ const child2 = (0, import_node_path40.join)(directory, entry.name);
37162
38591
  if (entry.isDirectory()) walk2(child2);
37163
38592
  else files.push(child2);
37164
38593
  }
@@ -37168,10 +38597,10 @@ function walkFiles(root) {
37168
38597
  }
37169
38598
  function bomPathFor(root) {
37170
38599
  try {
37171
- const registry2 = JSON.parse((0, import_node_fs42.readFileSync)((0, import_node_path38.join)(root, "surfaces.json"), "utf8"));
37172
- return (0, import_node_path38.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
38600
+ const registry2 = JSON.parse((0, import_node_fs44.readFileSync)((0, import_node_path40.join)(root, "surfaces.json"), "utf8"));
38601
+ return (0, import_node_path40.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
37173
38602
  } catch {
37174
- return (0, import_node_path38.join)(root, "distribution-bom.json");
38603
+ return (0, import_node_path40.join)(root, "distribution-bom.json");
37175
38604
  }
37176
38605
  }
37177
38606
  function rebuildTo(packageRoot, outDir) {
@@ -37184,35 +38613,35 @@ function rebuildTo(packageRoot, outDir) {
37184
38613
  });
37185
38614
  }
37186
38615
  function runDistStatus(root) {
37187
- const stage = (0, import_node_fs42.mkdtempSync)((0, import_node_path38.join)((0, import_node_os18.tmpdir)(), "mmi-dist-drift-"));
38616
+ const stage = (0, import_node_fs44.mkdtempSync)((0, import_node_path40.join)((0, import_node_os19.tmpdir)(), "mmi-dist-drift-"));
37188
38617
  let overlayCount = 0;
37189
38618
  try {
37190
- const cliOut = (0, import_node_path38.join)(stage, "cli-dist");
37191
- const hubOut = (0, import_node_path38.join)(stage, "hub-dist");
37192
- rebuildTo((0, import_node_path38.join)(root, "cli"), cliOut);
37193
- rebuildTo((0, import_node_path38.join)(root, "updater"), hubOut);
38619
+ const cliOut = (0, import_node_path40.join)(stage, "cli-dist");
38620
+ const hubOut = (0, import_node_path40.join)(stage, "hub-dist");
38621
+ rebuildTo((0, import_node_path40.join)(root, "cli"), cliOut);
38622
+ rebuildTo((0, import_node_path40.join)(root, "updater"), hubOut);
37194
38623
  const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
37195
38624
  const rebuilt = (path2) => {
37196
38625
  const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
37197
- return spec ? readOrNull((0, import_node_path38.join)(outDirFor(spec.packageDir), spec.output)) : null;
38626
+ return spec ? readOrNull((0, import_node_path40.join)(outDirFor(spec.packageDir), spec.output)) : null;
37198
38627
  };
37199
- const committed = (path2) => readOrNull((0, import_node_path38.join)(root, path2));
37200
- const tree = (path2) => readOrNull((0, import_node_path38.join)(root, path2));
37201
- const distRoot = (0, import_node_path38.join)(root, "cli", "dist");
37202
- const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path38.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
37203
- const bom = JSON.parse((0, import_node_fs42.readFileSync)(bomPathFor(root), "utf8"));
38628
+ const committed = (path2) => readOrNull((0, import_node_path40.join)(root, path2));
38629
+ const tree = (path2) => readOrNull((0, import_node_path40.join)(root, path2));
38630
+ const distRoot = (0, import_node_path40.join)(root, "cli", "dist");
38631
+ const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path40.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
38632
+ const bom = JSON.parse((0, import_node_fs44.readFileSync)(bomPathFor(root), "utf8"));
37204
38633
  const digest = (entries) => {
37205
- const overlay = (0, import_node_path38.join)(stage, `overlay-${overlayCount++}`);
38634
+ const overlay = (0, import_node_path40.join)(stage, `overlay-${overlayCount++}`);
37206
38635
  for (const entry of entries) {
37207
- const target = (0, import_node_path38.join)(overlay, entry.path);
37208
- (0, import_node_fs42.mkdirSync)((0, import_node_path38.dirname)(target), { recursive: true });
37209
- (0, import_node_fs42.writeFileSync)(target, entry.bytes);
38636
+ const target = (0, import_node_path40.join)(overlay, entry.path);
38637
+ (0, import_node_fs44.mkdirSync)((0, import_node_path40.dirname)(target), { recursive: true });
38638
+ (0, import_node_fs44.writeFileSync)(target, entry.bytes);
37210
38639
  }
37211
38640
  return digestPackedFiles(overlay, entries.map((entry) => entry.path));
37212
38641
  };
37213
38642
  return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
37214
38643
  } finally {
37215
- (0, import_node_fs42.rmSync)(stage, { recursive: true, force: true });
38644
+ (0, import_node_fs44.rmSync)(stage, { recursive: true, force: true });
37216
38645
  }
37217
38646
  }
37218
38647
 
@@ -37284,7 +38713,7 @@ function registerEdgeCommands(program3) {
37284
38713
 
37285
38714
  // src/schedules-lift-command.ts
37286
38715
  var import_promises8 = require("node:fs/promises");
37287
- var import_node_path39 = require("node:path");
38716
+ var import_node_path41 = require("node:path");
37288
38717
  var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
37289
38718
  var SCHEDULE_REPO_RE = /^[A-Za-z0-9_.-]+$/;
37290
38719
  var SchedulesLiftUsageError = class extends Error {
@@ -37311,7 +38740,7 @@ async function readWorkflowFiles(dir) {
37311
38740
  const files = [];
37312
38741
  for (const name of names.sort()) {
37313
38742
  if (!/\.ya?ml$/.test(name)) continue;
37314
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path39.join)(dir, name), "utf8") });
38743
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path41.join)(dir, name), "utf8") });
37315
38744
  }
37316
38745
  return files;
37317
38746
  }
@@ -37394,8 +38823,8 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
37394
38823
 
37395
38824
  // src/spawn-policy-core.ts
37396
38825
  var import_node_child_process16 = require("node:child_process");
37397
- var import_node_fs43 = require("node:fs");
37398
- var import_node_path40 = require("node:path");
38826
+ var import_node_fs45 = require("node:fs");
38827
+ var import_node_path42 = require("node:path");
37399
38828
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
37400
38829
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
37401
38830
  var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
@@ -37481,7 +38910,7 @@ function runSpawnPolicy(root) {
37481
38910
  for (const file of files) {
37482
38911
  let raw;
37483
38912
  try {
37484
- raw = (0, import_node_fs43.readFileSync)((0, import_node_path40.join)(root, file), "utf8");
38913
+ raw = (0, import_node_fs45.readFileSync)((0, import_node_path42.join)(root, file), "utf8");
37485
38914
  } catch {
37486
38915
  continue;
37487
38916
  }
@@ -37501,19 +38930,19 @@ function runSpawnPolicy(root) {
37501
38930
  function registerDeveloperCommands(program3) {
37502
38931
  const rules = program3.command("rules").description("org-managed .gitignore delivery");
37503
38932
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
37504
- const path2 = (0, import_node_path41.join)(process.cwd(), ".gitignore");
37505
- const current = (0, import_node_fs44.existsSync)(path2) ? (0, import_node_fs44.readFileSync)(path2, "utf8") : null;
38933
+ const path2 = (0, import_node_path43.join)(process.cwd(), ".gitignore");
38934
+ const current = (0, import_node_fs46.existsSync)(path2) ? (0, import_node_fs46.readFileSync)(path2, "utf8") : null;
37506
38935
  const plan = planManagedGitignore(current);
37507
38936
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
37508
38937
  if (opts.json) {
37509
- if (opts.write && plan.changed) (0, import_node_fs44.writeFileSync)(path2, plan.content, "utf8");
38938
+ if (opts.write && plan.changed) (0, import_node_fs46.writeFileSync)(path2, plan.content, "utf8");
37510
38939
  console.log(JSON.stringify(plan, null, 2));
37511
38940
  if (!opts.write && plan.changed) process.exitCode = 1;
37512
38941
  return;
37513
38942
  }
37514
38943
  if (opts.write) {
37515
38944
  if (plan.changed) {
37516
- (0, import_node_fs44.writeFileSync)(path2, plan.content, "utf8");
38945
+ (0, import_node_fs46.writeFileSync)(path2, plan.content, "utf8");
37517
38946
  console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
37518
38947
  } else {
37519
38948
  console.log("mmi-cli devops org rules gitignore: up to date");
@@ -37771,11 +39200,11 @@ var defaultDeps = {
37771
39200
  // #5813: the caller supplies that command line. `process.argv` is NOT it — the house shim splices
37772
39201
  // the house root out in place, so re-reading it here re-ran the removed flat alias (`release …`
37773
39202
  // instead of `devops release …`) and the converged child refused with exit 2.
37774
- reexec: (argv) => new Promise((resolve6) => {
39203
+ reexec: (argv) => new Promise((resolve7) => {
37775
39204
  const args = [...argv];
37776
39205
  const child2 = isWin2 ? (0, import_node_child_process17.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", windowsHide: true }) : (0, import_node_child_process17.spawn)("mmi-cli", args, { stdio: "inherit", windowsHide: true });
37777
- child2.on("exit", (code) => resolve6(code ?? 1));
37778
- child2.on("error", () => resolve6(1));
39206
+ child2.on("exit", (code) => resolve7(code ?? 1));
39207
+ child2.on("error", () => resolve7(1));
37779
39208
  }),
37780
39209
  // stderr only — stdout may be carrying --json output.
37781
39210
  warn: (line) => console.error(line)
@@ -37817,7 +39246,7 @@ function clean3(out) {
37817
39246
  return out.trim();
37818
39247
  }
37819
39248
  function sleeper(deps) {
37820
- return deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
39249
+ return deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
37821
39250
  }
37822
39251
  function normalizeHotfixVersion(input) {
37823
39252
  const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
@@ -38846,8 +40275,8 @@ async function announceRelease(deps, args) {
38846
40275
  }
38847
40276
 
38848
40277
  // src/train-commands.ts
38849
- var import_node_fs45 = require("node:fs");
38850
- var import_node_path42 = require("node:path");
40278
+ var import_node_fs47 = require("node:fs");
40279
+ var import_node_path44 = require("node:path");
38851
40280
  var RELEASE_BUMP_INTENTS = ["major", "minor", "patch"];
38852
40281
  function resolveReleaseBumpIntent(raw) {
38853
40282
  const intent = typeof raw === "string" ? raw.trim() : "";
@@ -38858,7 +40287,7 @@ function resolveReleaseBumpIntent(raw) {
38858
40287
  }
38859
40288
  function readRepoVersion() {
38860
40289
  try {
38861
- return JSON.parse((0, import_node_fs45.readFileSync)((0, import_node_path42.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
40290
+ return JSON.parse((0, import_node_fs47.readFileSync)((0, import_node_path44.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
38862
40291
  } catch {
38863
40292
  return void 0;
38864
40293
  }
@@ -39041,6 +40470,7 @@ function renderReleaseResume(r) {
39041
40470
  if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
39042
40471
  if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
39043
40472
  if (r.checkout) lines.push(` checkout: ${r.checkout.note}`);
40473
+ if (r.ledger) lines.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
39044
40474
  return lines.join("\n");
39045
40475
  }
39046
40476
  function renderReleaseAbort(r) {
@@ -39120,6 +40550,27 @@ async function runPostReleaseJervDoctor(repo) {
39120
40550
  };
39121
40551
  }
39122
40552
  }
40553
+ function buildReleaseVerdict(commandName, result, projectInfoSync) {
40554
+ const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
40555
+ const followUpStatus = deriveTrainFollowUpStatus({
40556
+ projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
40557
+ deploy: deployFollowUpOutcome(result.deployStatus),
40558
+ rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
40559
+ alignment: alignmentPending ? "unresolved" : "ok",
40560
+ foldPort: "ok"
40561
+ });
40562
+ return {
40563
+ followUpStatus,
40564
+ releaseVerdict: commandName === "release" ? {
40565
+ releaseStatus: "succeeded",
40566
+ followUpStatus,
40567
+ promoted: result.promoted,
40568
+ tag: result.tag,
40569
+ releaseUrl: result.release?.url ?? null,
40570
+ legs: releaseFollowUpLegs(result, projectInfoSync)
40571
+ } : void 0
40572
+ };
40573
+ }
39123
40574
  function renderTrainApply(commandName, r) {
39124
40575
  let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
39125
40576
  if (r.versionFold) base = `${base}; ${r.versionFold}`;
@@ -39148,9 +40599,14 @@ function renderTrainApply(commandName, r) {
39148
40599
  const failedLegs = v.legs.filter((leg) => leg.status === "failed");
39149
40600
  return [
39150
40601
  `${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`,
39151
- ...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`)
40602
+ ...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`),
40603
+ // #5987: the durable ledger's phase legs + derived typed verdict ride the same receipt.
40604
+ ...r.ledger ? formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`) : []
39152
40605
  ].join("\n");
39153
40606
  }
40607
+ if (r.ledger) {
40608
+ return [base, ...formatReleaseLedgerReport(r.ledger).map((line) => ` ${line}`)].join("\n");
40609
+ }
39154
40610
  return base;
39155
40611
  }
39156
40612
  function applyTrainFollowUpExit(status) {
@@ -39175,7 +40631,7 @@ function registerTrainCommandsAndInventory(program3) {
39175
40631
  }
39176
40632
  function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
39177
40633
  for (const commandName of ["rcand", "release"]) {
39178
- const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)");
40634
+ const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "with --apply, write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983)");
39179
40635
  const RELEASE_ONLY_FLAGS = [
39180
40636
  { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
39181
40637
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
@@ -39211,10 +40667,13 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
39211
40667
  if (o.retryPublish && commandName !== "release") {
39212
40668
  return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
39213
40669
  }
40670
+ if (o.out && !o.apply) {
40671
+ return fail(`${commandName}: --out writes the --apply result receipt only \u2014 the dry-run plan and --resume keep their stdout/JSON output. Drop --out${o.resume ? "" : " or rerun with --apply"}`);
40672
+ }
39214
40673
  if (o.retryPublish) {
39215
40674
  if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
39216
40675
  if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
39217
- if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
40676
+ if (o.announceSummaryFile || o.ack || o.dev || o.out) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
39218
40677
  if (o.repo) {
39219
40678
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
39220
40679
  if (!guard.ok) return fail(`release: ${guard.message}`);
@@ -39238,7 +40697,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
39238
40697
  if (o.abort) {
39239
40698
  if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
39240
40699
  if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
39241
- if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
40700
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.out) {
39242
40701
  return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
39243
40702
  }
39244
40703
  if (o.repo) {
@@ -39313,29 +40772,16 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
39313
40772
  projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
39314
40773
  }
39315
40774
  }
39316
- const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
39317
- const followUpStatus = deriveTrainFollowUpStatus({
39318
- projectInfo: projectInfoSync && "error" in projectInfoSync ? "failure" : "ok",
39319
- deploy: deployFollowUpOutcome(result.deployStatus),
39320
- rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
39321
- alignment: alignmentPending ? "unresolved" : "ok",
39322
- foldPort: "ok"
39323
- });
39324
- const releaseVerdict = commandName === "release" ? {
39325
- releaseStatus: "succeeded",
39326
- followUpStatus,
39327
- promoted: result.promoted,
39328
- tag: result.tag,
39329
- releaseUrl: result.release?.url ?? null,
39330
- legs: releaseFollowUpLegs(result, projectInfoSync)
39331
- } : void 0;
40775
+ const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
39332
40776
  const reported = {
39333
40777
  ...result,
39334
40778
  ...projectInfoSync ? { projectInfoSync } : {},
39335
40779
  ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {},
39336
40780
  ...releaseVerdict ? { releaseVerdict } : {}
39337
40781
  };
39338
- printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
40782
+ const output = o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported);
40783
+ if (!o.out) printLine(output);
40784
+ else printLine(`Wrote ${commandName} apply result to ${o.out} (UTF-8, ${writeUtf8Receipt(o.out, output)} bytes)`);
39339
40785
  applyTrainFollowUpExit(followUpStatus);
39340
40786
  return;
39341
40787
  } catch (e) {
@@ -39477,12 +40923,12 @@ ${r.stderr ?? ""}`).catch(() => "");
39477
40923
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
39478
40924
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
39479
40925
  function envHealLockPath(home) {
39480
- return (0, import_node_path43.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
40926
+ return (0, import_node_path45.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
39481
40927
  }
39482
40928
  async function withEnvHealLock(what, run) {
39483
40929
  try {
39484
40930
  return await withFileLock(
39485
- envHealLockPath((0, import_node_os19.homedir)()),
40931
+ envHealLockPath((0, import_node_os20.homedir)()),
39486
40932
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
39487
40933
  run
39488
40934
  );
@@ -39578,7 +41024,7 @@ function mmiDoctorDeps(opts = {}) {
39578
41024
  const configRoot = surfaceConfigRoot(surface);
39579
41025
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
39580
41026
  const plan = buildPluginCachePlan(
39581
- (0, import_node_os19.homedir)(),
41027
+ (0, import_node_os20.homedir)(),
39582
41028
  running,
39583
41029
  pluginCacheFsDeps(configRoot, () => 0),
39584
41030
  { configRoot, includeStaging: surface !== "codex" }
@@ -39606,14 +41052,14 @@ function mmiDoctorDeps(opts = {}) {
39606
41052
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
39607
41053
  const installed = installedActivePluginVersion(surface);
39608
41054
  const plan = buildPluginCachePlan(
39609
- (0, import_node_os19.homedir)(),
41055
+ (0, import_node_os20.homedir)(),
39610
41056
  running,
39611
41057
  pluginCacheFsDeps(configRoot, () => 0),
39612
41058
  { configRoot, includeStaging: surface !== "codex", installedVersion: installed }
39613
41059
  );
39614
41060
  const result = applyPluginCachePlan(
39615
41061
  plan,
39616
- (p) => (0, import_node_fs46.rmSync)(p, { recursive: true }),
41062
+ (p) => (0, import_node_fs48.rmSync)(p, { recursive: true }),
39617
41063
  stagingApplyFsGuard(configRoot)
39618
41064
  );
39619
41065
  return {
@@ -39648,7 +41094,7 @@ function mmiDoctorDeps(opts = {}) {
39648
41094
  // has no generated routing index would
39649
41095
  // get a permanent — demanding an artifact it never asked for.
39650
41096
  docsIndexState: (root) => {
39651
- if (!(0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, DOCS_INDEX_PATH))) return void 0;
41097
+ if (!(0, import_node_fs48.existsSync)((0, import_node_path45.join)(root, DOCS_INDEX_PATH))) return void 0;
39652
41098
  const real = createDocsIndexDeps(root);
39653
41099
  let docs;
39654
41100
  const listDocs = () => docs ??= real.listDocs();
@@ -39657,7 +41103,7 @@ function mmiDoctorDeps(opts = {}) {
39657
41103
  },
39658
41104
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
39659
41105
  healDocsIndex: (root) => {
39660
- if (!(0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
41106
+ if (!(0, import_node_fs48.existsSync)((0, import_node_path45.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
39661
41107
  const real = createDocsIndexDeps(root);
39662
41108
  let docs;
39663
41109
  const listDocs = () => docs ??= real.listDocs();
@@ -39678,8 +41124,8 @@ function mmiDoctorDeps(opts = {}) {
39678
41124
  });
39679
41125
  const raced = await Promise.race([
39680
41126
  work.then((r) => ({ ...r, timedOut: false })),
39681
- new Promise((resolve6) => {
39682
- ceiling = setTimeout(() => resolve6({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
41127
+ new Promise((resolve7) => {
41128
+ ceiling = setTimeout(() => resolve7({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
39683
41129
  })
39684
41130
  ]);
39685
41131
  if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
@@ -39712,8 +41158,8 @@ function mmiDoctorDeps(opts = {}) {
39712
41158
  incomplete: nb.incomplete,
39713
41159
  timedOut: false
39714
41160
  })),
39715
- new Promise((resolve6) => {
39716
- ceiling = setTimeout(() => resolve6({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
41161
+ new Promise((resolve7) => {
41162
+ ceiling = setTimeout(() => resolve7({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
39717
41163
  })
39718
41164
  ]);
39719
41165
  if (!raced.timedOut && raced.incomplete.length === 0) writeSchedulesDriftCache(cachePath, raced.driftLines);
@@ -40228,7 +41674,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
40228
41674
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
40229
41675
  if (o.secretsFile) {
40230
41676
  try {
40231
- vars.push(`secrets=${(0, import_node_fs46.readFileSync)(o.secretsFile, "utf8")}`);
41677
+ vars.push(`secrets=${(0, import_node_fs48.readFileSync)(o.secretsFile, "utf8")}`);
40232
41678
  } catch (e) {
40233
41679
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
40234
41680
  }
@@ -40541,16 +41987,16 @@ function ciAuditDeps2() {
40541
41987
  // gate re-seed step is skipped gracefully rather than failing mid-run.
40542
41988
  readSeedFile: (path2) => {
40543
41989
  if (!root) return null;
40544
- const fullPath = (0, import_node_path43.join)(root, path2);
40545
- return (0, import_node_fs46.existsSync)(fullPath) ? (0, import_node_fs46.readFileSync)(fullPath, "utf8") : null;
41990
+ const fullPath = (0, import_node_path45.join)(root, path2);
41991
+ return (0, import_node_fs48.existsSync)(fullPath) ? (0, import_node_fs48.readFileSync)(fullPath, "utf8") : null;
40546
41992
  }
40547
41993
  };
40548
41994
  }
40549
41995
  function hubRoot2() {
40550
- const fromPkg = (0, import_node_path43.join)(__dirname, "..", "..");
41996
+ const fromPkg = (0, import_node_path45.join)(__dirname, "..", "..");
40551
41997
  const marker = "skills/bootstrap/seeds/manifest.json";
40552
- if ((0, import_node_fs46.existsSync)((0, import_node_path43.join)(fromPkg, marker))) return fromPkg;
40553
- if ((0, import_node_fs46.existsSync)((0, import_node_path43.join)(process.cwd(), marker))) return process.cwd();
41998
+ if ((0, import_node_fs48.existsSync)((0, import_node_path45.join)(fromPkg, marker))) return fromPkg;
41999
+ if ((0, import_node_fs48.existsSync)((0, import_node_path45.join)(process.cwd(), marker))) return process.cwd();
40554
42000
  return null;
40555
42001
  }
40556
42002
  registerQueryCommands(program2);
@@ -40650,10 +42096,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
40650
42096
  targets = resolution.targets;
40651
42097
  }
40652
42098
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
40653
- const fileMatrix = (0, import_node_fs46.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs46.readFileSync)("access-matrix.json", "utf8")) : {};
42099
+ const fileMatrix = (0, import_node_fs48.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs48.readFileSync)("access-matrix.json", "utf8")) : {};
40654
42100
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
40655
42101
  const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
40656
- const sanctioned = (0, import_node_fs46.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs46.readFileSync)("access-matrix.json", "utf8")) : {};
42102
+ const sanctioned = (0, import_node_fs48.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs48.readFileSync)("access-matrix.json", "utf8")) : {};
40657
42103
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
40658
42104
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
40659
42105
  if (!report.ok) process.exitCode = 1;
@@ -40684,16 +42130,16 @@ function directoryBytes(path2) {
40684
42130
  let total = 0;
40685
42131
  let entries;
40686
42132
  try {
40687
- entries = (0, import_node_fs46.readdirSync)(path2, { withFileTypes: true });
42133
+ entries = (0, import_node_fs48.readdirSync)(path2, { withFileTypes: true });
40688
42134
  } catch {
40689
42135
  return 0;
40690
42136
  }
40691
42137
  for (const entry of entries) {
40692
- const child2 = (0, import_node_path43.join)(path2, entry.name);
42138
+ const child2 = (0, import_node_path45.join)(path2, entry.name);
40693
42139
  if (entry.isDirectory()) total += directoryBytes(child2);
40694
42140
  else {
40695
42141
  try {
40696
- total += (0, import_node_fs46.statSync)(child2).size;
42142
+ total += (0, import_node_fs48.statSync)(child2).size;
40697
42143
  } catch {
40698
42144
  }
40699
42145
  }
@@ -40701,25 +42147,25 @@ function directoryBytes(path2) {
40701
42147
  return total;
40702
42148
  }
40703
42149
  function listDirEntries(dir) {
40704
- return (0, import_node_fs46.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
42150
+ return (0, import_node_fs48.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
40705
42151
  }
40706
42152
  function readInstalledPluginRefs(configRoot) {
40707
42153
  const p = installedPluginsPathForConfig(configRoot);
40708
- if (!(0, import_node_fs46.existsSync)(p)) return [];
42154
+ if (!(0, import_node_fs48.existsSync)(p)) return [];
40709
42155
  try {
40710
- return installedPluginPaths((0, import_node_fs46.readFileSync)(p, "utf8"));
42156
+ return installedPluginPaths((0, import_node_fs48.readFileSync)(p, "utf8"));
40711
42157
  } catch {
40712
42158
  return null;
40713
42159
  }
40714
42160
  }
40715
42161
  function pluginCacheFsDeps(configRoot, dirBytes) {
40716
42162
  return {
40717
- exists: (p) => (0, import_node_fs46.existsSync)(p),
40718
- listVersionDirs: (root) => (0, import_node_fs46.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
42163
+ exists: (p) => (0, import_node_fs48.existsSync)(p),
42164
+ listVersionDirs: (root) => (0, import_node_fs48.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
40719
42165
  dirBytes,
40720
- listStagingDirs: (root) => (0, import_node_fs46.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
42166
+ listStagingDirs: (root) => (0, import_node_fs48.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
40721
42167
  try {
40722
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path43.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs46.statSync)(p).mtimeMs) };
42168
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path45.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs48.statSync)(p).mtimeMs) };
40723
42169
  } catch {
40724
42170
  return { name: d.name, mtimeMs: Date.now() };
40725
42171
  }
@@ -40733,10 +42179,10 @@ function stagingApplyFsGuard(configRoot) {
40733
42179
  return {
40734
42180
  referencedPaths: () => readInstalledPluginRefs(configRoot),
40735
42181
  mtimeMs: (name) => {
40736
- const p = (0, import_node_path43.join)(stagingRoot, name);
40737
- if (!(0, import_node_fs46.existsSync)(p)) return null;
42182
+ const p = (0, import_node_path45.join)(stagingRoot, name);
42183
+ if (!(0, import_node_fs48.existsSync)(p)) return null;
40738
42184
  try {
40739
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs46.statSync)(q).mtimeMs);
42185
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs48.statSync)(q).mtimeMs);
40740
42186
  } catch {
40741
42187
  return null;
40742
42188
  }
@@ -40756,13 +42202,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
40756
42202
  return;
40757
42203
  }
40758
42204
  const plan = buildPluginCachePlan(
40759
- (0, import_node_os19.homedir)(),
42205
+ (0, import_node_os20.homedir)(),
40760
42206
  running,
40761
42207
  pluginCacheFsDeps(configRoot, directoryBytes),
40762
42208
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
40763
42209
  );
40764
42210
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
40765
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs46.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
42211
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs48.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
40766
42212
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
40767
42213
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
40768
42214
  else console.log(renderPluginCachePlan(plan, result));