@mutmutco/cli 3.63.0 → 3.64.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 +447 -215
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -38,7 +38,8 @@ __export(index_exports, {
38
38
  isOrgRegisteredRepo: () => isOrgRegisteredRepo,
39
39
  registryClientDeps: () => registryClientDeps,
40
40
  repoSlug: () => repoSlug,
41
- shouldMarkWorktreeActivity: () => shouldMarkWorktreeActivity
41
+ shouldMarkWorktreeActivity: () => shouldMarkWorktreeActivity,
42
+ suggestCommandPath: () => suggestCommandPath
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
45
 
@@ -3412,8 +3413,8 @@ var program = new Command();
3412
3413
 
3413
3414
  // src/index.ts
3414
3415
  var import_promises8 = require("node:fs/promises");
3415
- var import_node_fs32 = require("node:fs");
3416
- var import_node_child_process13 = require("node:child_process");
3416
+ var import_node_fs33 = require("node:fs");
3417
+ var import_node_child_process14 = require("node:child_process");
3417
3418
 
3418
3419
  // src/cli-shared.ts
3419
3420
  var import_node_child_process3 = require("node:child_process");
@@ -3655,6 +3656,7 @@ function didYouMean(input, candidates) {
3655
3656
  const cand = strip(candidate);
3656
3657
  if (!cand) continue;
3657
3658
  if (cand === target) continue;
3659
+ if (cand === `${target}-file` || target === `${cand}-file`) continue;
3658
3660
  const distance = levenshtein(target, cand);
3659
3661
  const prefix = cand.startsWith(target) || target.startsWith(cand);
3660
3662
  const threshold = Math.max(2, Math.ceil(cand.length * 0.4));
@@ -5525,38 +5527,38 @@ function safeWorktreeRemoveCommand(safeCwd, targetPath) {
5525
5527
  function errorMessage(error) {
5526
5528
  return error instanceof Error ? error.message : String(error);
5527
5529
  }
5528
- async function fastForwardCurrentBranch(git) {
5530
+ async function fastForwardCurrentBranch(git2) {
5529
5531
  try {
5530
- const before = (await git(["rev-parse", "HEAD"])).trim();
5531
- await git(["pull", "--ff-only"]);
5532
- const after = (await git(["rev-parse", "HEAD"])).trim();
5532
+ const before = (await git2(["rev-parse", "HEAD"])).trim();
5533
+ await git2(["pull", "--ff-only"]);
5534
+ const after = (await git2(["rev-parse", "HEAD"])).trim();
5533
5535
  return { status: before === after ? "up-to-date" : "fast-forwarded" };
5534
5536
  } catch (e) {
5535
5537
  return { status: "failed", error: errorMessage(e) };
5536
5538
  }
5537
5539
  }
5538
- async function returnCheckoutToBase(git, base, mergedBranch, currentBranch2) {
5540
+ async function returnCheckoutToBase(git2, base, mergedBranch, currentBranch2) {
5539
5541
  if (currentBranch2 === mergedBranch) {
5540
- const porcelain = await git(["status", "--porcelain"]).catch(() => void 0);
5542
+ const porcelain = await git2(["status", "--porcelain"]).catch(() => void 0);
5541
5543
  const dirt = porcelain === void 0 ? "tracked-changes" : classifyWorktreeDirt(porcelain);
5542
5544
  if (!isRemovableDirt(dirt)) {
5543
5545
  return { report: { branch: base, switched: false, sync: "skipped", reason: dirt === "untracked-files" ? "untracked-files" : "dirty-worktree" }, canDeleteBranch: false };
5544
5546
  }
5545
5547
  try {
5546
- await git(["checkout", base]);
5548
+ await git2(["checkout", base]);
5547
5549
  } catch (e) {
5548
5550
  return {
5549
5551
  report: { branch: base, switched: false, sync: "skipped", reason: "checkout-failed", error: errorMessage(e) },
5550
5552
  canDeleteBranch: false
5551
5553
  };
5552
5554
  }
5553
- const ff2 = await fastForwardCurrentBranch(git);
5555
+ const ff2 = await fastForwardCurrentBranch(git2);
5554
5556
  return { report: { branch: base, switched: true, sync: ff2.status, ...ff2.error ? { error: ff2.error } : {} }, canDeleteBranch: true };
5555
5557
  }
5556
5558
  if (currentBranch2 !== base) {
5557
5559
  return { report: { branch: base, switched: false, sync: "skipped", reason: "not-on-base" }, canDeleteBranch: true };
5558
5560
  }
5559
- const ff = await fastForwardCurrentBranch(git);
5561
+ const ff = await fastForwardCurrentBranch(git2);
5560
5562
  return { report: { branch: base, switched: false, sync: ff.status, ...ff.error ? { error: ff.error } : {} }, canDeleteBranch: true };
5561
5563
  }
5562
5564
  async function cleanupPrMergeLocalBranch(branch, options) {
@@ -5597,17 +5599,17 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5597
5599
  const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath, {
5598
5600
  pathExists: options.pathExists
5599
5601
  });
5600
- const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
5602
+ const git2 = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
5601
5603
  const expectedHeadOid = options.expectedHeadOid;
5602
5604
  const verifyReviewedBranchHead = async () => {
5603
5605
  if (!expectedHeadOid) {
5604
- const remaining = await git(["branch", "--list", branch]).catch(() => "");
5606
+ const remaining = await git2(["branch", "--list", branch]).catch(() => "");
5605
5607
  report.localBranch = branchMissingFromList(branch, remaining) ? { name: branch, status: "already-gone" } : { name: branch, status: "not-attempted", reason: "branch-head-unverified" };
5606
5608
  return false;
5607
5609
  }
5608
- const currentHead = (await git(["rev-parse", `refs/heads/${branch}`]).catch(() => "") || "").trim();
5610
+ const currentHead = (await git2(["rev-parse", `refs/heads/${branch}`]).catch(() => "") || "").trim();
5609
5611
  if (!currentHead) {
5610
- const remaining = await git(["branch", "--list", branch]).catch(() => "");
5612
+ const remaining = await git2(["branch", "--list", branch]).catch(() => "");
5611
5613
  report.localBranch = branchMissingFromList(branch, remaining) ? { name: branch, status: "already-gone" } : { name: branch, status: "not-attempted", reason: "branch-head-unverified" };
5612
5614
  return false;
5613
5615
  }
@@ -5642,7 +5644,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5642
5644
  return report;
5643
5645
  }
5644
5646
  const removeDeps = {
5645
- git,
5647
+ git: git2,
5646
5648
  sleep: options.sleep ?? defaultSleep,
5647
5649
  detachReparsePoints: options.detachReparsePoints,
5648
5650
  // #3064: junction-safe deferred sweep too
@@ -5662,7 +5664,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5662
5664
  chdir: options.chdir
5663
5665
  });
5664
5666
  const outcome = await removeWorktreeWithRecovery(wtPath, {
5665
- git,
5667
+ git: git2,
5666
5668
  sleep: options.sleep ?? defaultSleep,
5667
5669
  detachReparsePoints: options.detachReparsePoints,
5668
5670
  // #3064: reach the actual pr-merge teardown path
@@ -5713,10 +5715,10 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5713
5715
  return report;
5714
5716
  }
5715
5717
  }
5716
- const current = (await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || "").trim();
5718
+ const current = (await git2(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || "").trim();
5717
5719
  if (options.returnToBranch && options.returnToBranch !== branch) {
5718
5720
  const { report: checkout, canDeleteBranch } = await returnCheckoutToBase(
5719
- git,
5721
+ git2,
5720
5722
  options.returnToBranch,
5721
5723
  branch,
5722
5724
  current
@@ -5735,16 +5737,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5735
5737
  }
5736
5738
  const reviewedHeadOid = expectedHeadOid;
5737
5739
  try {
5738
- await git(["update-ref", "-d", `refs/heads/${branch}`, reviewedHeadOid]);
5740
+ await git2(["update-ref", "-d", `refs/heads/${branch}`, reviewedHeadOid]);
5739
5741
  report.localBranch = { name: branch, status: "deleted" };
5740
5742
  } catch (e) {
5741
- const remaining = await git(["branch", "--list", branch]).catch(() => "");
5743
+ const remaining = await git2(["branch", "--list", branch]).catch(() => "");
5742
5744
  report.localBranch = branchMissingFromList(branch, remaining) ? { name: branch, status: "already-gone" } : { name: branch, status: "failed", error: errorMessage(e) };
5743
5745
  }
5744
5746
  if (report.localBranch.status === "deleted" || report.localBranch.status === "already-gone") {
5745
- await git(["config", "--local", "--unset-all", PRESERVED_WORKTREE_CONFIG, `^${escapeGitConfigValueRegex(branch)}$`]).catch(() => void 0);
5747
+ await git2(["config", "--local", "--unset-all", PRESERVED_WORKTREE_CONFIG, `^${escapeGitConfigValueRegex(branch)}$`]).catch(() => void 0);
5746
5748
  }
5747
- if (wtPath) await git(["worktree", "prune"]).catch(() => "");
5749
+ if (wtPath) await git2(["worktree", "prune"]).catch(() => "");
5748
5750
  return report;
5749
5751
  }
5750
5752
  function formatGcPlan(plan, apply) {
@@ -6242,9 +6244,9 @@ function defaultWorktreePath(repoRoot2, branch) {
6242
6244
  const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
6243
6245
  return (0, import_node_path10.join)((0, import_node_path10.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path10.basename)(repoRoot2), safe);
6244
6246
  }
6245
- async function primaryCheckoutRootOf(git) {
6247
+ async function primaryCheckoutRootOf(git2) {
6246
6248
  try {
6247
- const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
6249
+ const out = (await git2(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
6248
6250
  return out ? (0, import_node_path10.dirname)(out) : void 0;
6249
6251
  } catch {
6250
6252
  return void 0;
@@ -6321,7 +6323,7 @@ async function resolveWhoami(deps) {
6321
6323
  }
6322
6324
  function whoamiLine(report, caveat) {
6323
6325
  if (!report.login) return null;
6324
- const line = `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
6326
+ const line = `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. mmi-cli board claim <n> --for ${report.login}); do not ask who the user is.`;
6325
6327
  return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
6326
6328
  }
6327
6329
  function authorityLine(report) {
@@ -6363,7 +6365,7 @@ function commandLadderHint() {
6363
6365
  }
6364
6366
 
6365
6367
  // src/index.ts
6366
- var import_node_path30 = require("node:path");
6368
+ var import_node_path31 = require("node:path");
6367
6369
 
6368
6370
  // src/merge-ci-policy.ts
6369
6371
  function resolveMergeCiPolicy(input) {
@@ -11212,8 +11214,8 @@ function shouldSkipInactiveBoardNode(node, cfg) {
11212
11214
  }
11213
11215
  async function collectBoardItems(cfg, options, deps) {
11214
11216
  const client = deps.client ?? defaultGitHubClient();
11215
- const git = deps.git ?? defaultGit;
11216
- const currentRepo = options.repo ?? repoFromRemoteUrl(await git(["remote", "get-url", "origin"])) ?? "";
11217
+ const git2 = deps.git ?? defaultGit;
11218
+ const currentRepo = options.repo ?? repoFromRemoteUrl(await git2(["remote", "get-url", "origin"])) ?? "";
11217
11219
  if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
11218
11220
  const warnings = [];
11219
11221
  const nodes = [];
@@ -11324,8 +11326,8 @@ function findBoardItem(items, selector, board) {
11324
11326
  return found;
11325
11327
  }
11326
11328
  async function resolveCurrentRepo(options, deps) {
11327
- const git = deps.git ?? defaultGit;
11328
- const currentRepo = options.repo ?? repoFromRemoteUrl(await git(["remote", "get-url", "origin"])) ?? "";
11329
+ const git2 = deps.git ?? defaultGit;
11330
+ const currentRepo = options.repo ?? repoFromRemoteUrl(await git2(["remote", "get-url", "origin"])) ?? "";
11329
11331
  if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
11330
11332
  return currentRepo;
11331
11333
  }
@@ -11532,8 +11534,8 @@ async function claimBoardIssues(options, deps = {}) {
11532
11534
  async function moveBoardIssues(options, deps = {}) {
11533
11535
  const cfg = resolveBoardConfig(options.config);
11534
11536
  const client = deps.client ?? defaultGitHubClient();
11535
- const git = deps.git ?? defaultGit;
11536
- const currentRepo = options.repo ?? repoFromRemoteUrl(await git(["remote", "get-url", "origin"])) ?? "";
11537
+ const git2 = deps.git ?? defaultGit;
11538
+ const currentRepo = options.repo ?? repoFromRemoteUrl(await git2(["remote", "get-url", "origin"])) ?? "";
11537
11539
  if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
11538
11540
  const selectors = [];
11539
11541
  const seen = /* @__PURE__ */ new Set();
@@ -11617,8 +11619,8 @@ async function moveBoardIssues(options, deps = {}) {
11617
11619
  async function unclaimBoardIssue(options, deps = {}) {
11618
11620
  const cfg = resolveBoardConfig(options.config);
11619
11621
  const client = deps.client ?? defaultGitHubClient();
11620
- const git = deps.git ?? defaultGit;
11621
- const currentRepo = options.repo ?? repoFromRemoteUrl(await git(["remote", "get-url", "origin"])) ?? "";
11622
+ const git2 = deps.git ?? defaultGit;
11623
+ const currentRepo = options.repo ?? repoFromRemoteUrl(await git2(["remote", "get-url", "origin"])) ?? "";
11622
11624
  if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
11623
11625
  const selector = parseIssueSelector(options.selector, currentRepo);
11624
11626
  const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
@@ -12450,11 +12452,13 @@ var PRIMARY_GROUPS = [
12450
12452
  ["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
12451
12453
  ["Plan and work", ["board", "issue", "worktree", "stage"]],
12452
12454
  ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
12453
- ["Setup and support", ["bootstrap", "secrets", "docs"]],
12455
+ // `tests` sits beside `docs` deliberately: both are deterministic, repo-local gates a workflow
12456
+ // step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605).
12457
+ ["Setup and support", ["bootstrap", "secrets", "docs", "tests"]],
12454
12458
  ["Coordinate and improve", ["wave", "report", "skill-lesson"]]
12455
12459
  ];
12456
12460
  var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
12457
- var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "wave", "report", "skill-lesson"]);
12461
+ var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "wave", "report", "skill-lesson"]);
12458
12462
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
12459
12463
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
12460
12464
  var topLevelPosition = 0;
@@ -15031,31 +15035,31 @@ var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
15031
15035
  function checkHotfixCoverage(options = {}) {
15032
15036
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
15033
15037
  const ack = (options.ack ?? []).filter(Boolean);
15034
- const git = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
15038
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
15035
15039
  const revList = (range) => {
15036
- const out = git(["rev-list", "--no-merges", range]).trim();
15040
+ const out = git2(["rev-list", "--no-merges", range]).trim();
15037
15041
  return out ? out.split("\n") : [];
15038
15042
  };
15039
15043
  const isAncestor = (sha, ref) => {
15040
15044
  try {
15041
- git(["merge-base", "--is-ancestor", sha, ref]);
15045
+ git2(["merge-base", "--is-ancestor", sha, ref]);
15042
15046
  return true;
15043
15047
  } catch {
15044
15048
  return false;
15045
15049
  }
15046
15050
  };
15047
15051
  const patchId = (sha) => {
15048
- const diff = git(["show", "--no-color", "--pretty=format:", sha]);
15052
+ const diff = git2(["show", "--no-color", "--pretty=format:", sha]);
15049
15053
  if (!diff.trim()) return null;
15050
- const out = git(["patch-id", "--stable"], { input: diff }).trim();
15054
+ const out = git2(["patch-id", "--stable"], { input: diff }).trim();
15051
15055
  return out ? out.split(" ")[0] : null;
15052
15056
  };
15053
15057
  const changedPaths = (sha) => {
15054
- const out = git(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
15058
+ const out = git2(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
15055
15059
  return out ? out.split("\n") : [];
15056
15060
  };
15057
15061
  const cherrySources = (sha) => {
15058
- const message = git(["log", "-1", "--format=%B", sha]);
15062
+ const message = git2(["log", "-1", "--format=%B", sha]);
15059
15063
  return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
15060
15064
  };
15061
15065
  const mainOnly = revList(`${rcRef}..${mainRef}`);
@@ -15070,7 +15074,7 @@ function checkHotfixCoverage(options = {}) {
15070
15074
  }
15071
15075
  };
15072
15076
  const commits = mainOnly.map((sha) => {
15073
- const subject = git(["log", "-1", "--format=%s", sha]).trim();
15077
+ const subject = git2(["log", "-1", "--format=%s", sha]).trim();
15074
15078
  const paths = changedPaths(sha);
15075
15079
  if (paths.length > 0 && paths.every((p) => manifestPaths.includes(p))) {
15076
15080
  return { sha, subject, coverage: "exempt-distribution" };
@@ -15099,21 +15103,21 @@ function checkHotfixCoverage(options = {}) {
15099
15103
  }
15100
15104
  function checkHotfixCarries(options) {
15101
15105
  const { cwd = process.cwd(), branch, baseRef, targets } = options;
15102
- const git = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
15106
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
15103
15107
  const isAncestor = (sha, ref) => {
15104
15108
  try {
15105
- git(["merge-base", "--is-ancestor", sha, ref]);
15109
+ git2(["merge-base", "--is-ancestor", sha, ref]);
15106
15110
  return true;
15107
15111
  } catch {
15108
15112
  return false;
15109
15113
  }
15110
15114
  };
15111
15115
  const revList = (range) => {
15112
- const out = git(["rev-list", "--no-merges", range]).trim();
15116
+ const out = git2(["rev-list", "--no-merges", range]).trim();
15113
15117
  return out ? out.split("\n") : [];
15114
15118
  };
15115
15119
  const cherrySources = (sha) => {
15116
- const message = git(["log", "-1", "--format=%B", sha]);
15120
+ const message = git2(["log", "-1", "--format=%B", sha]);
15117
15121
  return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
15118
15122
  };
15119
15123
  const carried = /* @__PURE__ */ new Set();
@@ -16364,6 +16368,162 @@ function runDocRefs(root, deps = {}) {
16364
16368
  return { ok: findings.length === 0, findings, docCount: Object.keys(docs2).length };
16365
16369
  }
16366
16370
 
16371
+ // src/test-policy-core.ts
16372
+ var import_node_child_process10 = require("node:child_process");
16373
+ var import_node_fs19 = require("node:fs");
16374
+ var import_node_path17 = require("node:path");
16375
+ var POLICY_FILE = "test-policy.json";
16376
+ var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
16377
+ var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
16378
+ var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
16379
+ function translate(glob) {
16380
+ let out = "";
16381
+ for (let i = 0; i < glob.length; i++) {
16382
+ const c = glob[i];
16383
+ if (c === "*") {
16384
+ if (glob[i + 1] === "*") {
16385
+ if (glob[i + 2] === "/") {
16386
+ out += "(?:.*/)?";
16387
+ i += 2;
16388
+ } else {
16389
+ out += ".*";
16390
+ i += 1;
16391
+ }
16392
+ } else {
16393
+ out += "[^/]*";
16394
+ }
16395
+ continue;
16396
+ }
16397
+ if (c === "{") {
16398
+ const close = glob.indexOf("}", i);
16399
+ if (close !== -1) {
16400
+ const alts = glob.slice(i + 1, close).split(",").map((a) => translate(a));
16401
+ out += `(?:${alts.join("|")})`;
16402
+ i = close;
16403
+ continue;
16404
+ }
16405
+ }
16406
+ out += /[.+?^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
16407
+ }
16408
+ return out;
16409
+ }
16410
+ function globToRegExp(glob) {
16411
+ return new RegExp(`^${translate(glob)}$`);
16412
+ }
16413
+ function isTestPath(path2) {
16414
+ return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
16415
+ }
16416
+ function loadPolicy(root, readFile7 = readFileOrNull2) {
16417
+ const raw = readFile7((0, import_node_path17.join)(root, POLICY_FILE));
16418
+ if (raw == null) return { mandatory: [], declared: false };
16419
+ try {
16420
+ return { ...JSON.parse(raw), declared: true };
16421
+ } catch (error) {
16422
+ throw new Error(`${POLICY_FILE} is not valid JSON: ${error.message}`);
16423
+ }
16424
+ }
16425
+ function readFileOrNull2(path2) {
16426
+ try {
16427
+ return (0, import_node_fs19.readFileSync)(path2, "utf8");
16428
+ } catch {
16429
+ return null;
16430
+ }
16431
+ }
16432
+ function classify(changed, policy) {
16433
+ const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
16434
+ const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
16435
+ const testChanges = changed.filter((f) => isTestPath(f.path));
16436
+ const addedTests = testChanges.filter((f) => f.status === "A");
16437
+ 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 };
16440
+ }
16441
+ function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
16442
+ return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path17.join)(root, p)));
16443
+ }
16444
+ function evaluate(changed, policy) {
16445
+ const { mandatoryHits, testChanges, addedTests, removedProtected } = classify(changed, policy);
16446
+ const findings = [];
16447
+ if (removedProtected.length > 0) {
16448
+ findings.push({
16449
+ kind: "protected-removed",
16450
+ paths: removedProtected.map((f) => f.path),
16451
+ detail: `PROTECTED TEST FILE(S) REMOVED \u2014 this change deletes or renames away ${removedProtected.length} file(s) test-policy.json protects:
16452
+ ` + removedProtected.map((f) => ` ${f.path}
16453
+ ${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
+ });
16455
+ }
16456
+ if (mandatoryHits.length > 0 && testChanges.length === 0) {
16457
+ findings.push({
16458
+ 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."
16462
+ });
16463
+ }
16464
+ if (policy.declared !== false && addedTests.length > 0 && mandatoryHits.length === 0) {
16465
+ findings.push({
16466
+ kind: "unrequested-test-file",
16467
+ paths: addedTests.map((f) => f.path),
16468
+ detail: `UNREQUESTED TEST FILE(S) \u2014 this change adds ${addedTests.length} new test file(s) but touches nothing in the mandatory zone:
16469
+ ` + addedTests.map((f) => ` ${f.path}`).join("\n") + "\n Tests are OPT-IN in this estate. Write one only if its absence means catastrophe \u2014 money\n lost, data destroyed, a security breach, or production down. If a silent failure costs\n nothing but a re-run, delete the file and move on. If this test was asked for, add a commit\n trailer: Test-Policy-Override: <reason>"
16470
+ });
16471
+ }
16472
+ return findings;
16473
+ }
16474
+ function git(args, cwd) {
16475
+ return (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
16476
+ }
16477
+ function resolveBase(cwd, explicit) {
16478
+ const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
16479
+ for (const ref of candidates) {
16480
+ try {
16481
+ return git(["merge-base", "HEAD", ref], cwd).trim();
16482
+ } catch {
16483
+ }
16484
+ }
16485
+ return null;
16486
+ }
16487
+ function changedFilesSince(base, cwd) {
16488
+ const raw = git(["diff", "--name-status", "-M", `${base}...HEAD`], cwd).trim();
16489
+ if (!raw) return [];
16490
+ return raw.split("\n").map((line) => {
16491
+ const parts = line.split(" ");
16492
+ const norm = (p) => p.replace(/\\/g, "/");
16493
+ const row = { status: parts[0][0], path: norm(parts[parts.length - 1]) };
16494
+ if (parts.length > 2) row.from = norm(parts[1]);
16495
+ return row;
16496
+ });
16497
+ }
16498
+ function runTestPolicy(root, deps = {}) {
16499
+ const policy = deps.policy ?? loadPolicy(root);
16500
+ const exists = deps.exists ?? ((path2) => (0, import_node_fs19.existsSync)(path2));
16501
+ const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
16502
+ const unresolved = unresolvedProtectedEntries(policy, root, exists);
16503
+ 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:
16513
+ ` + 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
+ };
16516
+ }
16517
+ const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
16518
+ if (!deps.changed && !base) return { ok: true, findings: [], changedCount: 0, base: null, ...counts };
16519
+ const changed = deps.changed ?? changedFilesSince(base, root);
16520
+ 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);
16524
+ return { ok: findings.length === 0, findings, changedCount: changed.length, base, ...counts };
16525
+ }
16526
+
16367
16527
  // src/docs-audit-command.ts
16368
16528
  function serializeOutcome(outcome) {
16369
16529
  switch (outcome.kind) {
@@ -17305,8 +17465,8 @@ function writeError(res) {
17305
17465
  }
17306
17466
 
17307
17467
  // src/secrets-commands.ts
17308
- var import_node_fs19 = require("node:fs");
17309
- var import_node_path17 = require("node:path");
17468
+ var import_node_fs20 = require("node:fs");
17469
+ var import_node_path18 = require("node:path");
17310
17470
  var import_node_os5 = require("node:os");
17311
17471
 
17312
17472
  // src/project-runtime.ts
@@ -17430,18 +17590,18 @@ function collectMap(value, previous = []) {
17430
17590
  return [...previous, value];
17431
17591
  }
17432
17592
  async function decryptRailsCredentials(input) {
17433
- const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
17593
+ const appDir = (0, import_node_path18.resolve)(input.appDir ?? process.cwd());
17434
17594
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17435
17595
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
17436
- const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
17437
- const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
17596
+ const credentialsPath = (0, import_node_path18.resolve)(appDir, credentialsFile);
17597
+ const masterKeyPath = (0, import_node_path18.resolve)(appDir, masterKeyFile);
17438
17598
  const env = {
17439
17599
  ...process.env,
17440
17600
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
17441
17601
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
17442
17602
  };
17443
- if ((0, import_node_fs19.existsSync)(masterKeyPath)) {
17444
- env.RAILS_MASTER_KEY = (0, import_node_fs19.readFileSync)(masterKeyPath, "utf8").trim();
17603
+ if ((0, import_node_fs20.existsSync)(masterKeyPath)) {
17604
+ env.RAILS_MASTER_KEY = (0, import_node_fs20.readFileSync)(masterKeyPath, "utf8").trim();
17445
17605
  }
17446
17606
  const script = [
17447
17607
  'require "json"',
@@ -17451,9 +17611,9 @@ async function decryptRailsCredentials(input) {
17451
17611
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
17452
17612
  "puts JSON.generate(config.config)"
17453
17613
  ].join("\n");
17454
- const scriptDir = (0, import_node_fs19.mkdtempSync)((0, import_node_path17.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
17455
- const scriptPath = (0, import_node_path17.join)(scriptDir, "decrypt.rb");
17456
- (0, import_node_fs19.writeFileSync)(scriptPath, script, "utf8");
17614
+ const scriptDir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
17615
+ const scriptPath = (0, import_node_path18.join)(scriptDir, "decrypt.rb");
17616
+ (0, import_node_fs20.writeFileSync)(scriptPath, script, "utf8");
17457
17617
  try {
17458
17618
  const args = ["exec", "ruby", scriptPath];
17459
17619
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -17465,7 +17625,7 @@ async function decryptRailsCredentials(input) {
17465
17625
  });
17466
17626
  return JSON.parse(stdout);
17467
17627
  } finally {
17468
- (0, import_node_fs19.rmSync)(scriptDir, { recursive: true, force: true });
17628
+ (0, import_node_fs20.rmSync)(scriptDir, { recursive: true, force: true });
17469
17629
  }
17470
17630
  }
17471
17631
  async function readSecretStdin() {
@@ -17555,7 +17715,7 @@ function registerSecretsCommands(program3) {
17555
17715
  let body;
17556
17716
  if (o.file) {
17557
17717
  try {
17558
- body = (0, import_node_fs19.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
17718
+ body = (0, import_node_fs20.readFileSync)((0, import_node_path18.resolve)(o.file), "utf8");
17559
17719
  } catch (e) {
17560
17720
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
17561
17721
  }
@@ -17660,7 +17820,7 @@ function registerSecretsCommands(program3) {
17660
17820
  {
17661
17821
  ...d,
17662
17822
  decryptRailsCredentials,
17663
- removeFile: (path2) => (0, import_node_fs19.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
17823
+ removeFile: (path2) => (0, import_node_fs20.unlinkSync)((0, import_node_path18.resolve)(o.appDir ?? process.cwd(), path2))
17664
17824
  },
17665
17825
  {
17666
17826
  repo: o.repo,
@@ -17854,7 +18014,7 @@ function checkGithubPools(probe) {
17854
18014
  }
17855
18015
 
17856
18016
  // src/box-commands.ts
17857
- var import_node_fs20 = require("node:fs");
18017
+ var import_node_fs21 = require("node:fs");
17858
18018
 
17859
18019
  // src/box.ts
17860
18020
  var BOX_KEYS = {
@@ -18057,7 +18217,7 @@ function registerBoxCommands(program3) {
18057
18217
  }
18058
18218
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
18059
18219
  else if (o.ssh && o.script) {
18060
- (0, import_node_fs20.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
18220
+ (0, import_node_fs21.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
18061
18221
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
18062
18222
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
18063
18223
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -18072,7 +18232,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
18072
18232
 
18073
18233
  // src/schedules-commands.ts
18074
18234
  var import_promises2 = require("node:fs/promises");
18075
- var import_node_child_process10 = require("node:child_process");
18235
+ var import_node_child_process11 = require("node:child_process");
18076
18236
  var import_node_util7 = require("node:util");
18077
18237
 
18078
18238
  // src/schedules.ts
@@ -18512,7 +18672,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
18512
18672
  }
18513
18673
 
18514
18674
  // src/schedules-commands.ts
18515
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process10.execFile);
18675
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process11.execFile);
18516
18676
  var AWS_REGION = "eu-central-1";
18517
18677
  var AWS_TIMEOUT_MS = 3e4;
18518
18678
  var AWS_RETRY_DELAY_MS = 1500;
@@ -18749,7 +18909,7 @@ function registerSchedulesCommands(program3) {
18749
18909
 
18750
18910
  // src/file-lock.ts
18751
18911
  var import_promises3 = require("node:fs/promises");
18752
- var import_node_path18 = require("node:path");
18912
+ var import_node_path19 = require("node:path");
18753
18913
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
18754
18914
  var IMMEDIATE_RETRY_BUDGET = 3;
18755
18915
  var FileLockBusyError = class extends Error {
@@ -18834,7 +18994,7 @@ async function releaseFileLock(lockPath, guard) {
18834
18994
  }
18835
18995
  async function withFileLock(lockPath, opts, fn) {
18836
18996
  const resolved = resolveFileLockOpts(opts);
18837
- await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(lockPath), { recursive: true }).catch(() => void 0);
18997
+ await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(lockPath), { recursive: true }).catch(() => void 0);
18838
18998
  const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
18839
18999
  try {
18840
19000
  return await fn();
@@ -18845,7 +19005,7 @@ async function withFileLock(lockPath, opts, fn) {
18845
19005
 
18846
19006
  // src/schedules-lift-command.ts
18847
19007
  var import_promises4 = require("node:fs/promises");
18848
- var import_node_path19 = require("node:path");
19008
+ var import_node_path20 = require("node:path");
18849
19009
 
18850
19010
  // src/schedules-lift.ts
18851
19011
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -18951,7 +19111,7 @@ async function readWorkflowFiles(dir) {
18951
19111
  const files = [];
18952
19112
  for (const name of names.sort()) {
18953
19113
  if (!/\.ya?ml$/.test(name)) continue;
18954
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path19.join)(dir, name), "utf8") });
19114
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path20.join)(dir, name), "utf8") });
18955
19115
  }
18956
19116
  return files;
18957
19117
  }
@@ -19493,7 +19653,7 @@ function registerQueryCommands(program3) {
19493
19653
  }
19494
19654
 
19495
19655
  // src/bootstrap-commands.ts
19496
- var import_node_fs21 = require("node:fs");
19656
+ var import_node_fs22 = require("node:fs");
19497
19657
 
19498
19658
  // src/bootstrap-verify.ts
19499
19659
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -20036,7 +20196,7 @@ function registerBootstrapCommands(program3) {
20036
20196
  client: defaultGitHubClient(),
20037
20197
  projectMeta: meta,
20038
20198
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
20039
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
20199
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
20040
20200
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
20041
20201
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
20042
20202
  requiredGcpApis: (() => {
@@ -20091,12 +20251,12 @@ function registerBootstrapCommands(program3) {
20091
20251
  return fail(`bootstrap apply: ${e.message}`);
20092
20252
  }
20093
20253
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
20094
- if (!(0, import_node_fs21.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`);
20095
- const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
20254
+ if (!(0, import_node_fs22.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`);
20255
+ const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
20096
20256
  const baseBranch = o.class === "content" ? "main" : "development";
20097
20257
  const slug = parsedRepo.slug;
20098
20258
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
20099
- const readFile7 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
20259
+ const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
20100
20260
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
20101
20261
  const rawVars = {};
20102
20262
  for (const value of cmdOpts.var ?? []) {
@@ -20330,12 +20490,12 @@ LIVE apply to ${repo}:
20330
20490
  }
20331
20491
 
20332
20492
  // src/stage-commands.ts
20333
- var import_node_fs23 = require("node:fs");
20334
- var import_node_path21 = require("node:path");
20493
+ var import_node_fs24 = require("node:fs");
20494
+ var import_node_path22 = require("node:path");
20335
20495
 
20336
20496
  // src/port-registry.ts
20337
- var import_node_fs22 = require("node:fs");
20338
- var import_node_path20 = require("node:path");
20497
+ var import_node_fs23 = require("node:fs");
20498
+ var import_node_path21 = require("node:path");
20339
20499
 
20340
20500
  // ../infra/port-geometry.mjs
20341
20501
  var PORT_BLOCK = 100;
@@ -20349,8 +20509,8 @@ function nextPortBlock(registry2) {
20349
20509
  return [base, base + PORT_SPAN];
20350
20510
  }
20351
20511
  function loadPortRegistry(path2) {
20352
- if (!(0, import_node_fs22.existsSync)(path2)) return {};
20353
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
20512
+ if (!(0, import_node_fs23.existsSync)(path2)) return {};
20513
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
20354
20514
  const out = {};
20355
20515
  for (const [key, value] of Object.entries(raw)) {
20356
20516
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -20364,9 +20524,9 @@ function ensurePortRange(repo, path2) {
20364
20524
  const existing = registry2[repo];
20365
20525
  if (existing) return existing;
20366
20526
  const range = nextPortBlock(registry2);
20367
- const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
20527
+ const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
20368
20528
  raw[repo] = range;
20369
- (0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
20529
+ (0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
20370
20530
  return range;
20371
20531
  }
20372
20532
  function portCursorSeed(registry2) {
@@ -20388,22 +20548,22 @@ function existingPortRange(repo, registry2) {
20388
20548
  return registry2[repo] ?? null;
20389
20549
  }
20390
20550
  function portRangeInfraAt(root, source) {
20391
- const registryPath = (0, import_node_path20.join)(root, "infra", "port-ranges.json");
20392
- const ddbScriptPath = (0, import_node_path20.join)(root, "infra", "port-ddb.mjs");
20393
- if (!(0, import_node_fs22.existsSync)(registryPath) || !(0, import_node_fs22.existsSync)(ddbScriptPath)) return null;
20551
+ const registryPath = (0, import_node_path21.join)(root, "infra", "port-ranges.json");
20552
+ const ddbScriptPath = (0, import_node_path21.join)(root, "infra", "port-ddb.mjs");
20553
+ if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
20394
20554
  return { root, source, registryPath, ddbScriptPath };
20395
20555
  }
20396
20556
  function resolvePortRangeInfra(cwd, packageDir) {
20397
20557
  const direct = portRangeInfraAt(cwd, "cwd");
20398
20558
  if (direct) return direct;
20399
- for (let dir = cwd; ; dir = (0, import_node_path20.dirname)(dir)) {
20400
- const sibling = portRangeInfraAt((0, import_node_path20.join)(dir, "MMI-Hub"), "sibling-hub");
20559
+ for (let dir = cwd; ; dir = (0, import_node_path21.dirname)(dir)) {
20560
+ const sibling = portRangeInfraAt((0, import_node_path21.join)(dir, "MMI-Hub"), "sibling-hub");
20401
20561
  if (sibling) return sibling;
20402
- const parent = (0, import_node_path20.dirname)(dir);
20562
+ const parent = (0, import_node_path21.dirname)(dir);
20403
20563
  if (parent === dir) break;
20404
20564
  }
20405
20565
  if (packageDir) {
20406
- const pkgRoot = (0, import_node_path20.join)(packageDir, "..", "..");
20566
+ const pkgRoot = (0, import_node_path21.join)(packageDir, "..", "..");
20407
20567
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
20408
20568
  if (pkgFrom) return pkgFrom;
20409
20569
  }
@@ -20579,8 +20739,8 @@ function registerStageCommands(program3) {
20579
20739
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
20580
20740
  return decideStage({
20581
20741
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
20582
- hasCompose: (0, import_node_fs23.existsSync)((0, import_node_path21.join)(process.cwd(), "docker-compose.yml")),
20583
- hasEnvExample: (0, import_node_fs23.existsSync)((0, import_node_path21.join)(process.cwd(), ".env.example"))
20742
+ hasCompose: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), "docker-compose.yml")),
20743
+ hasEnvExample: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), ".env.example"))
20584
20744
  });
20585
20745
  }
20586
20746
  async function fetchStageVaultEnvMerge() {
@@ -21012,10 +21172,10 @@ function registerBoardCommands(program3) {
21012
21172
  }
21013
21173
 
21014
21174
  // src/merge-cleanup.ts
21015
- var import_node_fs26 = require("node:fs");
21175
+ var import_node_fs27 = require("node:fs");
21016
21176
  var import_promises6 = require("node:fs/promises");
21017
- var import_node_path25 = require("node:path");
21018
- var import_node_child_process11 = require("node:child_process");
21177
+ var import_node_path26 = require("node:path");
21178
+ var import_node_child_process12 = require("node:child_process");
21019
21179
 
21020
21180
  // src/board-advance.ts
21021
21181
  function repoOf2(ref) {
@@ -21101,7 +21261,7 @@ function boardAdvanceFailureMessage(result) {
21101
21261
 
21102
21262
  // src/deferred-registry-store.ts
21103
21263
  var import_promises5 = require("node:fs/promises");
21104
- var import_node_path22 = require("node:path");
21264
+ var import_node_path23 = require("node:path");
21105
21265
  var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
21106
21266
  async function atomicWrite(target, contents) {
21107
21267
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -21152,12 +21312,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
21152
21312
  },
21153
21313
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
21154
21314
  write: async (entries) => {
21155
- await (0, import_promises5.mkdir)((0, import_node_path22.dirname)(registryPath), { recursive: true });
21315
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
21156
21316
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
21157
21317
  },
21158
21318
  // Serialized read-modify-write under the repo-wide lock (#2846).
21159
21319
  update: async (mutate) => {
21160
- await (0, import_promises5.mkdir)((0, import_node_path22.dirname)(registryPath), { recursive: true });
21320
+ await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
21161
21321
  const deadline = Date.now() + opts.maxWaitMs;
21162
21322
  for (; ; ) {
21163
21323
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -21181,8 +21341,8 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
21181
21341
  }
21182
21342
 
21183
21343
  // src/plugin-guard-io.ts
21184
- var import_node_fs24 = require("node:fs");
21185
- var import_node_path23 = require("node:path");
21344
+ var import_node_fs25 = require("node:fs");
21345
+ var import_node_path24 = require("node:path");
21186
21346
  var import_node_os6 = require("node:os");
21187
21347
 
21188
21348
  // src/plugin-guard.ts
@@ -21315,11 +21475,11 @@ function runHostBin(bin, args, opts) {
21315
21475
  }
21316
21476
  var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
21317
21477
  const homeDir = surface === "codex" ? ".codex" : ".claude";
21318
- return (0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21478
+ return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21319
21479
  };
21320
21480
  function readInstalledPlugins(surface = detectSurface(process.env)) {
21321
21481
  try {
21322
- return JSON.parse((0, import_node_fs24.readFileSync)(installedPluginsPath2(surface), "utf8"));
21482
+ return JSON.parse((0, import_node_fs25.readFileSync)(installedPluginsPath2(surface), "utf8"));
21323
21483
  } catch {
21324
21484
  return null;
21325
21485
  }
@@ -21327,13 +21487,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
21327
21487
  function marketplaceCloneCandidates(surface, home) {
21328
21488
  if (surface === "codex") {
21329
21489
  return [
21330
- (0, import_node_path23.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
21331
- (0, import_node_path23.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
21490
+ (0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
21491
+ (0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
21332
21492
  ];
21333
21493
  }
21334
- return [(0, import_node_path23.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21494
+ return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21335
21495
  }
21336
- function marketplaceClonePresent(surface, home, exists = import_node_fs24.existsSync) {
21496
+ function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync) {
21337
21497
  return marketplaceCloneCandidates(surface, home).some(exists);
21338
21498
  }
21339
21499
  async function fetchNpmReleasedVersion() {
@@ -21361,7 +21521,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
21361
21521
  isOrgRepo,
21362
21522
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
21363
21523
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
21364
- pluginCachePresent: (0, import_node_fs24.existsSync)((0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21524
+ pluginCachePresent: (0, import_node_fs25.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21365
21525
  };
21366
21526
  }
21367
21527
  function claudePluginGuardState(isOrgRepo) {
@@ -21407,7 +21567,7 @@ async function applyPluginHeal(surface, log, opts) {
21407
21567
  const refSupported = await marketplaceAddRefSupported("claude");
21408
21568
  const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
21409
21569
  log(healBannerLine("claude", "claude", refSupported));
21410
- const pinsPath = (0, import_node_path23.join)((0, import_node_os6.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
21570
+ const pinsPath = (0, import_node_path24.join)((0, import_node_os6.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
21411
21571
  const pins = captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]);
21412
21572
  for (const step of steps) {
21413
21573
  if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
@@ -21430,7 +21590,7 @@ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
21430
21590
  }
21431
21591
  function readKnownMarketplacesFile(path2) {
21432
21592
  try {
21433
- return (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : void 0;
21593
+ return (0, import_node_fs25.existsSync)(path2) ? (0, import_node_fs25.readFileSync)(path2, "utf8") : void 0;
21434
21594
  } catch {
21435
21595
  return void 0;
21436
21596
  }
@@ -21441,7 +21601,7 @@ function restoreMarketplacePinsOnDisk(path2, pins) {
21441
21601
  const next = restoreMarketplacePins(after, pins);
21442
21602
  if (next === null) return void 0;
21443
21603
  try {
21444
- (0, import_node_fs24.writeFileSync)(path2, next, "utf8");
21604
+ (0, import_node_fs25.writeFileSync)(path2, next, "utf8");
21445
21605
  } catch {
21446
21606
  return `could NOT restore ${[...pins.keys()].join(", ")} \u2014 re-pin by hand`;
21447
21607
  }
@@ -21488,9 +21648,9 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
21488
21648
  }
21489
21649
 
21490
21650
  // src/worktree-ownership.ts
21491
- var import_node_fs25 = require("node:fs");
21651
+ var import_node_fs26 = require("node:fs");
21492
21652
  var import_node_os7 = require("node:os");
21493
- var import_node_path24 = require("node:path");
21653
+ var import_node_path25 = require("node:path");
21494
21654
  var OWNERS_FILE = "worktree-owners.json";
21495
21655
  var EVENTS_FILE = "worktree-events.jsonl";
21496
21656
  var WORKTREE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -21587,7 +21747,7 @@ function describeActorShort(actor) {
21587
21747
  }
21588
21748
  function readOwners(primaryRoot) {
21589
21749
  try {
21590
- return parseWorktreeOwners((0, import_node_fs25.readFileSync)(worktreeOwnersPath(primaryRoot), "utf8"));
21750
+ return parseWorktreeOwners((0, import_node_fs26.readFileSync)(worktreeOwnersPath(primaryRoot), "utf8"));
21591
21751
  } catch {
21592
21752
  return [];
21593
21753
  }
@@ -21595,8 +21755,8 @@ function readOwners(primaryRoot) {
21595
21755
  function writeOwners(primaryRoot, entries) {
21596
21756
  try {
21597
21757
  const path2 = worktreeOwnersPath(primaryRoot);
21598
- (0, import_node_fs25.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
21599
- (0, import_node_fs25.writeFileSync)(path2, serializeWorktreeOwners(entries), "utf8");
21758
+ (0, import_node_fs26.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
21759
+ (0, import_node_fs26.writeFileSync)(path2, serializeWorktreeOwners(entries), "utf8");
21600
21760
  } catch {
21601
21761
  }
21602
21762
  }
@@ -21629,8 +21789,8 @@ function dropWorktreeOwner(primaryRoot, path2) {
21629
21789
  function appendWorktreeEvent(primaryRoot, event) {
21630
21790
  try {
21631
21791
  const path2 = worktreeEventsPath(primaryRoot);
21632
- (0, import_node_fs25.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
21633
- (0, import_node_fs25.appendFileSync)(path2, `${JSON.stringify({ at: event.at ?? (/* @__PURE__ */ new Date()).toISOString(), ...event })}
21792
+ (0, import_node_fs26.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
21793
+ (0, import_node_fs26.appendFileSync)(path2, `${JSON.stringify({ at: event.at ?? (/* @__PURE__ */ new Date()).toISOString(), ...event })}
21634
21794
  `, "utf8");
21635
21795
  } catch {
21636
21796
  }
@@ -21638,7 +21798,7 @@ function appendWorktreeEvent(primaryRoot, event) {
21638
21798
  function readWorktreeEvents(primaryRoot, limit = 50) {
21639
21799
  let text;
21640
21800
  try {
21641
- text = (0, import_node_fs25.readFileSync)(worktreeEventsPath(primaryRoot), "utf8");
21801
+ text = (0, import_node_fs26.readFileSync)(worktreeEventsPath(primaryRoot), "utf8");
21642
21802
  } catch {
21643
21803
  return [];
21644
21804
  }
@@ -21788,7 +21948,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
21788
21948
  );
21789
21949
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
21790
21950
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
21791
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path25.dirname)((0, import_node_path25.dirname)(worktreeGitRoot)) : repoRoot2;
21951
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path26.dirname)((0, import_node_path26.dirname)(worktreeGitRoot)) : repoRoot2;
21792
21952
  const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
21793
21953
  const owners = readWorktreeOwners(primaryRepoRoot);
21794
21954
  const removalNow = Date.now();
@@ -21819,7 +21979,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
21819
21979
  const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
21820
21980
  beforeWorktrees,
21821
21981
  startingPath: branch.worktreePath,
21822
- pathExists: (p) => (0, import_node_fs26.existsSync)(p),
21982
+ pathExists: (p) => (0, import_node_fs27.existsSync)(p),
21823
21983
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
21824
21984
  teardownWorktreeStage,
21825
21985
  deferredStore,
@@ -21847,7 +22007,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
21847
22007
  for (const wt of worktreeDirsToRemove) {
21848
22008
  try {
21849
22009
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
21850
- realpath: (path2) => (0, import_node_fs26.realpathSync)(path2)
22010
+ realpath: (path2) => (0, import_node_fs27.realpathSync)(path2)
21851
22011
  });
21852
22012
  if (!cleanupTarget.ok) {
21853
22013
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -21998,7 +22158,7 @@ async function remoteBranchExists2(branch, options = {}) {
21998
22158
  }
21999
22159
  var COMPOSE_TIMEOUT_MS = 12e4;
22000
22160
  function spawnDeferredGcSweep() {
22001
- spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
22161
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
22002
22162
  }
22003
22163
  async function createDeferredWorktreeStore() {
22004
22164
  try {
@@ -22012,13 +22172,13 @@ var realWorktreeDirRemover = {
22012
22172
  probe: (p) => {
22013
22173
  let st;
22014
22174
  try {
22015
- st = (0, import_node_fs26.lstatSync)(p);
22175
+ st = (0, import_node_fs27.lstatSync)(p);
22016
22176
  } catch {
22017
22177
  return null;
22018
22178
  }
22019
22179
  if (st.isSymbolicLink()) return "link";
22020
22180
  try {
22021
- (0, import_node_fs26.readlinkSync)(p);
22181
+ (0, import_node_fs27.readlinkSync)(p);
22022
22182
  return "link";
22023
22183
  } catch {
22024
22184
  }
@@ -22026,7 +22186,7 @@ var realWorktreeDirRemover = {
22026
22186
  },
22027
22187
  readdir: (p) => {
22028
22188
  try {
22029
- return (0, import_node_fs26.readdirSync)(p);
22189
+ return (0, import_node_fs27.readdirSync)(p);
22030
22190
  } catch {
22031
22191
  return [];
22032
22192
  }
@@ -22035,9 +22195,9 @@ var realWorktreeDirRemover = {
22035
22195
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
22036
22196
  detachLink: (p) => {
22037
22197
  try {
22038
- (0, import_node_fs26.rmdirSync)(p);
22198
+ (0, import_node_fs27.rmdirSync)(p);
22039
22199
  } catch {
22040
- (0, import_node_fs26.unlinkSync)(p);
22200
+ (0, import_node_fs27.unlinkSync)(p);
22041
22201
  }
22042
22202
  },
22043
22203
  removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -22070,9 +22230,9 @@ async function worktreeHasStageState(worktreePath) {
22070
22230
  }
22071
22231
  }
22072
22232
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
22073
- if (!(0, import_node_fs26.existsSync)(statePath)) return false;
22233
+ if (!(0, import_node_fs27.existsSync)(statePath)) return false;
22074
22234
  try {
22075
- const state = JSON.parse((0, import_node_fs26.readFileSync)(statePath, "utf8"));
22235
+ const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
22076
22236
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
22077
22237
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
22078
22238
  } catch {
@@ -22279,8 +22439,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
22279
22439
  }
22280
22440
 
22281
22441
  // src/worktree-lifecycle-commands.ts
22282
- var import_node_fs27 = require("node:fs");
22283
- var import_node_path26 = require("node:path");
22442
+ var import_node_fs28 = require("node:fs");
22443
+ var import_node_path27 = require("node:path");
22284
22444
  var GH_TIMEOUT_MS = 2e4;
22285
22445
  var DEFAULT_BASE = "origin/development";
22286
22446
  var DEFAULT_REMOTE = "origin";
@@ -22383,7 +22543,7 @@ function classifyStaleLeaks(input) {
22383
22543
  var defaultOrphanDirScanDeps = {
22384
22544
  listDirs: (root) => {
22385
22545
  try {
22386
- return (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path26.join)(root, e.name));
22546
+ return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path27.join)(root, e.name));
22387
22547
  } catch {
22388
22548
  return [];
22389
22549
  }
@@ -22530,13 +22690,13 @@ function registerWorktreeCommands(program3) {
22530
22690
  if (PROTECTED_BRANCHES2.has(branch)) {
22531
22691
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
22532
22692
  }
22533
- const gitFile = (0, import_node_path26.join)(wtPath, ".git");
22534
- const isLinked = (0, import_node_fs27.existsSync)(gitFile) && (0, import_node_fs27.statSync)(gitFile).isFile();
22693
+ const gitFile = (0, import_node_path27.join)(wtPath, ".git");
22694
+ const isLinked = (0, import_node_fs28.existsSync)(gitFile) && (0, import_node_fs28.statSync)(gitFile).isFile();
22535
22695
  if (apply && !isLinked) {
22536
22696
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
22537
22697
  }
22538
22698
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
22539
- const primaryCheckout = commonDir ? (0, import_node_path26.dirname)(commonDir) : wtPath;
22699
+ const primaryCheckout = commonDir ? (0, import_node_path27.dirname)(commonDir) : wtPath;
22540
22700
  const stageSummary = readStageSummary(wtPath);
22541
22701
  const hasStage = stageSummary != null;
22542
22702
  const steps = landSteps(branch, true, hasStage);
@@ -22688,10 +22848,10 @@ async function gatherWorktreeContext() {
22688
22848
  if (s) stages.push({ path: wt.path, port: s.port });
22689
22849
  }
22690
22850
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
22691
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path26.dirname)((0, import_node_path26.dirname)(worktreeGitRoot)) : repoRoot2;
22851
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path27.dirname)((0, import_node_path27.dirname)(worktreeGitRoot)) : repoRoot2;
22692
22852
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
22693
22853
  let orphanDirs = [];
22694
- if ((0, import_node_fs27.existsSync)(wtRoot)) {
22854
+ if ((0, import_node_fs28.existsSync)(wtRoot)) {
22695
22855
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
22696
22856
  ...defaultOrphanDirScanDeps,
22697
22857
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
@@ -22714,7 +22874,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
22714
22874
  }
22715
22875
 
22716
22876
  // src/issue-commands.ts
22717
- var import_node_fs28 = require("node:fs");
22877
+ var import_node_fs29 = require("node:fs");
22718
22878
  var import_node_crypto5 = require("node:crypto");
22719
22879
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
22720
22880
  async function editIssue(client, options, deps = {}) {
@@ -22724,11 +22884,16 @@ async function editIssue(client, options, deps = {}) {
22724
22884
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
22725
22885
  const patch = {};
22726
22886
  let bodyChanged = false;
22727
- if (options.title !== void 0) patch.title = options.title;
22887
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs29.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
22888
+ if (options.titleFile !== void 0) {
22889
+ patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
22890
+ } else if (options.title !== void 0) {
22891
+ patch.title = options.title;
22892
+ }
22728
22893
  if (options.body !== void 0 || options.bodyFile !== void 0) {
22729
22894
  let body;
22730
22895
  if (options.bodyFile) {
22731
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs28.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
22896
+ const td = textDeps();
22732
22897
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
22733
22898
  } else {
22734
22899
  body = options.body ?? "";
@@ -23093,7 +23258,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
23093
23258
  const issue2 = program3.commands.find((c) => c.name() === "issue");
23094
23259
  if (!issue2) return;
23095
23260
  mutating(
23096
- issue2.command("edit <ref>").description("edit an issue title/body/labels or reparent it (--parent) \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
23261
+ issue2.command("edit <ref>").description("edit an issue title/body/labels or reparent it (--parent) \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--title-file <path|->", "read the new title from a UTF-8 file, or stdin with - (a title with backticks or newlines needs this, #3381)").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
23097
23262
  (opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
23098
23263
  ).action(async (ref, o) => {
23099
23264
  try {
@@ -23102,6 +23267,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
23102
23267
  ref,
23103
23268
  defaultRepo,
23104
23269
  title: o.title,
23270
+ titleFile: o.titleFile,
23105
23271
  body: o.body,
23106
23272
  bodyFile: o.bodyFile,
23107
23273
  addLabel: o.addLabel,
@@ -23225,7 +23391,7 @@ function extendCreateCommand(issue2, batchAttach) {
23225
23391
  if (opts.batch) {
23226
23392
  let specs;
23227
23393
  try {
23228
- const raw = (0, import_node_fs28.readFileSync)(opts.batch, "utf8");
23394
+ const raw = (0, import_node_fs29.readFileSync)(opts.batch, "utf8");
23229
23395
  specs = JSON.parse(raw);
23230
23396
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
23231
23397
  } catch (e) {
@@ -23283,8 +23449,8 @@ ${lines}`);
23283
23449
  }
23284
23450
 
23285
23451
  // src/train-commands.ts
23286
- var import_node_fs29 = require("node:fs");
23287
- var import_node_path27 = require("node:path");
23452
+ var import_node_fs30 = require("node:fs");
23453
+ var import_node_path28 = require("node:path");
23288
23454
 
23289
23455
  // src/train-status.ts
23290
23456
  function buildTrainStatusReport(input) {
@@ -23324,7 +23490,7 @@ function formatTrainStatus(r) {
23324
23490
  // src/train-commands.ts
23325
23491
  function readRepoVersion() {
23326
23492
  try {
23327
- return JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
23493
+ return JSON.parse((0, import_node_fs30.readFileSync)((0, import_node_path28.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
23328
23494
  } catch {
23329
23495
  return void 0;
23330
23496
  }
@@ -23470,9 +23636,9 @@ function registerDeployCommands(program3) {
23470
23636
  }
23471
23637
 
23472
23638
  // src/discovery-commands.ts
23473
- var import_node_fs30 = require("node:fs");
23639
+ var import_node_fs31 = require("node:fs");
23474
23640
  var import_node_os8 = require("node:os");
23475
- var import_node_path28 = require("node:path");
23641
+ var import_node_path29 = require("node:path");
23476
23642
  var GC_GH_TIMEOUT_MS3 = 2e4;
23477
23643
  async function collectStatus() {
23478
23644
  let branch = "";
@@ -23649,8 +23815,8 @@ async function collectOnboardStatus() {
23649
23815
  }
23650
23816
  const home = (0, import_node_os8.homedir)();
23651
23817
  const plugin = onboardPluginGate({
23652
- readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
23653
- readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
23818
+ readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs31.readFileSync),
23819
+ readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs31.readFileSync)
23654
23820
  });
23655
23821
  return { track, board, registry: registry2, secrets, plugin, nextCommand };
23656
23822
  }
@@ -25010,17 +25176,17 @@ function parseOriginRepo(remoteUrl) {
25010
25176
  }
25011
25177
  function ghHostsConfigPath(env, platform2) {
25012
25178
  const sep2 = platform2 === "win32" ? "\\" : "/";
25013
- const join25 = (...parts) => parts.join(sep2);
25179
+ const join26 = (...parts) => parts.join(sep2);
25014
25180
  const explicit = env.GH_CONFIG_DIR?.trim();
25015
- if (explicit) return join25(explicit, "hosts.yml");
25181
+ if (explicit) return join26(explicit, "hosts.yml");
25016
25182
  if (platform2 === "win32") {
25017
25183
  const appData = (env.AppData ?? env.APPDATA)?.trim();
25018
- return appData ? join25(appData, "GitHub CLI", "hosts.yml") : void 0;
25184
+ return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
25019
25185
  }
25020
25186
  const xdg = env.XDG_CONFIG_HOME?.trim();
25021
- if (xdg) return join25(xdg, "gh", "hosts.yml");
25187
+ if (xdg) return join26(xdg, "gh", "hosts.yml");
25022
25188
  const home = env.HOME?.trim();
25023
- return home ? join25(home, ".config", "gh", "hosts.yml") : void 0;
25189
+ return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
25024
25190
  }
25025
25191
  function parseGhHostsAccounts(yaml, host = "github.com") {
25026
25192
  let hostIndent = null;
@@ -25070,17 +25236,17 @@ function ghAccountCaveat(announcedLogin, accounts) {
25070
25236
  }
25071
25237
 
25072
25238
  // src/doctor-io.ts
25073
- var import_node_fs31 = require("node:fs");
25239
+ var import_node_fs32 = require("node:fs");
25074
25240
  var import_node_os9 = require("node:os");
25075
- var import_node_path29 = require("node:path");
25076
- var import_node_child_process12 = require("node:child_process");
25241
+ var import_node_path30 = require("node:path");
25242
+ var import_node_child_process13 = require("node:child_process");
25077
25243
  var import_node_util8 = require("node:util");
25078
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
25244
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process13.execFile);
25079
25245
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
25080
25246
  function installedClaudePluginVersion() {
25081
25247
  try {
25082
25248
  const file = JSON.parse(
25083
- (0, import_node_fs31.readFileSync)((0, import_node_path29.join)((0, import_node_os9.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
25249
+ (0, import_node_fs32.readFileSync)((0, import_node_path30.join)((0, import_node_os9.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
25084
25250
  );
25085
25251
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
25086
25252
  if (versions.length === 0) return void 0;
@@ -25091,7 +25257,7 @@ function installedClaudePluginVersion() {
25091
25257
  }
25092
25258
  function worktreeRootSync() {
25093
25259
  try {
25094
- const out = (0, import_node_child_process12.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
25260
+ const out = (0, import_node_child_process13.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
25095
25261
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
25096
25262
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
25097
25263
  return root || null;
@@ -25101,13 +25267,13 @@ function worktreeRootSync() {
25101
25267
  }
25102
25268
  var gitignorePath = () => {
25103
25269
  const root = worktreeRootSync();
25104
- return root === null ? null : (0, import_node_path29.join)(root, ".gitignore");
25270
+ return root === null ? null : (0, import_node_path30.join)(root, ".gitignore");
25105
25271
  };
25106
25272
  function readGitignore() {
25107
25273
  const path2 = gitignorePath();
25108
25274
  if (path2 === null) return null;
25109
25275
  try {
25110
- return (0, import_node_fs31.readFileSync)(path2, "utf8");
25276
+ return (0, import_node_fs32.readFileSync)(path2, "utf8");
25111
25277
  } catch {
25112
25278
  return null;
25113
25279
  }
@@ -25116,7 +25282,7 @@ function writeGitignore(content) {
25116
25282
  const path2 = gitignorePath();
25117
25283
  if (path2 === null) return false;
25118
25284
  try {
25119
- (0, import_node_fs31.writeFileSync)(path2, content, "utf8");
25285
+ (0, import_node_fs32.writeFileSync)(path2, content, "utf8");
25120
25286
  return true;
25121
25287
  } catch {
25122
25288
  return false;
@@ -25140,7 +25306,7 @@ async function repoRoot() {
25140
25306
  }
25141
25307
  function hasRepoLocalWorktrees() {
25142
25308
  const root = worktreeRootSync();
25143
- return root !== null && (0, import_node_fs31.existsSync)((0, import_node_path29.join)(root, ".worktrees"));
25309
+ return root !== null && (0, import_node_fs32.existsSync)((0, import_node_path30.join)(root, ".worktrees"));
25144
25310
  }
25145
25311
 
25146
25312
  // src/index.ts
@@ -25175,8 +25341,8 @@ ${r.stderr ?? ""}`).catch(() => "");
25175
25341
  function ghMultiAccountCaveat(announcedLogin) {
25176
25342
  try {
25177
25343
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
25178
- if (!hostsPath || !(0, import_node_fs32.existsSync)(hostsPath)) return void 0;
25179
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs32.readFileSync)(hostsPath, "utf8")));
25344
+ if (!hostsPath || !(0, import_node_fs33.existsSync)(hostsPath)) return void 0;
25345
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs33.readFileSync)(hostsPath, "utf8")));
25180
25346
  } catch {
25181
25347
  return void 0;
25182
25348
  }
@@ -25184,7 +25350,7 @@ function ghMultiAccountCaveat(announcedLogin) {
25184
25350
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
25185
25351
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
25186
25352
  function envHealLockPath(home) {
25187
- return (0, import_node_path30.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
25353
+ return (0, import_node_path31.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
25188
25354
  }
25189
25355
  async function withEnvHealLock(what, run) {
25190
25356
  try {
@@ -25335,8 +25501,8 @@ function mmiDoctorDeps(opts = {}) {
25335
25501
  const home = (0, import_node_os10.homedir)();
25336
25502
  return marketplaceRows(
25337
25503
  MMI_MARKETPLACE_NAME,
25338
- readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
25339
- readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
25504
+ readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs33.readFileSync),
25505
+ readFileSyncSafe((0, import_node_path31.join)(home, ".claude", "settings.json"), import_node_fs33.readFileSync)
25340
25506
  );
25341
25507
  } catch {
25342
25508
  return [];
@@ -25370,6 +25536,35 @@ function allLongFlags() {
25370
25536
  cachedLongFlags = [...acc];
25371
25537
  return cachedLongFlags;
25372
25538
  }
25539
+ var cachedCommandPaths;
25540
+ function allCommandPaths() {
25541
+ if (cachedCommandPaths) return cachedCommandPaths;
25542
+ const acc = [];
25543
+ const walk2 = (node, trail) => {
25544
+ if (!isCanonicalSuggestion(node)) return;
25545
+ const here = [...trail, node.name];
25546
+ if (here.length) acc.push(here.join(" "));
25547
+ for (const child2 of node.subcommands) walk2(child2, here);
25548
+ };
25549
+ for (const child2 of buildCommandManifest(program2).tree.subcommands) walk2(child2, []);
25550
+ cachedCommandPaths = acc;
25551
+ return cachedCommandPaths;
25552
+ }
25553
+ function suggestCommandPath(token, paths) {
25554
+ const all = [...paths];
25555
+ const leafOf = (p) => p.slice(p.lastIndexOf(" ") + 1);
25556
+ const exact = all.filter((p) => leafOf(p) === token);
25557
+ if (exact.length === 1) return exact[0];
25558
+ if (exact.length > 1) return void 0;
25559
+ const leaves = /* @__PURE__ */ new Map();
25560
+ for (const p of all) {
25561
+ const leaf = leafOf(p);
25562
+ leaves.set(leaf, [...leaves.get(leaf) ?? [], p]);
25563
+ }
25564
+ const near = didYouMean(token, leaves.keys());
25565
+ const hits = near ? leaves.get(near) ?? [] : [];
25566
+ return hits.length === 1 ? hits[0] : void 0;
25567
+ }
25373
25568
  function argvWantsJson2() {
25374
25569
  return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
25375
25570
  }
@@ -25378,6 +25573,7 @@ var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
25378
25573
  var DISCOVERY_HINT = "run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery";
25379
25574
  var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)`;
25380
25575
  var lastParseErrorKind = "other";
25576
+ var lastUnknownCommand;
25381
25577
  function classifyParseError(plain) {
25382
25578
  if (/unknown command/i.test(plain)) return "unknown-command";
25383
25579
  if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
@@ -25407,6 +25603,10 @@ function commandPath2(cmd) {
25407
25603
  return parts.join(" ");
25408
25604
  }
25409
25605
  function resolveParseHint() {
25606
+ if (lastParseErrorKind === "unknown-command") {
25607
+ const path3 = lastUnknownCommand ? suggestCommandPath(lastUnknownCommand, allCommandPaths()) : void 0;
25608
+ return path3 ? `(did you mean \`mmi-cli ${path3}\`? ${DISCOVERY_HINT})` : STALE_HINT;
25609
+ }
25410
25610
  if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
25411
25611
  const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
25412
25612
  if (!cmd) return `(${DISCOVERY_HINT})`;
@@ -25425,6 +25625,7 @@ function envelopeAwareWriteErr(str) {
25425
25625
  return;
25426
25626
  }
25427
25627
  lastParseErrorKind = classifyParseError(plain);
25628
+ lastUnknownCommand = /unknown command '?([\w:-]+)'?/.exec(plain)?.[1];
25428
25629
  const match = /unknown option '([^']+)'/.exec(plain);
25429
25630
  if (match) {
25430
25631
  const flag = match[1];
@@ -25480,19 +25681,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
25480
25681
  });
25481
25682
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
25482
25683
  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) => {
25483
- const path2 = (0, import_node_path30.join)(process.cwd(), ".gitignore");
25484
- const current = (0, import_node_fs32.existsSync)(path2) ? (0, import_node_fs32.readFileSync)(path2, "utf8") : null;
25684
+ const path2 = (0, import_node_path31.join)(process.cwd(), ".gitignore");
25685
+ const current = (0, import_node_fs33.existsSync)(path2) ? (0, import_node_fs33.readFileSync)(path2, "utf8") : null;
25485
25686
  const plan = planManagedGitignore(current);
25486
25687
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
25487
25688
  if (opts.json) {
25488
- if (opts.write && plan.changed) (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
25689
+ if (opts.write && plan.changed) (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
25489
25690
  console.log(JSON.stringify(plan, null, 2));
25490
25691
  if (!opts.write && plan.changed) process.exitCode = 1;
25491
25692
  return;
25492
25693
  }
25493
25694
  if (opts.write) {
25494
25695
  if (plan.changed) {
25495
- (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
25696
+ (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
25496
25697
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
25497
25698
  } else {
25498
25699
  console.log("mmi-cli org rules gitignore: up to date");
@@ -25618,8 +25819,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
25618
25819
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
25619
25820
  let root;
25620
25821
  if (o.root !== void 0) {
25621
- root = (0, import_node_path30.resolve)(o.root);
25622
- if (!(0, import_node_fs32.existsSync)(root) || !(0, import_node_fs32.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
25822
+ root = (0, import_node_path31.resolve)(o.root);
25823
+ if (!(0, import_node_fs33.existsSync)(root) || !(0, import_node_fs33.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
25623
25824
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
25624
25825
  if (isPathUnderDirectory(gcRepoRoot, root)) {
25625
25826
  return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
@@ -25661,7 +25862,7 @@ function runWorktreeInstall(command, cwd, quiet) {
25661
25862
  const file = isWin2 ? "cmd.exe" : bin;
25662
25863
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
25663
25864
  return new Promise((resolve5, reject) => {
25664
- const child2 = (0, import_node_child_process13.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
25865
+ const child2 = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
25665
25866
  const timer = setTimeout(() => {
25666
25867
  try {
25667
25868
  child2.kill();
@@ -25684,7 +25885,7 @@ async function primaryCheckoutRoot(from) {
25684
25885
  return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
25685
25886
  }
25686
25887
  async function unprovenWorktreeReason(wtPath, repoRoot2) {
25687
- if (!(0, import_node_fs32.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
25888
+ if (!(0, import_node_fs33.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
25688
25889
  const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
25689
25890
  const registered = parseWorktreePorcelainEntries(porcelain);
25690
25891
  if (!registered.length) {
@@ -25705,26 +25906,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
25705
25906
  function acquireWorktreeSetupLock(worktreeRoot) {
25706
25907
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
25707
25908
  const take = () => {
25708
- const fd = (0, import_node_fs32.openSync)(lockPath, "wx");
25909
+ const fd = (0, import_node_fs33.openSync)(lockPath, "wx");
25709
25910
  try {
25710
- (0, import_node_fs32.writeSync)(fd, String(Date.now()));
25911
+ (0, import_node_fs33.writeSync)(fd, String(Date.now()));
25711
25912
  } finally {
25712
- (0, import_node_fs32.closeSync)(fd);
25913
+ (0, import_node_fs33.closeSync)(fd);
25713
25914
  }
25714
25915
  return () => {
25715
25916
  try {
25716
- (0, import_node_fs32.rmSync)(lockPath, { force: true });
25917
+ (0, import_node_fs33.rmSync)(lockPath, { force: true });
25717
25918
  } catch {
25718
25919
  }
25719
25920
  };
25720
25921
  };
25721
25922
  try {
25722
- (0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(lockPath), { recursive: true });
25923
+ (0, import_node_fs33.mkdirSync)((0, import_node_path31.dirname)(lockPath), { recursive: true });
25723
25924
  return take();
25724
25925
  } catch {
25725
25926
  try {
25726
- if (Date.now() - (0, import_node_fs32.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
25727
- (0, import_node_fs32.rmSync)(lockPath, { force: true });
25927
+ if (Date.now() - (0, import_node_fs33.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
25928
+ (0, import_node_fs33.rmSync)(lockPath, { force: true });
25728
25929
  return take();
25729
25930
  }
25730
25931
  } catch {
@@ -25932,7 +26133,7 @@ function scheduleRelatedDiscovery(o) {
25932
26133
  try {
25933
26134
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
25934
26135
  if (o.repo) args.push("--repo", o.repo);
25935
- spawnDetachedSelf(args, { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
26136
+ spawnDetachedSelf(args, { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
25936
26137
  } catch {
25937
26138
  }
25938
26139
  }
@@ -25979,6 +26180,36 @@ docs.command("refs").description("deterministic doc reference gate: every backti
25979
26180
  await failGraceful(e.message);
25980
26181
  }
25981
26182
  });
26183
+ var tests = program2.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
26184
+ tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, changedCount, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").action(async (o) => {
26185
+ try {
26186
+ const root = await repoRoot();
26187
+ const result = runTestPolicy(root, { base: o.base });
26188
+ if (o.json) {
26189
+ consoleIo.log(JSON.stringify(result, null, 2));
26190
+ if (!result.ok) process.exitCode = 1;
26191
+ return;
26192
+ }
26193
+ if (result.base === null && result.ok) {
26194
+ console.log("tests policy: no comparison base (origin/development or origin/main) \u2014 skipping.");
26195
+ return;
26196
+ }
26197
+ if (result.overriddenBy) {
26198
+ console.log(`tests policy: overridden by commit trailer \u2014 ${result.overriddenBy}`);
26199
+ return;
26200
+ }
26201
+ if (result.ok) {
26202
+ console.log(
26203
+ `tests policy: OK (${result.changedCount} changed file(s); ${result.mandatoryCount} mandatory glob(s), ${result.protectedCount} protected file(s)).`
26204
+ );
26205
+ return;
26206
+ }
26207
+ for (const f of result.findings) console.error(`tests policy: ${f.detail}`);
26208
+ process.exitCode = 1;
26209
+ } catch (e) {
26210
+ await failGraceful(e.message);
26211
+ }
26212
+ });
25982
26213
  var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger \u2014 record a dated run verdict, or read the dead-man status back");
25983
26214
  docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").requiredOption("--outcome <kind>", "clean | refreshed | failed").option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
25984
26215
  try {
@@ -26194,7 +26425,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
26194
26425
  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`);
26195
26426
  if (o.secretsFile) {
26196
26427
  try {
26197
- vars.push(`secrets=${(0, import_node_fs32.readFileSync)(o.secretsFile, "utf8")}`);
26428
+ vars.push(`secrets=${(0, import_node_fs33.readFileSync)(o.secretsFile, "utf8")}`);
26198
26429
  } catch (e) {
26199
26430
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
26200
26431
  }
@@ -26850,11 +27081,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
26850
27081
  }
26851
27082
  });
26852
27083
  async function listCiWorkflowPaths(cwd = process.cwd()) {
26853
- const wfDir = (0, import_node_path30.join)(cwd, ".github", "workflows");
26854
- if (!(0, import_node_fs32.existsSync)(wfDir)) return [];
26855
- return (0, import_node_fs32.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
27084
+ const wfDir = (0, import_node_path31.join)(cwd, ".github", "workflows");
27085
+ if (!(0, import_node_fs33.existsSync)(wfDir)) return [];
27086
+ return (0, import_node_fs33.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
26856
27087
  try {
26857
- return workflowReportsPrChecks((0, import_node_fs32.readFileSync)((0, import_node_path30.join)(wfDir, name), "utf8"));
27088
+ return workflowReportsPrChecks((0, import_node_fs33.readFileSync)((0, import_node_path31.join)(wfDir, name), "utf8"));
26858
27089
  } catch {
26859
27090
  return true;
26860
27091
  }
@@ -26881,16 +27112,16 @@ function ciAuditDeps() {
26881
27112
  // gate re-seed step is skipped gracefully rather than failing mid-run.
26882
27113
  readSeedFile: (path2) => {
26883
27114
  if (!root) return null;
26884
- const fullPath = (0, import_node_path30.join)(root, path2);
26885
- return (0, import_node_fs32.existsSync)(fullPath) ? (0, import_node_fs32.readFileSync)(fullPath, "utf8") : null;
27115
+ const fullPath = (0, import_node_path31.join)(root, path2);
27116
+ return (0, import_node_fs33.existsSync)(fullPath) ? (0, import_node_fs33.readFileSync)(fullPath, "utf8") : null;
26886
27117
  }
26887
27118
  };
26888
27119
  }
26889
27120
  function hubRoot() {
26890
- const fromPkg = (0, import_node_path30.join)(__dirname, "..", "..");
27121
+ const fromPkg = (0, import_node_path31.join)(__dirname, "..", "..");
26891
27122
  const marker = "skills/bootstrap/seeds/manifest.json";
26892
- if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(fromPkg, marker))) return fromPkg;
26893
- if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), marker))) return process.cwd();
27123
+ if ((0, import_node_fs33.existsSync)((0, import_node_path31.join)(fromPkg, marker))) return fromPkg;
27124
+ if ((0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), marker))) return process.cwd();
26894
27125
  return null;
26895
27126
  }
26896
27127
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
@@ -27167,7 +27398,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
27167
27398
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
27168
27399
  beforeWorktrees,
27169
27400
  startingPath,
27170
- pathExists: (p) => (0, import_node_fs32.existsSync)(p),
27401
+ pathExists: (p) => (0, import_node_fs33.existsSync)(p),
27171
27402
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
27172
27403
  teardownWorktreeStage,
27173
27404
  deferredStore,
@@ -27508,10 +27739,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
27508
27739
  targets = resolution.targets;
27509
27740
  }
27510
27741
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
27511
- const fileMatrix = (0, import_node_fs32.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs32.readFileSync)("access-matrix.json", "utf8")) : {};
27742
+ const fileMatrix = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
27512
27743
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
27513
27744
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
27514
- const fileContracts = (0, import_node_fs32.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs32.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
27745
+ const fileContracts = (0, import_node_fs33.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs33.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
27515
27746
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
27516
27747
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
27517
27748
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -27545,16 +27776,16 @@ function directoryBytes(path2) {
27545
27776
  let total = 0;
27546
27777
  let entries;
27547
27778
  try {
27548
- entries = (0, import_node_fs32.readdirSync)(path2, { withFileTypes: true });
27779
+ entries = (0, import_node_fs33.readdirSync)(path2, { withFileTypes: true });
27549
27780
  } catch {
27550
27781
  return 0;
27551
27782
  }
27552
27783
  for (const entry of entries) {
27553
- const child2 = (0, import_node_path30.join)(path2, entry.name);
27784
+ const child2 = (0, import_node_path31.join)(path2, entry.name);
27554
27785
  if (entry.isDirectory()) total += directoryBytes(child2);
27555
27786
  else {
27556
27787
  try {
27557
- total += (0, import_node_fs32.statSync)(child2).size;
27788
+ total += (0, import_node_fs33.statSync)(child2).size;
27558
27789
  } catch {
27559
27790
  }
27560
27791
  }
@@ -27562,25 +27793,25 @@ function directoryBytes(path2) {
27562
27793
  return total;
27563
27794
  }
27564
27795
  function listDirEntries(dir) {
27565
- return (0, import_node_fs32.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
27796
+ return (0, import_node_fs33.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
27566
27797
  }
27567
27798
  function readInstalledPluginRefs(home) {
27568
27799
  const p = installedPluginsPath(home);
27569
- if (!(0, import_node_fs32.existsSync)(p)) return [];
27800
+ if (!(0, import_node_fs33.existsSync)(p)) return [];
27570
27801
  try {
27571
- return installedPluginPaths((0, import_node_fs32.readFileSync)(p, "utf8"));
27802
+ return installedPluginPaths((0, import_node_fs33.readFileSync)(p, "utf8"));
27572
27803
  } catch {
27573
27804
  return null;
27574
27805
  }
27575
27806
  }
27576
27807
  function pluginCacheFsDeps(home, dirBytes) {
27577
27808
  return {
27578
- exists: (p) => (0, import_node_fs32.existsSync)(p),
27579
- listVersionDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
27809
+ exists: (p) => (0, import_node_fs33.existsSync)(p),
27810
+ listVersionDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
27580
27811
  dirBytes,
27581
- listStagingDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
27812
+ listStagingDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
27582
27813
  try {
27583
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path30.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs32.statSync)(p).mtimeMs) };
27814
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path31.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs33.statSync)(p).mtimeMs) };
27584
27815
  } catch {
27585
27816
  return { name: d.name, mtimeMs: Date.now() };
27586
27817
  }
@@ -27594,10 +27825,10 @@ function stagingApplyFsGuard(home) {
27594
27825
  return {
27595
27826
  referencedPaths: () => readInstalledPluginRefs(home),
27596
27827
  mtimeMs: (name) => {
27597
- const p = (0, import_node_path30.join)(stagingRoot, name);
27598
- if (!(0, import_node_fs32.existsSync)(p)) return null;
27828
+ const p = (0, import_node_path31.join)(stagingRoot, name);
27829
+ if (!(0, import_node_fs33.existsSync)(p)) return null;
27599
27830
  try {
27600
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs32.statSync)(q).mtimeMs);
27831
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs33.statSync)(q).mtimeMs);
27601
27832
  } catch {
27602
27833
  return null;
27603
27834
  }
@@ -27613,7 +27844,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
27613
27844
  { withBytes: true }
27614
27845
  );
27615
27846
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
27616
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os10.homedir)())) : void 0;
27847
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os10.homedir)())) : void 0;
27617
27848
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
27618
27849
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
27619
27850
  else console.log(renderPluginCachePlan(plan, result));
@@ -27676,7 +27907,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
27676
27907
  for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
27677
27908
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
27678
27909
  if (worktreeBanner) {
27679
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
27910
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
27680
27911
  bannerIo.log(worktreeBanner);
27681
27912
  }
27682
27913
  if (isLinkedWorktree(process.cwd())) {
@@ -27701,5 +27932,6 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
27701
27932
  isOrgRegisteredRepo,
27702
27933
  registryClientDeps,
27703
27934
  repoSlug,
27704
- shouldMarkWorktreeActivity
27935
+ shouldMarkWorktreeActivity,
27936
+ suggestCommandPath
27705
27937
  });