@mutmutco/cli 4.0.10 → 4.0.12

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.
package/dist/main.cjs CHANGED
@@ -10827,10 +10827,10 @@ var rollout_plan_default = {
10827
10827
  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)."
10828
10828
  },
10829
10829
  baseline: {
10830
- version: "4.0.10",
10831
- tag: "v4.0.10",
10832
- commit: "428fa5003799",
10833
- npm: "@mutmutco/cli@4.0.10"
10830
+ version: "4.0.12",
10831
+ tag: "v4.0.12",
10832
+ commit: "0ead67177a73",
10833
+ npm: "@mutmutco/cli@4.0.12"
10834
10834
  },
10835
10835
  exitCriterion: "fleet-n-of-n",
10836
10836
  hubOnlyShortcut: "forbidden",
@@ -10847,14 +10847,14 @@ var rollout_plan_default = {
10847
10847
  repo: "mutmutco/mmi-hub",
10848
10848
  role: "canary",
10849
10849
  schedule: "train",
10850
- v3Target: "v4.0.10"
10850
+ v3Target: "v4.0.12"
10851
10851
  }
10852
10852
  ],
10853
10853
  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.",
10854
10854
  rollback: {
10855
10855
  independent: true,
10856
- mechanism: "npm dist-tag latest -> 4.0.10 and redeploy the Hub Lambda from tag v4.0.10 (428fa5003799); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10857
- v3Target: "v4.0.10 (@mutmutco/cli@4.0.10, tag commit 428fa5003799 \u2014 last known-good release carrying the repo-index v4-only contract)"
10856
+ mechanism: "npm dist-tag latest -> 4.0.12 and redeploy the Hub Lambda from tag v4.0.12 (0ead67177a73); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10857
+ v3Target: "v4.0.12 (@mutmutco/cli@4.0.12, tag commit 0ead67177a73 \u2014 last known-good release carrying the repo-index v4-only contract)"
10858
10858
  }
10859
10859
  },
10860
10860
  {
@@ -17552,6 +17552,12 @@ function evaluateClosingGuard(input, opts) {
17552
17552
  };
17553
17553
  }
17554
17554
  const closedSkipMessage = skippedClosed.length ? alreadyClosedCommitClosingMessage(skippedClosed, opts.context) : void 0;
17555
+ const unregistered = findClaimedButUnregisteredClosings(input.text, input.closing, input.alreadyClosed);
17556
+ if (unregistered.length) {
17557
+ const base = claimedClosingWarning(unregistered, opts.context);
17558
+ return closedSkipMessage ? { blocked: false, message: `${base}
17559
+ ${closedSkipMessage}` } : { blocked: false, message: base };
17560
+ }
17555
17561
  if (input.closing.length === 0) {
17556
17562
  return closedSkipMessage ? { blocked: false, message: closedSkipMessage } : { blocked: false };
17557
17563
  }
@@ -17575,9 +17581,25 @@ function evaluateClosingGuard(input, opts) {
17575
17581
  }
17576
17582
  return closedSkipMessage ? { blocked: false, message: closedSkipMessage } : { blocked: false };
17577
17583
  }
17584
+ function findClaimedButUnregisteredClosings(text, closing, alreadyClosed = []) {
17585
+ const registered = new Set(closing);
17586
+ const closed = new Set(alreadyClosed);
17587
+ const claimed = /* @__PURE__ */ new Set();
17588
+ for (const m of findClosingMentions(text)) {
17589
+ if (m.negated) continue;
17590
+ claimed.add(m.issue);
17591
+ }
17592
+ return [...claimed].filter((n) => !registered.has(n) && !closed.has(n));
17593
+ }
17594
+ function claimedClosingWarning(unregistered, context = "pr merge") {
17595
+ const named = unregistered.map((n) => `#${n}`).join(", ");
17596
+ const first = `#${unregistered[0] ?? "N"}`;
17597
+ return `${context}: WARNING \u2014 the PR text claims it will close ${named}, but GitHub has NOT registered ${named} as closing references, so the merge will NOT close ${first}. Link the issue in the PR's Development sidebar or keep the closing keyword where GitHub parses it (an already-closed issue is omitted silently \u2014 that case is fine). Otherwise ${first} stays open after the merge.`;
17598
+ }
17578
17599
  async function withAlreadyClosedCommitTargets(input, fetchIssueState) {
17579
17600
  if (!input) return void 0;
17580
- const candidates = (input.commitClosing ?? []).filter((n) => !input.closing.includes(n));
17601
+ const unregistered = findClaimedButUnregisteredClosings(input.text, input.closing);
17602
+ const candidates = [.../* @__PURE__ */ new Set([...(input.commitClosing ?? []).filter((n) => !input.closing.includes(n)), ...unregistered])];
17581
17603
  if (!candidates.length) return input;
17582
17604
  const alreadyClosed = [];
17583
17605
  await Promise.all(
@@ -17589,7 +17611,7 @@ async function withAlreadyClosedCommitTargets(input, fetchIssueState) {
17589
17611
  }
17590
17612
  })
17591
17613
  );
17592
- return alreadyClosed.length ? { ...input, alreadyClosed } : input;
17614
+ return alreadyClosed.length ? { ...input, alreadyClosed: [.../* @__PURE__ */ new Set([...input.alreadyClosed ?? [], ...alreadyClosed])] } : input;
17593
17615
  }
17594
17616
 
17595
17617
  // src/issue-comment.ts
@@ -23444,22 +23466,32 @@ ${chunk.blurb ?? ""}`;
23444
23466
  }
23445
23467
  }
23446
23468
  var V4_EMBED_TIMEOUT_MS = 15 * 6e4;
23447
- function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
23448
- if (!chunks.length) return { embeddings: [] };
23469
+ var V4_EMBED_RETRY = 1;
23470
+ function runEmbedderOnce(cwd, chunks, modelDirectory, createdAt) {
23471
+ if (!chunks.length) return { ok: true, embeddings: [] };
23449
23472
  const orchestratorRunner = (0, import_node_path23.join)(process.cwd(), "repo-indexer", "src", "batch.mjs");
23450
23473
  const targetRunner = (0, import_node_path23.join)(cwd, "repo-indexer", "src", "batch.mjs");
23451
23474
  const file = (0, import_node_fs25.existsSync)(orchestratorRunner) ? orchestratorRunner : targetRunner;
23452
- if (!(0, import_node_fs25.existsSync)(file)) return { embeddings: [], reason: "embeddings-unavailable" };
23475
+ if (!(0, import_node_fs25.existsSync)(file)) return { ok: false, reason: "embeddings-unavailable" };
23453
23476
  const request = { texts: chunks.map((chunk) => ({ id: chunk.id, text: embeddingInput(cwd, chunk) })), maxBatch: V4_EMBED_BATCH };
23454
23477
  const env = { ...process.env, ...modelDirectory ? { MMI_REPO_INDEXER_MODEL_DIR: modelDirectory } : {} };
23455
23478
  const result = (0, import_node_child_process13.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
23456
23479
  if (result.error || result.status !== 0) {
23457
- const detail = result.error?.message ?? result.signal ?? `exit status ${result.status ?? "unknown"}`;
23458
- throw new Error(`repo-index v4 embedding runner failed: ${detail}`);
23480
+ const cleanExit2 = result.error === void 0 && result.signal === void 0 && typeof result.status === "number" && result.status !== 0;
23481
+ let code;
23482
+ let stderrTail;
23483
+ if (cleanExit2) {
23484
+ try {
23485
+ code = JSON.parse(String(result.stdout))?.code;
23486
+ } catch {
23487
+ }
23488
+ stderrTail = String(result.stderr).trim().slice(-400) || void 0;
23489
+ }
23490
+ return { ok: false, code, stderrTail, cleanExit: cleanExit2, detail: result.error?.message ?? result.signal ?? `exit status ${result.status ?? "unknown"}` };
23459
23491
  }
23460
23492
  try {
23461
23493
  const response = JSON.parse(result.stdout);
23462
- if (!response.ok || !response.provenance || !Array.isArray(response.embeddings)) return { embeddings: [], reason: "embeddings-unavailable" };
23494
+ if (!response.ok || !response.provenance || !Array.isArray(response.embeddings)) return { ok: false, reason: "embeddings-unavailable" };
23463
23495
  const provenance = {
23464
23496
  provider: response.provenance.provider,
23465
23497
  model: response.provenance.model,
@@ -23470,10 +23502,23 @@ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
23470
23502
  };
23471
23503
  const byId = new Map(response.embeddings.map((e) => [e.id, e.vector]));
23472
23504
  const embeddings = chunks.map((chunk) => ({ chunkId: chunk.id, vector: byId.get(chunk.id), provenance })).filter((e) => Array.isArray(e.vector) && e.vector.length === provenance.dimensions && e.vector.every(Number.isFinite));
23473
- return embeddings.length === chunks.length ? { embeddings } : { embeddings: [], reason: "partial-coverage" };
23505
+ return embeddings.length === chunks.length ? { ok: true, embeddings } : { ok: false, reason: "partial-coverage" };
23474
23506
  } catch {
23475
- return { embeddings: [], reason: "embeddings-unavailable" };
23507
+ return { ok: false, reason: "embeddings-unavailable" };
23508
+ }
23509
+ }
23510
+ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
23511
+ if (!chunks.length) return { embeddings: [] };
23512
+ let last;
23513
+ for (let attempt = 1; attempt <= V4_EMBED_RETRY + 1; attempt++) {
23514
+ last = runEmbedderOnce(cwd, chunks, modelDirectory, createdAt);
23515
+ if (last.ok) return { embeddings: last.embeddings, reason: last.reason };
23516
+ if (attempt > V4_EMBED_RETRY || last.cleanExit !== true) break;
23476
23517
  }
23518
+ const runner = last;
23519
+ const code = runner.code ? ` code=${runner.code}` : "";
23520
+ const stderr = runner.stderrTail ? ` stderr=${runner.stderrTail}` : "";
23521
+ throw new Error(`repo-index v4 embedding runner failed: ${runner.detail ?? "unknown"}${code}${stderr}`);
23477
23522
  }
23478
23523
  async function buildRepoIndexV4(cwd, repo, opts = {}) {
23479
23524
  const { commit, defaultBranch, createdAt } = gitInfo(cwd);
@@ -23579,6 +23624,7 @@ function shardRepoIndexV4(envelope) {
23579
23624
 
23580
23625
  // src/repo-index-cloud-client.ts
23581
23626
  var RETRY_ATTEMPTS2 = 3;
23627
+ var WARMUP_MAX_PASSES = 3;
23582
23628
  async function probeRepoIndexV4ReadinessCloud(queries, deps) {
23583
23629
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
23584
23630
  const token = await deps.token();
@@ -23586,6 +23632,7 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
23586
23632
  const baseUrl = deps.baseUrl.replace(/\/$/, "");
23587
23633
  const headers = { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" "), "content-type": "application/json" };
23588
23634
  const shadowPass = async (readinessProbe) => {
23635
+ const ms = {};
23589
23636
  for (const query of queries) {
23590
23637
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/v4/shadow`, {
23591
23638
  method: "POST",
@@ -23594,12 +23641,18 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
23594
23641
  }, { attempts: RETRY_ATTEMPTS2, timeoutMs: 12e4, sleep: deps.retrySleep });
23595
23642
  const body = await res.json().catch(() => ({}));
23596
23643
  if (!res.ok) return { ok: false, error: `${query.id}: ${body.error ?? `v4 readiness probe HTTP ${res.status}`}${body.errorClass ? ` (${body.errorClass})` : ""}`, status: res.status };
23644
+ if (typeof body.v4?.ms === "number") ms[query.id] = body.v4.ms;
23597
23645
  }
23598
- return { ok: true };
23646
+ return { ok: true, ms };
23599
23647
  };
23648
+ const steady = (ms) => queries.every((query) => query.latencyMs === void 0 || ms[query.id] === void 0 || ms[query.id] <= query.latencyMs);
23600
23649
  try {
23601
- const warmup = await shadowPass(false);
23650
+ let warmup = await shadowPass(false);
23602
23651
  if (!warmup.ok) return warmup;
23652
+ for (let pass = 2; pass <= WARMUP_MAX_PASSES && !steady(warmup.ms); pass++) {
23653
+ warmup = await shadowPass(false);
23654
+ if (!warmup.ok) return warmup;
23655
+ }
23603
23656
  const verdict = await shadowPass(true);
23604
23657
  if (!verdict.ok) return verdict;
23605
23658
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/status`, {
@@ -24211,15 +24264,21 @@ function formatRepoIndexCloudStatus(status) {
24211
24264
  return `repo-index: cloud ${repo} v4 ${state}${commit}; ${readiness}`;
24212
24265
  }
24213
24266
  function defaultV4ReadinessSuite() {
24214
- const suite = repo_index_golden_queries_v4_default;
24215
- const modes = new Set(suite.queries?.map((query) => query.mode));
24216
- if (suite.schemaVersion !== 4 || !Array.isArray(suite.queries) || !["lexical", "semantic", "hybrid"].every((mode) => modes.has(mode))) {
24267
+ const raw = repo_index_golden_queries_v4_default;
24268
+ const queries = (raw.queries ?? []).map(({ id, query, mode, latency }) => ({
24269
+ id,
24270
+ query,
24271
+ mode,
24272
+ ...typeof latency?.p95Ms === "number" ? { latencyMs: latency.p95Ms } : {}
24273
+ }));
24274
+ const modes = new Set(queries.map((query) => query.mode));
24275
+ if (raw.schemaVersion !== 4 || !Array.isArray(raw.queries) || !["lexical", "semantic", "hybrid"].every((mode) => modes.has(mode))) {
24217
24276
  throw new Error("invalid bundled repo-index-golden-queries-v4.json");
24218
24277
  }
24219
- return suite;
24278
+ return { schemaVersion: 4, queries };
24220
24279
  }
24221
24280
  async function runRepoIndexV4ReadinessGate(opts) {
24222
- const queries = opts.suite.queries.map(({ id, query, mode }) => ({ id, query, mode }));
24281
+ const queries = opts.suite.queries.map(({ id, query, mode, latencyMs }) => ({ id, query, mode, latencyMs }));
24223
24282
  const probed = await opts.probe(queries);
24224
24283
  if (!probed.ok) return { ok: false, findings: [{ ok: false, code: "v4-probe-error", detail: probed.error }] };
24225
24284
  const ok = probed.readiness.verdict === "ready";
@@ -29480,9 +29539,25 @@ async function advanceClosedIssuesToDone(deps) {
29480
29539
  const issue2 = `#${ref.number}`;
29481
29540
  try {
29482
29541
  const outcome = await deps.moveIssueToDone(issue2);
29483
- entries.push(
29484
- outcome.moved ? { issue: issue2, moved: true, status: "moved" } : { issue: issue2, moved: false, status: "failed", error: outcome.error }
29485
- );
29542
+ if (!outcome.moved) {
29543
+ entries.push({ issue: issue2, moved: false, status: "failed", error: outcome.error });
29544
+ continue;
29545
+ }
29546
+ if (deps.readIssueStatus) {
29547
+ const verified = await deps.readIssueStatus(issue2);
29548
+ if ("error" in verified) {
29549
+ entries.push({ issue: issue2, moved: true, status: "moved", error: `advanced but could not verify on the board (${verified.error})` });
29550
+ } else if (verified.status !== "Done") {
29551
+ entries.push({
29552
+ issue: issue2,
29553
+ moved: false,
29554
+ status: "failed",
29555
+ error: `advanced to Done but the board reports ${verified.status} \u2014 left behind; move it to Done by hand`
29556
+ });
29557
+ continue;
29558
+ }
29559
+ }
29560
+ entries.push({ issue: issue2, moved: true, status: "moved" });
29486
29561
  } catch (e) {
29487
29562
  entries.push({ issue: issue2, moved: false, status: "failed", error: e.message });
29488
29563
  }
@@ -29559,6 +29634,20 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
29559
29634
  moveIssueToDone: async (ref) => {
29560
29635
  const moved = await moveBoardItem({ config: await loadConfigForBoardSelector2(ref, repoOption), selector: ref, status: "Done", repo: repoOption, allowPartial: true });
29561
29636
  return moved.partial ? { moved: false, error: moved.warning } : { moved: true };
29637
+ },
29638
+ // #5317: after a successful move, re-read the board to verify Done actually holds — a move the API
29639
+ // reported but the board did not reflect is named as left behind, never silently dropped.
29640
+ readIssueStatus: async (ref) => {
29641
+ const number = Number(ref.replace(/^#/, ""));
29642
+ if (!Number.isInteger(number) || number <= 0 || !repo) return { error: `unusable ref ${ref}` };
29643
+ try {
29644
+ const board = await readBoard({ config: await loadConfigForBoardSelector2(ref, repoOption), repo: repoOption });
29645
+ const buckets = [...board.primary.userOwned, ...board.primary.claimable, ...board.primary.taken, ...board.primary.unownedInFlight, ...board.secondary.userOwned, ...board.secondary.claimable, ...board.secondary.taken, ...board.secondary.unownedInFlight];
29646
+ const item = buckets.find((candidate2) => candidate2.repository.toLowerCase() === repo.toLowerCase() && candidate2.number === number);
29647
+ return item ? { status: item.status } : { error: `issue ${ref} not found on the board after advance` };
29648
+ } catch (e) {
29649
+ return { error: e.message };
29650
+ }
29562
29651
  }
29563
29652
  });
29564
29653
  }
@@ -35638,6 +35727,34 @@ ${pr2.body ?? ""}`;
35638
35727
  }
35639
35728
  }
35640
35729
 
35730
+ // src/pr-deploy-dev.ts
35731
+ function slugOfRepo(repo) {
35732
+ return repo.split("/")[1]?.toLowerCase() ?? "";
35733
+ }
35734
+ async function planDevDeployOnDevelopmentMerge(repo, baseRef, deps) {
35735
+ if (baseRef !== "development") return { applicable: false, reason: "not-development-base" };
35736
+ const slug = slugOfRepo(repo);
35737
+ if (!slug) return { applicable: false, reason: "unreadable" };
35738
+ let project2;
35739
+ try {
35740
+ project2 = await fetchProjectBySlug(slug, deps);
35741
+ } catch {
35742
+ return { applicable: false, reason: "unreadable" };
35743
+ }
35744
+ const model = project2?.deployModel;
35745
+ if (!model || !isCentralDispatchModel(model)) return { applicable: false, reason: "not-tenant" };
35746
+ return {
35747
+ applicable: true,
35748
+ // The issue's fallback ask, verbatim-shaped: the gap must be visible and actionable at the moment it opens.
35749
+ manualPointer: `dev not redeployed \u2014 run \`mmi-cli devops runtime tenant redeploy ${repo} dev\``
35750
+ };
35751
+ }
35752
+ async function dispatchDevDeploy(repo, deps) {
35753
+ const res = await tenantDeploy({ repo, stage: "dev" }, deps);
35754
+ if (res.ok) return { ok: true };
35755
+ return { ok: false, detail: res.body?.error ?? res.error ?? `HTTP ${res.status}` };
35756
+ }
35757
+
35641
35758
  // src/index.ts
35642
35759
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
35643
35760
  async function githubRepoReachProbe() {
@@ -37107,6 +37224,43 @@ ${filelessTransitionGuide(target, o.stage)}`
37107
37224
  const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
37108
37225
  return reportWrite("org project set-deploy", res);
37109
37226
  });
37227
+ var projectTenantTasks = project.command("tenant-tasks").description("project-admin self-declare of tenant tasks for your own repo's dev/rc stages (#5316, #5308) \u2014 new-task-only, dev/rc-only; editing an existing task or declaring a main-stage task stays master-only (`org project set --var tenantTasks=...`)");
37228
+ projectTenantTasks.command("declare <name>").description('register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on dev/rc. Body shape {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 stages limited to dev/rc here; artifact "required" tasks must carry {artifact} in the command argv.').option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", "task command as argv (repeatable; required unless --var carries it)").option("--stages <list>", "comma-separated dev,rc stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
37229
+ const cfg = await loadConfig();
37230
+ let target;
37231
+ try {
37232
+ target = await projectTarget("org project tenant-tasks declare", o.repo);
37233
+ } catch (e) {
37234
+ return fail(e.message);
37235
+ }
37236
+ const slug = slugOf(target);
37237
+ let task;
37238
+ if (o.var) {
37239
+ try {
37240
+ task = JSON.parse(o.var);
37241
+ } catch {
37242
+ return fail("org project tenant-tasks declare: --var must be valid JSON");
37243
+ }
37244
+ } else {
37245
+ if (!o.service || !o.command?.length || !o.stages) {
37246
+ return fail("org project tenant-tasks declare: pass --var <json> or all of --service, --command, --stages");
37247
+ }
37248
+ task = {
37249
+ service: o.service,
37250
+ command: o.command,
37251
+ stages: o.stages.split(",").map((s) => s.trim()).filter(Boolean),
37252
+ ...o.timeoutSeconds !== void 0 ? { timeoutSeconds: Number(o.timeoutSeconds) } : {},
37253
+ ...o.artifact !== void 0 ? { artifact: o.artifact } : {}
37254
+ };
37255
+ }
37256
+ const res = await postJson(`/projects/${encodeURIComponent(slug)}/tenant-tasks`, { repo: target, tenantTasks: { [name]: task } }, registryClientDeps(cfg), "POST", { noRetry: true });
37257
+ if (o.json) {
37258
+ console.log(JSON.stringify(res, null, 2));
37259
+ } else {
37260
+ reportWrite("org project tenant-tasks declare", res);
37261
+ }
37262
+ if (!res.ok) process.exitCode = 1;
37263
+ });
37110
37264
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
37111
37265
  registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
37112
37266
  const cfg = await loadConfig();
@@ -38000,11 +38154,16 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38000
38154
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
38001
38155
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
38002
38156
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
38003
- const headRef = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS }).then((r) => r.stdout.trim()).catch(async (e) => {
38157
+ const prMeta = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName", "--jq", "{head: .headRefName, base: .baseRefName}"], { timeout: GC_GH_TIMEOUT_MS }).then((r) => JSON.parse(r.stdout)).catch(async (e) => {
38004
38158
  if (!isGitHubRateLimitError(e) || !repoForPostCleanup) throw e;
38005
38159
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 reading PR #${number} via REST instead (#4588).`);
38006
- return (await fetchRestPrSnapshot(number, repoForPostCleanup)).headRef;
38160
+ const snapshot = await fetchRestPrSnapshot(number, repoForPostCleanup);
38161
+ return { head: snapshot.headRef, base: snapshot.baseRef };
38007
38162
  });
38163
+ const headRef = prMeta.head;
38164
+ const baseRef = prMeta.base;
38165
+ const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
38166
+ const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
38008
38167
  const closingGuardInput = await withAlreadyClosedCommitTargets(
38009
38168
  await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
38010
38169
  async (n) => {
@@ -38133,6 +38292,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38133
38292
  const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
38134
38293
  console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
38135
38294
  console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
38295
+ if (devDeployPlan.applicable) console.warn(`pr merge: ${devDeployPlan.manualPointer} once the merge lands (auto-merge does not redeploy the dev stage).`);
38136
38296
  if (upgradedToAuto && !o.auto) {
38137
38297
  console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
38138
38298
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
@@ -38156,6 +38316,8 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38156
38316
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
38157
38317
  const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
38158
38318
  invalidateStatuslineBoardCache();
38319
+ const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
38320
+ const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
38159
38321
  console.log(JSON.stringify({
38160
38322
  mergeStatus: "merged",
38161
38323
  merged: number,
@@ -38167,10 +38329,14 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38167
38329
  // tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
38168
38330
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
38169
38331
  boardAdvanceStatus: boardAdvance.status,
38170
- ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
38332
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
38333
+ ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
38171
38334
  }));
38172
38335
  const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
38173
38336
  if (boardAdvanceMessage) console.error(boardAdvanceMessage);
38337
+ if (devDeploy && !devDeploy.ok) {
38338
+ console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
38339
+ }
38174
38340
  if (crossRepoFilingIssue.status === "failed") {
38175
38341
  console.error(`pr merge: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
38176
38342
  }
@@ -439,22 +439,32 @@ ${chunk.blurb ?? ""}`;
439
439
  }
440
440
  }
441
441
  var V4_EMBED_TIMEOUT_MS = 15 * 6e4;
442
- function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
443
- if (!chunks.length) return { embeddings: [] };
442
+ var V4_EMBED_RETRY = 1;
443
+ function runEmbedderOnce(cwd, chunks, modelDirectory, createdAt) {
444
+ if (!chunks.length) return { ok: true, embeddings: [] };
444
445
  const orchestratorRunner = (0, import_node_path4.join)(process.cwd(), "repo-indexer", "src", "batch.mjs");
445
446
  const targetRunner = (0, import_node_path4.join)(cwd, "repo-indexer", "src", "batch.mjs");
446
447
  const file = (0, import_node_fs3.existsSync)(orchestratorRunner) ? orchestratorRunner : targetRunner;
447
- if (!(0, import_node_fs3.existsSync)(file)) return { embeddings: [], reason: "embeddings-unavailable" };
448
+ if (!(0, import_node_fs3.existsSync)(file)) return { ok: false, reason: "embeddings-unavailable" };
448
449
  const request = { texts: chunks.map((chunk) => ({ id: chunk.id, text: embeddingInput(cwd, chunk) })), maxBatch: V4_EMBED_BATCH };
449
450
  const env = { ...process.env, ...modelDirectory ? { MMI_REPO_INDEXER_MODEL_DIR: modelDirectory } : {} };
450
451
  const result = (0, import_node_child_process3.spawnSync)(process.execPath, [file], { input: JSON.stringify(request), encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
451
452
  if (result.error || result.status !== 0) {
452
- const detail = result.error?.message ?? result.signal ?? `exit status ${result.status ?? "unknown"}`;
453
- throw new Error(`repo-index v4 embedding runner failed: ${detail}`);
453
+ const cleanExit = result.error === void 0 && result.signal === void 0 && typeof result.status === "number" && result.status !== 0;
454
+ let code;
455
+ let stderrTail;
456
+ if (cleanExit) {
457
+ try {
458
+ code = JSON.parse(String(result.stdout))?.code;
459
+ } catch {
460
+ }
461
+ stderrTail = String(result.stderr).trim().slice(-400) || void 0;
462
+ }
463
+ return { ok: false, code, stderrTail, cleanExit, detail: result.error?.message ?? result.signal ?? `exit status ${result.status ?? "unknown"}` };
454
464
  }
455
465
  try {
456
466
  const response = JSON.parse(result.stdout);
457
- if (!response.ok || !response.provenance || !Array.isArray(response.embeddings)) return { embeddings: [], reason: "embeddings-unavailable" };
467
+ if (!response.ok || !response.provenance || !Array.isArray(response.embeddings)) return { ok: false, reason: "embeddings-unavailable" };
458
468
  const provenance = {
459
469
  provider: response.provenance.provider,
460
470
  model: response.provenance.model,
@@ -465,10 +475,23 @@ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
465
475
  };
466
476
  const byId = new Map(response.embeddings.map((e) => [e.id, e.vector]));
467
477
  const embeddings = chunks.map((chunk) => ({ chunkId: chunk.id, vector: byId.get(chunk.id), provenance })).filter((e) => Array.isArray(e.vector) && e.vector.length === provenance.dimensions && e.vector.every(Number.isFinite));
468
- return embeddings.length === chunks.length ? { embeddings } : { embeddings: [], reason: "partial-coverage" };
478
+ return embeddings.length === chunks.length ? { ok: true, embeddings } : { ok: false, reason: "partial-coverage" };
469
479
  } catch {
470
- return { embeddings: [], reason: "embeddings-unavailable" };
480
+ return { ok: false, reason: "embeddings-unavailable" };
481
+ }
482
+ }
483
+ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
484
+ if (!chunks.length) return { embeddings: [] };
485
+ let last;
486
+ for (let attempt = 1; attempt <= V4_EMBED_RETRY + 1; attempt++) {
487
+ last = runEmbedderOnce(cwd, chunks, modelDirectory, createdAt);
488
+ if (last.ok) return { embeddings: last.embeddings, reason: last.reason };
489
+ if (attempt > V4_EMBED_RETRY || last.cleanExit !== true) break;
471
490
  }
491
+ const runner = last;
492
+ const code = runner.code ? ` code=${runner.code}` : "";
493
+ const stderr = runner.stderrTail ? ` stderr=${runner.stderrTail}` : "";
494
+ throw new Error(`repo-index v4 embedding runner failed: ${runner.detail ?? "unknown"}${code}${stderr}`);
472
495
  }
473
496
  async function buildRepoIndexV4(cwd, repo, opts = {}) {
474
497
  const { commit, defaultBranch, createdAt } = gitInfo(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.0.10",
3
+ "version": "4.0.12",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",