@mutmutco/cli 4.0.9 → 4.0.11

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.9",
10831
- tag: "v4.0.9",
10832
- commit: "ed837f90a7da",
10833
- npm: "@mutmutco/cli@4.0.9"
10830
+ version: "4.0.11",
10831
+ tag: "v4.0.11",
10832
+ commit: "5b62441055eb",
10833
+ npm: "@mutmutco/cli@4.0.11"
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.9"
10850
+ v3Target: "v4.0.11"
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.9 and redeploy the Hub Lambda from tag v4.0.9 (ed837f90a7da); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10857
- v3Target: "v4.0.9 (@mutmutco/cli@4.0.9, tag commit ed837f90a7da \u2014 last known-good release carrying the repo-index v4-only contract)"
10856
+ mechanism: "npm dist-tag latest -> 4.0.11 and redeploy the Hub Lambda from tag v4.0.11 (5b62441055eb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
10857
+ v3Target: "v4.0.11 (@mutmutco/cli@4.0.11, tag commit 5b62441055eb \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,11 +23502,24 @@ 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" };
23476
23508
  }
23477
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;
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}`);
23522
+ }
23478
23523
  async function buildRepoIndexV4(cwd, repo, opts = {}) {
23479
23524
  const { commit, defaultBranch, createdAt } = gitInfo(cwd);
23480
23525
  const chunks = await buildStructuralChunks(cwd, repo, commit);
@@ -29480,9 +29525,25 @@ async function advanceClosedIssuesToDone(deps) {
29480
29525
  const issue2 = `#${ref.number}`;
29481
29526
  try {
29482
29527
  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
- );
29528
+ if (!outcome.moved) {
29529
+ entries.push({ issue: issue2, moved: false, status: "failed", error: outcome.error });
29530
+ continue;
29531
+ }
29532
+ if (deps.readIssueStatus) {
29533
+ const verified = await deps.readIssueStatus(issue2);
29534
+ if ("error" in verified) {
29535
+ entries.push({ issue: issue2, moved: true, status: "moved", error: `advanced but could not verify on the board (${verified.error})` });
29536
+ } else if (verified.status !== "Done") {
29537
+ entries.push({
29538
+ issue: issue2,
29539
+ moved: false,
29540
+ status: "failed",
29541
+ error: `advanced to Done but the board reports ${verified.status} \u2014 left behind; move it to Done by hand`
29542
+ });
29543
+ continue;
29544
+ }
29545
+ }
29546
+ entries.push({ issue: issue2, moved: true, status: "moved" });
29486
29547
  } catch (e) {
29487
29548
  entries.push({ issue: issue2, moved: false, status: "failed", error: e.message });
29488
29549
  }
@@ -29559,6 +29620,20 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
29559
29620
  moveIssueToDone: async (ref) => {
29560
29621
  const moved = await moveBoardItem({ config: await loadConfigForBoardSelector2(ref, repoOption), selector: ref, status: "Done", repo: repoOption, allowPartial: true });
29561
29622
  return moved.partial ? { moved: false, error: moved.warning } : { moved: true };
29623
+ },
29624
+ // #5317: after a successful move, re-read the board to verify Done actually holds — a move the API
29625
+ // reported but the board did not reflect is named as left behind, never silently dropped.
29626
+ readIssueStatus: async (ref) => {
29627
+ const number = Number(ref.replace(/^#/, ""));
29628
+ if (!Number.isInteger(number) || number <= 0 || !repo) return { error: `unusable ref ${ref}` };
29629
+ try {
29630
+ const board = await readBoard({ config: await loadConfigForBoardSelector2(ref, repoOption), repo: repoOption });
29631
+ 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];
29632
+ const item = buckets.find((candidate2) => candidate2.repository.toLowerCase() === repo.toLowerCase() && candidate2.number === number);
29633
+ return item ? { status: item.status } : { error: `issue ${ref} not found on the board after advance` };
29634
+ } catch (e) {
29635
+ return { error: e.message };
29636
+ }
29562
29637
  }
29563
29638
  });
29564
29639
  }
@@ -35638,6 +35713,34 @@ ${pr2.body ?? ""}`;
35638
35713
  }
35639
35714
  }
35640
35715
 
35716
+ // src/pr-deploy-dev.ts
35717
+ function slugOfRepo(repo) {
35718
+ return repo.split("/")[1]?.toLowerCase() ?? "";
35719
+ }
35720
+ async function planDevDeployOnDevelopmentMerge(repo, baseRef, deps) {
35721
+ if (baseRef !== "development") return { applicable: false, reason: "not-development-base" };
35722
+ const slug = slugOfRepo(repo);
35723
+ if (!slug) return { applicable: false, reason: "unreadable" };
35724
+ let project2;
35725
+ try {
35726
+ project2 = await fetchProjectBySlug(slug, deps);
35727
+ } catch {
35728
+ return { applicable: false, reason: "unreadable" };
35729
+ }
35730
+ const model = project2?.deployModel;
35731
+ if (!model || !isCentralDispatchModel(model)) return { applicable: false, reason: "not-tenant" };
35732
+ return {
35733
+ applicable: true,
35734
+ // The issue's fallback ask, verbatim-shaped: the gap must be visible and actionable at the moment it opens.
35735
+ manualPointer: `dev not redeployed \u2014 run \`mmi-cli devops runtime tenant redeploy ${repo} dev\``
35736
+ };
35737
+ }
35738
+ async function dispatchDevDeploy(repo, deps) {
35739
+ const res = await tenantDeploy({ repo, stage: "dev" }, deps);
35740
+ if (res.ok) return { ok: true };
35741
+ return { ok: false, detail: res.body?.error ?? res.error ?? `HTTP ${res.status}` };
35742
+ }
35743
+
35641
35744
  // src/index.ts
35642
35745
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
35643
35746
  async function githubRepoReachProbe() {
@@ -37107,6 +37210,43 @@ ${filelessTransitionGuide(target, o.stage)}`
37107
37210
  const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
37108
37211
  return reportWrite("org project set-deploy", res);
37109
37212
  });
37213
+ 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=...`)");
37214
+ 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) => {
37215
+ const cfg = await loadConfig();
37216
+ let target;
37217
+ try {
37218
+ target = await projectTarget("org project tenant-tasks declare", o.repo);
37219
+ } catch (e) {
37220
+ return fail(e.message);
37221
+ }
37222
+ const slug = slugOf(target);
37223
+ let task;
37224
+ if (o.var) {
37225
+ try {
37226
+ task = JSON.parse(o.var);
37227
+ } catch {
37228
+ return fail("org project tenant-tasks declare: --var must be valid JSON");
37229
+ }
37230
+ } else {
37231
+ if (!o.service || !o.command?.length || !o.stages) {
37232
+ return fail("org project tenant-tasks declare: pass --var <json> or all of --service, --command, --stages");
37233
+ }
37234
+ task = {
37235
+ service: o.service,
37236
+ command: o.command,
37237
+ stages: o.stages.split(",").map((s) => s.trim()).filter(Boolean),
37238
+ ...o.timeoutSeconds !== void 0 ? { timeoutSeconds: Number(o.timeoutSeconds) } : {},
37239
+ ...o.artifact !== void 0 ? { artifact: o.artifact } : {}
37240
+ };
37241
+ }
37242
+ const res = await postJson(`/projects/${encodeURIComponent(slug)}/tenant-tasks`, { repo: target, tenantTasks: { [name]: task } }, registryClientDeps(cfg), "POST", { noRetry: true });
37243
+ if (o.json) {
37244
+ console.log(JSON.stringify(res, null, 2));
37245
+ } else {
37246
+ reportWrite("org project tenant-tasks declare", res);
37247
+ }
37248
+ if (!res.ok) process.exitCode = 1;
37249
+ });
37110
37250
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
37111
37251
  registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
37112
37252
  const cfg = await loadConfig();
@@ -38000,11 +38140,16 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38000
38140
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
38001
38141
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
38002
38142
  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) => {
38143
+ 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
38144
  if (!isGitHubRateLimitError(e) || !repoForPostCleanup) throw e;
38005
38145
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 reading PR #${number} via REST instead (#4588).`);
38006
- return (await fetchRestPrSnapshot(number, repoForPostCleanup)).headRef;
38146
+ const snapshot = await fetchRestPrSnapshot(number, repoForPostCleanup);
38147
+ return { head: snapshot.headRef, base: snapshot.baseRef };
38007
38148
  });
38149
+ const headRef = prMeta.head;
38150
+ const baseRef = prMeta.base;
38151
+ const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
38152
+ const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
38008
38153
  const closingGuardInput = await withAlreadyClosedCommitTargets(
38009
38154
  await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
38010
38155
  async (n) => {
@@ -38133,6 +38278,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38133
38278
  const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
38134
38279
  console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
38135
38280
  console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
38281
+ if (devDeployPlan.applicable) console.warn(`pr merge: ${devDeployPlan.manualPointer} once the merge lands (auto-merge does not redeploy the dev stage).`);
38136
38282
  if (upgradedToAuto && !o.auto) {
38137
38283
  console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
38138
38284
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
@@ -38156,6 +38302,8 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38156
38302
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
38157
38303
  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
38304
  invalidateStatuslineBoardCache();
38305
+ const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
38306
+ const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
38159
38307
  console.log(JSON.stringify({
38160
38308
  mergeStatus: "merged",
38161
38309
  merged: number,
@@ -38167,10 +38315,14 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38167
38315
  // tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
38168
38316
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
38169
38317
  boardAdvanceStatus: boardAdvance.status,
38170
- ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
38318
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
38319
+ ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
38171
38320
  }));
38172
38321
  const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
38173
38322
  if (boardAdvanceMessage) console.error(boardAdvanceMessage);
38323
+ if (devDeploy && !devDeploy.ok) {
38324
+ console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
38325
+ }
38174
38326
  if (crossRepoFilingIssue.status === "failed") {
38175
38327
  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
38328
  }
@@ -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.9",
3
+ "version": "4.0.11",
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",