@mutmutco/cli 3.98.0 → 3.100.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 +719 -273
  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");
@@ -21021,7 +20923,7 @@ function authorizeBodyHasMismatch(body) {
21021
20923
  }
21022
20924
 
21023
20925
  // src/project-set.ts
21024
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
20926
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
21025
20927
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
21026
20928
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
21027
20929
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -21275,6 +21177,11 @@ function parseRuntimeVaultOnlyVar(raw) {
21275
21177
  if (raw === "false") return false;
21276
21178
  throw new Error("org project set: runtimeVaultOnly must be true or false");
21277
21179
  }
21180
+ function parseSeedCanaryVar(raw) {
21181
+ if (raw === "true") return true;
21182
+ if (raw === "false") return false;
21183
+ throw new Error("org project set: seedCanary must be true or false");
21184
+ }
21278
21185
  function parseConsumesDesignSystemVar(raw) {
21279
21186
  if (raw === "fofu") return raw;
21280
21187
  throw new Error('org project set: consumesDesignSystem must be "fofu"');
@@ -21364,7 +21271,8 @@ var SETTABLE_VAR_KEYS = [
21364
21271
  "ci",
21365
21272
  "requiredChecks",
21366
21273
  "gate",
21367
- "secrets"
21274
+ "secrets",
21275
+ "seedCanary"
21368
21276
  ];
21369
21277
  var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
21370
21278
  var SETTABLE_VAR_HINTS = {
@@ -21375,6 +21283,7 @@ var SETTABLE_VAR_HINTS = {
21375
21283
  fofuEnabled: "true|false",
21376
21284
  consumesDesignSystem: '"fofu"',
21377
21285
  runtimeVaultOnly: "true|false",
21286
+ seedCanary: "true|false",
21378
21287
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
21379
21288
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
21380
21289
  requiredGcpApis: "comma-string",
@@ -21471,6 +21380,8 @@ function buildProjectSetPatch(input) {
21471
21380
  patch[key] = parseFofuEnabledVar(raw);
21472
21381
  } else if (key === "runtimeVaultOnly") {
21473
21382
  patch[key] = parseRuntimeVaultOnlyVar(raw);
21383
+ } else if (key === "seedCanary") {
21384
+ patch[key] = parseSeedCanaryVar(raw);
21474
21385
  } else if (key === "consumesDesignSystem") {
21475
21386
  patch[key] = parseConsumesDesignSystemVar(raw);
21476
21387
  } else if (key === "publishDir") {
@@ -22503,7 +22414,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
22503
22414
 
22504
22415
  // src/schedules-commands.ts
22505
22416
  var import_promises4 = require("node:fs/promises");
22506
- var import_node_child_process15 = require("node:child_process");
22417
+ var import_node_child_process16 = require("node:child_process");
22507
22418
  var import_node_util7 = require("node:util");
22508
22419
 
22509
22420
  // src/schedules.ts
@@ -22814,7 +22725,7 @@ function cadenceStale(registryCadence, liveCadence) {
22814
22725
  if (!liveCrons.length) return false;
22815
22726
  return !liveCrons.every((cron) => registered.has(cron));
22816
22727
  }
22817
- function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
22728
+ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22818
22729
  const registryGithub = registry2.filter((r) => r.executor === "github-actions");
22819
22730
  const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
22820
22731
  const registryById = new Map(registryGithub.map((r) => [r.id, r]));
@@ -22845,6 +22756,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22845
22756
  continue;
22846
22757
  }
22847
22758
  if (activeWorkflowNames.has(r.id)) continue;
22759
+ if (disabledWorkflowNames.has(r.id)) {
22760
+ if (readRepos.has(r.repo)) {
22761
+ drifts.push({
22762
+ class: "registered-but-disabled",
22763
+ name: r.id,
22764
+ executor: r.executor || "github-actions",
22765
+ detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
22766
+ 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`
22767
+ });
22768
+ }
22769
+ continue;
22770
+ }
22848
22771
  if (readRepos.has(r.repo)) {
22849
22772
  drifts.push({
22850
22773
  class: "registered-but-dead",
@@ -22920,7 +22843,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
22920
22843
  }
22921
22844
  return drifts.sort((a, b) => a.name.localeCompare(b.name));
22922
22845
  }
22923
- function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
22846
+ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22924
22847
  if (registry2 === null) {
22925
22848
  return {
22926
22849
  reconciliation: [],
@@ -22930,7 +22853,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22930
22853
  }
22931
22854
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
22932
22855
  const drifts = [
22933
- ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
22856
+ ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
22934
22857
  ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
22935
22858
  schedulerRead: Boolean(harbour.schedulerRead),
22936
22859
  now: harbour.now
@@ -22940,7 +22863,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22940
22863
  }
22941
22864
 
22942
22865
  // src/schedules-commands.ts
22943
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
22866
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
22944
22867
  var AWS_REGION = "eu-central-1";
22945
22868
  var AWS_TIMEOUT_MS = 3e4;
22946
22869
  var AWS_RETRY_DELAY_MS = 1500;
@@ -22964,9 +22887,15 @@ async function repoWorkflowEntries(client, repo) {
22964
22887
  const entries = [];
22965
22888
  const failures = [];
22966
22889
  const workflows = [];
22890
+ const disabled = [];
22967
22891
  for (const wf of workflowsList) {
22968
- if (wf?.state !== "active" || typeof wf.path !== "string" || !wf.path) continue;
22892
+ if (typeof wf?.path !== "string" || !wf.path) continue;
22969
22893
  if (!wf.path.startsWith(".github/workflows/")) continue;
22894
+ if (wf.state !== "active") {
22895
+ const basename5 = wf.path.split("/").pop() ?? wf.path;
22896
+ disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
22897
+ continue;
22898
+ }
22970
22899
  try {
22971
22900
  const contents = await client.rest(
22972
22901
  "GET",
@@ -22985,7 +22914,7 @@ async function repoWorkflowEntries(client, repo) {
22985
22914
  else throw e;
22986
22915
  }
22987
22916
  }
22988
- return { entries, failures, workflows };
22917
+ return { entries, failures, workflows, disabled };
22989
22918
  }
22990
22919
  async function githubEntries(client) {
22991
22920
  const entries = [];
@@ -22993,12 +22922,13 @@ async function githubEntries(client) {
22993
22922
  const drift = [];
22994
22923
  const readRepos = [];
22995
22924
  const workflows = [];
22925
+ const disabledWorkflowNames = [];
22996
22926
  let repos;
22997
22927
  try {
22998
22928
  const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
22999
22929
  repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
23000
22930
  } catch (e) {
23001
- return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
22931
+ return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
23002
22932
  }
23003
22933
  const results = await Promise.all(
23004
22934
  repos.map(async (repo) => {
@@ -23015,6 +22945,7 @@ async function githubEntries(client) {
23015
22945
  readRepos.push(r.repo);
23016
22946
  entries.push(...r.entries);
23017
22947
  workflows.push(...r.workflows);
22948
+ disabledWorkflowNames.push(...r.disabled);
23018
22949
  if (r.failures.length) {
23019
22950
  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
22951
  }
@@ -23022,7 +22953,7 @@ async function githubEntries(client) {
23022
22953
  }
23023
22954
  const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
23024
22955
  drift.push(...reconciliation.map(renderDrift));
23025
- return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
22956
+ return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
23026
22957
  }
23027
22958
  async function awsJson(args) {
23028
22959
  const run = async () => {
@@ -23077,7 +23008,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
23077
23008
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
23078
23009
  awsEntries: aws.entries,
23079
23010
  schedulerRead: Boolean(aws.schedulerRead)
23080
- });
23011
+ }, new Set(gh.disabledWorkflowNames));
23081
23012
  const selfManaged = /* @__PURE__ */ new Set();
23082
23013
  for (const proj of projects ?? []) {
23083
23014
  if (proj?.schedulesMode !== "self-managed") continue;
@@ -23926,6 +23857,7 @@ var import_node_os11 = require("node:os");
23926
23857
  var import_node_path29 = require("node:path");
23927
23858
 
23928
23859
  // src/bootstrap-drift.ts
23860
+ var import_node_crypto6 = require("node:crypto");
23929
23861
  function byteComparableSeeds(manifest, cls) {
23930
23862
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
23931
23863
  }
@@ -23934,6 +23866,9 @@ function compareSeedBytes(hubContent, repoContent) {
23934
23866
  const normalize = (s) => s.replace(/\r\n/g, "\n");
23935
23867
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
23936
23868
  }
23869
+ function seedContentHash(content) {
23870
+ return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
23871
+ }
23937
23872
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23938
23873
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
23939
23874
  const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
@@ -23956,11 +23891,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23956
23891
  findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
23957
23892
  continue;
23958
23893
  }
23894
+ const repoContent = byTarget.get(seed.target);
23959
23895
  findings.push({
23960
23896
  repo,
23961
23897
  target: seed.target,
23962
23898
  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)"
23899
+ 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)",
23900
+ // #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
23901
+ ...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
23964
23902
  });
23965
23903
  }
23966
23904
  return findings;
@@ -23979,6 +23917,171 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
23979
23917
  return lines.join("\n");
23980
23918
  }
23981
23919
 
23920
+ // src/bootstrap-propagate.ts
23921
+ function assertPropagationCoverage(rosterCount, independentRegistryCount) {
23922
+ if (rosterCount === 0) {
23923
+ 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)");
23924
+ }
23925
+ if (rosterCount < independentRegistryCount) {
23926
+ throw new Error(
23927
+ `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)`
23928
+ );
23929
+ }
23930
+ }
23931
+ function assignWaves(repos, canarySlug) {
23932
+ const waves = /* @__PURE__ */ new Map();
23933
+ if (!canarySlug) return waves;
23934
+ const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
23935
+ const canary = repos.find((r) => r.slug === canarySlug);
23936
+ if (canary) waves.set(canary.repo, 0);
23937
+ const wave1Count = Math.ceil(rest.length * 0.25);
23938
+ rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
23939
+ return waves;
23940
+ }
23941
+ function statusFor(read) {
23942
+ if (!read) return { status: "pending", record: {} };
23943
+ if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
23944
+ const pr2 = read.pr;
23945
+ if (!pr2) return { status: "pending", record: {} };
23946
+ if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
23947
+ if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
23948
+ return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
23949
+ }
23950
+ var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
23951
+ function planPropagationTick(input) {
23952
+ const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
23953
+ const readByRepo = new Map(reads.map((r) => [r.repo, r]));
23954
+ const records = [];
23955
+ const opened = [];
23956
+ const waived = repos.filter((r) => r.waiver);
23957
+ for (const r of waived) {
23958
+ records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
23959
+ }
23960
+ const eligible = repos.filter((r) => !r.waiver);
23961
+ const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
23962
+ if (refusedNoCanary) {
23963
+ for (const r of eligible) {
23964
+ 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)" });
23965
+ }
23966
+ return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
23967
+ }
23968
+ const waveOf = assignWaves(eligible, canarySlug);
23969
+ const byWave = { 0: [], 1: [], 2: [] };
23970
+ for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
23971
+ let halted = false;
23972
+ let haltReason = null;
23973
+ let waveGateOpen = true;
23974
+ for (const waveNum of [0, 1, 2]) {
23975
+ const waveRepos = byWave[waveNum];
23976
+ if (!waveRepos.length) continue;
23977
+ if (!waveGateOpen || halted) {
23978
+ for (const r of waveRepos) {
23979
+ records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
23980
+ }
23981
+ continue;
23982
+ }
23983
+ let waveAllMatch = true;
23984
+ let waveHasRed = false;
23985
+ for (const r of waveRepos) {
23986
+ const { status, record } = statusFor(readByRepo.get(r.repo));
23987
+ const shouldOpen = status === "pending";
23988
+ if (shouldOpen) opened.push(r.repo);
23989
+ records.push({
23990
+ repo: r.repo,
23991
+ target,
23992
+ wave: waveNum,
23993
+ status,
23994
+ action: shouldOpen ? "open-pr" : "none",
23995
+ detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
23996
+ ...record
23997
+ });
23998
+ if (status !== "match") waveAllMatch = false;
23999
+ if (HALTING_STATUSES.has(status)) waveHasRed = true;
24000
+ }
24001
+ if (waveHasRed) {
24002
+ halted = true;
24003
+ haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
24004
+ waveGateOpen = false;
24005
+ continue;
24006
+ }
24007
+ if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
24008
+ waveGateOpen = false;
24009
+ continue;
24010
+ }
24011
+ waveGateOpen = waveAllMatch;
24012
+ }
24013
+ return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
24014
+ }
24015
+ function renderPropagationReport(plan) {
24016
+ const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
24017
+ if (plan.refusedNoCanary) {
24018
+ lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
24019
+ return lines.join("\n");
24020
+ }
24021
+ for (const r of plan.records) {
24022
+ const wave2 = r.wave == null ? "-" : String(r.wave);
24023
+ lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
24024
+ }
24025
+ lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
24026
+ return lines.join("\n");
24027
+ }
24028
+
24029
+ // src/bootstrap-rollback.ts
24030
+ function resolveRollbackRecord(records, repo, target) {
24031
+ const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
24032
+ if (candidates.length === 0) {
24033
+ return { found: false, reason: `no propagation record resolves for ${repo} + ${target} \u2014 refusing to guess a commit to revert` };
24034
+ }
24035
+ const clean4 = candidates.filter((r) => r.files.length === 1 && r.files[0] === target);
24036
+ if (clean4.length === 0) {
24037
+ return {
24038
+ found: false,
24039
+ reason: `${candidates.length} merged seed-propagate PR(s) found for ${repo} + ${target}, but none is a clean single-file propagation of exactly this target \u2014 refusing to guess a commit to revert`
24040
+ };
24041
+ }
24042
+ const latest = clean4.slice().sort((a, b) => a.mergedAt < b.mergedAt ? 1 : a.mergedAt > b.mergedAt ? -1 : 0)[0];
24043
+ return { found: true, record: latest };
24044
+ }
24045
+ function planRollback(repo, target, slug, records) {
24046
+ const resolution = resolveRollbackRecord(records, repo, target);
24047
+ if (!resolution.found) return { repo, target, resolution };
24048
+ const branch = `seed-rollback-${slug}`;
24049
+ const title = `revert: rollback org-owned ${target} in ${repo} (seed PR #${resolution.record.number})`;
24050
+ const body = renderRollbackPrBody(resolution.record);
24051
+ return { repo, target, resolution, branch, title, body };
24052
+ }
24053
+ function renderRollbackPrBody(record) {
24054
+ return [
24055
+ `Per-repo emergency revert of \`${record.target}\`'s seed-propagate merge (\`mmi-cli bootstrap rollback\`, #4240).`,
24056
+ "",
24057
+ `Reverts ${record.url} (merge ${record.mergeSha}) \u2014 restores this repo's copy of \`${record.target}\` to its content immediately before that merge. Nothing else in this repo changes.`,
24058
+ "",
24059
+ "This is the emergency stop, not the fix: MMI-Hub is still the source of truth during a rollback. Closure is the Hub reverting (or fixing forward) the bad seed commit on `development` \u2014 once it does, this repo matches the Hub again and the drift alarm closes itself. A revert with no Hub-side follow-up re-alarms as drift within a week \u2014 deliberately, so an emergency divergence can never silently become permanent.",
24060
+ "",
24061
+ "Never re-run `bootstrap propagate` with the pre-revert bytes before the Hub itself is fixed \u2014 that reopens exactly what this PR closes."
24062
+ ].join("\n");
24063
+ }
24064
+ function renderRollbackReport(plan) {
24065
+ if (!plan.resolution.found) {
24066
+ return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: REFUSED \u2014 ${plan.resolution.reason}`;
24067
+ }
24068
+ const r = plan.resolution.record;
24069
+ const opened = plan.prUrl ? ` \u2014 opened ${plan.prUrl}` : "";
24070
+ return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: reverting PR#${r.number} (merge ${r.mergeSha}${r.mergedAt ? `, merged ${r.mergedAt}` : ""}) on branch ${plan.branch}${opened}`;
24071
+ }
24072
+ function seedPrRecordFromPropagationRecord(record) {
24073
+ if (!record.mergeSha || record.prNumber == null || !record.prUrl) return null;
24074
+ return {
24075
+ repo: record.repo,
24076
+ target: record.target,
24077
+ number: record.prNumber,
24078
+ url: record.prUrl,
24079
+ mergeSha: record.mergeSha,
24080
+ mergedAt: "",
24081
+ files: [record.target]
24082
+ };
24083
+ }
24084
+
23982
24085
  // src/bootstrap-verify.ts
23983
24086
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
23984
24087
  var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
@@ -25065,6 +25168,304 @@ LIVE apply to ${repo}:
25065
25168
  ${applied.join("\n ")}`);
25066
25169
  }
25067
25170
  });
25171
+ 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 () => {
25172
+ const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
25173
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
25174
+ 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`);
25175
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
25176
+ const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
25177
+ if (!o.target) {
25178
+ return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
25179
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25180
+ }
25181
+ const seed = propagatable.find((s) => s.target === o.target);
25182
+ if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
25183
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25184
+ 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`);
25185
+ const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
25186
+ const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
25187
+ const cfg = await loadConfig();
25188
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
25189
+ if (!projects || projects.length === 0) {
25190
+ 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)");
25191
+ }
25192
+ const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
25193
+ let independentCount = rosterRepos2.length;
25194
+ if ((0, import_node_fs31.existsSync)("projects.json")) {
25195
+ try {
25196
+ const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
25197
+ const localRepos = /* @__PURE__ */ new Set();
25198
+ for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
25199
+ const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
25200
+ if (full !== "mutmutco/mmi-hub") localRepos.add(full);
25201
+ }
25202
+ if (localRepos.size > 0) independentCount = localRepos.size;
25203
+ } catch {
25204
+ }
25205
+ }
25206
+ try {
25207
+ assertPropagationCoverage(rosterRepos2.length, independentCount);
25208
+ } catch (e) {
25209
+ return fail(e.message);
25210
+ }
25211
+ const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
25212
+ const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
25213
+ const canaryProject = projects.find((p) => p.seedCanary === true);
25214
+ const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
25215
+ const repos = rosterRepos2.map((repo) => {
25216
+ const slug = repo.split("/").pop().toLowerCase();
25217
+ const waiver = seed.waivers?.[slug];
25218
+ return { repo, slug, waiver };
25219
+ });
25220
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
25221
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
25222
+ const branchPrefix = "seed-propagate";
25223
+ const reads = [];
25224
+ for (const r of repos) {
25225
+ if (r.waiver) continue;
25226
+ const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
25227
+ let content = null;
25228
+ try {
25229
+ const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
25230
+ const parsed = JSON.parse(resp.stdout);
25231
+ content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
25232
+ } catch {
25233
+ content = null;
25234
+ }
25235
+ const drift = compareSeedBytes(hubContent, content);
25236
+ let pr2;
25237
+ try {
25238
+ const branch = `${branchPrefix}-${r.slug}`;
25239
+ const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
25240
+ const arr = JSON.parse(listed.stdout || "[]");
25241
+ const p = arr[0];
25242
+ if (p) {
25243
+ const rollup = p.statusCheckRollup ?? [];
25244
+ 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";
25245
+ pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
25246
+ }
25247
+ } catch {
25248
+ pr2 = void 0;
25249
+ }
25250
+ reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
25251
+ }
25252
+ let functionGateSatisfied = !isWorkflowSeed;
25253
+ if (isWorkflowSeed && canarySlug) {
25254
+ const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
25255
+ const canaryRead = reads.find((r) => r.repo === canaryRepo);
25256
+ if (canaryRead?.drift === "match") {
25257
+ try {
25258
+ const workflowFile = seed.target.split("/").pop();
25259
+ const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
25260
+ const parsed = JSON.parse(runs.stdout);
25261
+ functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
25262
+ } catch {
25263
+ functionGateSatisfied = false;
25264
+ }
25265
+ }
25266
+ }
25267
+ const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
25268
+ if (o.execute) {
25269
+ 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)");
25270
+ const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
25271
+ for (const rec of plan.records) {
25272
+ if (rec.action !== "open-pr") continue;
25273
+ const repoEntry = repos.find((r) => r.repo === rec.repo);
25274
+ const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
25275
+ const branch = `${branchPrefix}-${repoEntry.slug}`;
25276
+ const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
25277
+ const baseSha = baseRef.stdout.trim();
25278
+ let branchExists = true;
25279
+ try {
25280
+ await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
25281
+ } catch {
25282
+ branchExists = false;
25283
+ }
25284
+ if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
25285
+ let existingSha;
25286
+ try {
25287
+ const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
25288
+ existingSha = JSON.parse(cur.stdout).sha;
25289
+ } catch {
25290
+ existingSha = void 0;
25291
+ }
25292
+ 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`);
25293
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
25294
+ try {
25295
+ await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
25296
+ } finally {
25297
+ try {
25298
+ (0, import_node_fs31.unlinkSync)(tmp);
25299
+ } catch {
25300
+ }
25301
+ }
25302
+ const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
25303
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
25304
+ let prUrl;
25305
+ if (prDecision.action === "reuse") {
25306
+ prUrl = prDecision.url;
25307
+ } else {
25308
+ const created = await ghCreate([
25309
+ "pr",
25310
+ "create",
25311
+ "--repo",
25312
+ rec.repo,
25313
+ "--base",
25314
+ baseBranch,
25315
+ "--head",
25316
+ branch,
25317
+ "--title",
25318
+ `chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
25319
+ "--body",
25320
+ `Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
25321
+
25322
+ Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
25323
+
25324
+ 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.`
25325
+ ]);
25326
+ prUrl = created.url;
25327
+ }
25328
+ if (rec.wave !== 0) {
25329
+ await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
25330
+ });
25331
+ }
25332
+ rec.prUrl = prUrl;
25333
+ }
25334
+ }
25335
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25336
+ else console.log(renderPropagationReport(plan));
25337
+ if (plan.halted) process.exitCode = 1;
25338
+ });
25339
+ bootstrap.command("rollback <repo>").description("#4240: open a per-repo revert PR of the recorded seed-propagate merge \u2014 never a fleet-wide overwrite; dry-run unless --execute").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--target <path>", "the manifest target to roll back (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--record <path>", "read propagation candidates from a persisted `bootstrap propagate --json` report instead of the live seed-propagate PR history").option("--execute", "LIVE revert via gh (master-gated) \u2014 opens/reuses the seed-rollback PR; dry-run prints the plan only").option("--json", "machine-readable output").action(async (repo) => {
25340
+ const o = {
25341
+ class: rawValue("--class", "deployable"),
25342
+ target: rawValue("--target", ""),
25343
+ record: rawValue("--record", ""),
25344
+ execute: rawFlag("--execute"),
25345
+ json: rawFlag("--json")
25346
+ };
25347
+ if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap rollback: --class must be deployable or content");
25348
+ let parsedRepo;
25349
+ try {
25350
+ parsedRepo = parseOwnerRepo(repo);
25351
+ } catch (e) {
25352
+ return fail(`bootstrap rollback: ${e.message}`);
25353
+ }
25354
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
25355
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
25356
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
25357
+ const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
25358
+ if (!o.target) {
25359
+ return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
25360
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25361
+ }
25362
+ const seed = propagatable.find((s) => s.target === o.target);
25363
+ if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
25364
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25365
+ const slug = parsedRepo.slug;
25366
+ const baseBranch = o.class === "content" ? "main" : "development";
25367
+ const branchPrefix = "seed-propagate";
25368
+ const propagateBranch = `${branchPrefix}-${slug}`;
25369
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
25370
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
25371
+ let candidates;
25372
+ if (o.record) {
25373
+ if (!(0, import_node_fs31.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
25374
+ let parsed;
25375
+ try {
25376
+ parsed = JSON.parse((0, import_node_fs31.readFileSync)(o.record, "utf8"));
25377
+ } catch (e) {
25378
+ return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
25379
+ }
25380
+ const recs = Array.isArray(parsed?.records) ? parsed.records : Array.isArray(parsed) ? parsed : [];
25381
+ candidates = recs.map((r) => {
25382
+ try {
25383
+ return seedPrRecordFromPropagationRecord(r);
25384
+ } catch {
25385
+ return null;
25386
+ }
25387
+ }).filter((r) => r !== null);
25388
+ } else {
25389
+ candidates = [];
25390
+ try {
25391
+ const listed = await gh(["pr", "list", "--repo", repo, "--head", propagateBranch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,files", "--limit", "20"]);
25392
+ const arr = JSON.parse(listed.stdout || "[]");
25393
+ for (const p of arr) {
25394
+ if (!p.mergeCommit?.oid) continue;
25395
+ candidates.push({
25396
+ repo,
25397
+ target: seed.target,
25398
+ number: p.number,
25399
+ url: p.url,
25400
+ mergeSha: p.mergeCommit.oid,
25401
+ mergedAt: p.mergedAt ?? "",
25402
+ files: (p.files ?? []).map((f) => f.path)
25403
+ });
25404
+ }
25405
+ } catch (e) {
25406
+ return fail(`bootstrap rollback: could not read ${repo}'s merged ${propagateBranch} PR history: ${e.message}`);
25407
+ }
25408
+ }
25409
+ const plan = planRollback(repo, seed.target, slug, candidates);
25410
+ if (!plan.resolution.found) {
25411
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25412
+ else console.log(renderRollbackReport(plan));
25413
+ return fail(`bootstrap rollback: ${plan.resolution.reason}`);
25414
+ }
25415
+ if (o.execute) {
25416
+ const record = plan.resolution.record;
25417
+ const parentResp = await gh(["api", `repos/${repo}/commits/${record.mergeSha}`, "--jq", ".parents[0].sha"]);
25418
+ const parentSha = parentResp.stdout.trim();
25419
+ if (!parentSha) return fail(`bootstrap rollback: could not resolve ${repo}@${record.mergeSha}'s parent commit \u2014 refusing to guess what to restore`);
25420
+ let preSeedContent = null;
25421
+ try {
25422
+ const resp = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${parentSha}`]);
25423
+ const parsed = JSON.parse(resp.stdout);
25424
+ preSeedContent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
25425
+ } catch {
25426
+ preSeedContent = null;
25427
+ }
25428
+ if (preSeedContent == null) return fail(`bootstrap rollback: '${seed.target}' did not exist in ${repo} at ${parentSha} (the commit before the seed merge) \u2014 nothing to restore; refusing to guess`);
25429
+ const baseRef = await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
25430
+ const baseSha = baseRef.stdout.trim();
25431
+ let branchExists = true;
25432
+ try {
25433
+ await gh(["api", `repos/${repo}/git/ref/heads/${plan.branch}`]);
25434
+ } catch {
25435
+ branchExists = false;
25436
+ }
25437
+ if (!branchExists) await gh(["api", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${plan.branch}`, "-f", `sha=${baseSha}`]);
25438
+ let existingSha;
25439
+ try {
25440
+ const cur = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${plan.branch}`]);
25441
+ existingSha = JSON.parse(cur.stdout).sha;
25442
+ } catch {
25443
+ existingSha = void 0;
25444
+ }
25445
+ const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
25446
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
25447
+ try {
25448
+ await gh(contentPutInputArgs(repo, seed.target, tmp));
25449
+ } finally {
25450
+ try {
25451
+ (0, import_node_fs31.unlinkSync)(tmp);
25452
+ } catch {
25453
+ }
25454
+ }
25455
+ const openPrs = await gh(["pr", "list", "--repo", repo, "--head", plan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
25456
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
25457
+ let prUrl;
25458
+ if (prDecision.action === "reuse") {
25459
+ prUrl = prDecision.url;
25460
+ } else {
25461
+ const created = await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]);
25462
+ prUrl = created.url;
25463
+ }
25464
+ plan.prUrl = prUrl;
25465
+ }
25466
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25467
+ else console.log(renderRollbackReport(plan));
25468
+ });
25068
25469
  }
25069
25470
 
25070
25471
  // src/stage-commands.ts
@@ -25605,6 +26006,7 @@ function registerBoardCommands(program3) {
25605
26006
  force: o.force,
25606
26007
  allowPartial: o.allowPartial
25607
26008
  });
26009
+ invalidateStatuslineBoardCache();
25608
26010
  if (o.json) return console.log(JSON.stringify(result));
25609
26011
  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
26012
  } catch (e) {
@@ -25621,6 +26023,7 @@ function registerBoardCommands(program3) {
25621
26023
  force: o.force,
25622
26024
  allowPartial: o.allowPartial
25623
26025
  });
26026
+ if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
25624
26027
  if (o.json) {
25625
26028
  console.log(JSON.stringify(bulk.results));
25626
26029
  } else {
@@ -25665,6 +26068,7 @@ function registerBoardCommands(program3) {
25665
26068
  repo: o.repo,
25666
26069
  allowPartial: o.allowPartial
25667
26070
  });
26071
+ if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
25668
26072
  if (o.json) {
25669
26073
  console.log(JSON.stringify(bulk.results));
25670
26074
  } else {
@@ -25689,6 +26093,7 @@ function registerBoardCommands(program3) {
25689
26093
  if (issueRefs.length === 1) {
25690
26094
  try {
25691
26095
  const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
26096
+ invalidateStatuslineBoardCache();
25692
26097
  if (o.json) return console.log(JSON.stringify(result));
25693
26098
  console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
25694
26099
  } catch (e) {
@@ -25740,6 +26145,7 @@ function registerBoardCommands(program3) {
25740
26145
  force: o.force,
25741
26146
  allowPartial: o.allowPartial
25742
26147
  });
26148
+ invalidateStatuslineBoardCache();
25743
26149
  if (o.json) return console.log(JSON.stringify(result));
25744
26150
  console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
25745
26151
  } catch (e) {
@@ -25778,7 +26184,7 @@ var import_node_fs34 = require("node:fs");
25778
26184
  var import_promises8 = require("node:fs/promises");
25779
26185
  var import_node_path33 = require("node:path");
25780
26186
  var import_node_os12 = require("node:os");
25781
- var import_node_child_process16 = require("node:child_process");
26187
+ var import_node_child_process17 = require("node:child_process");
25782
26188
 
25783
26189
  // src/board-advance.ts
25784
26190
  function repoOf2(ref) {
@@ -26061,6 +26467,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
26061
26467
  if (verdict.blocked) throw new Error(verdict.reason);
26062
26468
  return housekeeping;
26063
26469
  }
26470
+ async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
26471
+ const step = "close jerv worktree lease";
26472
+ try {
26473
+ await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
26474
+ return { step, status: "done" };
26475
+ } catch (e) {
26476
+ const err = e;
26477
+ const detail = `${err.message}
26478
+ ${err.stderr ?? ""}`;
26479
+ if (/ENOENT|not found|not recognized/i.test(detail)) {
26480
+ return { step, status: "skipped: jerv-cli not on PATH" };
26481
+ }
26482
+ const msg = (err.stderr?.trim() || err.message).split("\n")[0];
26483
+ return { step, status: `failed: ${msg}` };
26484
+ }
26485
+ }
26064
26486
  async function applyGcPlan(plan, remote, opts = {}) {
26065
26487
  const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
26066
26488
  const beforeWorktrees = parseWorktreePorcelain(
@@ -26109,6 +26531,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26109
26531
  removeWorktreeDir: wtDeps.removeWorktreeDir,
26110
26532
  removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
26111
26533
  });
26534
+ if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
26112
26535
  return cleanup;
26113
26536
  },
26114
26537
  cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
@@ -26155,6 +26578,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26155
26578
  owner,
26156
26579
  reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
26157
26580
  });
26581
+ await bestEffortLeaseClose(wt.path);
26158
26582
  } catch (e) {
26159
26583
  const error = e.message.split("\n")[0];
26160
26584
  result.failed.push(`${wt.path}: ${error}`);
@@ -26327,7 +26751,7 @@ async function remoteBranchExists2(branch, options = {}) {
26327
26751
  }
26328
26752
  var COMPOSE_TIMEOUT_MS = 12e4;
26329
26753
  function spawnDeferredGcSweep() {
26330
- spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process16.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26754
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26331
26755
  }
26332
26756
  async function createDeferredWorktreeStore() {
26333
26757
  try {
@@ -26736,6 +27160,7 @@ var import_node_fs35 = require("node:fs");
26736
27160
  var import_promises9 = require("node:fs/promises");
26737
27161
  var import_node_path34 = require("node:path");
26738
27162
  var GH_TIMEOUT_MS = 2e4;
27163
+ var STALE_PR_LOOKUP_LIMIT = 20;
26739
27164
  var DEFAULT_BASE = "origin/development";
26740
27165
  var DEFAULT_REMOTE = "origin";
26741
27166
  var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
@@ -26895,13 +27320,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
26895
27320
  }
26896
27321
  return candidates;
26897
27322
  }
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)`];
27323
+ function formatStaleLeaks(leaks, prLookupFailures = []) {
27324
+ const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
26901
27325
  for (const leak of leaks) {
26902
27326
  lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
26903
27327
  lines.push(` fix: ${leak.remediation}`);
26904
27328
  }
27329
+ if (prLookupFailures.length) {
27330
+ lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
27331
+ for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
27332
+ }
26905
27333
  return lines.join("\n");
26906
27334
  }
26907
27335
  async function repoRootOf() {
@@ -27172,6 +27600,7 @@ function registerWorktreeCommands(program3) {
27172
27600
  }
27173
27601
  }
27174
27602
  report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
27603
+ report.push(await bestEffortLeaseClose(wtPath));
27175
27604
  const result = {
27176
27605
  dryRun: false,
27177
27606
  ...plan,
@@ -27202,8 +27631,9 @@ function registerWorktreeCommands(program3) {
27202
27631
  const ctx = await gatherWorktreeContext();
27203
27632
  if (o.stale) {
27204
27633
  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));
27634
+ const failures = ctx.prLookupFailures ?? [];
27635
+ if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
27636
+ return console.log(formatStaleLeaks(leaks, failures));
27207
27637
  }
27208
27638
  if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
27209
27639
  if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
@@ -27227,22 +27657,18 @@ async function gatherWorktreeContext() {
27227
27657
  const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
27228
27658
  const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
27229
27659
  const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
27660
+ const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
27230
27661
  const openPrBranches = /* @__PURE__ */ new Set();
27231
27662
  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 {
27663
+ const byBranch = /* @__PURE__ */ new Map();
27664
+ for (const pr2 of prs) {
27665
+ const arr = byBranch.get(pr2.headRefName) ?? [];
27666
+ arr.push(pr2.state);
27667
+ byBranch.set(pr2.headRefName, arr);
27668
+ }
27669
+ for (const [br, states] of byBranch) {
27670
+ if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
27671
+ else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
27246
27672
  }
27247
27673
  const stages = [];
27248
27674
  for (const wt of worktrees) {
@@ -27259,7 +27685,7 @@ async function gatherWorktreeContext() {
27259
27685
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
27260
27686
  });
27261
27687
  }
27262
- return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
27688
+ return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
27263
27689
  }
27264
27690
  async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
27265
27691
  try {
@@ -27280,7 +27706,7 @@ ${err.stderr ?? ""}`;
27280
27706
 
27281
27707
  // src/issue-commands.ts
27282
27708
  var import_node_fs36 = require("node:fs");
27283
- var import_node_crypto6 = require("node:crypto");
27709
+ var import_node_crypto7 = require("node:crypto");
27284
27710
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
27285
27711
  var ReparentConflictError = class extends Error {
27286
27712
  constructor(message, payload) {
@@ -27356,6 +27782,44 @@ async function editIssue(client, options, deps = {}) {
27356
27782
  ...parentResult ? { parent: parentResult } : {}
27357
27783
  };
27358
27784
  }
27785
+ var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
27786
+ async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
27787
+ const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
27788
+ if (!match) {
27789
+ throw new Error(
27790
+ `--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
27791
+ );
27792
+ }
27793
+ const [, evidenceRepo, evidenceIssue, commentId] = match;
27794
+ if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
27795
+ throw new Error(
27796
+ `--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
27797
+ );
27798
+ }
27799
+ let comment;
27800
+ try {
27801
+ comment = await client.rest(
27802
+ "GET",
27803
+ `repos/${repo}/issues/comments/${commentId}`
27804
+ );
27805
+ } catch (e) {
27806
+ if (e instanceof GitHubApiError && e.status === 404) {
27807
+ throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
27808
+ }
27809
+ throw e;
27810
+ }
27811
+ const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
27812
+ if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
27813
+ throw new Error(
27814
+ `--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
27815
+ );
27816
+ }
27817
+ if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
27818
+ throw new Error(
27819
+ `--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
27820
+ );
27821
+ }
27822
+ }
27359
27823
  async function closeIssue(client, options, deps = {}) {
27360
27824
  const parsed = parseIssueRef(options.ref);
27361
27825
  const repo = parsed.repo ?? options.defaultRepo;
@@ -27363,6 +27827,9 @@ async function closeIssue(client, options, deps = {}) {
27363
27827
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
27364
27828
  const reason = options.reason ?? "completed";
27365
27829
  const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
27830
+ if (options.evidence !== void 0) {
27831
+ await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
27832
+ }
27366
27833
  await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
27367
27834
  body: { state: "closed", state_reason: stateReason }
27368
27835
  });
@@ -27530,7 +27997,7 @@ function rowIdempotencyKey(batchKey, spec) {
27530
27997
  const identity = `${spec.type}
27531
27998
  ${spec.title.trim()}
27532
27999
  ${spec.body ?? ""}`;
27533
- const hash = (0, import_node_crypto6.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
28000
+ const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
27534
28001
  return `${batchKey}:${hash}`;
27535
28002
  }
27536
28003
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -27749,7 +28216,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27749
28216
  }
27750
28217
  });
27751
28218
  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)"),
28219
+ 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
28220
  (opts, args) => {
27754
28221
  let reason;
27755
28222
  try {
@@ -27778,7 +28245,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27778
28245
  defaultRepo,
27779
28246
  reason: parsed.reason,
27780
28247
  duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
27781
- comment: o.comment
28248
+ comment: o.comment,
28249
+ evidence: o.evidence
27782
28250
  });
27783
28251
  console.log(JSON.stringify(result));
27784
28252
  } catch (e) {
@@ -27909,6 +28377,7 @@ ${lines}`, {
27909
28377
  surface: batchSurface,
27910
28378
  noSurface: batchNoSurface
27911
28379
  }, { attach: batchAttach });
28380
+ if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
27912
28381
  console.log(JSON.stringify(result));
27913
28382
  if (result.failures.length) process.exitCode = 1;
27914
28383
  } catch (e) {
@@ -31286,9 +31755,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
31286
31755
  var import_node_fs39 = require("node:fs");
31287
31756
  var import_node_os14 = require("node:os");
31288
31757
  var import_node_path37 = require("node:path");
31289
- var import_node_child_process17 = require("node:child_process");
31758
+ var import_node_child_process18 = require("node:child_process");
31290
31759
  var import_node_util8 = require("node:util");
31291
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process17.execFile);
31760
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
31292
31761
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
31293
31762
  function installedClaudePluginVersion() {
31294
31763
  try {
@@ -31334,12 +31803,12 @@ function installedSurfacePluginVersion(surface) {
31334
31803
  if (token === "claude") return installedClaudePluginVersion();
31335
31804
  if (token !== "codex") return void 0;
31336
31805
  try {
31337
- const raw = process.platform === "win32" ? (0, import_node_child_process17.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31806
+ const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31338
31807
  encoding: "utf8",
31339
31808
  stdio: ["ignore", "pipe", "ignore"],
31340
31809
  timeout: 15e3,
31341
31810
  windowsHide: true
31342
- }) : (0, import_node_child_process17.execFileSync)("codex", ["plugin", "list", "--json"], {
31811
+ }) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
31343
31812
  encoding: "utf8",
31344
31813
  stdio: ["ignore", "pipe", "ignore"],
31345
31814
  timeout: 15e3,
@@ -31357,7 +31826,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
31357
31826
  }
31358
31827
  function worktreeRootSync() {
31359
31828
  try {
31360
- const out = (0, import_node_child_process17.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31829
+ const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31361
31830
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
31362
31831
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
31363
31832
  return root || null;
@@ -31411,23 +31880,6 @@ function hasRepoLocalWorktrees() {
31411
31880
 
31412
31881
  // src/index.ts
31413
31882
  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
31883
  async function githubRepoReachProbe() {
31432
31884
  const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
31433
31885
  const repo = parseOriginRepo(remote);
@@ -31694,6 +32146,7 @@ function mmiDoctorDeps(opts = {}) {
31694
32146
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31695
32147
  removalContext
31696
32148
  );
32149
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
31697
32150
  return {
31698
32151
  removed: result.removed,
31699
32152
  stillQueued: result.stillDeferred.length,
@@ -32148,6 +32601,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
32148
32601
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
32149
32602
  { removalContext }
32150
32603
  );
32604
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
32151
32605
  if (o.json) return console.log(JSON.stringify(result));
32152
32606
  if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
32153
32607
  if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
@@ -32163,7 +32617,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
32163
32617
  process.exit(process.exitCode ?? 0);
32164
32618
  });
32165
32619
  });
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) => {
32620
+ 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
32621
  if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
32168
32622
  if (o.scratch) {
32169
32623
  try {
@@ -32195,13 +32649,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
32195
32649
  if (o.apply) {
32196
32650
  const deferredStore = await createDeferredWorktreeStore();
32197
32651
  const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
32198
- await sweepDeferredWorktrees(
32652
+ const sweepResult = await sweepDeferredWorktrees(
32199
32653
  deferredStore,
32200
32654
  // #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
32201
32655
  // git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
32202
32656
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
32203
32657
  removalContext
32204
32658
  ).catch(() => void 0);
32659
+ for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
32205
32660
  applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
32206
32661
  }
32207
32662
  if (o.json) {
@@ -32222,11 +32677,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
32222
32677
  function runWorktreeInstall(command, cwd, quiet, opts) {
32223
32678
  const stdio = quiet ? "ignore" : "inherit";
32224
32679
  return new Promise((resolve5, reject) => {
32225
- const child2 = opts?.shell ? (0, import_node_child_process18.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
32680
+ const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
32226
32681
  const [bin, ...args] = command.split(" ");
32227
32682
  const file = isWin2 ? "cmd.exe" : bin;
32228
32683
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
32229
- return (0, import_node_child_process18.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
32684
+ return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
32230
32685
  })();
32231
32686
  const timer = setTimeout(() => {
32232
32687
  try {
@@ -32437,6 +32892,34 @@ withExamples(mutating(
32437
32892
  const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
32438
32893
  recordWorktreeOwner(repoRoot2, owner);
32439
32894
  appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
32895
+ let lease;
32896
+ try {
32897
+ await execFileP2("jerv-cli", [
32898
+ "lease",
32899
+ "open",
32900
+ "--kind",
32901
+ "worktree",
32902
+ "--ref",
32903
+ wtPath,
32904
+ ...selector ? ["--repo", selector.repo] : [],
32905
+ // 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
32906
+ // an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
32907
+ // erring short reaps a tree someone is still using between sessions.
32908
+ "--ttl",
32909
+ "72",
32910
+ "--note",
32911
+ `${branch} via mmi-cli worktree create`
32912
+ ], { timeout: GIT_TIMEOUT_MS });
32913
+ lease = { ok: true };
32914
+ } catch (e) {
32915
+ const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
32916
+ lease = { ok: false, error: detail };
32917
+ if (!o.json) {
32918
+ console.error(
32919
+ ` 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).`
32920
+ );
32921
+ }
32922
+ }
32440
32923
  if (o.json) {
32441
32924
  return console.log(JSON.stringify({
32442
32925
  branch,
@@ -32444,6 +32927,7 @@ withExamples(mutating(
32444
32927
  base,
32445
32928
  resumed,
32446
32929
  ...report,
32930
+ lease,
32447
32931
  ...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
32448
32932
  ...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
32449
32933
  }, null, 2));
@@ -32555,7 +33039,7 @@ function scheduleRelatedDiscovery(o) {
32555
33039
  try {
32556
33040
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
32557
33041
  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() });
33042
+ spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
32559
33043
  } catch {
32560
33044
  }
32561
33045
  }
@@ -32883,48 +33367,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
32883
33367
  await failGraceful(e.message);
32884
33368
  }
32885
33369
  });
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
33370
  async function reportWrite(label, res) {
32929
33371
  if (res.ok) {
32930
33372
  console.log(JSON.stringify(res.body));
@@ -33584,6 +34026,7 @@ withExamples(mutating(
33584
34026
  }
33585
34027
  }
33586
34028
  if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
34029
+ invalidateStatuslineBoardCache();
33587
34030
  console.log(JSON.stringify({
33588
34031
  ...created,
33589
34032
  label: issueType,
@@ -33884,6 +34327,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
33884
34327
  return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
33885
34328
  }
33886
34329
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
34330
+ invalidateStatuslineBoardCache();
33887
34331
  console.log(JSON.stringify(created));
33888
34332
  }), [
33889
34333
  'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
@@ -34283,6 +34727,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
34283
34727
  preserveWorktree: o.preserveWorktree,
34284
34728
  removalContext
34285
34729
  });
34730
+ if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
34286
34731
  } catch (e) {
34287
34732
  localCleanup = {
34288
34733
  branch: headRef,
@@ -34295,6 +34740,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
34295
34740
  };
34296
34741
  }
34297
34742
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
34743
+ invalidateStatuslineBoardCache();
34298
34744
  console.log(JSON.stringify({
34299
34745
  ...buildPrMergeResultPayload({
34300
34746
  number,
@@ -34941,7 +35387,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
34941
35387
  for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
34942
35388
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
34943
35389
  if (worktreeBanner) {
34944
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
35390
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
34945
35391
  bannerIo.log(worktreeBanner);
34946
35392
  }
34947
35393
  if (isLinkedWorktree(process.cwd())) {