@mutmutco/cli 4.5.98 → 4.5.99

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 +305 -204
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5731,6 +5731,182 @@ init_github_quota();
5731
5731
  // src/pr-checks-rest.ts
5732
5732
  init_cli_shared();
5733
5733
 
5734
+ // ../shared/github-quota-scope.ts
5735
+ var import_node_crypto3 = require("node:crypto");
5736
+ function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
5737
+ return `github-app-v1:${(0, import_node_crypto3.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
5738
+ }
5739
+
5740
+ // src/app-actor.ts
5741
+ var import_node_crypto4 = require("node:crypto");
5742
+ var APP_ACTOR_ENV = "MMI_ACTOR";
5743
+ var APP_VAULT_REPO = "mutmutco/MMI-Hub";
5744
+ var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
5745
+ var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
5746
+ "pr checks-wait",
5747
+ "pr view",
5748
+ "issue list",
5749
+ "issue view",
5750
+ "api get"
5751
+ ]);
5752
+ var APP_POLL_PERMISSIONS = {
5753
+ metadata: "read",
5754
+ contents: "read",
5755
+ pull_requests: "read",
5756
+ // #7185: without issues:read GraphQL silently drops pr view's closingIssuesReferences.
5757
+ issues: "read",
5758
+ checks: "read",
5759
+ statuses: "read",
5760
+ actions: "read"
5761
+ };
5762
+ var APP_JWT_BACKDATE_S = 60;
5763
+ var APP_JWT_TTL_S = 9 * 60;
5764
+ var AppActorError = class extends Error {
5765
+ constructor(message2, code = "mint-failed") {
5766
+ super(message2);
5767
+ this.code = code;
5768
+ }
5769
+ code;
5770
+ };
5771
+ function commandPathOf(cmd) {
5772
+ const parts = [];
5773
+ let cursor = cmd;
5774
+ while (cursor && cursor.parent) {
5775
+ parts.unshift(cursor.name());
5776
+ cursor = cursor.parent;
5777
+ }
5778
+ return parts.join(" ");
5779
+ }
5780
+ function b64url(input) {
5781
+ return Buffer.from(input).toString("base64url");
5782
+ }
5783
+ function mintAppJwt(appId, privateKeyPem, nowSec) {
5784
+ if (!Number.isFinite(nowSec)) throw new AppActorError("app actor: invalid clock reading for JWT mint");
5785
+ const now = Math.floor(nowSec);
5786
+ const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
5787
+ const payload = b64url(JSON.stringify({
5788
+ iat: now - APP_JWT_BACKDATE_S,
5789
+ exp: now + APP_JWT_TTL_S,
5790
+ iss: appId
5791
+ }));
5792
+ const signer = (0, import_node_crypto4.createSign)("RSA-SHA256");
5793
+ signer.update(`${header}.${payload}`);
5794
+ return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
5795
+ }
5796
+ var activeQuotaScope;
5797
+ function activeAppQuotaScope() {
5798
+ return activeQuotaScope;
5799
+ }
5800
+ async function mintInstallationToken(deps) {
5801
+ const [appId, installationId, privateKey] = await Promise.all(APP_VAULT_KEYS.map((k) => deps.fetchSecret(k)));
5802
+ if (!appId || !installationId || !privateKey) {
5803
+ const missing = APP_VAULT_KEYS.filter((_, i) => ![appId, installationId, privateKey][i]);
5804
+ if (missing.length === APP_VAULT_KEYS.length) {
5805
+ throw new AppActorError(
5806
+ `app actor: vault denied all App credentials (${APP_VAULT_REPO} tier) \u2014 not an automation host, or no vault access`,
5807
+ "vault-denied"
5808
+ );
5809
+ }
5810
+ throw new AppActorError(
5811
+ `app actor: vault carries a PARTIAL App credential set \u2014 missing ${missing.join(", ")} (${APP_VAULT_REPO} tier); fix the vault. Automation fails closed, no personal-token fallback`,
5812
+ "mint-failed"
5813
+ );
5814
+ }
5815
+ const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1e3));
5816
+ let res;
5817
+ try {
5818
+ const jwt = mintAppJwt(appId.trim(), privateKey, nowSec());
5819
+ res = await deps.fetch(`https://api.github.com/app/installations/${installationId.trim()}/access_tokens`, {
5820
+ method: "POST",
5821
+ headers: {
5822
+ authorization: `Bearer ${jwt}`,
5823
+ accept: "application/vnd.github+json",
5824
+ "content-type": "application/json",
5825
+ "user-agent": "mmi-cli-app-actor"
5826
+ },
5827
+ body: JSON.stringify({ permissions: APP_POLL_PERMISSIONS })
5828
+ });
5829
+ } catch (e) {
5830
+ if (e instanceof AppActorError) throw e;
5831
+ throw new AppActorError(`app actor: token mint transport failed (${e?.constructor?.name ?? "Error"}) \u2014 automation fails closed, no personal-token fallback`);
5832
+ }
5833
+ if (!res.ok) {
5834
+ throw new AppActorError(`app actor: installation token mint failed (HTTP ${res.status}) \u2014 automation fails closed, no personal-token fallback`);
5835
+ }
5836
+ let body;
5837
+ try {
5838
+ body = await res.json();
5839
+ } catch {
5840
+ throw new AppActorError("app actor: mint response unparsable \u2014 automation fails closed");
5841
+ }
5842
+ if (!body.token || !body.expires_at) {
5843
+ throw new AppActorError("app actor: mint response carried no token \u2014 automation fails closed");
5844
+ }
5845
+ return { token: body.token, expiresAt: body.expires_at, quotaScope: appQuotaScope(appId, installationId, process.env.GITHUB_API_URL) };
5846
+ }
5847
+ async function pollTokenOrUndefined(deps) {
5848
+ try {
5849
+ return (await mintInstallationToken(deps)).token;
5850
+ } catch {
5851
+ return void 0;
5852
+ }
5853
+ }
5854
+ var APP_POLL_HEADROOM_FRACTION = 0.4;
5855
+ var APP_POLL_RESET_DEFAULT_MS = 36e5;
5856
+ var appPollGuardState = {};
5857
+ function appPollGuardSnapshot() {
5858
+ return { ...appPollGuardState };
5859
+ }
5860
+ function appPollAllowsApp(nowMs) {
5861
+ if (appPollGuardState.fallbackUntilMs !== void 0) {
5862
+ if (nowMs < appPollGuardState.fallbackUntilMs) return false;
5863
+ appPollGuardState = {};
5864
+ }
5865
+ const { remaining, limit } = appPollGuardState;
5866
+ if (remaining === void 0 || limit === void 0 || limit <= 0) return true;
5867
+ return remaining / limit >= APP_POLL_HEADROOM_FRACTION;
5868
+ }
5869
+ function latchAppPollFallback(nowMs) {
5870
+ appPollGuardState.fallbackUntilMs = appPollGuardState.resetAtMs ?? nowMs + APP_POLL_RESET_DEFAULT_MS;
5871
+ }
5872
+ function noteAppPollResponse(headers, rateLimited, nowMs) {
5873
+ if (headers.remaining !== void 0) appPollGuardState.remaining = headers.remaining;
5874
+ if (headers.limit !== void 0) appPollGuardState.limit = headers.limit;
5875
+ if (headers.resetSec !== void 0 && headers.resetSec > 0) appPollGuardState.resetAtMs = headers.resetSec * 1e3;
5876
+ const { remaining, limit } = appPollGuardState;
5877
+ const belowFloor = !rateLimited && remaining !== void 0 && limit !== void 0 && limit > 0 && remaining / limit < APP_POLL_HEADROOM_FRACTION;
5878
+ const headroomUnverifiable = !rateLimited && (headers.remaining === void 0 || headers.limit === void 0);
5879
+ if (rateLimited || belowFloor || headroomUnverifiable && appPollGuardState.resetAtMs !== void 0) {
5880
+ latchAppPollFallback(nowMs);
5881
+ }
5882
+ }
5883
+ function resetAppPollGuard() {
5884
+ appPollGuardState = {};
5885
+ }
5886
+ var notedIneligibleCommand = false;
5887
+ async function activateAppActor(commandPath3, env, mint, note = (line) => process.stderr.write(`${line}
5888
+ `)) {
5889
+ activeQuotaScope = void 0;
5890
+ const requested = (env[APP_ACTOR_ENV] ?? "").trim();
5891
+ if (!requested) return "personal";
5892
+ if (requested !== "app") {
5893
+ throw new AppActorError(`app actor: unknown ${APP_ACTOR_ENV} value \u2014 only 'app' is supported; refusing to guess an identity`);
5894
+ }
5895
+ if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
5896
+ if (!notedIneligibleCommand) {
5897
+ notedIneligibleCommand = true;
5898
+ note(`app actor: '${commandPath3}' is personal-only (viewer/@me/authorship) \u2014 running on the personal token despite ${APP_ACTOR_ENV}=app`);
5899
+ }
5900
+ return "personal";
5901
+ }
5902
+ delete env.GH_TOKEN;
5903
+ delete env.GITHUB_TOKEN;
5904
+ const minted = await mint();
5905
+ env.GH_TOKEN = minted.token;
5906
+ activeQuotaScope = minted.quotaScope;
5907
+ return "app";
5908
+ }
5909
+
5734
5910
  // src/merge-ci-policy.ts
5735
5911
  function resolveMergeCiPolicy(input) {
5736
5912
  if (input.registryCi === "none") {
@@ -6111,12 +6287,19 @@ function isReleaseShaDeployEnumerationCandidate(row, nonDeployEvents) {
6111
6287
  }
6112
6288
 
6113
6289
  // src/pr-checks-rest.ts
6290
+ init_github_quota();
6114
6291
  var REST_GH_TIMEOUT_MS = 2e4;
6115
6292
  var pollTokenProvider;
6116
6293
  var pollTokenOnce;
6294
+ var lastPollSource;
6295
+ function lastRestPollSource() {
6296
+ return lastPollSource;
6297
+ }
6117
6298
  function setPollTokenProvider(provider) {
6118
6299
  pollTokenProvider = provider;
6119
6300
  pollTokenOnce = void 0;
6301
+ resetAppPollGuard();
6302
+ lastPollSource = void 0;
6120
6303
  }
6121
6304
  async function pollToken() {
6122
6305
  if (!pollTokenProvider) return void 0;
@@ -6137,24 +6320,60 @@ function parseIncludedOutput(out) {
6137
6320
  });
6138
6321
  return { body, status, etag, pages };
6139
6322
  }
6323
+ function parseRateLimitHeaders(out) {
6324
+ const first = (name) => {
6325
+ const match = new RegExp(`^x-ratelimit-${name}:\\s*(\\d+)\\s*$`, "im").exec(out);
6326
+ return match ? Number(match[1]) : void 0;
6327
+ };
6328
+ return { remaining: first("remaining"), limit: first("limit"), resetSec: first("reset") };
6329
+ }
6140
6330
  var etagCache = /* @__PURE__ */ new Map();
6331
+ function appPoolDetail() {
6332
+ const guard = appPollGuardSnapshot();
6333
+ const counts = guard.remaining !== void 0 && guard.limit !== void 0 ? `App pool at ${guard.remaining}/${guard.limit}` : "App pool headroom unverified";
6334
+ return guard.resetAtMs !== void 0 ? `${counts}, resets ${new Date(guard.resetAtMs).toISOString()}` : counts;
6335
+ }
6141
6336
  async function defaultGhApi(args) {
6142
- const key = args.join("\0");
6143
- const cached = etagCache.get(key);
6144
- let stdout;
6145
- try {
6146
- ({ stdout } = await execFileP("gh", ["api", "--include", ...cached ? ["-H", `If-None-Match: ${cached.etag}`] : [], ...args], {
6147
- timeout: REST_GH_TIMEOUT_MS
6148
- }));
6149
- } catch (e) {
6150
- const out = e.stdout;
6151
- if (cached && typeof out === "string" && parseIncludedOutput(out).status === 304) return cached.body;
6152
- throw e;
6337
+ const appToken = await pollToken();
6338
+ let source = appToken && appPollAllowsApp(Date.now()) ? "app" : "user";
6339
+ if (lastPollSource !== void 0 && lastPollSource !== source) {
6340
+ console.warn(source === "user" ? `poll credential: ${appPoolDetail()} \u2014 below the ${APP_POLL_HEADROOM_FRACTION * 100}% App-poll headroom floor (#7843); REST poll reads switch to the ambient user token until the App pool resets` : "poll credential: App pool reset passed \u2014 REST poll reads return to the App installation token (#7843)");
6341
+ }
6342
+ for (; ; ) {
6343
+ const key = `${source}\0${args.join("\0")}`;
6344
+ const cached = etagCache.get(key);
6345
+ let stdout;
6346
+ try {
6347
+ ({ stdout } = await execFileP("gh", ["api", "--include", ...cached ? ["-H", `If-None-Match: ${cached.etag}`] : [], ...args], {
6348
+ timeout: REST_GH_TIMEOUT_MS,
6349
+ // Only on the App source: passing `env` at all replaces the inherited environment wholesale,
6350
+ // and `gh` needs the rest of it (PATH, GH_HOST, config home). Ambient rides the inherited env.
6351
+ ...source === "app" && appToken ? { env: { ...process.env, GH_TOKEN: appToken } } : {}
6352
+ }));
6353
+ } catch (e) {
6354
+ const out = e.stdout;
6355
+ const errOut = typeof out === "string" ? out : void 0;
6356
+ const status = errOut !== void 0 ? parseIncludedOutput(errOut).status : void 0;
6357
+ if (cached && status === 304) {
6358
+ lastPollSource = source;
6359
+ return cached.body;
6360
+ }
6361
+ if (source === "app" && appToken && (status === 403 || status === 429)) {
6362
+ noteAppPollResponse(parseRateLimitHeaders(errOut), true, Date.now());
6363
+ console.warn(`poll credential: App pool refused the read (HTTP ${status}, ${appPoolDetail()}) \u2014 REST poll reads switch to the ambient user token until the App pool resets (#7843)`);
6364
+ lastPollSource = "user";
6365
+ source = "user";
6366
+ continue;
6367
+ }
6368
+ throw e;
6369
+ }
6370
+ const { body, etag, pages } = parseIncludedOutput(stdout);
6371
+ if (source === "app") noteAppPollResponse(parseRateLimitHeaders(stdout), false, Date.now());
6372
+ lastPollSource = source;
6373
+ if (etag && pages === 1) etagCache.set(key, { etag, body });
6374
+ else etagCache.delete(key);
6375
+ return body;
6153
6376
  }
6154
- const { body, etag, pages } = parseIncludedOutput(stdout);
6155
- if (etag && pages === 1) etagCache.set(key, { etag, body });
6156
- else etagCache.delete(key);
6157
- return body;
6158
6377
  }
6159
6378
  function classifyCheckRun(run) {
6160
6379
  if (run.status !== "completed") return "pending";
@@ -6300,12 +6519,20 @@ async function ambientGhApi(args) {
6300
6519
  const { stdout } = await execFileP("gh", ["api", ...args], { timeout: REST_GH_TIMEOUT_MS });
6301
6520
  return stdout;
6302
6521
  }
6303
- async function fetchRestRepoMergeSettings(repo, gh2 = defaultGhApi, ambientGh = ambientGhApi) {
6304
- const primary = await readRepoMergeSettingsOnce(repo, gh2);
6305
- if (primary.proven) return primary;
6306
- const fallback = await readRepoMergeSettingsOnce(repo, ambientGh);
6307
- if (fallback.proven) return fallback;
6308
- return { proven: false, reason: `${primary.reason}; ambient fallback: ${fallback.reason}` };
6522
+ var REPO_SETTINGS_RATE_LIMIT_DELAYS_MS = [15e3, 45e3];
6523
+ async function fetchRestRepoMergeSettings(repo, gh2 = defaultGhApi, ambientGh = ambientGhApi, options = {}) {
6524
+ const delays = options.delaysMs ?? REPO_SETTINGS_RATE_LIMIT_DELAYS_MS;
6525
+ const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6526
+ for (let attempt = 0; ; attempt++) {
6527
+ const primary = await readRepoMergeSettingsOnce(repo, gh2);
6528
+ if (primary.proven) return primary;
6529
+ const fallback = await readRepoMergeSettingsOnce(repo, ambientGh);
6530
+ if (fallback.proven) return fallback;
6531
+ const reason = `${primary.reason}; ambient fallback: ${fallback.reason}`;
6532
+ if (attempt >= delays.length || !isRateLimitText(reason)) return { proven: false, reason };
6533
+ console.warn(`pr merge: repository settings read was rate-limited; retrying in ${Math.round(delays[attempt] / 1e3)}s`);
6534
+ await sleep2(delays[attempt]);
6535
+ }
6309
6536
  }
6310
6537
  var PR_SNAPSHOT_READ_RETRIES = 3;
6311
6538
  var PR_SNAPSHOT_READ_DELAY_MS = 2e3;
@@ -7496,9 +7723,9 @@ function extractWorkflowCrons(yamlText) {
7496
7723
  function workflowEntry(repo, workflowPath, yamlText) {
7497
7724
  const crons = extractWorkflowCrons(yamlText);
7498
7725
  if (!crons.length) return null;
7499
- const basename3 = workflowPath.split("/").pop() ?? workflowPath;
7726
+ const basename4 = workflowPath.split("/").pop() ?? workflowPath;
7500
7727
  return {
7501
- name: `${repo}/${basename3.replace(/\.ya?ml$/, "")}`,
7728
+ name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
7502
7729
  cadence: crons.join(" + "),
7503
7730
  executor: "github-actions",
7504
7731
  llm: llmFromHeader(yamlText),
@@ -7827,8 +8054,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7827
8054
  const crons = extractWorkflowCrons(yamlText);
7828
8055
  const header = parseScheduleHeader(yamlText);
7829
8056
  if (!crons.length && !header.schedule) return null;
7830
- const basename3 = workflowPath.split("/").pop() ?? workflowPath;
7831
- const expectedId = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
8057
+ const basename4 = workflowPath.split("/").pop() ?? workflowPath;
8058
+ const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
7832
8059
  if (isHarbourUnmanaged(yamlText)) return null;
7833
8060
  const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
7834
8061
  if (missing.length) {
@@ -9247,7 +9474,7 @@ init_clean_exit();
9247
9474
  var import_promises = require("node:fs/promises");
9248
9475
  var import_node_os4 = require("node:os");
9249
9476
  var import_node_path9 = require("node:path");
9250
- var import_node_crypto3 = require("node:crypto");
9477
+ var import_node_crypto5 = require("node:crypto");
9251
9478
 
9252
9479
  // src/board-priority.ts
9253
9480
  var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
@@ -9334,7 +9561,7 @@ async function bodyArgsViaFile(args, deps = {}) {
9334
9561
  const remove2 = deps.remove ?? import_promises.unlink;
9335
9562
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
9336
9563
  const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
9337
- const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
9564
+ const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
9338
9565
  await ensureDir((0, import_node_path9.dirname)(file), { recursive: true }).catch(() => {
9339
9566
  });
9340
9567
  await write(file, args[i + 1], "utf8");
@@ -9664,7 +9891,7 @@ async function defaultRestCreatePr(exec2, input, deps = {}) {
9664
9891
  const remove2 = deps.remove ?? import_promises.unlink;
9665
9892
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
9666
9893
  const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
9667
- const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.json`);
9894
+ const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.json`);
9668
9895
  const payload = {
9669
9896
  title: input.title,
9670
9897
  head: input.head,
@@ -11064,7 +11291,7 @@ function findDuplicateLesson(source, openLessons) {
11064
11291
  }
11065
11292
 
11066
11293
  // src/session-identity.ts
11067
- var import_node_crypto4 = require("node:crypto");
11294
+ var import_node_crypto6 = require("node:crypto");
11068
11295
  var import_node_os8 = require("node:os");
11069
11296
 
11070
11297
  // src/plugin-guard-io.ts
@@ -11948,7 +12175,7 @@ function readSessionIdSource(env) {
11948
12175
  var synthesizedSessionId;
11949
12176
  function fallbackSessionId(surface) {
11950
12177
  if (!synthesizedSessionId) {
11951
- synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0, import_node_crypto4.randomBytes)(3).toString("hex")}`;
12178
+ synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0, import_node_crypto6.randomBytes)(3).toString("hex")}`;
11952
12179
  }
11953
12180
  return synthesizedSessionId;
11954
12181
  }
@@ -14646,7 +14873,7 @@ async function propagateMayAutoMerge(repo, repoClass, rest) {
14646
14873
  }
14647
14874
 
14648
14875
  // src/bootstrap-apply.ts
14649
- var import_node_crypto5 = require("node:crypto");
14876
+ var import_node_crypto7 = require("node:crypto");
14650
14877
 
14651
14878
  // src/bootstrap-seeds.ts
14652
14879
  var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
@@ -15877,7 +16104,7 @@ function planSeedAction(seed, exists) {
15877
16104
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
15878
16105
  }
15879
16106
  var GATE_RENDER_NOTE = "# Rendered from the registry gate config by mmi-cli devops bootstrap apply <repo> --only gate.yml --from-config; edit the config, not this file.\n";
15880
- var gateHash = (body) => (0, import_node_crypto5.createHash)("sha256").update(body, "utf8").digest("hex");
16107
+ var gateHash = (body) => (0, import_node_crypto7.createHash)("sha256").update(body, "utf8").digest("hex");
15881
16108
  function parseGateRenderStamp(content) {
15882
16109
  const match = /^# mmi-gate-render: sha256=([a-f0-9]{64})\n/.exec(content);
15883
16110
  return match ? { hash: match[1], body: content.slice(match[0].length) } : null;
@@ -17345,10 +17572,10 @@ var rollout_plan_default = {
17345
17572
  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)."
17346
17573
  },
17347
17574
  baseline: {
17348
- version: "4.5.98",
17349
- tag: "v4.5.98",
17350
- commit: "125c59187d10",
17351
- npm: "@mutmutco/cli@4.5.98"
17575
+ version: "4.5.99",
17576
+ tag: "v4.5.99",
17577
+ commit: "44a40b83250e",
17578
+ npm: "@mutmutco/cli@4.5.99"
17352
17579
  },
17353
17580
  exitCriterion: "fleet-n-of-n",
17354
17581
  hubOnlyShortcut: "forbidden",
@@ -17365,14 +17592,14 @@ var rollout_plan_default = {
17365
17592
  repo: "mutmutco/mmi-hub",
17366
17593
  role: "canary",
17367
17594
  schedule: "train",
17368
- v3Target: "v4.5.98"
17595
+ v3Target: "v4.5.99"
17369
17596
  }
17370
17597
  ],
17371
17598
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
17372
17599
  rollback: {
17373
17600
  independent: true,
17374
- mechanism: "npm dist-tag latest -> 4.5.98 and redeploy the Hub Lambda from tag v4.5.98 (125c59187d10); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17375
- v3Target: "v4.5.98 (@mutmutco/cli@4.5.98, tag commit 125c59187d10 \u2014 last known-good release carrying the repo-index v4-only contract)"
17601
+ mechanism: "npm dist-tag latest -> 4.5.99 and redeploy the Hub Lambda from tag v4.5.99 (44a40b83250e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17602
+ v3Target: "v4.5.99 (@mutmutco/cli@4.5.99, tag commit 44a40b83250e \u2014 last known-good release carrying the repo-index v4-only contract)"
17376
17603
  }
17377
17604
  },
17378
17605
  {
@@ -19730,7 +19957,9 @@ async function correlateRun(deps, args) {
19730
19957
  return Number.isFinite(created) && created < threshold;
19731
19958
  });
19732
19959
  const tokens2 = args.mode === "dispatch" ? args.titleIncludes : void 0;
19733
- const titleMatch = tokens2 && tokens2.length ? candidates.find((c) => tokens2.every((t) => (c.row.displayTitle ?? "").includes(t))) : void 0;
19960
+ const fieldsFrom = args.mode === "dispatch" ? args.titleFieldsFrom : void 0;
19961
+ const titleHas = (title) => fieldsFrom === void 0 ? tokens2.every((t) => title.includes(t)) : tokens2.every((t, i) => title.split(" ")[fieldsFrom + i] === t);
19962
+ const titleMatch = tokens2 && tokens2.length ? candidates.find((c) => titleHas(c.row.displayTitle ?? "")) : void 0;
19734
19963
  const match = titleMatch ?? (!("requireTitleMatch" in args && args.requireTitleMatch) && (args.mode === "workflow" || windowProvenWhole) ? candidates[0] : void 0);
19735
19964
  if (match) return { runId: match.row.databaseId, runUrl: match.row.url, read: "found" };
19736
19965
  if (!windowProvenWhole) truncatedWindow = true;
@@ -19756,7 +19985,7 @@ function correlateGetPublishRun(deps, since, product, mode) {
19756
19985
  return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {} });
19757
19986
  }
19758
19987
  function correlateControlRun(deps, since, titleIncludes) {
19759
- return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
19988
+ return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes, titleFieldsFrom: 2, ...titleIncludes?.length ? { requireTitleMatch: true } : {} });
19760
19989
  }
19761
19990
  function correlateReconcileRun(deps, since, titleIncludes) {
19762
19991
  return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
@@ -21562,7 +21791,7 @@ async function runTenantControl(deps, options) {
21562
21791
  const watch = options.watch ?? tenantControlWatches(action);
21563
21792
  const base = { command: "tenant-control", repo, stage, action };
21564
21793
  const since = (deps.now ?? Date.now)();
21565
- const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
21794
+ const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
21566
21795
  if (!d.ok) {
21567
21796
  const transport = d.category === "transport-failed";
21568
21797
  return {
@@ -22778,7 +23007,7 @@ function checkLineEndings(probe) {
22778
23007
  // src/release-ledger.ts
22779
23008
  var import_node_fs22 = require("node:fs");
22780
23009
  var import_node_path22 = require("node:path");
22781
- var import_node_crypto6 = require("node:crypto");
23010
+ var import_node_crypto8 = require("node:crypto");
22782
23011
 
22783
23012
  // src/file-lock.ts
22784
23013
  var import_promises2 = require("node:fs/promises");
@@ -23140,7 +23369,7 @@ function releaseLedgerPath(cwd) {
23140
23369
  }
23141
23370
  function atomicWriteJson(path2, ledger) {
23142
23371
  (0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
23143
- const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}`;
23372
+ const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
23144
23373
  (0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
23145
23374
  `, "utf8");
23146
23375
  (0, import_node_fs22.renameSync)(tmp, path2);
@@ -29869,150 +30098,6 @@ function renderPluginCachePlan(plan, applied) {
29869
30098
  return lines2.join("\n");
29870
30099
  }
29871
30100
 
29872
- // ../shared/github-quota-scope.ts
29873
- var import_node_crypto7 = require("node:crypto");
29874
- function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
29875
- return `github-app-v1:${(0, import_node_crypto7.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
29876
- }
29877
-
29878
- // src/app-actor.ts
29879
- var import_node_crypto8 = require("node:crypto");
29880
- var APP_ACTOR_ENV = "MMI_ACTOR";
29881
- var APP_VAULT_REPO = "mutmutco/MMI-Hub";
29882
- var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
29883
- var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
29884
- "pr checks-wait",
29885
- "pr view",
29886
- "issue list",
29887
- "issue view",
29888
- "api get"
29889
- ]);
29890
- var APP_POLL_PERMISSIONS = {
29891
- metadata: "read",
29892
- contents: "read",
29893
- pull_requests: "read",
29894
- // #7185: without issues:read GraphQL silently drops pr view's closingIssuesReferences.
29895
- issues: "read",
29896
- checks: "read",
29897
- statuses: "read",
29898
- actions: "read"
29899
- };
29900
- var APP_JWT_BACKDATE_S = 60;
29901
- var APP_JWT_TTL_S = 9 * 60;
29902
- var AppActorError = class extends Error {
29903
- constructor(message2, code = "mint-failed") {
29904
- super(message2);
29905
- this.code = code;
29906
- }
29907
- code;
29908
- };
29909
- function commandPathOf(cmd) {
29910
- const parts = [];
29911
- let cursor = cmd;
29912
- while (cursor && cursor.parent) {
29913
- parts.unshift(cursor.name());
29914
- cursor = cursor.parent;
29915
- }
29916
- return parts.join(" ");
29917
- }
29918
- function b64url(input) {
29919
- return Buffer.from(input).toString("base64url");
29920
- }
29921
- function mintAppJwt(appId, privateKeyPem, nowSec) {
29922
- if (!Number.isFinite(nowSec)) throw new AppActorError("app actor: invalid clock reading for JWT mint");
29923
- const now = Math.floor(nowSec);
29924
- const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
29925
- const payload = b64url(JSON.stringify({
29926
- iat: now - APP_JWT_BACKDATE_S,
29927
- exp: now + APP_JWT_TTL_S,
29928
- iss: appId
29929
- }));
29930
- const signer = (0, import_node_crypto8.createSign)("RSA-SHA256");
29931
- signer.update(`${header}.${payload}`);
29932
- return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
29933
- }
29934
- var activeQuotaScope;
29935
- function activeAppQuotaScope() {
29936
- return activeQuotaScope;
29937
- }
29938
- async function mintInstallationToken(deps) {
29939
- const [appId, installationId, privateKey] = await Promise.all(APP_VAULT_KEYS.map((k) => deps.fetchSecret(k)));
29940
- if (!appId || !installationId || !privateKey) {
29941
- const missing = APP_VAULT_KEYS.filter((_, i) => ![appId, installationId, privateKey][i]);
29942
- if (missing.length === APP_VAULT_KEYS.length) {
29943
- throw new AppActorError(
29944
- `app actor: vault denied all App credentials (${APP_VAULT_REPO} tier) \u2014 not an automation host, or no vault access`,
29945
- "vault-denied"
29946
- );
29947
- }
29948
- throw new AppActorError(
29949
- `app actor: vault carries a PARTIAL App credential set \u2014 missing ${missing.join(", ")} (${APP_VAULT_REPO} tier); fix the vault. Automation fails closed, no personal-token fallback`,
29950
- "mint-failed"
29951
- );
29952
- }
29953
- const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1e3));
29954
- let res;
29955
- try {
29956
- const jwt = mintAppJwt(appId.trim(), privateKey, nowSec());
29957
- res = await deps.fetch(`https://api.github.com/app/installations/${installationId.trim()}/access_tokens`, {
29958
- method: "POST",
29959
- headers: {
29960
- authorization: `Bearer ${jwt}`,
29961
- accept: "application/vnd.github+json",
29962
- "content-type": "application/json",
29963
- "user-agent": "mmi-cli-app-actor"
29964
- },
29965
- body: JSON.stringify({ permissions: APP_POLL_PERMISSIONS })
29966
- });
29967
- } catch (e) {
29968
- if (e instanceof AppActorError) throw e;
29969
- throw new AppActorError(`app actor: token mint transport failed (${e?.constructor?.name ?? "Error"}) \u2014 automation fails closed, no personal-token fallback`);
29970
- }
29971
- if (!res.ok) {
29972
- throw new AppActorError(`app actor: installation token mint failed (HTTP ${res.status}) \u2014 automation fails closed, no personal-token fallback`);
29973
- }
29974
- let body;
29975
- try {
29976
- body = await res.json();
29977
- } catch {
29978
- throw new AppActorError("app actor: mint response unparsable \u2014 automation fails closed");
29979
- }
29980
- if (!body.token || !body.expires_at) {
29981
- throw new AppActorError("app actor: mint response carried no token \u2014 automation fails closed");
29982
- }
29983
- return { token: body.token, expiresAt: body.expires_at, quotaScope: appQuotaScope(appId, installationId, process.env.GITHUB_API_URL) };
29984
- }
29985
- async function pollTokenOrUndefined(deps) {
29986
- try {
29987
- return (await mintInstallationToken(deps)).token;
29988
- } catch {
29989
- return void 0;
29990
- }
29991
- }
29992
- var notedIneligibleCommand = false;
29993
- async function activateAppActor(commandPath3, env, mint, note = (line) => process.stderr.write(`${line}
29994
- `)) {
29995
- activeQuotaScope = void 0;
29996
- const requested = (env[APP_ACTOR_ENV] ?? "").trim();
29997
- if (!requested) return "personal";
29998
- if (requested !== "app") {
29999
- throw new AppActorError(`app actor: unknown ${APP_ACTOR_ENV} value \u2014 only 'app' is supported; refusing to guess an identity`);
30000
- }
30001
- if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
30002
- if (!notedIneligibleCommand) {
30003
- notedIneligibleCommand = true;
30004
- note(`app actor: '${commandPath3}' is personal-only (viewer/@me/authorship) \u2014 running on the personal token despite ${APP_ACTOR_ENV}=app`);
30005
- }
30006
- return "personal";
30007
- }
30008
- delete env.GH_TOKEN;
30009
- delete env.GITHUB_TOKEN;
30010
- const minted = await mint();
30011
- env.GH_TOKEN = minted.token;
30012
- activeQuotaScope = minted.quotaScope;
30013
- return "app";
30014
- }
30015
-
30016
30101
  // src/board-commands.ts
30017
30102
  init_cli_shared();
30018
30103
 
@@ -42667,6 +42752,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
42667
42752
  const repo = await requireRepo(repoOption);
42668
42753
  const { client, throttledPaths } = withRateLimitRetry(withAppClaimMarkerRead(deps.client ?? defaultGitHubClient(), deps.appClient), deps);
42669
42754
  const actor = deps.actor ?? describeSessionIdentity();
42755
+ let viewerLogin = deps.viewerLogin;
42756
+ if (!viewerLogin) {
42757
+ try {
42758
+ viewerLogin = (await client.rest("GET", "user"))?.login;
42759
+ } catch {
42760
+ }
42761
+ }
42670
42762
  const source = deps.actorSource ?? (deps.actor ? "the caller-supplied identity" : readSessionIdSource(process.env) ?? "a synthesized fallback");
42671
42763
  const requester = `requesting lane: ${actor.surface}/${actor.session}@${actor.host} (session id from ${source})`;
42672
42764
  const checkContest = deps.checkContest ?? checkLaneContest;
@@ -42680,7 +42772,7 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
42680
42772
  }
42681
42773
  }
42682
42774
  if (state?.toLowerCase() === "closed") continue;
42683
- const contest = await checkContest(client, { repository: repo, number }, actor);
42775
+ const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin);
42684
42776
  if (!contest.contested) continue;
42685
42777
  const ref = `${repo}#${number}`;
42686
42778
  const commentsPath = `repos/${repo}/issues/${number}/comments`;
@@ -44459,6 +44551,9 @@ function renderPrMergeCleanupLines(cleanup) {
44459
44551
 
44460
44552
  // src/worktree-residue-sweep.ts
44461
44553
  var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
44554
+ function canonicalWorktreeRoot(primary) {
44555
+ return (0, import_node_path41.join)((0, import_node_path41.dirname)(primary), "mmi-worktrees", (0, import_node_path41.basename)(primary));
44556
+ }
44462
44557
  function norm(p) {
44463
44558
  const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
44464
44559
  return process.platform === "win32" ? s.toLowerCase() : s;
@@ -44527,16 +44622,14 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
44527
44622
  }
44528
44623
  }
44529
44624
  if (recorded.length) writePaths(file, remaining, deps);
44530
- const roots = /* @__PURE__ */ new Set();
44531
- for (const p of [...registered.slice(1), ...recorded]) roots.add((0, import_node_path41.dirname)(p));
44532
44625
  const keptKeys = new Set(remaining.map(norm));
44533
- for (const root of roots) {
44534
- if (!isDir(root) || live.has(norm(root)) || norm(root) === primary) continue;
44535
- let children;
44626
+ const root = registered[0] ? canonicalWorktreeRoot(registered[0]) : void 0;
44627
+ if (root && isDir(root) && !live.has(norm(root))) {
44628
+ report.root = root;
44629
+ let children = [];
44536
44630
  try {
44537
44631
  children = readDir(root);
44538
44632
  } catch {
44539
- continue;
44540
44633
  }
44541
44634
  for (const name of children) {
44542
44635
  const child2 = (0, import_node_path41.join)(root, name);
@@ -44565,10 +44658,13 @@ function renderResidueSweepLines(report) {
44565
44658
  if (!report) return [];
44566
44659
  const lines2 = [];
44567
44660
  const cleaned = report.swept.length + report.emptyRemoved.length;
44568
- if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges`);
44661
+ const where = report.root ? ` (worktree root ${report.root})` : "";
44662
+ if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
44569
44663
  for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
44570
44664
  if (report.unrecorded.length) {
44571
- lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root were left untouched (not recorded by a merge): ${report.unrecorded.join(", ")}`);
44665
+ const shown = report.unrecorded.slice(0, 5).join(", ");
44666
+ const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
44667
+ lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${shown}${more}`);
44572
44668
  }
44573
44669
  return lines2;
44574
44670
  }
@@ -45798,7 +45894,7 @@ ${list}`);
45798
45894
  console.error(line);
45799
45895
  }
45800
45896
  }
45801
- const credential = "user";
45897
+ const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
45802
45898
  if (o.json) printLine(JSON.stringify({ ...result, credential }));
45803
45899
  else printLine(
45804
45900
  `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
@@ -46385,8 +46481,8 @@ function trainApplyDeps() {
46385
46481
  // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
46386
46482
  // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
46387
46483
  // outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
46388
- dispatchTenantControl: async ({ repo, stage, action, lines: lines2, task, artifact, confirmDataLoss }) => {
46389
- const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
46484
+ dispatchTenantControl: async ({ repo, stage, action, lines: lines2, since, until, task, artifact, confirmDataLoss }) => {
46485
+ const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...since ? { since } : {}, ...until ? { until } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
46390
46486
  if (res.ok) return { ok: true };
46391
46487
  const body = res.body;
46392
46488
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
@@ -51568,8 +51664,8 @@ async function repoWorkflowEntries(client, repo) {
51568
51664
  for (const wf of workflowsList) {
51569
51665
  if (typeof wf?.path !== "string" || !wf.path) continue;
51570
51666
  if (!wf.path.startsWith(".github/workflows/")) continue;
51571
- const basename3 = wf.path.split("/").pop() ?? wf.path;
51572
- const name = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
51667
+ const basename4 = wf.path.split("/").pop() ?? wf.path;
51668
+ const name = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
51573
51669
  if (wf.state !== "active") {
51574
51670
  disabled.push(name);
51575
51671
  continue;
@@ -54110,7 +54206,7 @@ async function reportWrite(label, res) {
54110
54206
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
54111
54207
  }
54112
54208
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
54113
- tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
54209
+ tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
54114
54210
  try {
54115
54211
  if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
54116
54212
  if (action === "retire" && (stage === "dev" || stage === "main")) {
@@ -54129,12 +54225,17 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
54129
54225
  lines2 = Number(o.lines);
54130
54226
  if (!Number.isInteger(lines2) || lines2 < 1 || lines2 > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
54131
54227
  }
54228
+ for (const [flag, value] of [["--since", o.since], ["--until", o.until]]) {
54229
+ if (value === void 0) continue;
54230
+ if (action !== "logs") return fail(`runtime tenant control: ${flag} is valid only for the logs action`);
54231
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?Z$/.test(value)) return fail(`runtime tenant control: ${flag} must be a UTC time like 2026-10-07T13:10Z`);
54232
+ }
54132
54233
  if (action === "run-task") {
54133
54234
  if (!o.task) return fail("runtime tenant control: run-task requires --task <declared-name>");
54134
54235
  } else if (o.task || o.artifact) {
54135
54236
  return fail("runtime tenant control: --task/--artifact are valid only for run-task");
54136
54237
  }
54137
- const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
54238
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
54138
54239
  if (!o.json && action === "verify-secrets" && result.secrets) {
54139
54240
  const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
54140
54241
  const { lines: lines3, failure } = renderVerifySecrets(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.98",
3
+ "version": "4.5.99",
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",