@mutmutco/cli 3.136.0 → 3.138.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 +443 -89
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5849,6 +5849,21 @@ async function sweepDeferredWorktrees(store, deps, removalContext) {
5849
5849
  continue;
5850
5850
  }
5851
5851
  const owner = removalContext ? lookupWorktreeOwner(removalContext.primaryRoot, entry.path) : void 0;
5852
+ if (!current && deps.pathExists?.(entry.path) === false) {
5853
+ removed.push(entry.path);
5854
+ if (removalContext) {
5855
+ recordWorktreeRemoval(removalContext.primaryRoot, {
5856
+ action: "removed",
5857
+ command: removalContext.command,
5858
+ target: entry.path,
5859
+ branch: entry.branch,
5860
+ actor: removalContext.actor,
5861
+ owner,
5862
+ reason: "deferred worktree already absent from Git registration and filesystem"
5863
+ });
5864
+ }
5865
+ continue;
5866
+ }
5852
5867
  if (removalContext) {
5853
5868
  const activeRoot = removalContext.activeWorkspaceRoot ?? resolveActiveWorkspaceRoot();
5854
5869
  const activeGuard = decideActiveWorkspaceGuard(entry.path, activeRoot, process.platform, {
@@ -8398,6 +8413,12 @@ function loadBootstrapSeeds(manifestJson) {
8398
8413
  if (s.ownership !== "org" && s.ownership !== "repo") {
8399
8414
  throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
8400
8415
  }
8416
+ if (s.managedBlock) {
8417
+ const block = s.managedBlock;
8418
+ if (!block.source || !block.begin || !block.end || block.begin === block.end || /[\r\n]/.test(block.begin + block.end)) {
8419
+ throw new Error(`invalid managedBlock for ${s.target} (needs source and distinct single-line begin/end markers)`);
8420
+ }
8421
+ }
8401
8422
  }
8402
8423
  return {
8403
8424
  seeds,
@@ -8413,6 +8434,68 @@ function missingPlaceholders(rendered) {
8413
8434
  for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
8414
8435
  return [...out];
8415
8436
  }
8437
+ function isPropagatableSeed(seed) {
8438
+ return seed.ownership === "org" && seed.source === "self" || seed.managedBlock != null;
8439
+ }
8440
+ function sourceLines(content) {
8441
+ const lines = [];
8442
+ const newline = /\r\n|\n|\r/g;
8443
+ let start = 0;
8444
+ for (const match of content.matchAll(newline)) {
8445
+ const index = match.index;
8446
+ lines.push({ start, textEnd: index, end: index + match[0].length, text: content.slice(start, index) });
8447
+ start = index + match[0].length;
8448
+ }
8449
+ lines.push({ start, textEnd: content.length, end: content.length, text: content.slice(start) });
8450
+ return lines;
8451
+ }
8452
+ function normalizeEol(content) {
8453
+ return content.replace(/\r\n|\r|\n/g, "\n");
8454
+ }
8455
+ function upsertManagedSeedBlock(current, desired, begin, end) {
8456
+ const desiredLines = sourceLines(desired);
8457
+ const desiredBegins = desiredLines.filter((line) => line.text === begin);
8458
+ const desiredEnds = desiredLines.filter((line) => line.text === end);
8459
+ if (desiredBegins.length !== 1 || desiredEnds.length !== 1 || desiredEnds[0].start <= desiredBegins[0].start) {
8460
+ return { ok: false, reason: "managed block source must contain exactly one well-ordered marker pair" };
8461
+ }
8462
+ const desiredBegin = desiredBegins[0];
8463
+ const desiredEnd = desiredEnds[0];
8464
+ if (desired.slice(0, desiredBegin.start).trim() || desired.slice(desiredEnd.end).trim()) {
8465
+ return { ok: false, reason: "managed block source must contain only the bounded block" };
8466
+ }
8467
+ const canonical = desired.slice(desiredBegin.start, desiredEnd.textEnd);
8468
+ const currentLines = sourceLines(current);
8469
+ const begins = currentLines.filter((line) => line.text === begin);
8470
+ const ends = currentLines.filter((line) => line.text === end);
8471
+ if (begins.length === 0 && ends.length === 0) {
8472
+ const eol2 = current.match(/\r\n|\n|\r/)?.[0] ?? "\n";
8473
+ const block2 = normalizeEol(canonical).replace(/\n/g, eol2);
8474
+ if (current.length === 0) return { ok: true, content: `${block2}${eol2}`, changed: true, action: "insert" };
8475
+ const separator = /(?:\r\n|\n|\r)$/.test(current) ? eol2 : `${eol2}${eol2}`;
8476
+ return { ok: true, content: `${current}${separator}${block2}${eol2}`, changed: true, action: "insert" };
8477
+ }
8478
+ if (begins.length !== 1 || ends.length !== 1) {
8479
+ return { ok: false, reason: `malformed managed block markers (begin=${begins.length}, end=${ends.length})` };
8480
+ }
8481
+ const currentBegin = begins[0];
8482
+ const currentEnd = ends[0];
8483
+ if (currentEnd.start <= currentBegin.start) {
8484
+ return { ok: false, reason: "malformed managed block markers (end precedes begin)" };
8485
+ }
8486
+ const existing = current.slice(currentBegin.start, currentEnd.textEnd);
8487
+ if (normalizeEol(existing) === normalizeEol(canonical)) {
8488
+ return { ok: true, content: current, changed: false, action: "current" };
8489
+ }
8490
+ const eol = current.match(/\r\n|\n|\r/)?.[0] ?? "\n";
8491
+ const block = normalizeEol(canonical).replace(/\n/g, eol);
8492
+ return {
8493
+ ok: true,
8494
+ content: `${current.slice(0, currentBegin.start)}${block}${current.slice(currentEnd.textEnd)}`,
8495
+ changed: true,
8496
+ action: "replace"
8497
+ };
8498
+ }
8416
8499
  async function resolveHubSeedSource(execGit, defaultBranch = "development") {
8417
8500
  let status;
8418
8501
  try {
@@ -9477,6 +9560,9 @@ function seedMatchesProjectType(seed, projectType) {
9477
9560
  return projectType != null && seed.projectTypes.includes(projectType);
9478
9561
  }
9479
9562
  function planSeedAction(seed, exists) {
9563
+ if (seed.managedBlock) {
9564
+ return exists ? { target: seed.target, action: "update", ownership: "repo", reason: "repo-owned file; Hub-managed block reconciled in place" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing; full template created with Hub-managed block" };
9565
+ }
9480
9566
  if (seed.source === "managed-block") {
9481
9567
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
9482
9568
  }
@@ -9535,6 +9621,18 @@ function resolveSeedContent(seed, vars, readFile9) {
9535
9621
  }
9536
9622
  return null;
9537
9623
  }
9624
+ function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
9625
+ if (!seed.managedBlock) {
9626
+ return { ok: true, content: resolveSeedContent(seed, vars, readFile9), managed: seed.source === "managed-block" };
9627
+ }
9628
+ const base = remoteContent ?? resolveSeedContent(seed, vars, readFile9);
9629
+ if (base == null) return { ok: true, content: null, managed: true };
9630
+ const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
9631
+ const desired = resolveSeedContent(blockSeed, vars, readFile9);
9632
+ if (desired == null) return { ok: true, content: null, managed: true };
9633
+ const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
9634
+ return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
9635
+ }
9538
9636
  function buildRegisterPayload(repo, cls, vars, options = {}) {
9539
9637
  const parsedRepo = parseOwnerRepo(repo);
9540
9638
  const slug = parsedRepo.slug;
@@ -14122,6 +14220,11 @@ function semverRelation(a, b) {
14122
14220
  function classifyRow(row) {
14123
14221
  const findings = [];
14124
14222
  const reasons = /* @__PURE__ */ new Set();
14223
+ if (row.onboarding.status === "drift") {
14224
+ findings.push(`canonical onboarding link drift: ${row.onboarding.detail}`);
14225
+ } else if (row.onboarding.status === "unknown") {
14226
+ reasons.add(`canonical onboarding link evidence unknown: ${row.onboarding.detail}`);
14227
+ }
14125
14228
  const releaseRequired = row.track !== "trunk" && !row.classifications.includes("no-deploy-surface");
14126
14229
  const tagIsVersion = Boolean(row.release.version) && row.unknowns.some((u) => u.startsWith(NO_VERSION_LOCK_PREFIX));
14127
14230
  for (const unknown of row.unknowns) {
@@ -14384,10 +14487,10 @@ var rollout_plan_default = {
14384
14487
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
14385
14488
  },
14386
14489
  baseline: {
14387
- version: "3.136.0",
14388
- tag: "v3.136.0",
14389
- commit: "acba68c21013",
14390
- npm: "@mutmutco/cli@3.136.0"
14490
+ version: "3.138.0",
14491
+ tag: "v3.138.0",
14492
+ commit: "8735687f4693",
14493
+ npm: "@mutmutco/cli@3.138.0"
14391
14494
  },
14392
14495
  exitCriterion: "fleet-n-of-n",
14393
14496
  hubOnlyShortcut: "forbidden",
@@ -14404,14 +14507,14 @@ var rollout_plan_default = {
14404
14507
  repo: "mutmutco/mmi-hub",
14405
14508
  role: "canary",
14406
14509
  schedule: "train",
14407
- v3Target: "v3.136.0"
14510
+ v3Target: "v3.138.0"
14408
14511
  }
14409
14512
  ],
14410
14513
  rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
14411
14514
  rollback: {
14412
14515
  independent: true,
14413
- mechanism: "npm dist-tag latest -> 3.136.0 and redeploy the Hub Lambda from tag v3.136.0 (acba68c21013); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
14414
- v3Target: "v3.136.0 (@mutmutco/cli@3.136.0, tag commit acba68c21013 \u2014 the preserved latest-v3 distribution, D6b)"
14516
+ mechanism: "npm dist-tag latest -> 3.138.0 and redeploy the Hub Lambda from tag v3.138.0 (8735687f4693); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
14517
+ v3Target: "v3.138.0 (@mutmutco/cli@3.138.0, tag commit 8735687f4693 \u2014 the preserved latest-v3 distribution, D6b)"
14415
14518
  }
14416
14519
  },
14417
14520
  {
@@ -15619,6 +15722,7 @@ function registerQueryCommands(program3) {
15619
15722
 
15620
15723
  // src/fleet-track-inventory.ts
15621
15724
  var NO_DEPLOY_SURFACE_MODELS = /* @__PURE__ */ new Set(["none", "content"]);
15725
+ var CANONICAL_ONBOARDING_URL = "https://github.com/mutmutco/MMI-Hub/blob/main/docs/Architecture/agentic-dev-environment.md";
15622
15726
  function ghErrorText(e) {
15623
15727
  const err = e;
15624
15728
  return String(err?.stderr || err?.message || e).trim().replace(/\s+/g, " ");
@@ -15649,6 +15753,23 @@ async function readFleetPluginProbe(deps, repo, branch) {
15649
15753
  return isGhNotFound2(e) ? { repo, state: "absent" } : { repo, state: "unknown", unknown: `plugin surface read failed: ${ghErrorText(e)}` };
15650
15754
  }
15651
15755
  }
15756
+ async function readFleetOnboardingProbe(deps, repo, branch) {
15757
+ const [owner, name] = repo.split("/");
15758
+ const ref = branch ? `?ref=${encodeURIComponent(branch)}` : "";
15759
+ try {
15760
+ const raw = await deps.ghJson([
15761
+ "api",
15762
+ `repos/${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}/contents/README.md${ref}`
15763
+ ]);
15764
+ if (raw.encoding !== "base64" || typeof raw.content !== "string") {
15765
+ return { repo, status: "unknown", detail: "README contents read returned no base64 content" };
15766
+ }
15767
+ const readme = Buffer.from(raw.content.replace(/\s/g, ""), "base64").toString("utf8");
15768
+ return readme.includes(CANONICAL_ONBOARDING_URL) ? { repo, status: "canonical", detail: CANONICAL_ONBOARDING_URL } : { repo, status: "drift", detail: `README.md does not link ${CANONICAL_ONBOARDING_URL}` };
15769
+ } catch (e) {
15770
+ return isGhNotFound2(e) ? { repo, status: "drift", detail: `README.md is absent; expected ${CANONICAL_ONBOARDING_URL}` } : { repo, status: "unknown", detail: `onboarding README read failed: ${ghErrorText(e)}` };
15771
+ }
15772
+ }
15652
15773
  async function mapBounded2(values, limit, read) {
15653
15774
  const results = new Array(values.length);
15654
15775
  let next = 0;
@@ -15679,9 +15800,10 @@ function classificationsOf(surfaces) {
15679
15800
  if (surfaces.deploy === "absent") classes.push("no-deploy-surface");
15680
15801
  return classes;
15681
15802
  }
15682
- function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt) {
15803
+ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt, onboardingProbes = []) {
15683
15804
  const releaseByRepo = new Map(releaseProbes.map((p) => [p.repo.toLowerCase(), p]));
15684
15805
  const pluginByRepo = new Map(pluginProbes.map((p) => [p.repo.toLowerCase(), p]));
15806
+ const onboardingByRepo = new Map(onboardingProbes.map((p) => [p.repo.toLowerCase(), p]));
15685
15807
  const projectByRepo = /* @__PURE__ */ new Map();
15686
15808
  const anomalies = [];
15687
15809
  for (const project2 of projects) {
@@ -15703,11 +15825,13 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
15703
15825
  const project2 = projectByRepo.get(repo.toLowerCase());
15704
15826
  const release = releaseByRepo.get(repo.toLowerCase());
15705
15827
  const plugin = pluginByRepo.get(repo.toLowerCase());
15828
+ const onboarding = onboardingByRepo.get(repo.toLowerCase()) ?? { repo, status: "unknown", detail: "onboarding README was not probed" };
15706
15829
  const surfaces = classifySurfaces(project2, plugin);
15707
15830
  const unknowns = [...release?.unknowns ?? []];
15708
15831
  if (!release) unknowns.push("GitHub release evidence was not collected");
15709
15832
  if (plugin?.unknown) unknowns.push(plugin.unknown);
15710
15833
  if (!plugin) unknowns.push("plugin surface was not probed");
15834
+ if (onboarding.status === "unknown") unknowns.push(onboarding.detail);
15711
15835
  return {
15712
15836
  repo,
15713
15837
  slug: String(project2?.slug ?? repo.split("/")[1] ?? repo).toLowerCase(),
@@ -15719,6 +15843,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
15719
15843
  surfaces,
15720
15844
  classifications: classificationsOf(surfaces),
15721
15845
  pluginVersion: plugin?.version ?? null,
15846
+ onboarding,
15722
15847
  release: {
15723
15848
  tag: release?.releaseTag ?? null,
15724
15849
  version: release?.releasedVersion ?? null,
@@ -15731,7 +15856,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
15731
15856
  requiredChecks: Array.isArray(project2?.requiredChecks) ? project2.requiredChecks : null,
15732
15857
  exemptReason: typeof project2?.ciExemptReason === "string" ? project2.ciExemptReason : null
15733
15858
  },
15734
- source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live" },
15859
+ source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live", onboarding: "github-readme-live" },
15735
15860
  freshness: { readAt: generatedAt, releasedAt: release?.releasedAt ?? null },
15736
15861
  unknowns
15737
15862
  };
@@ -15744,6 +15869,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
15744
15869
  roster: "hub-registry-live",
15745
15870
  releases: "github-releases-live",
15746
15871
  plugin: "github-contents-live",
15872
+ onboarding: "github-readme-live",
15747
15873
  committedInventory: "never"
15748
15874
  },
15749
15875
  counts: {
@@ -15754,6 +15880,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
15754
15880
  noVault: repos.filter((r) => r.classifications.includes("no-vault")).length,
15755
15881
  noPlugin: repos.filter((r) => r.classifications.includes("no-plugin")).length,
15756
15882
  noDeploySurface: repos.filter((r) => r.classifications.includes("no-deploy-surface")).length,
15883
+ onboardingDrift: repos.filter((r) => r.onboarding.status === "drift").length,
15757
15884
  unknownRows: repos.filter((r) => r.unknowns.length > 0).length,
15758
15885
  anomalies: anomalies.length
15759
15886
  },
@@ -15785,11 +15912,12 @@ async function readFleetTrackInventory(deps = defaultFleetTrackInventoryDeps())
15785
15912
  }
15786
15913
  }
15787
15914
  }
15788
- const [releaseProbes, pluginProbes] = await Promise.all([
15915
+ const [releaseProbes, pluginProbes, onboardingProbes] = await Promise.all([
15789
15916
  mapBounded2(repos, 6, (repo) => readOrgVersionProbe(deps.query, repo)),
15790
- mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
15917
+ mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase()))),
15918
+ mapBounded2(repos, 6, (repo) => readFleetOnboardingProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
15791
15919
  ]);
15792
- return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString());
15920
+ return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString(), onboardingProbes);
15793
15921
  }
15794
15922
  function formatFleetTrackInventory(report) {
15795
15923
  const { counts } = report;
@@ -15797,13 +15925,13 @@ function formatFleetTrackInventory(report) {
15797
15925
  `fleet version-track inventory \u2014 ${counts.registeredRepos} registered repositories (${counts.projects} registry projects)`,
15798
15926
  `source: live Hub registry + live GitHub releases/contents \xB7 read ${report.generatedAt} \xB7 committed inventory: never`,
15799
15927
  `tracks: ${counts.tracks.full} full \xB7 ${counts.tracks.direct} direct \xB7 ${counts.tracks.trunk} trunk`,
15800
- `classified: ${counts.noBoard} no-board \xB7 ${counts.noVault} no-vault \xB7 ${counts.noPlugin} no-plugin \xB7 ${counts.noDeploySurface} no-deploy-surface \xB7 ${counts.unknownRows} rows with unknowns`,
15928
+ `classified: ${counts.noBoard} no-board \xB7 ${counts.noVault} no-vault \xB7 ${counts.noPlugin} no-plugin \xB7 ${counts.noDeploySurface} no-deploy-surface \xB7 ${counts.onboardingDrift} onboarding-link drift \xB7 ${counts.unknownRows} rows with unknowns`,
15801
15929
  "",
15802
- "repository | track | deploy model | released | plugin | classifications"
15930
+ "repository | track | deploy model | released | plugin | onboarding | classifications"
15803
15931
  ];
15804
15932
  for (const row of report.repos) {
15805
15933
  lines.push(
15806
- `${row.repo} | ${row.track}${row.declaredTrack ? "" : " (derived)"} | ${row.deployModel ?? "UNDECLARED"} | ${row.release.version ?? "UNKNOWN"} | ${row.pluginVersion ?? (row.surfaces.plugin === "present" ? "present" : row.surfaces.plugin)} | ${row.classifications.join(", ") || "none"}`
15934
+ `${row.repo} | ${row.track}${row.declaredTrack ? "" : " (derived)"} | ${row.deployModel ?? "UNDECLARED"} | ${row.release.version ?? "UNKNOWN"} | ${row.pluginVersion ?? (row.surfaces.plugin === "present" ? "present" : row.surfaces.plugin)} | ${row.onboarding.status} | ${row.classifications.join(", ") || "none"}`
15807
15935
  );
15808
15936
  for (const unknown of row.unknowns) lines.push(` ? ${unknown}`);
15809
15937
  }
@@ -15818,7 +15946,7 @@ function formatFleetTrackInventory(report) {
15818
15946
  function registerFleetTrackInventory(program3) {
15819
15947
  const train = program3.commands.find((c) => c.name() === "train");
15820
15948
  if (!train) throw new Error("train inventory registration requires the train command group");
15821
- train.command("inventory").description("live fleet version-track inventory (#4451) \u2014 every registered repo's track, deploy model, CI contract, release evidence, and explicit no-board/no-vault/no-plugin/no-deploy-surface classification").option("--json", "machine-readable output (the typed D2b input shape)").action(async (o) => {
15949
+ train.command("inventory").description("live fleet version-track inventory (#4451) \u2014 every registered repo's track, release evidence, surface classification, and canonical Hub onboarding-link evidence").option("--json", "machine-readable output (the typed D2b input shape)").action(async (o) => {
15822
15950
  try {
15823
15951
  const report = await readFleetTrackInventory();
15824
15952
  console.log(o.json ? JSON.stringify(report, null, 2) : formatFleetTrackInventory(report));
@@ -20069,7 +20197,7 @@ var import_node_net = require("node:net");
20069
20197
  var import_node_util5 = require("node:util");
20070
20198
 
20071
20199
  // src/rules-sync.ts
20072
- function normalizeEol(s) {
20200
+ function normalizeEol2(s) {
20073
20201
  return s.replace(/\r\n/g, "\n");
20074
20202
  }
20075
20203
 
@@ -20122,8 +20250,8 @@ function envFileKeys(content) {
20122
20250
  return keys;
20123
20251
  }
20124
20252
  function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
20125
- const example = normalizeEol(exampleContent);
20126
- const target = normalizeEol(targetContent);
20253
+ const example = normalizeEol2(exampleContent);
20254
+ const target = normalizeEol2(targetContent);
20127
20255
  const exampleKeys = envFileKeys(example);
20128
20256
  const targetKeys = envFileKeys(target);
20129
20257
  for (const key of exampleKeys) {
@@ -25448,7 +25576,7 @@ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync
25448
25576
  return [];
25449
25577
  }
25450
25578
  }
25451
- function rebuildRepoIndex(cwd, repoSlug2) {
25579
+ function rebuildRepoIndex(cwd, repoSlug3) {
25452
25580
  const candidates = listCandidatePaths(cwd).filter(isIndexablePath);
25453
25581
  const ignored = defaultIsIgnored(cwd, candidates);
25454
25582
  const readmeHints = loadReadmeHints(cwd, candidates.filter((p) => !ignored.has(p)));
@@ -25480,7 +25608,7 @@ function rebuildRepoIndex(cwd, repoSlug2) {
25480
25608
  entries.sort((a, b) => a.path.localeCompare(b.path));
25481
25609
  const projection = {
25482
25610
  schema: REPO_INDEX_SCHEMA,
25483
- repo: repoSlug2,
25611
+ repo: repoSlug3,
25484
25612
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
25485
25613
  entries
25486
25614
  };
@@ -28756,6 +28884,71 @@ function renderPropagationReport(plan) {
28756
28884
  return lines.join("\n");
28757
28885
  }
28758
28886
 
28887
+ // src/bootstrap-propagation-identity.ts
28888
+ var import_node_crypto8 = require("node:crypto");
28889
+ var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
28890
+ var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
28891
+ function safeBranchPart(value, maxLength, fallback) {
28892
+ const safe = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/g, "");
28893
+ return safe || fallback;
28894
+ }
28895
+ function repoSlug2(repo) {
28896
+ return repo.trim().split("/").pop()?.toLowerCase() || "repo";
28897
+ }
28898
+ function propagationBranch(repo, target) {
28899
+ const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
28900
+ const targetPart = safeBranchPart(target, 48, "target");
28901
+ const hash = (0, import_node_crypto8.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
28902
+ return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
28903
+ }
28904
+ function legacyPropagationBranch(repo) {
28905
+ return `${PROPAGATION_BRANCH_PREFIX}${repoSlug2(repo)}`;
28906
+ }
28907
+ function renderPropagationTargetMarker(target) {
28908
+ return `<!-- ${TARGET_MARKER_NAME}: ${JSON.stringify(target)} -->`;
28909
+ }
28910
+ function targetMarkers(body) {
28911
+ const present = body.includes(`${TARGET_MARKER_NAME}:`);
28912
+ const targets = [];
28913
+ const marker = new RegExp(`<!--\\s*${TARGET_MARKER_NAME}:\\s*(.*?)\\s*-->`, "g");
28914
+ for (const match of body.matchAll(marker)) {
28915
+ try {
28916
+ const parsed = JSON.parse(match[1]);
28917
+ if (typeof parsed === "string") targets.push(parsed);
28918
+ } catch {
28919
+ }
28920
+ }
28921
+ return { present, targets };
28922
+ }
28923
+ function canonicalProseTargets(body) {
28924
+ const targets = [];
28925
+ const command = /bootstrap propagate --target (.+?) --execute`/g;
28926
+ const prose = /(?:org-owned copy of|declared marker-bounded block inside repo-owned) `([^`]+)` — the file this PR carries and nothing else/g;
28927
+ for (const match of body.matchAll(command)) targets.push(match[1]);
28928
+ for (const match of body.matchAll(prose)) targets.push(match[1]);
28929
+ return targets;
28930
+ }
28931
+ function propagationPrMatchesTarget(pr2, target, identity) {
28932
+ const body = pr2.body ?? "";
28933
+ const markers = targetMarkers(body);
28934
+ if (markers.present) {
28935
+ if (markers.targets.length !== 1 || markers.targets[0] !== target) return false;
28936
+ return !pr2.files?.length || pr2.files.length === 1 && pr2.files[0] === target;
28937
+ }
28938
+ if (identity === "target-specific") return false;
28939
+ const proseTargets = canonicalProseTargets(body);
28940
+ if (proseTargets.some((candidate) => candidate !== target)) return false;
28941
+ if (pr2.files?.length) {
28942
+ return pr2.files.length === 1 && pr2.files[0] === target;
28943
+ }
28944
+ return proseTargets.length > 0;
28945
+ }
28946
+ function propagationPrCandidatesForTarget(target, targetSpecificPrs, legacyPrs) {
28947
+ const targetSpecific = targetSpecificPrs.filter((pr2) => propagationPrMatchesTarget(pr2, target, "target-specific"));
28948
+ if (targetSpecific.length) return targetSpecific;
28949
+ return legacyPrs.filter((pr2) => propagationPrMatchesTarget(pr2, target, "legacy"));
28950
+ }
28951
+
28759
28952
  // src/bootstrap-rollback.ts
28760
28953
  function resolveRollbackRecord(records, repo, target) {
28761
28954
  const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
@@ -28819,6 +29012,11 @@ var ENVIRONMENT_BRANCHES = {
28819
29012
  rc: "rc",
28820
29013
  main: "main"
28821
29014
  };
29015
+ function classifyEnvironmentDrift(drift) {
29016
+ if (drift.missing) return { environment: drift.environment, action: "CREATE", reason: "environment is missing" };
29017
+ const why = [...drift.problems, ...drift.notes].join("; ");
29018
+ return why ? { environment: drift.environment, action: "UPDATE", reason: why } : { environment: drift.environment, action: "SKIP", reason: "already current" };
29019
+ }
28822
29020
  var TRACK_ENVIRONMENT_EXCEPTIONS = {
28823
29021
  direct: { rc: "direct release track has no rc stage" },
28824
29022
  trunk: {
@@ -28857,9 +29055,9 @@ function reviewerKey(reviewer) {
28857
29055
  return `${reviewer.type ?? ""}:${reviewer.id ?? ""}`;
28858
29056
  }
28859
29057
  function environmentDrift(live, desired) {
28860
- if (!live) return ["environment is missing"];
29058
+ if (!live) return { environment: desired.name, missing: true, problems: ["environment is missing"], notes: [] };
28861
29059
  const problems = [];
28862
- if (live.wait_timer !== desired.waitTimer) problems.push(`wait_timer=${live.wait_timer ?? "unset"} (want ${desired.waitTimer})`);
29060
+ const notes = [];
28863
29061
  const policy = live.deployment_branch_policy;
28864
29062
  if (policy?.protected_branches !== desired.deploymentBranchPolicy.protectedBranches) {
28865
29063
  problems.push(`protected_branches=${String(policy?.protected_branches)} (want true)`);
@@ -28867,16 +29065,67 @@ function environmentDrift(live, desired) {
28867
29065
  if (policy?.custom_branch_policies !== desired.deploymentBranchPolicy.customBranchPolicies) {
28868
29066
  problems.push(`custom_branch_policies=${String(policy?.custom_branch_policies)} (want false)`);
28869
29067
  }
29068
+ if (live.wait_timer !== desired.waitTimer) notes.push(`wait_timer=${live.wait_timer ?? "unset"} (want ${desired.waitTimer})`);
28870
29069
  const reviewers = (live.reviewers ?? []).map(reviewerKey).sort();
28871
- if (reviewers.length !== 0) problems.push(`reviewers=${reviewers.join(", ")} (want none)`);
28872
- return problems;
29070
+ if (reviewers.length !== 0) notes.push(`reviewers=${reviewers.join(", ")} (want none)`);
29071
+ return { environment: desired.name, missing: false, problems, notes };
29072
+ }
29073
+ function environmentBody(spec) {
29074
+ return {
29075
+ wait_timer: spec.waitTimer,
29076
+ reviewers: spec.reviewers,
29077
+ deployment_branch_policy: {
29078
+ protected_branches: spec.deploymentBranchPolicy.protectedBranches,
29079
+ custom_branch_policies: spec.deploymentBranchPolicy.customBranchPolicies
29080
+ }
29081
+ };
29082
+ }
29083
+ var PLAN_GATED_PROTECTION_RE = /billing plan/i;
29084
+ function isPlanGatedProtectionError(error) {
29085
+ return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
29086
+ }
29087
+ async function reconcileRepoEnvironments(repo, releaseTrack, client) {
29088
+ const raw = await client.rest("GET", `repos/${repo}/environments`);
29089
+ const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
29090
+ const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
29091
+ const created = [];
29092
+ const updated = [];
29093
+ const skipped = [];
29094
+ const planGated = [];
29095
+ for (const spec of planned) {
29096
+ const current = live.get(spec.name);
29097
+ const drift = environmentDrift(current, spec);
29098
+ if (drift.problems.length === 0 && drift.notes.length === 0) {
29099
+ skipped.push(spec.name);
29100
+ continue;
29101
+ }
29102
+ const path2 = `repos/${repo}/environments/${encodeURIComponent(spec.name)}`;
29103
+ let gatedProtections = [];
29104
+ try {
29105
+ await client.rest("PUT", path2, { body: environmentBody(spec) });
29106
+ } catch (error) {
29107
+ if (!isPlanGatedProtectionError(error)) throw error;
29108
+ await client.rest("PUT", path2, {
29109
+ body: {
29110
+ deployment_branch_policy: {
29111
+ protected_branches: spec.deploymentBranchPolicy.protectedBranches,
29112
+ custom_branch_policies: spec.deploymentBranchPolicy.customBranchPolicies
29113
+ }
29114
+ }
29115
+ });
29116
+ gatedProtections = ["wait_timer", "reviewers"];
29117
+ }
29118
+ if (gatedProtections.length) planGated.push({ environment: spec.name, protections: gatedProtections });
29119
+ (current ? updated : created).push(spec.name);
29120
+ }
29121
+ return { repo, planned, created, updated, skipped, planGated };
28873
29122
  }
28874
29123
  async function verifyRepoEnvironments(repo, releaseTrack, client) {
28875
29124
  const raw = await client.rest("GET", `repos/${repo}/environments`);
28876
29125
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
28877
29126
  return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
28878
- const problems = environmentDrift(live.get(spec.name), spec);
28879
- return { environment: spec.name, missing: !live.has(spec.name), problems };
29127
+ const drift = environmentDrift(live.get(spec.name), spec);
29128
+ return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
28880
29129
  });
28881
29130
  }
28882
29131
 
@@ -29127,7 +29376,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
29127
29376
  checks.push({
29128
29377
  ok: drift.problems.length === 0,
29129
29378
  label: `repository environment protected: ${drift.environment}`,
29130
- detail: drift.problems.length ? drift.problems.join("; ") : void 0
29379
+ detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
29131
29380
  });
29132
29381
  }
29133
29382
  for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full")).filter((entry) => entry.exception)) {
@@ -29574,7 +29823,7 @@ function registerBootstrapCommands(program3) {
29574
29823
  }
29575
29824
  if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
29576
29825
  });
29577
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
29826
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
29578
29827
  const o = {
29579
29828
  class: rawValue("--class", "deployable"),
29580
29829
  projectType: rawValue("--project-type", ""),
@@ -29595,6 +29844,45 @@ function registerBootstrapCommands(program3) {
29595
29844
  } catch (e) {
29596
29845
  return fail(`bootstrap apply: ${e.message}`);
29597
29846
  }
29847
+ let effectiveTrack = bootstrapReleaseTrack;
29848
+ if (!o.releaseTrack) {
29849
+ try {
29850
+ const meta = await fetchProjectBySlug(parsedRepo.slug, registryClientDeps(await loadConfig()));
29851
+ if (meta?.releaseTrack && isReleaseTrack(meta.releaseTrack)) effectiveTrack = meta.releaseTrack;
29852
+ } catch {
29853
+ }
29854
+ }
29855
+ if (o.only.trim() === "environments") {
29856
+ const client = defaultGitHubClient();
29857
+ const envJson = { repo, track: effectiveTrack };
29858
+ if (!o.execute) {
29859
+ const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
29860
+ const classified = drift.map(classifyEnvironmentDrift);
29861
+ envJson.plan = classified;
29862
+ if (o.json) console.log(JSON.stringify(envJson, null, 2));
29863
+ else {
29864
+ console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
29865
+ for (const c of classified) console.log(` ${c.action} ${c.environment} \u2014 ${c.reason}`);
29866
+ console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
29867
+ }
29868
+ } else {
29869
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
29870
+ const lines = [];
29871
+ if (envResult.created.length) lines.push(`environments created: ${envResult.created.join(", ")}`);
29872
+ if (envResult.updated.length) lines.push(`environments updated: ${envResult.updated.join(", ")}`);
29873
+ if (envResult.skipped.length) lines.push(`environments current: ${envResult.skipped.join(", ")}`);
29874
+ for (const gated of envResult.planGated) {
29875
+ lines.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
29876
+ }
29877
+ envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
29878
+ if (o.json) console.log(JSON.stringify(envJson, null, 2));
29879
+ else {
29880
+ console.log(`LIVE environments apply to ${repo}:`);
29881
+ lines.forEach((l) => console.log(` ${l}`));
29882
+ }
29883
+ }
29884
+ return;
29885
+ }
29598
29886
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
29599
29887
  if (!(0, import_node_fs38.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
29600
29888
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
@@ -29609,6 +29897,7 @@ function registerBootstrapCommands(program3) {
29609
29897
  return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
29610
29898
  ${known}`);
29611
29899
  }
29900
+ const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
29612
29901
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
29613
29902
  const readFile9 = (p) => (0, import_node_fs38.existsSync)(p) ? (0, import_node_fs38.readFileSync)(p, "utf8") : null;
29614
29903
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
@@ -29716,9 +30005,22 @@ function registerBootstrapCommands(program3) {
29716
30005
  exists = false;
29717
30006
  }
29718
30007
  const planned = planSeedAction(resolved, exists);
29719
- const isBlock = resolved.source === "managed-block";
29720
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile9) : null;
29721
- const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
30008
+ const isLegacyBlock = resolved.source === "managed-block";
30009
+ let content = null;
30010
+ let isManaged = isLegacyBlock || resolved.managedBlock != null;
30011
+ if (planned.action === "create" || planned.action === "update") {
30012
+ if (isLegacyBlock) {
30013
+ content = upsertManagedGitignoreBlock(remoteContent).content;
30014
+ } else {
30015
+ const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, remoteContent);
30016
+ if (!writeContent.ok) {
30017
+ return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
30018
+ }
30019
+ content = writeContent.content;
30020
+ isManaged = writeContent.managed;
30021
+ }
30022
+ }
30023
+ const action = reconcileSeedAction(planned, content, isManaged, remoteContent);
29722
30024
  actions.push(action);
29723
30025
  const docBody = content ?? remoteContent;
29724
30026
  if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
@@ -29771,11 +30073,11 @@ function registerBootstrapCommands(program3) {
29771
30073
  "--head",
29772
30074
  seedPlan.branch,
29773
30075
  "--title",
29774
- onlyTarget ? `chore: propagate org-owned ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
30076
+ onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
29775
30077
  "--body",
29776
30078
  onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
29777
30079
 
29778
- \`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR brings this repo's copy to the Hub's. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
30080
+ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owned \`${onlyTarget}\`` : `The org-owned \`${onlyTarget}\` seed`} is declared by MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR reconciles that declared scope and preserves everything outside it. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
29779
30081
 
29780
30082
  \`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli devops bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected"). Seeds are from MMI-Hub@${seedSource.sha}.`
29781
30083
  ]);
@@ -29830,6 +30132,19 @@ function registerBootstrapCommands(program3) {
29830
30132
  }
29831
30133
  }
29832
30134
  }
30135
+ if (o.execute && !onlyTarget) {
30136
+ try {
30137
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
30138
+ if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
30139
+ if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
30140
+ if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
30141
+ for (const gated of envResult.planGated) {
30142
+ applied.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
30143
+ }
30144
+ } catch (e) {
30145
+ applied.push(`environments (failed: ${e.message})`);
30146
+ }
30147
+ }
29833
30148
  if (o.execute && !onlyTarget) {
29834
30149
  for (const l of manifest.labels) {
29835
30150
  try {
@@ -29881,23 +30196,24 @@ LIVE apply to ${repo}:
29881
30196
  ${applied.join("\n ")}`);
29882
30197
  }
29883
30198
  });
29884
- 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 () => {
30199
+ 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 org-owned whole file or declared Hub-managed block)").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 () => {
29885
30200
  const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
29886
30201
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
29887
30202
  if (!(0, import_node_fs38.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`);
29888
30203
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
29889
30204
  if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
29890
30205
  const manifest = loadBootstrapSeeds((0, import_node_fs38.readFileSync)(manifestPath, "utf8"));
29891
- const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
30206
+ const propagatable = manifest.seeds.filter(isPropagatableSeed);
29892
30207
  if (!o.target) {
29893
30208
  return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
29894
30209
  ${propagatable.map((s) => s.target).join("\n ")}`);
29895
30210
  }
29896
30211
  const seed = propagatable.find((s) => s.target === o.target);
29897
- if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
30212
+ if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable targets:
29898
30213
  ${propagatable.map((s) => s.target).join("\n ")}`);
29899
- if (!(0, import_node_fs38.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
29900
- const hubContent = (0, import_node_fs38.readFileSync)(seed.target, "utf8");
30214
+ if (!seed.managedBlock && !(0, import_node_fs38.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
30215
+ const hubContent = seed.managedBlock ? null : (0, import_node_fs38.readFileSync)(seed.target, "utf8");
30216
+ const readSeedFile = (path2) => (0, import_node_fs38.existsSync)(path2) ? (0, import_node_fs38.readFileSync)(path2, "utf8") : null;
29901
30217
  const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
29902
30218
  const cfg = await loadConfig();
29903
30219
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -29953,11 +30269,12 @@ LIVE apply to ${repo}:
29953
30269
  });
29954
30270
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
29955
30271
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
29956
- const branchPrefix = "seed-propagate";
29957
30272
  const reads = [];
30273
+ const desiredByRepo = /* @__PURE__ */ new Map();
29958
30274
  for (const r of repos) {
29959
30275
  if (r.waiver) continue;
29960
- const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
30276
+ const repoClass = classOf(r.repo);
30277
+ const baseBranch = repoClass === "content" ? "main" : "development";
29961
30278
  let content = null;
29962
30279
  try {
29963
30280
  const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
@@ -29966,13 +30283,35 @@ LIVE apply to ${repo}:
29966
30283
  } catch {
29967
30284
  content = null;
29968
30285
  }
29969
- const drift = compareSeedBytes(hubContent, content);
30286
+ let desired;
30287
+ if (seed.managedBlock) {
30288
+ const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass);
30289
+ const resolved = resolveSeedWriteContent(seed, vars, readSeedFile, content);
30290
+ if (!resolved.ok) {
30291
+ return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.reason} \u2014 refusing to overwrite repo-owned content`);
30292
+ }
30293
+ if (resolved.content == null) {
30294
+ return fail(`bootstrap propagate: cannot resolve the full template or managed block source for '${seed.target}' \u2014 refusing a partial fleet plan`);
30295
+ }
30296
+ desired = resolved.content;
30297
+ } else {
30298
+ desired = hubContent;
30299
+ }
30300
+ desiredByRepo.set(r.repo, desired);
30301
+ const drift = compareSeedBytes(desired, content);
29970
30302
  let pr2;
29971
30303
  try {
29972
- const branch = `${branchPrefix}-${r.slug}`;
29973
- const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body", "--limit", "1"]);
29974
- const arr = JSON.parse(listed.stdout || "[]");
29975
- const p = arr[0];
30304
+ const listBranchPrs = async (branch) => {
30305
+ const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body,files", "--limit", "100"]);
30306
+ return JSON.parse(listed.stdout || "[]");
30307
+ };
30308
+ const targetSpecific = (await listBranchPrs(propagationBranch(r.repo, seed.target))).map((p2) => ({ ...p2, files: p2.files?.map((f) => f.path) }));
30309
+ let identified = propagationPrCandidatesForTarget(seed.target, targetSpecific, []);
30310
+ if (!identified.length) {
30311
+ const legacy = (await listBranchPrs(legacyPropagationBranch(r.repo))).map((p2) => ({ ...p2, files: p2.files?.map((f) => f.path) }));
30312
+ identified = propagationPrCandidatesForTarget(seed.target, [], legacy);
30313
+ }
30314
+ const p = identified[0];
29976
30315
  if (p) {
29977
30316
  const rollup = p.statusCheckRollup ?? [];
29978
30317
  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";
@@ -30005,9 +30344,8 @@ LIVE apply to ${repo}:
30005
30344
  const headSha = seedSource.sha;
30006
30345
  for (const rec of plan.records) {
30007
30346
  if (rec.action !== "open-pr") continue;
30008
- const repoEntry = repos.find((r) => r.repo === rec.repo);
30009
30347
  const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
30010
- const branch = `${branchPrefix}-${repoEntry.slug}`;
30348
+ const branch = propagationBranch(rec.repo, seed.target);
30011
30349
  const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
30012
30350
  const baseSha = baseRef.stdout.trim();
30013
30351
  let branchExists = true;
@@ -30025,7 +30363,9 @@ LIVE apply to ${repo}:
30025
30363
  existingSha = void 0;
30026
30364
  }
30027
30365
  const tmp = (0, import_node_path36.join)((0, import_node_os16.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
30028
- (0, import_node_fs38.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
30366
+ const desiredContent = desiredByRepo.get(rec.repo);
30367
+ if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
30368
+ (0, import_node_fs38.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
30029
30369
  try {
30030
30370
  await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
30031
30371
  } finally {
@@ -30034,8 +30374,14 @@ LIVE apply to ${repo}:
30034
30374
  } catch {
30035
30375
  }
30036
30376
  }
30037
- const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
30038
- const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
30377
+ const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url,body,files"]);
30378
+ const listedOpenPrs = JSON.parse(openPrs.stdout || "[]");
30379
+ const identifiedOpenPrs = propagationPrCandidatesForTarget(
30380
+ seed.target,
30381
+ listedOpenPrs.map((p) => ({ ...p, files: p.files?.map((f) => f.path) })),
30382
+ []
30383
+ );
30384
+ const prDecision = decideSeedPrAction(identifiedOpenPrs);
30039
30385
  let prUrl;
30040
30386
  if (prDecision.action === "reuse") {
30041
30387
  prUrl = prDecision.url;
@@ -30050,11 +30396,12 @@ LIVE apply to ${repo}:
30050
30396
  "--head",
30051
30397
  branch,
30052
30398
  "--title",
30053
- `chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
30399
+ `chore: propagate ${seed.managedBlock ? "Hub-managed block in" : "org-owned"} ${seed.target} from MMI-Hub (wave ${rec.wave})`,
30054
30400
  "--body",
30055
30401
  `Auto-opened by \`mmi-cli devops bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
30056
30402
 
30057
- Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
30403
+ Propagates MMI-Hub@${headSha}'s ${seed.managedBlock ? "declared marker-bounded block inside repo-owned" : "org-owned copy of"} \`${seed.target}\` \u2014 the file this PR carries and nothing else; content outside a managed block remains untouched.
30404
+ ${renderPropagationTargetMarker(seed.target)}
30058
30405
 
30059
30406
  Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
30060
30407
  ]);
@@ -30092,18 +30439,18 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
30092
30439
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
30093
30440
  if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
30094
30441
  const manifest = loadBootstrapSeeds((0, import_node_fs38.readFileSync)(manifestPath, "utf8"));
30095
- const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
30442
+ const propagatable = manifest.seeds.filter(isPropagatableSeed);
30096
30443
  if (!o.target) {
30097
30444
  return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
30098
30445
  ${propagatable.map((s) => s.target).join("\n ")}`);
30099
30446
  }
30100
30447
  const seed = propagatable.find((s) => s.target === o.target);
30101
- if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
30448
+ if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
30102
30449
  ${propagatable.map((s) => s.target).join("\n ")}`);
30103
30450
  const slug = parsedRepo.slug;
30104
30451
  const baseBranch = o.class === "content" ? "main" : "development";
30105
- const branchPrefix = "seed-propagate";
30106
- const propagateBranch = `${branchPrefix}-${slug}`;
30452
+ const targetPropagateBranch = propagationBranch(repo, seed.target);
30453
+ const legacyPropagateBranch = legacyPropagationBranch(repo);
30107
30454
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
30108
30455
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
30109
30456
  let candidates;
@@ -30126,9 +30473,17 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
30126
30473
  } else {
30127
30474
  candidates = [];
30128
30475
  try {
30129
- const listed = await gh(["pr", "list", "--repo", repo, "--head", propagateBranch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,files", "--limit", "20"]);
30130
- const arr = JSON.parse(listed.stdout || "[]");
30131
- for (const p of arr) {
30476
+ const listMergedBranchPrs = async (branch) => {
30477
+ const listed = await gh(["pr", "list", "--repo", repo, "--head", branch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,body,files", "--limit", "100"]);
30478
+ return JSON.parse(listed.stdout || "[]");
30479
+ };
30480
+ const targetSpecific = (await listMergedBranchPrs(targetPropagateBranch)).map((p) => ({ ...p, files: p.files?.map((f) => f.path) }));
30481
+ let identified = propagationPrCandidatesForTarget(seed.target, targetSpecific, []);
30482
+ if (!identified.length) {
30483
+ const legacy = (await listMergedBranchPrs(legacyPropagateBranch)).map((p) => ({ ...p, files: p.files?.map((f) => f.path) }));
30484
+ identified = propagationPrCandidatesForTarget(seed.target, [], legacy);
30485
+ }
30486
+ for (const p of identified) {
30132
30487
  if (!p.mergeCommit?.oid) continue;
30133
30488
  candidates.push({
30134
30489
  repo,
@@ -30137,11 +30492,11 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
30137
30492
  url: p.url,
30138
30493
  mergeSha: p.mergeCommit.oid,
30139
30494
  mergedAt: p.mergedAt ?? "",
30140
- files: (p.files ?? []).map((f) => f.path)
30495
+ files: p.files ?? []
30141
30496
  });
30142
30497
  }
30143
30498
  } catch (e) {
30144
- return fail(`bootstrap rollback: could not read ${repo}'s merged ${propagateBranch} PR history: ${e.message}`);
30499
+ return fail(`bootstrap rollback: could not read ${repo}'s merged ${targetPropagateBranch} or positively matching legacy PR history: ${e.message}`);
30145
30500
  }
30146
30501
  }
30147
30502
  const plan = planRollback(repo, seed.target, slug, candidates);
@@ -32136,7 +32491,7 @@ var import_node_os18 = require("node:os");
32136
32491
  var import_node_path42 = require("node:path");
32137
32492
 
32138
32493
  // src/worktree-install-cache.ts
32139
- var import_node_crypto8 = require("node:crypto");
32494
+ var import_node_crypto9 = require("node:crypto");
32140
32495
  var import_node_fs42 = require("node:fs");
32141
32496
  var import_node_path41 = require("node:path");
32142
32497
  var CACHE_DIR = "worktree-install-cache";
@@ -32164,7 +32519,7 @@ var realWorktreeInstallCacheFs = {
32164
32519
  }
32165
32520
  };
32166
32521
  function hashLockfileBytes(contents) {
32167
- return (0, import_node_crypto8.createHash)("sha256").update(contents).digest("hex");
32522
+ return (0, import_node_crypto9.createHash)("sha256").update(contents).digest("hex");
32168
32523
  }
32169
32524
  function resolveWorktreeInstallLockfile(packageDir, fs2 = realWorktreeInstallCacheFs) {
32170
32525
  for (const name of LOCKFILE_NAMES) {
@@ -33190,7 +33545,7 @@ ${err.stderr ?? ""}`;
33190
33545
 
33191
33546
  // src/issue-commands.ts
33192
33547
  var import_node_fs44 = require("node:fs");
33193
- var import_node_crypto9 = require("node:crypto");
33548
+ var import_node_crypto10 = require("node:crypto");
33194
33549
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
33195
33550
  var ReparentConflictError = class extends Error {
33196
33551
  constructor(message, payload) {
@@ -33481,7 +33836,7 @@ function rowIdempotencyKey(batchKey, spec) {
33481
33836
  const identity = `${spec.type}
33482
33837
  ${spec.title.trim()}
33483
33838
  ${spec.body ?? ""}`;
33484
- const hash = (0, import_node_crypto9.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
33839
+ const hash = (0, import_node_crypto10.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
33485
33840
  return `${batchKey}:${hash}`;
33486
33841
  }
33487
33842
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -34160,7 +34515,7 @@ function onboardPluginGate(deps) {
34160
34515
  declared,
34161
34516
  settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
34162
34517
  }).effective;
34163
- return autoUpdate ? { ok: false, detail: "background auto-update is ON \u2014 duplicate writer; run `mmi-updater reconcile` outside Claude Code to disable it" } : { ok: true, detail: "background auto-update off; mmi-updater is the single release-gated writer" };
34518
+ return autoUpdate ? { ok: false, detail: "background auto-update is ON \u2014 duplicate writer; run `mmi-hub update` outside Claude Code to disable it" } : { ok: true, detail: "background auto-update off; mmi-hub is the single release-gated writer" };
34164
34519
  }
34165
34520
  async function collectOnboardStatus(opts = {}) {
34166
34521
  const cfg = await loadConfig();
@@ -36460,13 +36815,13 @@ var surfaces_default = {
36460
36815
  publishVisibility: "public"
36461
36816
  },
36462
36817
  {
36463
- id: "mmi-updater",
36818
+ id: "mmi-hub",
36464
36819
  classification: "capability",
36465
36820
  kind: "cli",
36466
36821
  ownerPath: "updater/package.json",
36467
36822
  deliveryPath: "updater/package.json",
36468
36823
  delivery: "npm",
36469
- applicability: "editor-agnostic agent surfaces",
36824
+ applicability: "editor-agnostic MMI installation and maintenance; npm package @mutmutco/hub exposing mmi-hub",
36470
36825
  versionCoordinated: true,
36471
36826
  versionPaths: [
36472
36827
  {
@@ -36944,7 +37299,7 @@ function checkCliVersion(input, releasedNote) {
36944
37299
  warn: true,
36945
37300
  label: "mmi-cli",
36946
37301
  detail: `${report.currentVersion} \u2014 freshness UNKNOWN, the published version could not be read`,
36947
- fix: "check it directly: `mmi-cli --version` against `npm view @mutmutco/cli version`; update with `npm install -g @mutmutco/cli@<released>`",
37302
+ fix: "check actual maintenance state with `mmi-hub status`; converge with `mmi-hub update`",
36948
37303
  verbose: evidence
36949
37304
  };
36950
37305
  }
@@ -36954,7 +37309,7 @@ function checkCliVersion(input, releasedNote) {
36954
37309
  ok: false,
36955
37310
  label: "mmi-cli",
36956
37311
  detail: `${report.currentVersion} \u2192 ${report.releasedVersion}`,
36957
- fix: report.fix,
37312
+ fix: "run `mmi-hub update` to converge the CLI and every present host surface",
36958
37313
  verbose: evidence
36959
37314
  };
36960
37315
  }
@@ -36967,8 +37322,8 @@ function checkFleetDrift(probe) {
36967
37322
  id,
36968
37323
  ok: true,
36969
37324
  label,
36970
- detail: "no updater journal \u2014 no reconciler scheduled on this machine",
36971
- verbose: [`journal: ${probe.journalPath} (absent)`, "schedule one with `mmi-updater install` (@mutmutco/updater)"]
37325
+ detail: "no MMI Hub maintenance journal on this machine",
37326
+ verbose: [`journal: ${probe.journalPath} (absent)`, "bootstrap with `npm install -g @mutmutco/hub` then `mmi-hub install`"]
36972
37327
  };
36973
37328
  }
36974
37329
  if (probe.unreadable) {
@@ -36979,14 +37334,14 @@ function checkFleetDrift(probe) {
36979
37334
  warn: true,
36980
37335
  label,
36981
37336
  detail: `journal could not be read \u2014 ${probe.unreadable}`,
36982
- fix: `read ${probe.journalPath} directly, or re-register the reconciler with \`mmi-updater install\``,
37337
+ fix: `read ${probe.journalPath} directly, or repair maintenance with \`mmi-hub install\``,
36983
37338
  verbose: [`journal: ${probe.journalPath}`, `read failed: ${probe.unreadable}`]
36984
37339
  };
36985
37340
  }
36986
37341
  const evidence = [
36987
37342
  `journal: ${probe.journalPath}${probe.lastEventAt ? ` (last event ${probe.lastEventAt})` : ""}${probe.truncated ? ` \u2014 newest ${JOURNAL_TAIL_BYTES / 1024} KiB read` : ""}`,
36988
37343
  `expected: ${probe.expected ?? "(no gated release recorded yet)"}`,
36989
- ...probe.surfaces.map((s) => `${s.surface}: installed ${s.installed ?? "unknown"} \u2014 ${s.verdict}${s.detail ? ` (${s.detail})` : ""}${s.compat ? ` [compat ${s.compat}]` : ""}`),
37344
+ ...probe.surfaces.map((s) => `${s.surface}: last recorded ${s.installed ?? "unknown"} \u2014 ${s.verdict}${s.detail ? ` (${s.detail})` : ""}${s.compat ? ` [compat ${s.compat}]` : ""}`),
36990
37345
  ...probe.absent?.length ? [`absent since their last arm, skipped by the reconciler: ${probe.absent.join(", ")}`] : []
36991
37346
  ];
36992
37347
  if (!probe.expected) {
@@ -37031,7 +37386,7 @@ function checkFleetDrift(probe) {
37031
37386
  warn: true,
37032
37387
  label,
37033
37388
  detail: `${probe.expected} \u2014 ${probe.surfaces.length - unverified.length} converged, ${unverifiedNote}: no installed version recorded`,
37034
- fix: `read why in ${probe.journalPath} (each arm records its own reason) \u2014 \`mmi-updater reconcile\` retries now; doctor never installs`,
37389
+ fix: `read why in ${probe.journalPath} (each arm records its own reason) \u2014 \`mmi-hub update\` retries now; doctor never installs`,
37035
37390
  verbose: evidence
37036
37391
  };
37037
37392
  }
@@ -37042,7 +37397,7 @@ function checkFleetDrift(probe) {
37042
37397
  label,
37043
37398
  ...unverified.length ? { verified: false } : {},
37044
37399
  detail: `${drifted.length} of ${probe.surfaces.length} surface(s) behind ${probe.expected}: ${drifted.map((s) => `${s.surface} ${s.installed}`).join(", ")}${unverifiedNote ? `; ${unverifiedNote}` : ""}`,
37045
- fix: `the scheduled updater converges these on its next tick \u2014 \`mmi-updater reconcile\` runs one now; doctor never installs${unverified.length ? `. The unverified surface(s) recorded no version \u2014 read their reason in ${probe.journalPath}` : ""}`,
37400
+ fix: `hourly Hub maintenance converges these on its next tick \u2014 \`mmi-hub update\` runs one now; doctor never installs${unverified.length ? `. The unverified surface(s) recorded no version \u2014 read their reason in ${probe.journalPath}` : ""}`,
37046
37401
  verbose: evidence
37047
37402
  };
37048
37403
  }
@@ -37335,7 +37690,7 @@ function gcReapable(plan) {
37335
37690
  async function runDoctorClean(opts, io, deps) {
37336
37691
  const full = !opts.fast && !opts.banner && !opts.preflight;
37337
37692
  const applyEnv = full;
37338
- const applyRepo = full && opts.repoWrites !== false;
37693
+ const applyRepo = full && opts.repoWrites === true;
37339
37694
  const lane = {
37340
37695
  banner: Boolean(opts.banner),
37341
37696
  fast: Boolean(opts.fast),
@@ -37450,7 +37805,7 @@ async function runDoctorClean(opts, io, deps) {
37450
37805
  ...cli,
37451
37806
  ok: false,
37452
37807
  detail: cli.ok ? `missing commands: ${missing.join(", ")}` : cli.detail,
37453
- fix: `run \`${cliUpdateCommand(released)}\` \u2014 the CLI lacks ${missing.join(", ")} (or wait for the scheduled updater's next tick)`,
37808
+ fix: `run \`mmi-hub update\` \u2014 the CLI lacks ${missing.join(", ")} (or wait for hourly Hub maintenance)`,
37454
37809
  verbose: [...cli.verbose ?? [], `missing commands: ${missing.join(", ")}`]
37455
37810
  });
37456
37811
  }
@@ -37483,7 +37838,7 @@ async function runDoctorClean(opts, io, deps) {
37483
37838
  emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
37484
37839
  restartPending = true;
37485
37840
  } else {
37486
- emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor` (without --no-repo-writes) to write the org-managed .gitignore block", verbose: giEvidence });
37841
+ emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
37487
37842
  }
37488
37843
  }
37489
37844
  async function runPluginCacheRow() {
@@ -37906,7 +38261,7 @@ async function runDoctorClean(opts, io, deps) {
37906
38261
  // #3574: `${n} stale` was prepended to `fix` because the ✗ branch would not render `detail` —
37907
38262
  // the same fact this row already puts in `detail` on its ✓ path four lines up. One convention now.
37908
38263
  detail: `${n} stale`,
37909
- fix: "run `mmi-cli doctor` (without --no-repo-writes) to reap merged branches, stale refs, and dead worktrees",
38264
+ fix: "run `mmi-cli doctor --apply` to reap merged branches, stale refs, and dead worktrees",
37910
38265
  verbose: gcEvidence
37911
38266
  });
37912
38267
  }
@@ -37924,7 +38279,7 @@ async function runDoctorClean(opts, io, deps) {
37924
38279
  restartPending = true;
37925
38280
  }
37926
38281
  } else {
37927
- emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor` (without --no-repo-writes)", verbose: scratchEvidence });
38282
+ emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
37928
38283
  }
37929
38284
  }
37930
38285
  const prefix = [
@@ -38386,9 +38741,8 @@ function mmiDoctorDeps(opts = {}) {
38386
38741
  pluginTrustState: () => codexHookTrustState(),
38387
38742
  releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
38388
38743
  releasedVersionNote: throttled ? throttled.note : void 0,
38389
- // #4954: the fleet reconciler's journal, read. `@mutmutco/updater` owns every npm install now, so
38390
- // doctor's own CLI self-update (#3272) is gone and this read is what took its place — the same
38391
- // journal `mmi-updater stamp` reports from, parsed here into one row.
38744
+ // #4954/#5023: the Hub maintenance journal is read-only last-run evidence. `@mutmutco/hub`
38745
+ // owns every version install; `mmi-hub status` separately probes actual installed state.
38392
38746
  fleetDrift: () => readFleetDrift(process.env),
38393
38747
  // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
38394
38748
  // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
@@ -39245,7 +39599,7 @@ withExamples(mutating(
39245
39599
  return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
39246
39600
  }
39247
39601
  const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
39248
- const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
39602
+ const wtPath = o.path ? (0, import_node_path46.resolve)(o.path) : defaultWorktreePath(repoRoot2, branch);
39249
39603
  const { base: fallbackBase, fetchBranch, preferRemote } = resolveWorktreeBase(fromRef, o.remote);
39250
39604
  let base = fallbackBase;
39251
39605
  step = `fetch the base ref ${fromRef}`;
@@ -42001,14 +42355,14 @@ access.command("audit").description("audit collaborator roles + train-branch pus
42001
42355
  });
42002
42356
  access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
42003
42357
  var isWin2 = process.platform === "win32";
42004
- program2.command("doctor").description("heal plugin wiring and clean up repo cruft \u2014 repairs run by default (#3975); CLI/plugin updates belong to `mmi-updater` and are only REPORTED here (#4954); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes (#4199, canon); heal env drift with a plain run or --no-repo-writes").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format \u2014 repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals the plugin install and cleans repo cruft (gitignore block, merged branches, dead\nworktrees, aged scratch) automatically (#3975). CLI and plugin UPDATES belong to the scheduled\n`mmi-updater` (#4954); doctor reports the lag and the fleet-drift row and never installs.\n--no-repo-writes keeps the working tree untouched for train preflights; --banner/--fast/--self are\nread-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
42358
+ program2.command("doctor").description("safely heal active plugin wiring and report hygiene; repository cleanup requires explicit --apply (#5023), while version convergence belongs to `mmi-hub update`").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the canonical MMI Agentic Onboarding URL").option("--json", "machine-readable output (an output format \u2014 repairs still run by lane)").option("--apply", "advanced explicit lane: also apply guarded repository cleanup (gitignore, docs index, board mechanics, merged branches, dead worktrees, aged scratch)").option("--no-repo-writes", "compatibility spelling for the safe default: env/plugin repairs only, never mutate the repository").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run may heal safe machine-global plugin wiring but never mutates the repository. Run\n`mmi-cli doctor --apply` only when you explicitly want the guarded full repository cleanup lane.\nCLI and host version convergence belongs to `mmi-hub update`; doctor reports lag and last-run evidence.\n--banner/--fast/--self/--preflight are read-only lanes.\n").action(async (opts) => {
42005
42359
  if (opts.guide) {
42006
- consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
42360
+ consoleIo.log(`MMI Agentic Onboarding: ${CANONICAL_ONBOARDING_URL}`);
42007
42361
  return;
42008
42362
  }
42009
42363
  process.exitCode = await runDoctorClean(
42010
42364
  {
42011
- repoWrites: opts.repoWrites,
42365
+ repoWrites: opts.apply === true && opts.repoWrites !== false,
42012
42366
  banner: opts.banner,
42013
42367
  preflight: opts.preflight,
42014
42368
  fast: opts.fast || opts.self,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.136.0",
3
+ "version": "3.138.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",