@mutmutco/cli 3.64.0 → 3.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +106 -40
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5165,16 +5165,20 @@ function validateDeadWorktreeDirContent(planned, inspected) {
5165
5165
  return { ok: false, reason: "unknown dead worktree cleanup kind" };
5166
5166
  }
5167
5167
  function validateBranchCleanupHead(planned, currentHeads) {
5168
+ const currentHeadOid = currentHeads.find((h) => h.branch === planned.branch)?.oid;
5169
+ if (planned.containedInBase && planned.plannedHeadOid) {
5170
+ if (!currentHeadOid) return { ok: false, reason: "local branch head could not be read" };
5171
+ return currentHeadOid === planned.plannedHeadOid ? { ok: true } : { ok: false, reason: `branch moved since planning (${currentHeadOid} != ${planned.plannedHeadOid})` };
5172
+ }
5168
5173
  const reviewedHeadOids = planned.reviewedHeadOids?.filter(Boolean) ?? [];
5169
5174
  if (!reviewedHeadOids.length) {
5170
5175
  return { ok: false, reason: "planned branch head was not verified against a PR head" };
5171
5176
  }
5172
- const currentHead = currentHeads.find((h) => h.branch === planned.branch)?.oid;
5173
- if (!currentHead) {
5177
+ if (!currentHeadOid) {
5174
5178
  return { ok: false, reason: "local branch head could not be verified against the PR head" };
5175
5179
  }
5176
- if (!reviewedHeadOids.includes(currentHead)) {
5177
- return { ok: false, reason: `${currentHead} != ${reviewedHeadOids.join("|")}` };
5180
+ if (!reviewedHeadOids.includes(currentHeadOid)) {
5181
+ return { ok: false, reason: `${currentHeadOid} != ${reviewedHeadOids.join("|")}` };
5178
5182
  }
5179
5183
  return { ok: true };
5180
5184
  }
@@ -5274,6 +5278,7 @@ function buildGcPlan(inputs) {
5274
5278
  const unpushedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.unpushed).map((w) => w.branch));
5275
5279
  const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
5276
5280
  const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
5281
+ const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
5277
5282
  const skipped = [];
5278
5283
  const branches = [];
5279
5284
  const skipTrackingBranches = /* @__PURE__ */ new Set();
@@ -5313,7 +5318,8 @@ function buildGcPlan(inputs) {
5313
5318
  continue;
5314
5319
  }
5315
5320
  const localHead = branchHeads?.get(branch);
5316
- if (worktree2?.unpushed || branchHeads && (!localHead || !state.headOids.length || !state.headOids.includes(localHead))) {
5321
+ const containedInBase = mergedIntoBase.has(branch);
5322
+ if (!containedInBase && (worktree2?.unpushed || branchHeads && (!localHead || !state.headOids.length || !state.headOids.includes(localHead)))) {
5317
5323
  const detail = worktree2?.unpushed ? worktree2.path : localHead && state.headOids.length ? `${localHead} != ${state.headOids.join("|")}` : "local branch head could not be verified against the PR head";
5318
5324
  skipped.push({ branch, reason: "unpushed-branch", detail });
5319
5325
  skipTrackingBranches.add(branch);
@@ -5324,7 +5330,8 @@ function buildGcPlan(inputs) {
5324
5330
  prState: state.state,
5325
5331
  prNumbers: state.numbers,
5326
5332
  worktreePath: worktree2?.path,
5327
- ...state.headOids.length ? { reviewedHeadOids: state.headOids } : {}
5333
+ ...state.headOids.length ? { reviewedHeadOids: state.headOids } : {},
5334
+ ...containedInBase ? { containedInBase: true, ...localHead ? { plannedHeadOid: localHead } : {} } : {}
5328
5335
  });
5329
5336
  }
5330
5337
  const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
@@ -10811,7 +10818,7 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
10811
10818
  return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
10812
10819
  }
10813
10820
  async function gcPlan(remote, limit, opts = {}) {
10814
- const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved] = await Promise.all([
10821
+ const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
10815
10822
  gitOut(["branch", "--format=%(refname:short)"]),
10816
10823
  localBranchHeads(),
10817
10824
  gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
@@ -10821,7 +10828,8 @@ async function gcPlan(remote, limit, opts = {}) {
10821
10828
  ghPrs(limit),
10822
10829
  worktreeBranches(),
10823
10830
  siblingWorktreeDirs(opts.root),
10824
- preservedBranches()
10831
+ preservedBranches(),
10832
+ branchesMergedIntoBase(remote)
10825
10833
  ]);
10826
10834
  return buildGcPlan({
10827
10835
  localBranches: branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean),
@@ -10832,9 +10840,23 @@ async function gcPlan(remote, limit, opts = {}) {
10832
10840
  pullRequests: prs,
10833
10841
  worktrees,
10834
10842
  remote,
10835
- preservedBranches: preserved
10843
+ preservedBranches: preserved,
10844
+ mergedIntoBase
10836
10845
  });
10837
10846
  }
10847
+ async function branchesMergedIntoBase(remote) {
10848
+ const out = /* @__PURE__ */ new Set();
10849
+ for (const base of ["development", "main", "master"]) {
10850
+ let listed;
10851
+ try {
10852
+ listed = await gitOut(["branch", "--merged", `${remote}/${base}`, "--format=%(refname:short)"]);
10853
+ } catch {
10854
+ continue;
10855
+ }
10856
+ for (const b of listed.split(/\r?\n/).map((x) => x.trim()).filter(Boolean)) out.add(b);
10857
+ }
10858
+ return [...out];
10859
+ }
10838
10860
  var STRAY_ROOT_WALK_BUDGET_MS = 4e3;
10839
10861
  function measureWorktreeRoot(root, deadline) {
10840
10862
  let dirs = 0;
@@ -16429,20 +16451,37 @@ function readFileOrNull2(path2) {
16429
16451
  return null;
16430
16452
  }
16431
16453
  }
16432
- function classify(changed, policy) {
16454
+ function removedPaths(changed) {
16455
+ return new Set(
16456
+ changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string")
16457
+ );
16458
+ }
16459
+ function classify(changed, policy, present = () => false) {
16433
16460
  const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
16434
16461
  const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
16435
16462
  const testChanges = changed.filter((f) => isTestPath(f.path));
16436
16463
  const addedTests = testChanges.filter((f) => f.status === "A");
16464
+ const removed = removedPaths(changed);
16465
+ const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
16466
+ const untestedHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path) && !discharged(m)));
16437
16467
  const protectedBy = new Map((policy.protected ?? []).map((p) => [p.path, p.why ?? ""]));
16438
- const removedProtected = changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string" && protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
16439
- return { mandatoryHits, testChanges, addedTests, removedProtected };
16468
+ for (const m of policy.mandatory ?? []) {
16469
+ for (const p of m.satisfiedBy ?? []) {
16470
+ if (!protectedBy.has(p)) protectedBy.set(p, `the standing coverage \`${m.glob}\` is discharged by. Removing it silently empties that glob.`);
16471
+ }
16472
+ }
16473
+ const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
16474
+ return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
16440
16475
  }
16441
16476
  function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
16442
16477
  return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path17.join)(root, p)));
16443
16478
  }
16444
- function evaluate(changed, policy) {
16445
- const { mandatoryHits, testChanges, addedTests, removedProtected } = classify(changed, policy);
16479
+ function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
16480
+ const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
16481
+ return [...new Set(declared)].filter((p) => !exists((0, import_node_path17.join)(root, p)));
16482
+ }
16483
+ function evaluate(changed, policy, present = () => false) {
16484
+ const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
16446
16485
  const findings = [];
16447
16486
  if (removedProtected.length > 0) {
16448
16487
  findings.push({
@@ -16453,12 +16492,12 @@ function evaluate(changed, policy) {
16453
16492
  ${f.why}`).join("\n") + "\n If you are removing one because it looked unrequired, read the reason above first \u2014 it is\n the answer to exactly that question. If this removal was asked for, add a commit trailer:\n Test-Policy-Override: <reason>"
16454
16493
  });
16455
16494
  }
16456
- if (mandatoryHits.length > 0 && testChanges.length === 0) {
16495
+ if (untestedHits.length > 0 && testChanges.length === 0) {
16457
16496
  findings.push({
16458
16497
  kind: "mandatory-zone-untested",
16459
- paths: mandatoryHits.map((f) => f.path),
16460
- detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${mandatoryHits.length} path(s) in the mandatory zone but changes no test:
16461
- ` + mandatoryHits.map((f) => ` ${f.path}`).join("\n") + "\n These are the paths where a silent failure costs real money, real data, a security breach,\n or production. Add or update a test that would fail if the guarded property broke."
16498
+ paths: untestedHits.map((f) => f.path),
16499
+ detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${untestedHits.length} path(s) in the mandatory zone but changes no test:
16500
+ ` + untestedHits.map((f) => ` ${f.path}`).join("\n") + "\n These are the paths where a silent failure costs real money, real data, a security breach,\n or production. Add or update a test that would fail if the guarded property broke."
16462
16501
  });
16463
16502
  }
16464
16503
  if (policy.declared !== false && addedTests.length > 0 && mandatoryHits.length === 0) {
@@ -16499,28 +16538,37 @@ function runTestPolicy(root, deps = {}) {
16499
16538
  const policy = deps.policy ?? loadPolicy(root);
16500
16539
  const exists = deps.exists ?? ((path2) => (0, import_node_fs19.existsSync)(path2));
16501
16540
  const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
16502
- const unresolved = unresolvedProtectedEntries(policy, root, exists);
16541
+ const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
16542
+ const changed = deps.changed ?? (base ? changedFilesSince(base, root) : []);
16543
+ const override = deps.overrideReason !== void 0 ? deps.overrideReason : !deps.changed && base ? OVERRIDE_RE.exec(git(["log", `${base}..HEAD`, "--format=%B"], root))?.[1]?.trim() ?? null : null;
16544
+ if (override) return { ok: true, findings: [], changedCount: changed.length, base, overriddenBy: override, ...counts };
16545
+ const present = (path2) => exists((0, import_node_path17.join)(root, path2));
16546
+ const removedByThisDiff = removedPaths(changed);
16547
+ const staleFindings = [];
16548
+ const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
16503
16549
  if (unresolved.length > 0) {
16504
- return {
16505
- ok: false,
16506
- base: null,
16507
- changedCount: 0,
16508
- ...counts,
16509
- findings: [{
16510
- kind: "stale-protected-entry",
16511
- paths: unresolved,
16512
- detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
16550
+ staleFindings.push({
16551
+ kind: "stale-protected-entry",
16552
+ paths: unresolved,
16553
+ detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
16513
16554
  ` + unresolved.map((p) => ` ${p}`).join("\n") + "\n An entry naming a missing file reads as protection while protecting nothing.\n Restore the file, or remove its entry."
16514
- }]
16515
- };
16555
+ });
16556
+ }
16557
+ const staleSatisfiers = unresolvedSatisfiers(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
16558
+ if (staleSatisfiers.length > 0) {
16559
+ staleFindings.push({
16560
+ kind: "stale-satisfied-by",
16561
+ paths: staleSatisfiers,
16562
+ detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
16563
+ ` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + "\n A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.\n Restore the file, or drop it from satisfiedBy so the glob asks for a test again."
16564
+ });
16565
+ }
16566
+ if (staleFindings.length > 0) {
16567
+ return { ok: false, base, changedCount: changed.length, ...counts, findings: staleFindings };
16516
16568
  }
16517
- const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
16518
16569
  if (!deps.changed && !base) return { ok: true, findings: [], changedCount: 0, base: null, ...counts };
16519
- const changed = deps.changed ?? changedFilesSince(base, root);
16520
16570
  if (changed.length === 0) return { ok: true, findings: [], changedCount: 0, base, ...counts };
16521
- const override = deps.overrideReason !== void 0 ? deps.overrideReason : OVERRIDE_RE.exec(git(["log", `${base}..HEAD`, "--format=%B"], root))?.[1]?.trim() ?? null;
16522
- if (override) return { ok: true, findings: [], changedCount: changed.length, base, overriddenBy: override, ...counts };
16523
- const findings = evaluate(changed, policy);
16571
+ const findings = evaluate(changed, policy, present);
16524
16572
  return { ok: findings.length === 0, findings, changedCount: changed.length, base, ...counts };
16525
16573
  }
16526
16574
 
@@ -21875,7 +21923,18 @@ function recordGcWorktreeRemoval(primaryRoot, actor, owners, path2, branch) {
21875
21923
  });
21876
21924
  dropWorktreeOwner(primaryRoot, path2);
21877
21925
  }
21878
- function renderGcApplyResult(result) {
21926
+ function renderPrLandCleanupLines(cleanup) {
21927
+ const report = cleanup;
21928
+ const wt = report?.worktree;
21929
+ if (!wt?.path) return [];
21930
+ const lines = [];
21931
+ if (wt.status === "removed") lines.push(`pr land: removed worktree ${wt.path} \u2014 that directory is gone; cd elsewhere before your next command`);
21932
+ else if (wt.status === "deferred") lines.push(`pr land: worktree ${wt.path} could not be removed yet and is registered for a later sweep`);
21933
+ else lines.push(`pr land: worktree ${wt.path} \u2014 ${wt.status ?? "unknown"}${wt.reason ? ` (${wt.reason})` : ""}`);
21934
+ if (wt.deferredNote) lines.push(`pr land: ${wt.deferredNote}`);
21935
+ return lines;
21936
+ }
21937
+ function renderGcApplyResult(result, skipped = []) {
21879
21938
  const lines = ["worktree gc apply result:"];
21880
21939
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
21881
21940
  lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
@@ -21892,9 +21951,15 @@ function renderGcApplyResult(result) {
21892
21951
  }
21893
21952
  const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
21894
21953
  if (removedNothing && !result.refused.length) {
21895
- lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
21896
- lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
21897
- lines.push(" directory and re-run to clear its registration.");
21954
+ const actionable = skipped.filter((s) => s.reason !== "protected" && s.reason !== "current-branch");
21955
+ if (actionable.length) {
21956
+ lines.push(` NOTHING WAS REMOVED \u2014 every candidate was skipped for a stated reason (${actionable.length}):`);
21957
+ for (const s of actionable) lines.push(` - ${s.branch}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
21958
+ } else {
21959
+ lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
21960
+ lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
21961
+ lines.push(" directory and re-run to clear its registration.");
21962
+ }
21898
21963
  }
21899
21964
  return lines.join("\n");
21900
21965
  }
@@ -25848,7 +25913,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
25848
25913
  console.log(formatGcPlan(plan, false));
25849
25914
  } else {
25850
25915
  if (applyResult) console.log(`
25851
- ${renderGcApplyResult(applyResult)}`);
25916
+ ${renderGcApplyResult(applyResult, plan.skipped)}`);
25852
25917
  }
25853
25918
  if (applyResult?.failed.length) process.exitCode = 1;
25854
25919
  } catch (e) {
@@ -27268,6 +27333,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
27268
27333
  if (o.json) printLine(JSON.stringify(result));
27269
27334
  else {
27270
27335
  printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
27336
+ for (const line of renderPrLandCleanupLines(result.cleanup)) printLine(line);
27271
27337
  if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
27272
27338
  }
27273
27339
  if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.64.0",
3
+ "version": "3.65.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",