@mutmutco/codex-plugin 4.5.129 → 4.5.131

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.129",
3
+ "version": "4.5.131",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -8162,6 +8162,11 @@ var DECLARED_ENTRIES = [
8162
8162
  // archived stage (including a stage whose containers are down) into scratch Postgres and compare
8163
8163
  // row counts, and read-verify every storage archive against its listing; per-stage PASS/FAIL.
8164
8164
  { name: "mmi-backup-restore-check (mm-central, mm-work)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); owner-approved mmi-oguz rebuild as CI-only mmi-runner2 with full erasure and no backups (#7859); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
8165
+ // #8176: continuous WAL archiving of the mm-work dev Postgres (owner targets RPO <= 5 min, RTO <= 1 h). Postgres copies each
8166
+ // finished WAL file into a host spool (MM-Work#4170); this timer ships it encrypted to the Storage Box with WAL-G.
8167
+ { name: "mmi-walg-ship@mm-work-dev (mm-work)", cadence: "every 30s (systemd OnUnitInactiveSec=30s)", executor: "box systemd", llm: "no", resolved: "declared", source: "/etc/systemd/system/mmi-walg-ship@.timer \u2192 /opt/mmi-control/walg-ship.sh mm-work-dev; spool /srv/mm-work/walspool/mm-work-dev \u2192 Storage Box backups/walg/mmi-tools/mm-work-dev, libsodium-encrypted (#8176; source scripts/walg/; status /var/lib/mmi-backup-status/mm-work-dev.ship.json)" },
8168
+ { name: "mmi-walg-base (mm-work)", cadence: "30 3 * * 0", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-walg-base \u2192 /opt/mmi-control/walg-base.sh mm-work-dev; weekly WAL-G base backup + age-based retention (keep the base at or before now-7d and everything after, #8193) (#8176; source scripts/walg/; log /var/log/mmi-walg.log; status /var/lib/mmi-backup-status/mm-work-dev.base.json)" },
8169
+ { name: "mmi-walg-drill (mm-work)", cadence: "0 4 2 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-walg-drill \u2192 /opt/mmi-control/walg-drill.sh mm-work-dev; monthly restore drill measuring RPO/RTO against the owner targets (300s/3600s) into a throwaway --network none container (#8177; source scripts/walg/; status /var/lib/mmi-backup-status/mm-work-dev.drill.json; alarm: backup-health.yml)" },
8165
8170
  // #7662 (MM-Work#1520): Vercel Cron's 15-minute central mail call, moved to the box with MM-Work. The app
8166
8171
  // calls its own endpoint with its own CRON_SECRET via docker exec, so the secret stays in the container.
8167
8172
  { name: "mm-work-mail-tick (mm-work)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mm-work-mail-tick \u2192 /opt/mmi-control/mm-work-mail-tick.sh; GET /api/internal/mail per stage listed in MM_WORK_MAIL_STAGES (source scripts/mm-work-mail-tick.sh; log /var/log/mm-work-mail-tick.log)" },
@@ -15380,6 +15385,17 @@ function missingPlaceholders(rendered) {
15380
15385
  function isPropagatableSeed(seed) {
15381
15386
  return seed.ownership === "org" && seed.source === "self" || seed.managedBlock != null;
15382
15387
  }
15388
+ function seedSourcePath(seed) {
15389
+ const source = seed.managedBlock?.source ?? seed.source;
15390
+ return source.startsWith("seed:") ? `skills/bootstrap/seeds/${source.slice(5)}` : seed.target;
15391
+ }
15392
+ var RUNBOOK_UNAFFECTED_PATH = "skills/bootstrap/seeds/runbook-unaffected.json";
15393
+ function runbookUnaffectedReason(declarations, target, sourceBlob) {
15394
+ const entry = declarations?.[target];
15395
+ if (!entry || entry.blob !== sourceBlob || typeof entry.reason !== "string") return void 0;
15396
+ const reason = entry.reason.trim();
15397
+ return reason && !/[\r\n]/.test(reason) ? reason : void 0;
15398
+ }
15383
15399
  function sourceLines(content) {
15384
15400
  const lines2 = [];
15385
15401
  const newline = /\r\n|\n|\r/g;
@@ -16670,6 +16686,7 @@ function gateExtraStepsYaml(steps) {
16670
16686
  return steps.map((step) => [
16671
16687
  `
16672
16688
  - name: ${yamlScalar(step.name)}`,
16689
+ "\n if: ${{ steps.development-gate.outputs.reuse != 'true' }}",
16673
16690
  step.env && Object.keys(step.env).length ? `
16674
16691
  env:${envYaml(step.env, " ")}` : "",
16675
16692
  "\n uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}",
@@ -17037,9 +17054,12 @@ function planSeedDelivery(branchRules, slug, baseBranch, branchPrefix = "bootstr
17037
17054
  }
17038
17055
  return { mode: "direct", ref: baseBranch, reason: decision.reason };
17039
17056
  }
17040
- function contentPutBody(path2, content, branch, sha) {
17057
+ function contentPutBody(path2, content, branch, sha, runbookUnaffected) {
17041
17058
  const body = {
17042
- message: `bootstrap: seed ${path2}`,
17059
+ // #8169: a reviewed seed revision may declare it leaves runbooks correct; the tenant guard reads the trailer.
17060
+ message: `bootstrap: seed ${path2}${runbookUnaffected ? `
17061
+
17062
+ Runbook-Unaffected: ${runbookUnaffected}` : ""}`,
17043
17063
  content: encodeGitHubContents(content),
17044
17064
  branch
17045
17065
  };
@@ -17226,23 +17246,33 @@ async function installAppFoldDeps(deps) {
17226
17246
  }
17227
17247
  var SHARED_INSTALLER_PACKAGES = ["@mutmutco/installer-face", "@mutmutco/installer-launcher", "@mutmutco/installer-gate"];
17228
17248
  async function refreshSharedInstallerDeps(deps) {
17229
- let declared;
17249
+ let lockPaths;
17230
17250
  try {
17231
- const lock = JSON.parse(await deps.run("git", ["show", "HEAD:package-lock.json"]));
17232
- const present = Object.keys(lock.packages ?? {});
17233
- declared = SHARED_INSTALLER_PACKAGES.filter((name) => present.some((key) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`)));
17234
- } catch {
17235
- return null;
17236
- }
17237
- if (declared.length === 0) return null;
17238
- try {
17239
- await deps.run("npm", ["update", ...declared, "--package-lock-only", "--no-audit", "--no-fund"]);
17251
+ lockPaths = (await deps.run("git", ["ls-tree", "-r", "--name-only", "HEAD"])).split("\n").filter((path2) => path2 === "package-lock.json" || path2.endsWith("/package-lock.json"));
17240
17252
  } catch (e) {
17241
- const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
17242
- deps.warn?.(`shared installer refresh skipped (${why}) \u2014 shipping the locked versions`);
17243
- return `shared installer refresh skipped (${why}); the locked ${declared.join(", ")} still satisfy their ranges`;
17253
+ throw trainReadFailure(
17254
+ "shared installer refresh: `git ls-tree -r --name-only HEAD`",
17255
+ e,
17256
+ "rerun in a readable checkout \u2014 lockfile roots must be discovered, never guessed"
17257
+ );
17244
17258
  }
17245
- return `shared installer deps refreshed to the newest their ranges allow (${declared.join(", ")})`;
17259
+ const notes = [];
17260
+ for (const lockPath of lockPaths) {
17261
+ const lock = JSON.parse(await deps.run("git", ["show", `HEAD:${lockPath}`]));
17262
+ const present = Object.keys(lock.packages ?? {});
17263
+ const declared = SHARED_INSTALLER_PACKAGES.filter((name) => present.some((key) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`)));
17264
+ if (declared.length === 0) continue;
17265
+ const prefix = lockPath.includes("/") ? lockPath.slice(0, lockPath.lastIndexOf("/")) : null;
17266
+ try {
17267
+ await deps.run("npm", [...prefix ? ["--prefix", prefix] : [], "update", ...declared, "--package-lock-only", "--no-audit", "--no-fund"]);
17268
+ notes.push(`shared installer deps refreshed in ${prefix ?? "."} (${declared.join(", ")})`);
17269
+ } catch (e) {
17270
+ const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
17271
+ deps.warn?.(`shared installer refresh skipped for ${prefix ?? "."} (${why}) \u2014 shipping the locked versions`);
17272
+ notes.push(`shared installer refresh skipped for ${prefix ?? "."} (${why}); the locked ${declared.join(", ")} still satisfy their ranges`);
17273
+ }
17274
+ }
17275
+ return notes.length > 0 ? notes.join("; ") : null;
17246
17276
  }
17247
17277
  async function restoreMainNpmPackIdentities(deps, version) {
17248
17278
  const bomPath = "distribution-bom.json";
@@ -18244,10 +18274,10 @@ var rollout_plan_default = {
18244
18274
  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)."
18245
18275
  },
18246
18276
  baseline: {
18247
- version: "4.5.129",
18248
- tag: "v4.5.129",
18249
- commit: "a19e76403fdf",
18250
- npm: "@mutmutco/cli@4.5.129"
18277
+ version: "4.5.131",
18278
+ tag: "v4.5.131",
18279
+ commit: "1070f097f90f",
18280
+ npm: "@mutmutco/cli@4.5.131"
18251
18281
  },
18252
18282
  exitCriterion: "fleet-n-of-n",
18253
18283
  hubOnlyShortcut: "forbidden",
@@ -18264,14 +18294,14 @@ var rollout_plan_default = {
18264
18294
  repo: "mutmutco/mmi-hub",
18265
18295
  role: "canary",
18266
18296
  schedule: "train",
18267
- v3Target: "v4.5.129"
18297
+ v3Target: "v4.5.131"
18268
18298
  }
18269
18299
  ],
18270
18300
  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.",
18271
18301
  rollback: {
18272
18302
  independent: true,
18273
- mechanism: "npm dist-tag latest -> 4.5.129 and redeploy the Hub Lambda from tag v4.5.129 (a19e76403fdf); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18274
- v3Target: "v4.5.129 (@mutmutco/cli@4.5.129, tag commit a19e76403fdf \u2014 last known-good release carrying the repo-index v4-only contract)"
18303
+ mechanism: "npm dist-tag latest -> 4.5.131 and redeploy the Hub Lambda from tag v4.5.131 (1070f097f90f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18304
+ v3Target: "v4.5.131 (@mutmutco/cli@4.5.131, tag commit 1070f097f90f \u2014 last known-good release carrying the repo-index v4-only contract)"
18275
18305
  }
18276
18306
  },
18277
18307
  {
@@ -28689,6 +28719,7 @@ var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
28689
28719
  var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
28690
28720
  var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
28691
28721
  var DEV_STAGE_FRESH_LABEL = "tenant dev stage runs the development tip (#7536)";
28722
+ var NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
28692
28723
  var DEV_STAGE_STALE_TOLERANCE_MS = 30 * 60 * 1e3;
28693
28724
  function devStageFreshnessCheck(input) {
28694
28725
  const { repo, deployedTag, tipSha, tipCommittedAt, nowMs } = input;
@@ -29372,7 +29403,6 @@ async function auditRepoCi(repo, callerDeps) {
29372
29403
  detail: `${policy} (${reason})`
29373
29404
  });
29374
29405
  }
29375
- const NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
29376
29406
  if (deployableGated || repoClass === "hub") {
29377
29407
  const allPaths = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
29378
29408
  const prPaths = await filterPullRequestTriggered(deps, repo, baseBranch, allPaths);
@@ -29503,10 +29533,17 @@ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
29503
29533
  }
29504
29534
  return lines2.join("\n");
29505
29535
  }
29536
+ function unauditedReconcileError(reason) {
29537
+ return `cannot audit \u2014 ${reason}; no reconcile change made (unaudited, not canonical)`;
29538
+ }
29506
29539
  async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
29507
29540
  const applied = [];
29508
29541
  const skipped = [];
29509
29542
  const errors = [];
29543
+ if (report.unauditedReason) {
29544
+ errors.push(unauditedReconcileError(report.unauditedReason));
29545
+ return { repo, applied, skipped, errors };
29546
+ }
29510
29547
  const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
29511
29548
  const needsPatch = mergeChecks.some((c) => !c.ok);
29512
29549
  if (!needsPatch) {
@@ -29810,6 +29847,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
29810
29847
  return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
29811
29848
  }
29812
29849
  const report = await auditRepoCi(repo, deps);
29850
+ if (report.unauditedReason) {
29851
+ return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
29852
+ }
29813
29853
  const finish = (result2, pendingReason) => options.productRulesetOnly ? Promise.resolve(result2) : finalizeCiReconcile(repo, deps, result2, report, pendingReason);
29814
29854
  const baseBranch = "development";
29815
29855
  let raw = report.class === "deployable" && !report.explicitNoCi ? await fetchRulesetSeedBody(deps, repo) : null;
@@ -36404,6 +36444,8 @@ LIVE apply to ${repo}:
36404
36444
  if (o.execute) {
36405
36445
  if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
36406
36446
  const headSha = seedSource.sha;
36447
+ const sourceBlob = (await execGitForSeedSource(["rev-parse", `${headSha}:${seedSourcePath(seed)}`])).trim();
36448
+ const runbookUnaffected = (0, import_node_fs30.existsSync)(RUNBOOK_UNAFFECTED_PATH) ? runbookUnaffectedReason(JSON.parse((0, import_node_fs30.readFileSync)(RUNBOOK_UNAFFECTED_PATH, "utf8")), seed.target, sourceBlob) : void 0;
36407
36449
  for (const rec of plan.records) {
36408
36450
  if (rec.action !== "open-pr") continue;
36409
36451
  const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
@@ -36428,7 +36470,7 @@ LIVE apply to ${repo}:
36428
36470
  const tmp = (0, import_node_path29.join)((0, import_node_os18.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
36429
36471
  const desiredContent = desiredByRepo.get(rec.repo);
36430
36472
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
36431
- (0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
36473
+ (0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha, runbookUnaffected)), "utf8");
36432
36474
  try {
36433
36475
  await gh2(contentPutInputArgs(rec.repo, seed.target, tmp));
36434
36476
  } finally {
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.129",
3
+ "version": "4.5.131",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.129",
3
+ "version": "4.5.131",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -50,6 +50,7 @@ concurrency:
50
50
  cancel-in-progress: true
51
51
 
52
52
  permissions:
53
+ actions: read # #8206: exact-SHA development workflow verdict
53
54
  contents: read
54
55
  # MMI-Hub#7542: the product image build below reads org private packages with the workflow's
55
56
  # own ephemeral token (NODE_AUTH_TOKEN) — the same reader the central deploy uses.
@@ -65,12 +66,40 @@ jobs:
65
66
  defaults:
66
67
  run: { working-directory: {{GATE_WORKDIR}} }{{GATE_SERVICES_YAML}}
67
68
  steps:
68
- - uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
69
+ # #8206: keep a fresh successful tag gate; promote the exact development verdict, not a ref.
70
+ - name: Reuse development gate for rc tag
71
+ id: development-gate
72
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
73
+ with:
74
+ script: |
75
+ core.setOutput('reuse', 'false');
76
+ if (context.eventName !== 'push' || !/^refs\/tags\/v.+-rc\.\d+$/.test(context.ref) || !/^[0-9a-f]{40}$/.test(context.sha)) return;
77
+ try {
78
+ const { data: current } = await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId });
79
+ const { data } = await github.rest.actions.listWorkflowRuns({
80
+ ...context.repo, workflow_id: current.workflow_id, branch: 'development',
81
+ event: 'push', head_sha: context.sha, per_page: 100
82
+ });
83
+ // API returns newest first: a newer red/pending rerun must not reuse an older green.
84
+ const run = data.workflow_runs[0];
85
+ if (!run || run.head_sha !== context.sha || run.head_branch !== 'development' ||
86
+ run.event !== 'push' || run.status !== 'completed' || run.conclusion !== 'success') return;
87
+ const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
88
+ ...context.repo, run_id: run.id, filter: 'latest', per_page: 100
89
+ });
90
+ if (!jobs.some(job => job.name === 'gate' && job.status === 'completed' && job.conclusion === 'success')) return;
91
+ core.setOutput('reuse', 'true');
92
+ core.info('Reusing successful development gate run ' + run.id + ' for exact SHA ' + context.sha);
93
+ } catch (error) {
94
+ core.warning('Development gate proof unavailable; running normal gate: ' + error.message);
95
+ }
96
+ - if: ${{ steps.development-gate.outputs.reuse != 'true' }}
97
+ uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
69
98
  with:
70
99
  lockfile-path: {{GATE_CACHE_DEP_PATH}}
71
100
  fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
72
101
  - name: Runbook guard
73
- if: ${{ github.event_name == 'pull_request' }}
102
+ if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
74
103
  uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
75
104
  with:
76
105
  base-ref: origin/${{ github.base_ref }}
@@ -81,11 +110,13 @@ jobs:
81
110
  # `npm config get cache` for any explicit cache restore). Job-level `env:` cannot use the
82
111
  # `runner` context — write via GITHUB_ENV from the shell `$RUNNER_TEMP` instead.
83
112
  - name: Isolate npm cache (per-job)
113
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
84
114
  shell: bash
85
115
  run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
86
116
  # MMI-Hub#5111: Windows CRLF in tracked shell scripts breaks Linux runners before the real
87
117
  # check can explain it. Inspect only tracked files before install; .gitattributes is the fix.
88
118
  - name: Line-ending guard
119
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
89
120
  shell: bash
90
121
  run: |
91
122
  offenders=$(mktemp)
@@ -116,6 +147,7 @@ jobs:
116
147
  # files (Dockerfile + docker-compose.yml) skip in a second — no new job, no new required
117
148
  # context, no ruleset or registry change (#7012 needs nothing to cover).
118
149
  - name: Product image build (tenant-container only)
150
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
119
151
  env:
120
152
  # The documented build-secret opt-in (compose `build.secrets` + registry
121
153
  # requiredBuildSecrets `NODE_AUTH_TOKEN=@github-packages-token`): the workflow's own
@@ -146,25 +178,27 @@ jobs:
146
178
  # MMI-Hub#5871: declare the npm MAJOR rather than inheriting whatever the runner's node bundles.
147
179
  # A bare major, never an exact patch (it goes stale and then prescribes downgrading operator
148
180
  # hosts toward it, #5862) and never `latest` (it takes the next major silently, #4782).
149
- - if: ${{ '{{GATE_RUNTIME}}' == 'node' }}
181
+ - if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'node') }}
150
182
  uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
151
183
  with:
152
184
  node-version: 24
153
185
  npm-version: '12'
154
- - if: ${{ '{{GATE_RUNTIME}}' == 'python' }}
186
+ - if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
155
187
  uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
156
188
  with: { python-version: '{{GATE_PY_VERSION}}' }
157
189
  # MMI-Hub#3446: assert the runtime toolchain is on PATH BEFORE the install step. Node uses the
158
190
  # runner-node-toolchain composite above (setup-node + preflight in one action, #5610). Python
159
191
  # keeps the inline preflight below.
160
192
  - name: Toolchain preflight
161
- if: ${{ '{{GATE_RUNTIME}}' == 'python' }}
193
+ if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
162
194
  run: |
163
195
  python3 --version || { echo "::error title=CI runner toolchain::RUNNER NOT PROVISIONED — python3 not on PATH after setup-python. This is the runner, not your diff; no install or test ran. Re-run the job; if it persists the mmi-runner python toolchain needs attention (docs/Guides/gh-runner-runbook.md in MMI-Hub)."; exit 1; }
164
196
  - run: {{GATE_INSTALL_CMD}}
197
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
165
198
  # #3178: the check runs under the org wall-clock budget — a hung suite dies at the ceiling instead
166
199
  # of squatting a runner lane for GitHub's 6h default. ci-audit + the release train enforce this step.
167
200
  - uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
201
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
168
202
  with:
169
203
  command: {{GATE_CMD}}
170
204
  max-seconds: {{GATE_MAX_SECONDS}}
@@ -0,0 +1,6 @@
1
+ {
2
+ ".github/workflows/agent-pr.yml": {
3
+ "blob": "3d53d026c1b71f5439ef5781f693802a0375b306",
4
+ "reason": "comment-only CI waiver placement (#8167), no runtime or operator behaviour change"
5
+ }
6
+ }
@@ -8,6 +8,7 @@ concurrency:
8
8
  group: gate-${{ github.event.pull_request.number || github.ref }}
9
9
  cancel-in-progress: true
10
10
  permissions:
11
+ actions: read # #8206: exact-SHA development workflow verdict
11
12
  contents: read
12
13
  packages: read
13
14
  jobs:
@@ -15,26 +16,59 @@ jobs:
15
16
  runs-on: [self-hosted, linux, x64, mmi-live]
16
17
  timeout-minutes: 10
17
18
  steps:
18
- - uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
19
+ # #8206: keep a fresh successful tag gate; promote the exact development verdict, not a ref.
20
+ - name: Reuse development gate for rc tag
21
+ id: development-gate
22
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
23
+ with:
24
+ script: |
25
+ core.setOutput('reuse', 'false');
26
+ if (context.eventName !== 'push' || !/^refs\/tags\/v.+-rc\.\d+$/.test(context.ref) || !/^[0-9a-f]{40}$/.test(context.sha)) return;
27
+ try {
28
+ const { data: current } = await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId });
29
+ const { data } = await github.rest.actions.listWorkflowRuns({
30
+ ...context.repo, workflow_id: current.workflow_id, branch: 'development',
31
+ event: 'push', head_sha: context.sha, per_page: 100
32
+ });
33
+ // API returns newest first: a newer red/pending rerun must not reuse an older green.
34
+ const run = data.workflow_runs[0];
35
+ if (!run || run.head_sha !== context.sha || run.head_branch !== 'development' ||
36
+ run.event !== 'push' || run.status !== 'completed' || run.conclusion !== 'success') return;
37
+ const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
38
+ ...context.repo, run_id: run.id, filter: 'latest', per_page: 100
39
+ });
40
+ if (!jobs.some(job => job.name === 'gate' && job.status === 'completed' && job.conclusion === 'success')) return;
41
+ core.setOutput('reuse', 'true');
42
+ core.info('Reusing successful development gate run ' + run.id + ' for exact SHA ' + context.sha);
43
+ } catch (error) {
44
+ core.warning('Development gate proof unavailable; running normal gate: ' + error.message);
45
+ }
46
+ - if: ${{ steps.development-gate.outputs.reuse != 'true' }}
47
+ uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
19
48
  with:
20
49
  lockfile-path: package-lock.json
21
50
  fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
22
51
  - name: Runbook guard
23
- if: ${{ github.event_name == 'pull_request' }}
52
+ if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
24
53
  uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
25
54
  with:
26
55
  base-ref: origin/${{ github.base_ref }}
27
56
  - name: Isolate npm cache
57
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
28
58
  run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
29
59
  - uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
60
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
30
61
  with:
31
62
  node-version: 24
32
63
  npm-version: '12'
33
64
  - run: npm ci --no-audit --no-fund
65
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
34
66
  - run: npm ci --prefix module --no-audit --no-fund
67
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
35
68
  env:
36
69
  NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37
70
  - uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
71
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
38
72
  with:
39
73
  command: npm run typecheck && npm run build && npm --prefix module run check
40
74
  max-seconds: {{GATE_MAX_SECONDS}}