@mutmutco/cli 3.100.0 → 3.101.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 +120 -19
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12168,6 +12168,16 @@ async function auditRepoCi(repo, deps) {
12168
12168
  });
12169
12169
  }
12170
12170
  }
12171
+ if (repoClass === "deployable") {
12172
+ const exemptReason = meta?.ciExemptReason?.trim();
12173
+ const declaredExempt = statusChecks.size === 0 && !!exemptReason;
12174
+ checks.push({
12175
+ ok: statusChecks.size > 0 || declaredExempt,
12176
+ label: declaredExempt ? "deployable repo declared CI-exempt, not required to gate (#4265)" : "deployable repo requires at least one status check (#4260)",
12177
+ detail: declaredExempt ? `declared-exempt: ${exemptReason}` : statusChecks.size === 0 ? "zero required contexts on an active branch ruleset \u2014 any PR, including an automation seed PR, merges on an empty gate" : void 0,
12178
+ remediation: declaredExempt || statusChecks.size > 0 ? void 0 : `Determine this repo's correct gate context from its own PR-triggered workflow, set it via mmi-cli org project set ${repo} --var requiredChecks=[...], then mmi-cli ci reconcile --repo ${repo} --apply, or, if this repo genuinely has no CI surface, declare it via mmi-cli org project set ${repo} --var ciExemptReason="<why>" (#4265)`
12179
+ });
12180
+ }
12171
12181
  if (deployableGated || repoClass === "hub") {
12172
12182
  const gate = await readDefaultBranchGate(repo, deps.client, baseBranch, Date.now(), gateWorkflowFiles(prWorkflowPaths));
12173
12183
  checks.push({
@@ -13865,6 +13875,12 @@ async function postSchedulesMode(slug, mode, deps) {
13865
13875
  async function postSchedulesRun(id, deps) {
13866
13876
  return postJson("/schedules/run", { id }, deps, "POST", { noRetry: true });
13867
13877
  }
13878
+ async function postSchedulesPark(id, reason, deps) {
13879
+ return postJson("/schedules/park", { id, reason }, deps);
13880
+ }
13881
+ async function postSchedulesUnpark(id, deps) {
13882
+ return postJson("/schedules/unpark", { id }, deps);
13883
+ }
13868
13884
  async function tenantControl(payload, deps) {
13869
13885
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
13870
13886
  }
@@ -20923,7 +20939,7 @@ function authorizeBodyHasMismatch(body) {
20923
20939
  }
20924
20940
 
20925
20941
  // src/project-set.ts
20926
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
20942
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "ciExemptReason", "gate", "seedCanary"];
20927
20943
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
20928
20944
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
20929
20945
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -21218,6 +21234,11 @@ function parseRequiredChecksVar(raw) {
21218
21234
  }
21219
21235
  return parsed.map((c) => c.trim());
21220
21236
  }
21237
+ function parseCiExemptReasonVar(raw) {
21238
+ const trimmed = raw.trim();
21239
+ if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
21240
+ return trimmed;
21241
+ }
21221
21242
  function parseGateVar(raw) {
21222
21243
  let parsed;
21223
21244
  try {
@@ -21270,6 +21291,7 @@ var SETTABLE_VAR_KEYS = [
21270
21291
  "portRange",
21271
21292
  "ci",
21272
21293
  "requiredChecks",
21294
+ "ciExemptReason",
21273
21295
  "gate",
21274
21296
  "secrets",
21275
21297
  "seedCanary"
@@ -21296,6 +21318,7 @@ var SETTABLE_VAR_HINTS = {
21296
21318
  portRange: "JSON {start,end} or [start,end]",
21297
21319
  ci: "none \u2014 declare intentional no-ci",
21298
21320
  requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
21321
+ ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
21299
21322
  gate: "JSON {runtime,cmd,workdir,cacheDepPath,pyVersion}"
21300
21323
  };
21301
21324
  function settableVarHelp() {
@@ -21393,6 +21416,8 @@ function buildProjectSetPatch(input) {
21393
21416
  patch[key] = raw;
21394
21417
  } else if (key === "requiredChecks") {
21395
21418
  patch[key] = parseRequiredChecksVar(raw);
21419
+ } else if (key === "ciExemptReason") {
21420
+ patch[key] = parseCiExemptReasonVar(raw);
21396
21421
  } else if (key === "gate") {
21397
21422
  patch[key] = parseGateVar(raw);
21398
21423
  } else {
@@ -22730,6 +22755,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22730
22755
  const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
22731
22756
  const registryById = new Map(registryGithub.map((r) => [r.id, r]));
22732
22757
  const drifts = [];
22758
+ const parked = [];
22733
22759
  for (const e of liveGithub) {
22734
22760
  if (!registryById.has(e.name)) {
22735
22761
  drifts.push({
@@ -22742,6 +22768,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22742
22768
  }
22743
22769
  }
22744
22770
  for (const r of registryGithub) {
22771
+ if (r.parkedReason) {
22772
+ parked.push({ id: r.id, executor: r.executor, repo: r.repo, reason: r.parkedReason });
22773
+ continue;
22774
+ }
22745
22775
  const live = liveByName.get(r.id);
22746
22776
  if (live) {
22747
22777
  if (cadenceStale(r.cadence, live.cadence)) {
@@ -22763,7 +22793,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22763
22793
  name: r.id,
22764
22794
  executor: r.executor || "github-actions",
22765
22795
  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`
22796
+ remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm, or if this is a deliberate hold run \`mmi-cli org schedules park ${r.id} --reason <ref>\` (#4257) so it stops reading as drift \u2014 never prune a lane parked on purpose`
22767
22797
  });
22768
22798
  }
22769
22799
  continue;
@@ -22778,7 +22808,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22778
22808
  });
22779
22809
  }
22780
22810
  }
22781
- return drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name));
22811
+ return {
22812
+ drifts: drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name)),
22813
+ parked: parked.sort((a, b) => a.id.localeCompare(b.id))
22814
+ };
22782
22815
  }
22783
22816
  function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
22784
22817
  return drifts.filter((d) => {
@@ -22789,6 +22822,9 @@ function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
22789
22822
  function renderDrift(d) {
22790
22823
  return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
22791
22824
  }
22825
+ function renderParked(p) {
22826
+ return `parked (${p.reason}): ${p.id}`;
22827
+ }
22792
22828
  function strayCronDrift(name) {
22793
22829
  return {
22794
22830
  class: "stray-cron",
@@ -22824,12 +22860,17 @@ function unlauncheredLlmDrifts(entries) {
22824
22860
  }
22825
22861
  var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
22826
22862
  function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
22827
- if (!opts.schedulerRead) return [];
22863
+ if (!opts.schedulerRead) return { drifts: [], parked: [] };
22828
22864
  const now = opts.now ?? Date.now();
22829
22865
  const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
22830
22866
  const drifts = [];
22867
+ const parked = [];
22831
22868
  for (const row of registry2) {
22832
22869
  if (row.executor === "github-actions") continue;
22870
+ if (row.parkedReason) {
22871
+ parked.push({ id: row.id, executor: row.executor, repo: row.repo, reason: row.parkedReason });
22872
+ continue;
22873
+ }
22833
22874
  if (armed.has(row.id)) continue;
22834
22875
  const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
22835
22876
  if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
@@ -22841,25 +22882,36 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
22841
22882
  remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
22842
22883
  });
22843
22884
  }
22844
- return drifts.sort((a, b) => a.name.localeCompare(b.name));
22885
+ return {
22886
+ drifts: drifts.sort((a, b) => a.name.localeCompare(b.name)),
22887
+ parked: parked.sort((a, b) => a.id.localeCompare(b.id))
22888
+ };
22845
22889
  }
22846
22890
  function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22847
22891
  if (registry2 === null) {
22848
22892
  return {
22849
22893
  reconciliation: [],
22850
22894
  driftLines: [],
22895
+ parked: [],
22896
+ parkedLines: [],
22851
22897
  incomplete: ["registry: could not read GET /schedules/list \u2014 reconciliation skipped (Hub API unreachable or not authenticated)"]
22852
22898
  };
22853
22899
  }
22854
22900
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
22855
- const drifts = [
22856
- ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
22857
- ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
22858
- schedulerRead: Boolean(harbour.schedulerRead),
22859
- now: harbour.now
22860
- })
22861
- ];
22862
- return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
22901
+ const githubResult = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames);
22902
+ const harbourResult = registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
22903
+ schedulerRead: Boolean(harbour.schedulerRead),
22904
+ now: harbour.now
22905
+ });
22906
+ const drifts = [...githubResult.drifts, ...harbourResult.drifts];
22907
+ const parked = [...githubResult.parked, ...harbourResult.parked].sort((a, b) => a.id.localeCompare(b.id));
22908
+ return {
22909
+ reconciliation: drifts,
22910
+ driftLines: drifts.map(renderDrift),
22911
+ parked,
22912
+ parkedLines: parked.map(renderParked),
22913
+ incomplete: []
22914
+ };
22863
22915
  }
22864
22916
 
22865
22917
  // src/schedules-commands.ts
@@ -23023,7 +23075,9 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
23023
23075
  entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
23024
23076
  incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
23025
23077
  drift: driftLines,
23026
- reconciliation
23078
+ reconciliation,
23079
+ parked: recon.parked,
23080
+ parkedLines: recon.parkedLines
23027
23081
  };
23028
23082
  }
23029
23083
  async function defaultProjectsRead() {
@@ -23036,10 +23090,13 @@ function warnIncomplete2(incomplete) {
23036
23090
  function reportDrift(drift) {
23037
23091
  for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
23038
23092
  }
23093
+ function reportParked(parked) {
23094
+ for (const p of parked) console.log(`org schedules: ${p}`);
23095
+ }
23039
23096
  function registerSchedulesCommands(program3) {
23040
23097
  const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "optional snapshot only \u2014 splice inventory between schedules:inventory markers; live `org schedules` / --json remains SSOT (Hub#4120)").action(async (o) => {
23041
23098
  try {
23042
- const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
23099
+ const { entries, incomplete, drift, reconciliation, parked, parkedLines } = await fetchNotebook();
23043
23100
  if (o.doc) {
23044
23101
  if (incomplete.length) {
23045
23102
  warnIncomplete2(incomplete);
@@ -23055,9 +23112,10 @@ function registerSchedulesCommands(program3) {
23055
23112
  }
23056
23113
  if (o.json) {
23057
23114
  const now = /* @__PURE__ */ new Date();
23058
- console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation }, null, 2));
23115
+ console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation, parked }, null, 2));
23059
23116
  } else {
23060
23117
  console.log(formatSchedulesTable(entries));
23118
+ reportParked(parkedLines);
23061
23119
  reportDrift(drift);
23062
23120
  warnIncomplete2(incomplete);
23063
23121
  }
@@ -23078,6 +23136,30 @@ function registerSchedulesCommands(program3) {
23078
23136
  await failGraceful(e.message);
23079
23137
  }
23080
23138
  });
23139
+ schedules.command("park <schedule-id>").description("record a deliberate hold on a SCHEDULE# row \u2014 the reconciler skips it and `org schedules` renders `parked (<reason>)` instead of DRIFT (#4257)").requiredOption("--reason <ref>", "why the lane is held, e.g. an issue ref (#1774)").action(async (scheduleId, o) => {
23140
+ try {
23141
+ const res = await postSchedulesPark(scheduleId, o.reason, registryClientDeps(await loadConfig()));
23142
+ if (!res.ok) {
23143
+ await failGraceful(`schedules park: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
23144
+ return;
23145
+ }
23146
+ console.log(JSON.stringify(res.body, null, 2));
23147
+ } catch (e) {
23148
+ await failGraceful(e.message);
23149
+ }
23150
+ });
23151
+ schedules.command("unpark <schedule-id>").description("clear a deliberate hold on a SCHEDULE# row \u2014 reconciliation resumes normally (#4257)").action(async (scheduleId) => {
23152
+ try {
23153
+ const res = await postSchedulesUnpark(scheduleId, registryClientDeps(await loadConfig()));
23154
+ if (!res.ok) {
23155
+ await failGraceful(`schedules unpark: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
23156
+ return;
23157
+ }
23158
+ console.log(JSON.stringify(res.body, null, 2));
23159
+ } catch (e) {
23160
+ await failGraceful(e.message);
23161
+ }
23162
+ });
23081
23163
  schedules.command("mode <mode>").description("fleet schedules doctrine for a repo: 'managed' (default \u2014 Hub registry + drift enforcement) or 'self-managed' (opt out; the repo owns its crons). Project-admin self-serve (#3228).").option("--repo <name>", "bare repo name (defaults to the origin remote basename)").action(async (mode, o) => {
23082
23164
  try {
23083
23165
  if (mode !== "managed" && mode !== "self-managed") {
@@ -25210,9 +25292,28 @@ LIVE apply to ${repo}:
25210
25292
  }
25211
25293
  const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
25212
25294
  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) => {
25295
+ const inClassRepos = rosterRepos2.filter((repo) => seed.classes.includes(classOf(repo)));
25296
+ const classPriority = [.../* @__PURE__ */ new Set(["deployable", ...seed.classes])].filter((c) => seed.classes.includes(c));
25297
+ let canaryProject = null;
25298
+ for (const cls of classPriority) {
25299
+ const inClass = projects.filter((p) => p.seedCanary === true && (p.class ?? "deployable") === cls);
25300
+ if (inClass.length === 0) continue;
25301
+ if (inClass.length > 1) {
25302
+ const slugs = inClass.map((p) => (p.repos ?? [])[0]?.split("/").pop() ?? "?").join(", ");
25303
+ return fail(
25304
+ `bootstrap propagate: target '${seed.target}' (classes [${seed.classes.join(", ")}]) matches ${inClass.length} seedCanary:true repos in class '${cls}' (${slugs}) \u2014 exactly one seedCanary:true repo per class is required`
25305
+ );
25306
+ }
25307
+ canaryProject = inClass[0];
25308
+ break;
25309
+ }
25310
+ if (!canaryProject) {
25311
+ return fail(
25312
+ `bootstrap propagate: target '${seed.target}' needs classes [${seed.classes.join(", ")}] but no registry repo with seedCanary:true carries a matching class \u2014 set seedCanary:true on an in-class repo (mmi-cli org project set <slug> --var seedCanary=true)`
25313
+ );
25314
+ }
25315
+ const canarySlug = (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null;
25316
+ const repos = inClassRepos.map((repo) => {
25216
25317
  const slug = repo.split("/").pop().toLowerCase();
25217
25318
  const waiver = seed.waivers?.[slug];
25218
25319
  return { repo, slug, waiver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.100.0",
3
+ "version": "3.101.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",