@mutmutco/cli 3.98.0 → 3.99.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 +522 -271
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3417,7 +3417,7 @@ var program = new Command();
3417
3417
  // src/index.ts
3418
3418
  var import_promises11 = require("node:fs/promises");
3419
3419
  var import_node_fs40 = require("node:fs");
3420
- var import_node_child_process18 = require("node:child_process");
3420
+ var import_node_child_process19 = require("node:child_process");
3421
3421
 
3422
3422
  // src/cli-shared.ts
3423
3423
  var import_node_child_process3 = require("node:child_process");
@@ -5779,6 +5779,7 @@ function buildGcPlan(inputs) {
5779
5779
  const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
5780
5780
  const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
5781
5781
  const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
5782
+ const prLookupFailures = new Map((inputs.prLookupFailures ?? []).map((f) => [f.branch.trim(), f.detail]));
5782
5783
  const skipped = [];
5783
5784
  const branches = [];
5784
5785
  const skipTrackingBranches = /* @__PURE__ */ new Set();
@@ -5794,6 +5795,11 @@ function buildGcPlan(inputs) {
5794
5795
  skipped.push({ branch, reason: "protected" });
5795
5796
  continue;
5796
5797
  }
5798
+ if (prLookupFailures.has(branch)) {
5799
+ skipped.push({ branch, reason: "pr-lookup-failed", detail: prLookupFailures.get(branch) });
5800
+ skipTrackingBranches.add(branch);
5801
+ continue;
5802
+ }
5797
5803
  const prSet = prs.get(branch);
5798
5804
  if (prSet?.some((pr2) => pr2.state === "OPEN")) {
5799
5805
  skipped.push({ branch, reason: "open-pr" });
@@ -5838,6 +5844,7 @@ function buildGcPlan(inputs) {
5838
5844
  const branch = branchForTrackingRef(ref, remote);
5839
5845
  if (!branch || protectedBranches.has(branch)) return null;
5840
5846
  if (branch === inputs.currentBranch || dirtyWorktrees.has(branch) || unpushedWorktrees.has(branch) || preservedBranches2.has(branch) || preservedWorktrees.has(branch) || skipTrackingBranches.has(branch)) return null;
5847
+ if (prLookupFailures.has(branch)) return null;
5841
5848
  const prSet = prs.get(branch);
5842
5849
  if (prSet?.some((pr2) => pr2.state === "OPEN")) return null;
5843
5850
  const state = closedState(prSet);
@@ -13797,23 +13804,6 @@ async function fetchSchedulesList(deps) {
13797
13804
  return null;
13798
13805
  }
13799
13806
  }
13800
- async function fetchDocsAuditList(deps) {
13801
- if (!deps.baseUrl) return { notArmed: true };
13802
- try {
13803
- const token = await deps.token();
13804
- if (!token) return { notArmed: true };
13805
- const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
13806
- method: "GET",
13807
- headers: { Authorization: `Bearer ${token}` }
13808
- });
13809
- if (res.status === 404) return { notArmed: true };
13810
- if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
13811
- const body = await res.json();
13812
- return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
13813
- } catch (e) {
13814
- return { ok: false, error: e.message };
13815
- }
13816
- }
13817
13807
  async function fetchOrgConfig(deps) {
13818
13808
  if (!deps.baseUrl) return null;
13819
13809
  const token = await deps.token();
@@ -13864,9 +13854,6 @@ async function retireProject(slug, deps) {
13864
13854
  async function setDeployCoords(slug, payload, deps) {
13865
13855
  return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
13866
13856
  }
13867
- async function recordDocsAudit(verdict, deps) {
13868
- return postJson("/docs-audit/record", { ...verdict }, deps);
13869
- }
13870
13857
  async function postSchedulesLift(repo, schedules, deps) {
13871
13858
  const res = await postJson("/schedules/lift", { repo, schedules }, deps);
13872
13859
  if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
@@ -15587,13 +15574,27 @@ async function isOrgRegisteredRepo(cfg, deps = {}) {
15587
15574
  if (!read.ok) return false;
15588
15575
  return read.project !== null;
15589
15576
  }
15590
- async function ghPrs(limit) {
15591
- const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
15592
- const [open2, closed] = await Promise.all([
15593
- execFileP2("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
15594
- execFileP2("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
15595
- ]);
15596
- return [...JSON.parse(open2.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
15577
+ var GC_PR_LOOKUP_CONCURRENCY = 8;
15578
+ async function resolveBranchPrs(branches, limit) {
15579
+ const queue = [...new Set(branches.map((b) => b.trim()).filter(Boolean))];
15580
+ const prs = [];
15581
+ const failures = [];
15582
+ const args = (branch) => ["pr", "list", "--head", branch, "--state", "all", "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
15583
+ let next = 0;
15584
+ const worker = async () => {
15585
+ for (let i = next++; i < queue.length; i = next++) {
15586
+ const branch = queue[i];
15587
+ try {
15588
+ const { stdout } = await execFileP2("gh", args(branch), { timeout: GC_GH_TIMEOUT_MS });
15589
+ const rows = JSON.parse(stdout || "[]");
15590
+ prs.push(...rows.filter((pr2) => pr2.headRefName?.trim() === branch));
15591
+ } catch (e) {
15592
+ failures.push({ branch, detail: e.message });
15593
+ }
15594
+ }
15595
+ };
15596
+ await Promise.all(Array.from({ length: Math.min(GC_PR_LOOKUP_CONCURRENCY, queue.length) }, worker));
15597
+ return { prs, failures };
15597
15598
  }
15598
15599
  async function localBranchHeads() {
15599
15600
  try {
@@ -15737,21 +15738,26 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
15737
15738
  return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
15738
15739
  }
15739
15740
  async function gcPlan(remote, limit, opts = {}) {
15740
- const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
15741
+ const [branches, heads, current, stale, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
15741
15742
  gitOut(["branch", "--format=%(refname:short)"]),
15742
15743
  localBranchHeads(),
15743
15744
  gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
15744
15745
  collectStaleTrackingRefs(remote, {
15745
15746
  execGit: (args) => execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })
15746
15747
  }),
15747
- ghPrs(limit),
15748
15748
  worktreeBranches(),
15749
15749
  siblingWorktreeDirs(opts.root),
15750
15750
  preservedBranches(),
15751
15751
  branchesMergedIntoBase(remote)
15752
15752
  ]);
15753
+ const localBranches = branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
15754
+ const { prs, failures } = await resolveBranchPrs(
15755
+ [...localBranches, ...stale.map((ref) => branchForTrackingRef(ref, remote)).filter((b) => Boolean(b))].filter((b) => !isProtectedBranch(b)),
15756
+ limit
15757
+ );
15753
15758
  return buildGcPlan({
15754
- localBranches: branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean),
15759
+ localBranches,
15760
+ prLookupFailures: failures,
15755
15761
  localBranchHeads: heads,
15756
15762
  currentBranch: current,
15757
15763
  siblingWorktreeDirs: siblingDirs,
@@ -17853,8 +17859,6 @@ function consolidateCommandNamespaces(program3) {
17853
17859
  move(program3, plugin, "plugin-heal", "heal");
17854
17860
  move(program3, plugin, "plugin-prune", "prune");
17855
17861
  move(program3, plugin, "session-start");
17856
- const docs2 = child(program3, "docs");
17857
- move(program3, docs2, "docs-audit", "audit");
17858
17862
  const train = child(program3, "train");
17859
17863
  const fullTrack2 = child(program3, "full-track");
17860
17864
  move(fullTrack2, train, "readiness");
@@ -18156,6 +18160,22 @@ function healPiPluginRegistration(home, env, installedVersion) {
18156
18160
  }
18157
18161
  }
18158
18162
 
18163
+ // src/statusline-invalidate.ts
18164
+ var import_node_child_process10 = require("node:child_process");
18165
+ function invalidateStatuslineBoardCache() {
18166
+ try {
18167
+ const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
18168
+ detached: true,
18169
+ stdio: "ignore",
18170
+ windowsHide: true
18171
+ });
18172
+ child2.on("error", () => {
18173
+ });
18174
+ child2.unref();
18175
+ } catch {
18176
+ }
18177
+ }
18178
+
18159
18179
  // src/skill-lesson.ts
18160
18180
  var SKILL_LESSON_LABEL = "skill-lesson";
18161
18181
  var SKILL_NAMES = ["bootstrap", "browser-automation", "doctor", "epic", "hotfix", "mmi", "onboard", "rcand", "release", "resume", "secrets", "stage", "worktree"];
@@ -18395,12 +18415,12 @@ function renderVerifyBroker(input) {
18395
18415
  }
18396
18416
 
18397
18417
  // src/hotfix-coverage.ts
18398
- var import_node_child_process10 = require("node:child_process");
18418
+ var import_node_child_process11 = require("node:child_process");
18399
18419
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
18400
18420
  function checkHotfixCoverage(options = {}) {
18401
18421
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
18402
18422
  const ack = (options.ack ?? []).filter(Boolean);
18403
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18423
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18404
18424
  const revList = (range) => {
18405
18425
  const out = git2(["rev-list", "--no-merges", range]).trim();
18406
18426
  return out ? out.split("\n") : [];
@@ -18468,7 +18488,7 @@ function checkHotfixCoverage(options = {}) {
18468
18488
  }
18469
18489
  function checkHotfixCarries(options) {
18470
18490
  const { cwd = process.cwd(), branch, baseRef, targets } = options;
18471
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18491
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18472
18492
  const isAncestor = (sha, ref) => {
18473
18493
  try {
18474
18494
  git2(["merge-base", "--is-ancestor", sha, ref]);
@@ -19582,7 +19602,7 @@ function renderAccessReport(report) {
19582
19602
 
19583
19603
  // src/repo-index.ts
19584
19604
  var import_node_crypto4 = require("node:crypto");
19585
- var import_node_child_process11 = require("node:child_process");
19605
+ var import_node_child_process12 = require("node:child_process");
19586
19606
  var import_node_fs23 = require("node:fs");
19587
19607
  var import_node_path21 = require("node:path");
19588
19608
  var REPO_INDEX_SCHEMA = 1;
@@ -19728,7 +19748,7 @@ function loadReadmeHints(cwd, candidatePaths) {
19728
19748
  function toPosix(p) {
19729
19749
  return p.split(import_node_path21.sep).join("/");
19730
19750
  }
19731
- function listCandidatePaths(cwd, exec = import_node_child_process11.execFileSync) {
19751
+ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync) {
19732
19752
  try {
19733
19753
  const out = exec("git", ["ls-files", "-z", "-c", "-o", "--exclude-standard"], {
19734
19754
  cwd,
@@ -19851,7 +19871,7 @@ function searchRepoIndex(idx, query, limit = 20) {
19851
19871
  }
19852
19872
  return out;
19853
19873
  }
19854
- function inferRepoSlug(cwd, exec = import_node_child_process11.execFileSync) {
19874
+ function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
19855
19875
  try {
19856
19876
  const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
19857
19877
  const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
@@ -20001,7 +20021,7 @@ async function gcRepoIndexCloud(deps) {
20001
20021
  var import_node_fs24 = require("node:fs");
20002
20022
  var import_node_os9 = require("node:os");
20003
20023
  var import_node_path22 = require("node:path");
20004
- var import_node_child_process12 = require("node:child_process");
20024
+ var import_node_child_process13 = require("node:child_process");
20005
20025
  var MAX_EMBED_BACKFILL_ROUNDS = 40;
20006
20026
  function normalizeRepo(raw) {
20007
20027
  const t = raw.trim().replace(/\.git$/, "");
@@ -20021,7 +20041,7 @@ function rosterRepos(projects) {
20021
20041
  }
20022
20042
  function shallowClone(repo, dest, token) {
20023
20043
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
20024
- (0, import_node_child_process12.execFileSync)(
20044
+ (0, import_node_child_process13.execFileSync)(
20025
20045
  "git",
20026
20046
  ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
20027
20047
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
@@ -20288,7 +20308,7 @@ async function runRepoIndexHealth(opts) {
20288
20308
  }
20289
20309
 
20290
20310
  // src/spawn-policy-core.ts
20291
- var import_node_child_process13 = require("node:child_process");
20311
+ var import_node_child_process14 = require("node:child_process");
20292
20312
  var import_node_fs26 = require("node:fs");
20293
20313
  var import_node_path23 = require("node:path");
20294
20314
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
@@ -20359,7 +20379,7 @@ function findViolationsInSource(raw) {
20359
20379
  return found;
20360
20380
  }
20361
20381
  function policedFiles(root) {
20362
- const r = (0, import_node_child_process13.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20382
+ const r = (0, import_node_child_process14.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20363
20383
  cwd: root,
20364
20384
  encoding: "utf8",
20365
20385
  windowsHide: true,
@@ -20393,7 +20413,7 @@ function runSpawnPolicy(root) {
20393
20413
  }
20394
20414
 
20395
20415
  // src/test-policy-core.ts
20396
- var import_node_child_process14 = require("node:child_process");
20416
+ var import_node_child_process15 = require("node:child_process");
20397
20417
  var import_node_fs27 = require("node:fs");
20398
20418
  var import_node_path24 = require("node:path");
20399
20419
  var POLICY_FILE = "test-policy.json";
@@ -20523,14 +20543,14 @@ function evaluate(changed, policy, present = () => false) {
20523
20543
  return findings;
20524
20544
  }
20525
20545
  function git(args, cwd) {
20526
- return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
20546
+ return (0, import_node_child_process15.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
20527
20547
  }
20528
20548
  var COAUTHOR_KEY = "Co-authored-by";
20529
20549
  var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
20530
20550
  var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
20531
20551
  function parseTrailers(message, cwd) {
20532
20552
  try {
20533
- return (0, import_node_child_process14.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
20553
+ return (0, import_node_child_process15.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
20534
20554
  windowsHide: true,
20535
20555
  cwd,
20536
20556
  input: message,
@@ -20715,124 +20735,6 @@ function runTestPolicy(root, deps = {}) {
20715
20735
  return result;
20716
20736
  }
20717
20737
 
20718
- // src/docs-audit-command.ts
20719
- function serializeOutcome(outcome) {
20720
- switch (outcome.kind) {
20721
- case "clean":
20722
- return "clean";
20723
- case "refreshed":
20724
- return `refreshed-${outcome.count}`;
20725
- case "failed":
20726
- return "failed";
20727
- }
20728
- }
20729
- var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
20730
- function isValidIsoDate(date) {
20731
- if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
20732
- const parsed = Date.parse(`${date}T00:00:00Z`);
20733
- if (Number.isNaN(parsed)) return false;
20734
- return new Date(parsed).toISOString().slice(0, 10) === date;
20735
- }
20736
- var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
20737
- function isValidOutcomeWire(outcome) {
20738
- return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
20739
- }
20740
- function docsAuditRecord(input) {
20741
- const repo = input.repo.trim();
20742
- const date = input.date.trim();
20743
- const shaRange = input.shaRange.trim();
20744
- const checkerVendor = input.checkerVendor.trim();
20745
- if (!repo) throw new Error("docs audit record: repo is required");
20746
- if (!date) throw new Error("docs audit record: date is required");
20747
- if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
20748
- if (!shaRange) throw new Error("docs audit record: shaRange is required");
20749
- if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
20750
- if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
20751
- throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
20752
- }
20753
- if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
20754
- throw new Error("docs audit record: a `failed` outcome must carry a reason");
20755
- }
20756
- const outcome = serializeOutcome(input.outcome);
20757
- if (!isValidOutcomeWire(outcome)) {
20758
- throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
20759
- }
20760
- return { repo, date, shaRange, outcome, checkerVendor };
20761
- }
20762
- function ageInDays(verdictDate, today) {
20763
- const a = Date.parse(`${verdictDate}T00:00:00Z`);
20764
- const b = Date.parse(`${today}T00:00:00Z`);
20765
- return Math.max(0, Math.round((b - a) / 864e5));
20766
- }
20767
- function docsAuditStatus(fetch2, opts) {
20768
- if ("notArmed" in fetch2) {
20769
- return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
20770
- }
20771
- if (!fetch2.ok) {
20772
- return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
20773
- }
20774
- if (!isValidIsoDate(opts.today)) {
20775
- throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
20776
- }
20777
- if (fetch2.verdict === null) {
20778
- const armedAt = opts.armedAt;
20779
- if (armedAt && isValidIsoDate(armedAt)) {
20780
- const cadenceDays = opts.cadenceDays ?? 7;
20781
- const graceDays = opts.graceDays ?? 3;
20782
- const sinceArmed = ageInDays(armedAt, opts.today);
20783
- if (sinceArmed <= cadenceDays + graceDays) {
20784
- return {
20785
- ok: true,
20786
- state: "awaiting-first-tick",
20787
- line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
20788
- };
20789
- }
20790
- return {
20791
- ok: false,
20792
- state: "missing",
20793
- line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
20794
- };
20795
- }
20796
- return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
20797
- }
20798
- const verdict = fetch2.verdict;
20799
- for (const field of ["repo", "shaRange", "checkerVendor"]) {
20800
- const value = verdict[field];
20801
- if (typeof value !== "string" || !value.trim()) {
20802
- const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
20803
- return {
20804
- ok: false,
20805
- state: "malformed",
20806
- line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
20807
- };
20808
- }
20809
- }
20810
- if (!isValidIsoDate(verdict.date)) {
20811
- return {
20812
- ok: false,
20813
- state: "malformed",
20814
- line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
20815
- };
20816
- }
20817
- if (!isValidOutcomeWire(verdict.outcome)) {
20818
- return {
20819
- ok: false,
20820
- state: "malformed",
20821
- line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
20822
- };
20823
- }
20824
- const cadence = opts.cadenceDays ?? 7;
20825
- const grace = opts.graceDays ?? 3;
20826
- const age = ageInDays(verdict.date, opts.today);
20827
- if (age > cadence + grace) {
20828
- return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
20829
- }
20830
- if (verdict.outcome === "failed") {
20831
- return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
20832
- }
20833
- return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
20834
- }
20835
-
20836
20738
  // src/project-info-sync.ts
20837
20739
  var import_node_fs28 = require("node:fs");
20838
20740
  var import_node_path25 = require("node:path");
@@ -22503,7 +22405,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
22503
22405
 
22504
22406
  // src/schedules-commands.ts
22505
22407
  var import_promises4 = require("node:fs/promises");
22506
- var import_node_child_process15 = require("node:child_process");
22408
+ var import_node_child_process16 = require("node:child_process");
22507
22409
  var import_node_util7 = require("node:util");
22508
22410
 
22509
22411
  // src/schedules.ts
@@ -22814,7 +22716,7 @@ function cadenceStale(registryCadence, liveCadence) {
22814
22716
  if (!liveCrons.length) return false;
22815
22717
  return !liveCrons.every((cron) => registered.has(cron));
22816
22718
  }
22817
- function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
22719
+ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22818
22720
  const registryGithub = registry2.filter((r) => r.executor === "github-actions");
22819
22721
  const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
22820
22722
  const registryById = new Map(registryGithub.map((r) => [r.id, r]));
@@ -22845,6 +22747,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22845
22747
  continue;
22846
22748
  }
22847
22749
  if (activeWorkflowNames.has(r.id)) continue;
22750
+ if (disabledWorkflowNames.has(r.id)) {
22751
+ if (readRepos.has(r.repo)) {
22752
+ drifts.push({
22753
+ class: "registered-but-disabled",
22754
+ name: r.id,
22755
+ executor: r.executor || "github-actions",
22756
+ detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
22757
+ remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm (or, if it was retired on purpose, re-run schedules register so the replace prunes the SCHEDULE# row) \u2014 never prune a lane parked on purpose`
22758
+ });
22759
+ }
22760
+ continue;
22761
+ }
22848
22762
  if (readRepos.has(r.repo)) {
22849
22763
  drifts.push({
22850
22764
  class: "registered-but-dead",
@@ -22920,7 +22834,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
22920
22834
  }
22921
22835
  return drifts.sort((a, b) => a.name.localeCompare(b.name));
22922
22836
  }
22923
- function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
22837
+ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22924
22838
  if (registry2 === null) {
22925
22839
  return {
22926
22840
  reconciliation: [],
@@ -22930,7 +22844,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22930
22844
  }
22931
22845
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
22932
22846
  const drifts = [
22933
- ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
22847
+ ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
22934
22848
  ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
22935
22849
  schedulerRead: Boolean(harbour.schedulerRead),
22936
22850
  now: harbour.now
@@ -22940,7 +22854,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22940
22854
  }
22941
22855
 
22942
22856
  // src/schedules-commands.ts
22943
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
22857
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
22944
22858
  var AWS_REGION = "eu-central-1";
22945
22859
  var AWS_TIMEOUT_MS = 3e4;
22946
22860
  var AWS_RETRY_DELAY_MS = 1500;
@@ -22964,9 +22878,15 @@ async function repoWorkflowEntries(client, repo) {
22964
22878
  const entries = [];
22965
22879
  const failures = [];
22966
22880
  const workflows = [];
22881
+ const disabled = [];
22967
22882
  for (const wf of workflowsList) {
22968
- if (wf?.state !== "active" || typeof wf.path !== "string" || !wf.path) continue;
22883
+ if (typeof wf?.path !== "string" || !wf.path) continue;
22969
22884
  if (!wf.path.startsWith(".github/workflows/")) continue;
22885
+ if (wf.state !== "active") {
22886
+ const basename5 = wf.path.split("/").pop() ?? wf.path;
22887
+ disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
22888
+ continue;
22889
+ }
22970
22890
  try {
22971
22891
  const contents = await client.rest(
22972
22892
  "GET",
@@ -22985,7 +22905,7 @@ async function repoWorkflowEntries(client, repo) {
22985
22905
  else throw e;
22986
22906
  }
22987
22907
  }
22988
- return { entries, failures, workflows };
22908
+ return { entries, failures, workflows, disabled };
22989
22909
  }
22990
22910
  async function githubEntries(client) {
22991
22911
  const entries = [];
@@ -22993,12 +22913,13 @@ async function githubEntries(client) {
22993
22913
  const drift = [];
22994
22914
  const readRepos = [];
22995
22915
  const workflows = [];
22916
+ const disabledWorkflowNames = [];
22996
22917
  let repos;
22997
22918
  try {
22998
22919
  const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
22999
22920
  repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
23000
22921
  } catch (e) {
23001
- return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
22922
+ return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
23002
22923
  }
23003
22924
  const results = await Promise.all(
23004
22925
  repos.map(async (repo) => {
@@ -23015,6 +22936,7 @@ async function githubEntries(client) {
23015
22936
  readRepos.push(r.repo);
23016
22937
  entries.push(...r.entries);
23017
22938
  workflows.push(...r.workflows);
22939
+ disabledWorkflowNames.push(...r.disabled);
23018
22940
  if (r.failures.length) {
23019
22941
  drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
23020
22942
  }
@@ -23022,7 +22944,7 @@ async function githubEntries(client) {
23022
22944
  }
23023
22945
  const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
23024
22946
  drift.push(...reconciliation.map(renderDrift));
23025
- return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
22947
+ return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
23026
22948
  }
23027
22949
  async function awsJson(args) {
23028
22950
  const run = async () => {
@@ -23077,7 +22999,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
23077
22999
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
23078
23000
  awsEntries: aws.entries,
23079
23001
  schedulerRead: Boolean(aws.schedulerRead)
23080
- });
23002
+ }, new Set(gh.disabledWorkflowNames));
23081
23003
  const selfManaged = /* @__PURE__ */ new Set();
23082
23004
  for (const proj of projects ?? []) {
23083
23005
  if (proj?.schedulesMode !== "self-managed") continue;
@@ -23926,6 +23848,7 @@ var import_node_os11 = require("node:os");
23926
23848
  var import_node_path29 = require("node:path");
23927
23849
 
23928
23850
  // src/bootstrap-drift.ts
23851
+ var import_node_crypto6 = require("node:crypto");
23929
23852
  function byteComparableSeeds(manifest, cls) {
23930
23853
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
23931
23854
  }
@@ -23934,6 +23857,9 @@ function compareSeedBytes(hubContent, repoContent) {
23934
23857
  const normalize = (s) => s.replace(/\r\n/g, "\n");
23935
23858
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
23936
23859
  }
23860
+ function seedContentHash(content) {
23861
+ return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
23862
+ }
23937
23863
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23938
23864
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
23939
23865
  const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
@@ -23956,11 +23882,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23956
23882
  findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
23957
23883
  continue;
23958
23884
  }
23885
+ const repoContent = byTarget.get(seed.target);
23959
23886
  findings.push({
23960
23887
  repo,
23961
23888
  target: seed.target,
23962
23889
  state,
23963
- detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate with `mmi-cli bootstrap apply <repo> --only <target> --execute`, or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)"
23890
+ detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 fix this in MMI-Hub and let it propagate (#4233); `mmi-cli bootstrap apply <repo> --only <target> --execute` remains for THIS repo's own bootstrap/onboarding, never a fleet-wide fan-out (#4241 fails a hand-edited fleet copy at the merge gate on repos carrying the new seed), or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)",
23891
+ // #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
23892
+ ...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
23964
23893
  });
23965
23894
  }
23966
23895
  return findings;
@@ -23979,6 +23908,115 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
23979
23908
  return lines.join("\n");
23980
23909
  }
23981
23910
 
23911
+ // src/bootstrap-propagate.ts
23912
+ function assertPropagationCoverage(rosterCount, independentRegistryCount) {
23913
+ if (rosterCount === 0) {
23914
+ throw new Error("bootstrap propagate: roster read 0 repos \u2014 refusing to report a plan from a roster this command could not establish (#4232 coverage invariant)");
23915
+ }
23916
+ if (rosterCount < independentRegistryCount) {
23917
+ throw new Error(
23918
+ `bootstrap propagate: roster read ${rosterCount} repo(s) but the independent registry count is ${independentRegistryCount} \u2014 this read did not cover the fleet (#4232 coverage invariant)`
23919
+ );
23920
+ }
23921
+ }
23922
+ function assignWaves(repos, canarySlug) {
23923
+ const waves = /* @__PURE__ */ new Map();
23924
+ if (!canarySlug) return waves;
23925
+ const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
23926
+ const canary = repos.find((r) => r.slug === canarySlug);
23927
+ if (canary) waves.set(canary.repo, 0);
23928
+ const wave1Count = Math.ceil(rest.length * 0.25);
23929
+ rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
23930
+ return waves;
23931
+ }
23932
+ function statusFor(read) {
23933
+ if (!read) return { status: "pending", record: {} };
23934
+ if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
23935
+ const pr2 = read.pr;
23936
+ if (!pr2) return { status: "pending", record: {} };
23937
+ if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
23938
+ if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
23939
+ return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
23940
+ }
23941
+ var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
23942
+ function planPropagationTick(input) {
23943
+ const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
23944
+ const readByRepo = new Map(reads.map((r) => [r.repo, r]));
23945
+ const records = [];
23946
+ const opened = [];
23947
+ const waived = repos.filter((r) => r.waiver);
23948
+ for (const r of waived) {
23949
+ records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
23950
+ }
23951
+ const eligible = repos.filter((r) => !r.waiver);
23952
+ const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
23953
+ if (refusedNoCanary) {
23954
+ for (const r of eligible) {
23955
+ records.push({ repo: r.repo, target, wave: null, status: "not-yet-reached", action: "none", detail: "no canary declared for this target \u2014 refusing to plan any wave (#4238 canary requirement)" });
23956
+ }
23957
+ return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
23958
+ }
23959
+ const waveOf = assignWaves(eligible, canarySlug);
23960
+ const byWave = { 0: [], 1: [], 2: [] };
23961
+ for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
23962
+ let halted = false;
23963
+ let haltReason = null;
23964
+ let waveGateOpen = true;
23965
+ for (const waveNum of [0, 1, 2]) {
23966
+ const waveRepos = byWave[waveNum];
23967
+ if (!waveRepos.length) continue;
23968
+ if (!waveGateOpen || halted) {
23969
+ for (const r of waveRepos) {
23970
+ records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
23971
+ }
23972
+ continue;
23973
+ }
23974
+ let waveAllMatch = true;
23975
+ let waveHasRed = false;
23976
+ for (const r of waveRepos) {
23977
+ const { status, record } = statusFor(readByRepo.get(r.repo));
23978
+ const shouldOpen = status === "pending";
23979
+ if (shouldOpen) opened.push(r.repo);
23980
+ records.push({
23981
+ repo: r.repo,
23982
+ target,
23983
+ wave: waveNum,
23984
+ status,
23985
+ action: shouldOpen ? "open-pr" : "none",
23986
+ detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
23987
+ ...record
23988
+ });
23989
+ if (status !== "match") waveAllMatch = false;
23990
+ if (HALTING_STATUSES.has(status)) waveHasRed = true;
23991
+ }
23992
+ if (waveHasRed) {
23993
+ halted = true;
23994
+ haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
23995
+ waveGateOpen = false;
23996
+ continue;
23997
+ }
23998
+ if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
23999
+ waveGateOpen = false;
24000
+ continue;
24001
+ }
24002
+ waveGateOpen = waveAllMatch;
24003
+ }
24004
+ return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
24005
+ }
24006
+ function renderPropagationReport(plan) {
24007
+ const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
24008
+ if (plan.refusedNoCanary) {
24009
+ lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
24010
+ return lines.join("\n");
24011
+ }
24012
+ for (const r of plan.records) {
24013
+ const wave2 = r.wave == null ? "-" : String(r.wave);
24014
+ lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
24015
+ }
24016
+ lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
24017
+ return lines.join("\n");
24018
+ }
24019
+
23982
24020
  // src/bootstrap-verify.ts
23983
24021
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
23984
24022
  var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
@@ -25065,6 +25103,174 @@ LIVE apply to ${repo}:
25065
25103
  ${applied.join("\n ")}`);
25066
25104
  }
25067
25105
  });
25106
+ bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
25107
+ const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
25108
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
25109
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
25110
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
25111
+ const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
25112
+ if (!o.target) {
25113
+ return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
25114
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25115
+ }
25116
+ const seed = propagatable.find((s) => s.target === o.target);
25117
+ if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
25118
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25119
+ if (!(0, import_node_fs31.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
25120
+ const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
25121
+ const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
25122
+ const cfg = await loadConfig();
25123
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
25124
+ if (!projects || projects.length === 0) {
25125
+ return failGraceful("bootstrap propagate: the registry roster is unreadable or empty \u2014 refusing to plan a tick from a scope this command could not establish (#4232 coverage invariant)");
25126
+ }
25127
+ const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
25128
+ let independentCount = rosterRepos2.length;
25129
+ if ((0, import_node_fs31.existsSync)("projects.json")) {
25130
+ try {
25131
+ const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
25132
+ const localRepos = /* @__PURE__ */ new Set();
25133
+ for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
25134
+ const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
25135
+ if (full !== "mutmutco/mmi-hub") localRepos.add(full);
25136
+ }
25137
+ if (localRepos.size > 0) independentCount = localRepos.size;
25138
+ } catch {
25139
+ }
25140
+ }
25141
+ try {
25142
+ assertPropagationCoverage(rosterRepos2.length, independentCount);
25143
+ } catch (e) {
25144
+ return fail(e.message);
25145
+ }
25146
+ const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
25147
+ const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
25148
+ const canaryProject = projects.find((p) => p.seedCanary === true);
25149
+ const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
25150
+ const repos = rosterRepos2.map((repo) => {
25151
+ const slug = repo.split("/").pop().toLowerCase();
25152
+ const waiver = seed.waivers?.[slug];
25153
+ return { repo, slug, waiver };
25154
+ });
25155
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
25156
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
25157
+ const branchPrefix = "seed-propagate";
25158
+ const reads = [];
25159
+ for (const r of repos) {
25160
+ if (r.waiver) continue;
25161
+ const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
25162
+ let content = null;
25163
+ try {
25164
+ const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
25165
+ const parsed = JSON.parse(resp.stdout);
25166
+ content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
25167
+ } catch {
25168
+ content = null;
25169
+ }
25170
+ const drift = compareSeedBytes(hubContent, content);
25171
+ let pr2;
25172
+ try {
25173
+ const branch = `${branchPrefix}-${r.slug}`;
25174
+ const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
25175
+ const arr = JSON.parse(listed.stdout || "[]");
25176
+ const p = arr[0];
25177
+ if (p) {
25178
+ const rollup = p.statusCheckRollup ?? [];
25179
+ const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
25180
+ pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
25181
+ }
25182
+ } catch {
25183
+ pr2 = void 0;
25184
+ }
25185
+ reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
25186
+ }
25187
+ let functionGateSatisfied = !isWorkflowSeed;
25188
+ if (isWorkflowSeed && canarySlug) {
25189
+ const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
25190
+ const canaryRead = reads.find((r) => r.repo === canaryRepo);
25191
+ if (canaryRead?.drift === "match") {
25192
+ try {
25193
+ const workflowFile = seed.target.split("/").pop();
25194
+ const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
25195
+ const parsed = JSON.parse(runs.stdout);
25196
+ functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
25197
+ } catch {
25198
+ functionGateSatisfied = false;
25199
+ }
25200
+ }
25201
+ }
25202
+ const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
25203
+ if (o.execute) {
25204
+ if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
25205
+ const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
25206
+ for (const rec of plan.records) {
25207
+ if (rec.action !== "open-pr") continue;
25208
+ const repoEntry = repos.find((r) => r.repo === rec.repo);
25209
+ const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
25210
+ const branch = `${branchPrefix}-${repoEntry.slug}`;
25211
+ const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
25212
+ const baseSha = baseRef.stdout.trim();
25213
+ let branchExists = true;
25214
+ try {
25215
+ await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
25216
+ } catch {
25217
+ branchExists = false;
25218
+ }
25219
+ if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
25220
+ let existingSha;
25221
+ try {
25222
+ const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
25223
+ existingSha = JSON.parse(cur.stdout).sha;
25224
+ } catch {
25225
+ existingSha = void 0;
25226
+ }
25227
+ const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
25228
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
25229
+ try {
25230
+ await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
25231
+ } finally {
25232
+ try {
25233
+ (0, import_node_fs31.unlinkSync)(tmp);
25234
+ } catch {
25235
+ }
25236
+ }
25237
+ const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
25238
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
25239
+ let prUrl;
25240
+ if (prDecision.action === "reuse") {
25241
+ prUrl = prDecision.url;
25242
+ } else {
25243
+ const created = await ghCreate([
25244
+ "pr",
25245
+ "create",
25246
+ "--repo",
25247
+ rec.repo,
25248
+ "--base",
25249
+ baseBranch,
25250
+ "--head",
25251
+ branch,
25252
+ "--title",
25253
+ `chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
25254
+ "--body",
25255
+ `Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
25256
+
25257
+ Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
25258
+
25259
+ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
25260
+ ]);
25261
+ prUrl = created.url;
25262
+ }
25263
+ if (rec.wave !== 0) {
25264
+ await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
25265
+ });
25266
+ }
25267
+ rec.prUrl = prUrl;
25268
+ }
25269
+ }
25270
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25271
+ else console.log(renderPropagationReport(plan));
25272
+ if (plan.halted) process.exitCode = 1;
25273
+ });
25068
25274
  }
25069
25275
 
25070
25276
  // src/stage-commands.ts
@@ -25605,6 +25811,7 @@ function registerBoardCommands(program3) {
25605
25811
  force: o.force,
25606
25812
  allowPartial: o.allowPartial
25607
25813
  });
25814
+ invalidateStatuslineBoardCache();
25608
25815
  if (o.json) return console.log(JSON.stringify(result));
25609
25816
  console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
25610
25817
  } catch (e) {
@@ -25621,6 +25828,7 @@ function registerBoardCommands(program3) {
25621
25828
  force: o.force,
25622
25829
  allowPartial: o.allowPartial
25623
25830
  });
25831
+ if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
25624
25832
  if (o.json) {
25625
25833
  console.log(JSON.stringify(bulk.results));
25626
25834
  } else {
@@ -25665,6 +25873,7 @@ function registerBoardCommands(program3) {
25665
25873
  repo: o.repo,
25666
25874
  allowPartial: o.allowPartial
25667
25875
  });
25876
+ if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
25668
25877
  if (o.json) {
25669
25878
  console.log(JSON.stringify(bulk.results));
25670
25879
  } else {
@@ -25689,6 +25898,7 @@ function registerBoardCommands(program3) {
25689
25898
  if (issueRefs.length === 1) {
25690
25899
  try {
25691
25900
  const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
25901
+ invalidateStatuslineBoardCache();
25692
25902
  if (o.json) return console.log(JSON.stringify(result));
25693
25903
  console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
25694
25904
  } catch (e) {
@@ -25740,6 +25950,7 @@ function registerBoardCommands(program3) {
25740
25950
  force: o.force,
25741
25951
  allowPartial: o.allowPartial
25742
25952
  });
25953
+ invalidateStatuslineBoardCache();
25743
25954
  if (o.json) return console.log(JSON.stringify(result));
25744
25955
  console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
25745
25956
  } catch (e) {
@@ -25778,7 +25989,7 @@ var import_node_fs34 = require("node:fs");
25778
25989
  var import_promises8 = require("node:fs/promises");
25779
25990
  var import_node_path33 = require("node:path");
25780
25991
  var import_node_os12 = require("node:os");
25781
- var import_node_child_process16 = require("node:child_process");
25992
+ var import_node_child_process17 = require("node:child_process");
25782
25993
 
25783
25994
  // src/board-advance.ts
25784
25995
  function repoOf2(ref) {
@@ -26061,6 +26272,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
26061
26272
  if (verdict.blocked) throw new Error(verdict.reason);
26062
26273
  return housekeeping;
26063
26274
  }
26275
+ async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
26276
+ const step = "close jerv worktree lease";
26277
+ try {
26278
+ await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
26279
+ return { step, status: "done" };
26280
+ } catch (e) {
26281
+ const err = e;
26282
+ const detail = `${err.message}
26283
+ ${err.stderr ?? ""}`;
26284
+ if (/ENOENT|not found|not recognized/i.test(detail)) {
26285
+ return { step, status: "skipped: jerv-cli not on PATH" };
26286
+ }
26287
+ const msg = (err.stderr?.trim() || err.message).split("\n")[0];
26288
+ return { step, status: `failed: ${msg}` };
26289
+ }
26290
+ }
26064
26291
  async function applyGcPlan(plan, remote, opts = {}) {
26065
26292
  const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
26066
26293
  const beforeWorktrees = parseWorktreePorcelain(
@@ -26109,6 +26336,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26109
26336
  removeWorktreeDir: wtDeps.removeWorktreeDir,
26110
26337
  removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
26111
26338
  });
26339
+ if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
26112
26340
  return cleanup;
26113
26341
  },
26114
26342
  cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
@@ -26155,6 +26383,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26155
26383
  owner,
26156
26384
  reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
26157
26385
  });
26386
+ await bestEffortLeaseClose(wt.path);
26158
26387
  } catch (e) {
26159
26388
  const error = e.message.split("\n")[0];
26160
26389
  result.failed.push(`${wt.path}: ${error}`);
@@ -26327,7 +26556,7 @@ async function remoteBranchExists2(branch, options = {}) {
26327
26556
  }
26328
26557
  var COMPOSE_TIMEOUT_MS = 12e4;
26329
26558
  function spawnDeferredGcSweep() {
26330
- spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process16.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26559
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26331
26560
  }
26332
26561
  async function createDeferredWorktreeStore() {
26333
26562
  try {
@@ -26736,6 +26965,7 @@ var import_node_fs35 = require("node:fs");
26736
26965
  var import_promises9 = require("node:fs/promises");
26737
26966
  var import_node_path34 = require("node:path");
26738
26967
  var GH_TIMEOUT_MS = 2e4;
26968
+ var STALE_PR_LOOKUP_LIMIT = 20;
26739
26969
  var DEFAULT_BASE = "origin/development";
26740
26970
  var DEFAULT_REMOTE = "origin";
26741
26971
  var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
@@ -26895,13 +27125,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
26895
27125
  }
26896
27126
  return candidates;
26897
27127
  }
26898
- function formatStaleLeaks(leaks) {
26899
- if (!leaks.length) return "worktree list --stale: no leaks found";
26900
- const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
27128
+ function formatStaleLeaks(leaks, prLookupFailures = []) {
27129
+ const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
26901
27130
  for (const leak of leaks) {
26902
27131
  lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
26903
27132
  lines.push(` fix: ${leak.remediation}`);
26904
27133
  }
27134
+ if (prLookupFailures.length) {
27135
+ lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
27136
+ for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
27137
+ }
26905
27138
  return lines.join("\n");
26906
27139
  }
26907
27140
  async function repoRootOf() {
@@ -27172,6 +27405,7 @@ function registerWorktreeCommands(program3) {
27172
27405
  }
27173
27406
  }
27174
27407
  report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
27408
+ report.push(await bestEffortLeaseClose(wtPath));
27175
27409
  const result = {
27176
27410
  dryRun: false,
27177
27411
  ...plan,
@@ -27202,8 +27436,9 @@ function registerWorktreeCommands(program3) {
27202
27436
  const ctx = await gatherWorktreeContext();
27203
27437
  if (o.stale) {
27204
27438
  const leaks = classifyStaleLeaks(ctx);
27205
- if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length }, null, 2));
27206
- return console.log(formatStaleLeaks(leaks));
27439
+ const failures = ctx.prLookupFailures ?? [];
27440
+ if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
27441
+ return console.log(formatStaleLeaks(leaks, failures));
27207
27442
  }
27208
27443
  if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
27209
27444
  if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
@@ -27227,22 +27462,18 @@ async function gatherWorktreeContext() {
27227
27462
  const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
27228
27463
  const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
27229
27464
  const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
27465
+ const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
27230
27466
  const openPrBranches = /* @__PURE__ */ new Set();
27231
27467
  const closedBranches = /* @__PURE__ */ new Set();
27232
- try {
27233
- const { stdout } = await execFileP2("gh", ["pr", "list", "--state", "all", "--limit", "200", "--json", "headRefName,state"], { timeout: GH_TIMEOUT_MS });
27234
- const prs = JSON.parse(stdout || "[]");
27235
- const byBranch = /* @__PURE__ */ new Map();
27236
- for (const pr2 of prs) {
27237
- const arr = byBranch.get(pr2.headRefName) ?? [];
27238
- arr.push(pr2.state);
27239
- byBranch.set(pr2.headRefName, arr);
27240
- }
27241
- for (const [br, states] of byBranch) {
27242
- if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
27243
- else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
27244
- }
27245
- } catch {
27468
+ const byBranch = /* @__PURE__ */ new Map();
27469
+ for (const pr2 of prs) {
27470
+ const arr = byBranch.get(pr2.headRefName) ?? [];
27471
+ arr.push(pr2.state);
27472
+ byBranch.set(pr2.headRefName, arr);
27473
+ }
27474
+ for (const [br, states] of byBranch) {
27475
+ if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
27476
+ else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
27246
27477
  }
27247
27478
  const stages = [];
27248
27479
  for (const wt of worktrees) {
@@ -27259,7 +27490,7 @@ async function gatherWorktreeContext() {
27259
27490
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
27260
27491
  });
27261
27492
  }
27262
- return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
27493
+ return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
27263
27494
  }
27264
27495
  async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
27265
27496
  try {
@@ -27280,7 +27511,7 @@ ${err.stderr ?? ""}`;
27280
27511
 
27281
27512
  // src/issue-commands.ts
27282
27513
  var import_node_fs36 = require("node:fs");
27283
- var import_node_crypto6 = require("node:crypto");
27514
+ var import_node_crypto7 = require("node:crypto");
27284
27515
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
27285
27516
  var ReparentConflictError = class extends Error {
27286
27517
  constructor(message, payload) {
@@ -27356,6 +27587,44 @@ async function editIssue(client, options, deps = {}) {
27356
27587
  ...parentResult ? { parent: parentResult } : {}
27357
27588
  };
27358
27589
  }
27590
+ var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
27591
+ async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
27592
+ const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
27593
+ if (!match) {
27594
+ throw new Error(
27595
+ `--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
27596
+ );
27597
+ }
27598
+ const [, evidenceRepo, evidenceIssue, commentId] = match;
27599
+ if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
27600
+ throw new Error(
27601
+ `--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
27602
+ );
27603
+ }
27604
+ let comment;
27605
+ try {
27606
+ comment = await client.rest(
27607
+ "GET",
27608
+ `repos/${repo}/issues/comments/${commentId}`
27609
+ );
27610
+ } catch (e) {
27611
+ if (e instanceof GitHubApiError && e.status === 404) {
27612
+ throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
27613
+ }
27614
+ throw e;
27615
+ }
27616
+ const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
27617
+ if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
27618
+ throw new Error(
27619
+ `--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
27620
+ );
27621
+ }
27622
+ if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
27623
+ throw new Error(
27624
+ `--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
27625
+ );
27626
+ }
27627
+ }
27359
27628
  async function closeIssue(client, options, deps = {}) {
27360
27629
  const parsed = parseIssueRef(options.ref);
27361
27630
  const repo = parsed.repo ?? options.defaultRepo;
@@ -27363,6 +27632,9 @@ async function closeIssue(client, options, deps = {}) {
27363
27632
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
27364
27633
  const reason = options.reason ?? "completed";
27365
27634
  const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
27635
+ if (options.evidence !== void 0) {
27636
+ await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
27637
+ }
27366
27638
  await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
27367
27639
  body: { state: "closed", state_reason: stateReason }
27368
27640
  });
@@ -27530,7 +27802,7 @@ function rowIdempotencyKey(batchKey, spec) {
27530
27802
  const identity = `${spec.type}
27531
27803
  ${spec.title.trim()}
27532
27804
  ${spec.body ?? ""}`;
27533
- const hash = (0, import_node_crypto6.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
27805
+ const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
27534
27806
  return `${batchKey}:${hash}`;
27535
27807
  }
27536
27808
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -27749,7 +28021,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27749
28021
  }
27750
28022
  });
27751
28023
  mutating(
27752
- issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
28024
+ issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--evidence <comment-url>", "URL of an existing comment on this issue carrying the close evidence \u2014 verified before closing (JPT#4668; the sanctioned agent-shell close shape)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
27753
28025
  (opts, args) => {
27754
28026
  let reason;
27755
28027
  try {
@@ -27778,7 +28050,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27778
28050
  defaultRepo,
27779
28051
  reason: parsed.reason,
27780
28052
  duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
27781
- comment: o.comment
28053
+ comment: o.comment,
28054
+ evidence: o.evidence
27782
28055
  });
27783
28056
  console.log(JSON.stringify(result));
27784
28057
  } catch (e) {
@@ -27909,6 +28182,7 @@ ${lines}`, {
27909
28182
  surface: batchSurface,
27910
28183
  noSurface: batchNoSurface
27911
28184
  }, { attach: batchAttach });
28185
+ if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
27912
28186
  console.log(JSON.stringify(result));
27913
28187
  if (result.failures.length) process.exitCode = 1;
27914
28188
  } catch (e) {
@@ -31286,9 +31560,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
31286
31560
  var import_node_fs39 = require("node:fs");
31287
31561
  var import_node_os14 = require("node:os");
31288
31562
  var import_node_path37 = require("node:path");
31289
- var import_node_child_process17 = require("node:child_process");
31563
+ var import_node_child_process18 = require("node:child_process");
31290
31564
  var import_node_util8 = require("node:util");
31291
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process17.execFile);
31565
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
31292
31566
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
31293
31567
  function installedClaudePluginVersion() {
31294
31568
  try {
@@ -31334,12 +31608,12 @@ function installedSurfacePluginVersion(surface) {
31334
31608
  if (token === "claude") return installedClaudePluginVersion();
31335
31609
  if (token !== "codex") return void 0;
31336
31610
  try {
31337
- const raw = process.platform === "win32" ? (0, import_node_child_process17.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31611
+ const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31338
31612
  encoding: "utf8",
31339
31613
  stdio: ["ignore", "pipe", "ignore"],
31340
31614
  timeout: 15e3,
31341
31615
  windowsHide: true
31342
- }) : (0, import_node_child_process17.execFileSync)("codex", ["plugin", "list", "--json"], {
31616
+ }) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
31343
31617
  encoding: "utf8",
31344
31618
  stdio: ["ignore", "pipe", "ignore"],
31345
31619
  timeout: 15e3,
@@ -31357,7 +31631,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
31357
31631
  }
31358
31632
  function worktreeRootSync() {
31359
31633
  try {
31360
- const out = (0, import_node_child_process17.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31634
+ const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31361
31635
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
31362
31636
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
31363
31637
  return root || null;
@@ -31411,23 +31685,6 @@ function hasRepoLocalWorktrees() {
31411
31685
 
31412
31686
  // src/index.ts
31413
31687
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
31414
- async function currentRepoFullName() {
31415
- const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
31416
- const parts = remote.split(/[:/]/).filter(Boolean);
31417
- if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
31418
- return `mutmutco/${await repoSlug()}`;
31419
- }
31420
- async function readDocsAuditFetch(repo) {
31421
- const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
31422
- if ("notArmed" in list) return { notArmed: true };
31423
- if (!list.ok) return { ok: false, error: list.error };
31424
- const wanted = repo.toLowerCase();
31425
- const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
31426
- return {
31427
- ok: true,
31428
- verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
31429
- };
31430
- }
31431
31688
  async function githubRepoReachProbe() {
31432
31689
  const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
31433
31690
  const repo = parseOriginRepo(remote);
@@ -31694,6 +31951,7 @@ function mmiDoctorDeps(opts = {}) {
31694
31951
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31695
31952
  removalContext
31696
31953
  );
31954
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
31697
31955
  return {
31698
31956
  removed: result.removed,
31699
31957
  stillQueued: result.stillDeferred.length,
@@ -32148,6 +32406,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
32148
32406
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
32149
32407
  { removalContext }
32150
32408
  );
32409
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
32151
32410
  if (o.json) return console.log(JSON.stringify(result));
32152
32411
  if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
32153
32412
  if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
@@ -32163,7 +32422,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
32163
32422
  process.exit(process.exitCode ?? 0);
32164
32423
  });
32165
32424
  });
32166
- gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
32425
+ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to read PER BRANCH ? an effort bound, not a correctness one: merge state is resolved per branch, so no branch is ever skipped for being old (#4227)", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
32167
32426
  if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
32168
32427
  if (o.scratch) {
32169
32428
  try {
@@ -32195,13 +32454,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
32195
32454
  if (o.apply) {
32196
32455
  const deferredStore = await createDeferredWorktreeStore();
32197
32456
  const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
32198
- await sweepDeferredWorktrees(
32457
+ const sweepResult = await sweepDeferredWorktrees(
32199
32458
  deferredStore,
32200
32459
  // #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
32201
32460
  // git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
32202
32461
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
32203
32462
  removalContext
32204
32463
  ).catch(() => void 0);
32464
+ for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
32205
32465
  applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
32206
32466
  }
32207
32467
  if (o.json) {
@@ -32222,11 +32482,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
32222
32482
  function runWorktreeInstall(command, cwd, quiet, opts) {
32223
32483
  const stdio = quiet ? "ignore" : "inherit";
32224
32484
  return new Promise((resolve5, reject) => {
32225
- const child2 = opts?.shell ? (0, import_node_child_process18.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
32485
+ const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
32226
32486
  const [bin, ...args] = command.split(" ");
32227
32487
  const file = isWin2 ? "cmd.exe" : bin;
32228
32488
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
32229
- return (0, import_node_child_process18.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
32489
+ return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
32230
32490
  })();
32231
32491
  const timer = setTimeout(() => {
32232
32492
  try {
@@ -32437,6 +32697,34 @@ withExamples(mutating(
32437
32697
  const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
32438
32698
  recordWorktreeOwner(repoRoot2, owner);
32439
32699
  appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
32700
+ let lease;
32701
+ try {
32702
+ await execFileP2("jerv-cli", [
32703
+ "lease",
32704
+ "open",
32705
+ "--kind",
32706
+ "worktree",
32707
+ "--ref",
32708
+ wtPath,
32709
+ ...selector ? ["--repo", selector.repo] : [],
32710
+ // 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
32711
+ // an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
32712
+ // erring short reaps a tree someone is still using between sessions.
32713
+ "--ttl",
32714
+ "72",
32715
+ "--note",
32716
+ `${branch} via mmi-cli worktree create`
32717
+ ], { timeout: GIT_TIMEOUT_MS });
32718
+ lease = { ok: true };
32719
+ } catch (e) {
32720
+ const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
32721
+ lease = { ok: false, error: detail };
32722
+ if (!o.json) {
32723
+ console.error(
32724
+ ` worktree created, but no jerv lease was opened for it: ${detail} \u2014 nothing on the lease plane can expire this tree, so it will need manual cleanup (MMI-Hub#4231).`
32725
+ );
32726
+ }
32727
+ }
32440
32728
  if (o.json) {
32441
32729
  return console.log(JSON.stringify({
32442
32730
  branch,
@@ -32444,6 +32732,7 @@ withExamples(mutating(
32444
32732
  base,
32445
32733
  resumed,
32446
32734
  ...report,
32735
+ lease,
32447
32736
  ...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
32448
32737
  ...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
32449
32738
  }, null, 2));
@@ -32555,7 +32844,7 @@ function scheduleRelatedDiscovery(o) {
32555
32844
  try {
32556
32845
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
32557
32846
  if (o.repo) args.push("--repo", o.repo);
32558
- spawnDetachedSelf(args, { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
32847
+ spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
32559
32848
  } catch {
32560
32849
  }
32561
32850
  }
@@ -32883,48 +33172,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
32883
33172
  await failGraceful(e.message);
32884
33173
  }
32885
33174
  });
32886
- var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger ? record a dated run verdict, or read the dead-man status back");
32887
- 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)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["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) => {
32888
- try {
32889
- const repo = o.repo ?? await currentRepoFullName();
32890
- const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32891
- let outcome;
32892
- if (o.outcome === "clean") outcome = { kind: "clean" };
32893
- else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
32894
- else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
32895
- else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
32896
- const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
32897
- await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
32898
- } catch (e) {
32899
- await failGraceful(e.message);
32900
- }
32901
- });
32902
- docsAudit.command("status").description("read the janitor dead-man verdict back for one repo ? missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output ? the discrete state rather than the sentence").action(async (o) => {
32903
- try {
32904
- const repo = o.repo ?? await currentRepoFullName();
32905
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32906
- const fetched = await readDocsAuditFetch(repo);
32907
- const result = docsAuditStatus(fetched, { repo, today });
32908
- if (o.json) {
32909
- const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
32910
- console.log(JSON.stringify({
32911
- repo,
32912
- armed: !("notArmed" in fetched),
32913
- ok: result.ok,
32914
- state: result.state,
32915
- date: verdict?.date ?? null,
32916
- outcome: verdict?.outcome ?? null,
32917
- checkerVendor: verdict?.checkerVendor ?? null,
32918
- line: result.line
32919
- }, null, 2));
32920
- } else {
32921
- console.log(result.line);
32922
- }
32923
- if (!result.ok) process.exitCode = 1;
32924
- } catch (e) {
32925
- await failGraceful(e.message);
32926
- }
32927
- });
32928
33175
  async function reportWrite(label, res) {
32929
33176
  if (res.ok) {
32930
33177
  console.log(JSON.stringify(res.body));
@@ -33584,6 +33831,7 @@ withExamples(mutating(
33584
33831
  }
33585
33832
  }
33586
33833
  if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
33834
+ invalidateStatuslineBoardCache();
33587
33835
  console.log(JSON.stringify({
33588
33836
  ...created,
33589
33837
  label: issueType,
@@ -33884,6 +34132,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
33884
34132
  return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
33885
34133
  }
33886
34134
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
34135
+ invalidateStatuslineBoardCache();
33887
34136
  console.log(JSON.stringify(created));
33888
34137
  }), [
33889
34138
  'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
@@ -34283,6 +34532,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
34283
34532
  preserveWorktree: o.preserveWorktree,
34284
34533
  removalContext
34285
34534
  });
34535
+ if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
34286
34536
  } catch (e) {
34287
34537
  localCleanup = {
34288
34538
  branch: headRef,
@@ -34295,6 +34545,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
34295
34545
  };
34296
34546
  }
34297
34547
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
34548
+ invalidateStatuslineBoardCache();
34298
34549
  console.log(JSON.stringify({
34299
34550
  ...buildPrMergeResultPayload({
34300
34551
  number,
@@ -34941,7 +35192,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
34941
35192
  for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
34942
35193
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
34943
35194
  if (worktreeBanner) {
34944
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
35195
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
34945
35196
  bannerIo.log(worktreeBanner);
34946
35197
  }
34947
35198
  if (isLinkedWorktree(process.cwd())) {