@mutmutco/codex-plugin 3.131.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 (45) hide show
  1. package/.codex-plugin/plugin.json +30 -0
  2. package/bin/mmi-cli +6 -0
  3. package/bin/mmi-cli.cmd +3 -0
  4. package/bin/mmi-hook +2 -0
  5. package/bin/mmi-hook-console.cmd +10 -0
  6. package/bin/mmi-hook.exe +0 -0
  7. package/hooks/codex-hooks.json +41 -0
  8. package/package.json +21 -0
  9. package/scripts/command-ladder-core.mjs +334 -0
  10. package/scripts/command-ladder-gate.mjs +126 -0
  11. package/scripts/deny-gate-crash.mjs +179 -0
  12. package/scripts/edit-tool-paths.mjs +113 -0
  13. package/scripts/env-write-lint.mjs +137 -0
  14. package/scripts/hook-io.mjs +22 -0
  15. package/scripts/hook-policy.mjs +73 -0
  16. package/scripts/hook-run.mjs +437 -0
  17. package/scripts/hook-trace.mjs +151 -0
  18. package/scripts/pretooluse-shell-gates.mjs +424 -0
  19. package/scripts/secret-echo-lint.mjs +177 -0
  20. package/scripts/secret-redact.mjs +552 -0
  21. package/scripts/throttle-core.mjs +324 -0
  22. package/scripts/validate-hook.mjs +156 -0
  23. package/scripts/vault-edit-gate.mjs +94 -0
  24. package/skills/bootstrap/SKILL.md +550 -0
  25. package/skills/bootstrap/seeds/Dockerfile.template +30 -0
  26. package/skills/bootstrap/seeds/README.template.md +37 -0
  27. package/skills/bootstrap/seeds/architecture.template.md +34 -0
  28. package/skills/bootstrap/seeds/decisions-readme.template.md +45 -0
  29. package/skills/bootstrap/seeds/docker-compose.template.yml +26 -0
  30. package/skills/bootstrap/seeds/gate.template.yml +85 -0
  31. package/skills/bootstrap/seeds/google-login.template.md +33 -0
  32. package/skills/bootstrap/seeds/manifest.json +26 -0
  33. package/skills/bootstrap/seeds/mmi-product-required-checks.template.json +23 -0
  34. package/skills/browser-automation/SKILL.md +95 -0
  35. package/skills/epic/SKILL.md +104 -0
  36. package/skills/hotfix/SKILL.md +165 -0
  37. package/skills/mmi/SKILL.md +404 -0
  38. package/skills/mmi-doctor/SKILL.md +63 -0
  39. package/skills/onboard/SKILL.md +85 -0
  40. package/skills/rcand/SKILL.md +208 -0
  41. package/skills/release/SKILL.md +599 -0
  42. package/skills/resume/SKILL.md +90 -0
  43. package/skills/secrets/SKILL.md +159 -0
  44. package/skills/stage/SKILL.md +153 -0
  45. package/skills/worktree/SKILL.md +151 -0
@@ -0,0 +1,599 @@
1
+ ---
2
+ name: release
3
+ description: Ship rc or direct-track development to main and production.
4
+ ---
5
+
6
+ **Host-native invocation:** Claude `/mmi:release` · Codex `$mmi:release` · jervcode/Kimi `/skill:release` · Kilo `skill` tool. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
7
+
8
+ **Argument (#4929):** `/release minor`, `/release major`, or `/release patch` — a bare bump word, never a
9
+ `--flag` or env var. Map it to `MMI_BUMP_INTENT` exported for the whole run (the train and
10
+ `next-version.mjs` both read it). No argument → `patch`: a `/release patch` from development is a
11
+ legitimate ordinary release; `/hotfix` remains the cherry-pick promotion lane.
12
+
13
+ # /release — ship to main + prod
14
+
15
+ Full-track repos ship **exactly what is on `rc`** (never pulls `development`): merge `rc → main`, tag
16
+ `vX.Y.0`, publish a GitHub Release, dispatch the Hub central tenant deploy workflow for prod, then roll
17
+ `development` forward. Direct-track repos — product repos with `releaseTrack: direct`, plus MMI-Hub via the `isHubControlRepo` special-case (its `releaseTrack` stays unset) — skip rc: release
18
+ merges `development → main`, tags, publishes the GitHub Release, and the release event fires the repo's own
19
+ deploy/publish workflow (for MMI-Hub, `deploy.yml` + `publish.yml`).
20
+ `rc` is **ephemeral**: `/rcand` creates the rc runtime, `/release` retires it — after a confirmed full-track
21
+ prod deploy the train stops the rc stage (reported as `rcRetirement` in the result; never fatal to the
22
+ release). Full-track repos may also pass **`--dev`** to release `development → main` directly, skipping rc —
23
+ the default stays `rc → main`. `--dev` **fails closed** when `origin/rc` carries content not yet in
24
+ `development` (a dev → main release would drop it), and is a friendly no-op on direct-track repos.
25
+ **Train-authority gated (D14):** the repo's project-admin or the master; the Hub repo's train is
26
+ master-only. This is the **only** sanctioned prod path; a prod release still needs the authorized human's
27
+ explicit per-turn go — an agent never self-initiates it. The board needs no
28
+ touch — items reached `Done` when their PRs merged to `development`; `rc`/`main` are deploy stages, not lanes.
29
+
30
+ Authority is structural + server-checked: step 0 asks the Hub (`mmi-cli oracle org access role`), and step 3 pushes
31
+ to the protected `main` branch, whose per-repo allowlist carries the same people (master + that repo's
32
+ project-admins; Hub: master + App only). Gate ordering: the tag lands the release SHA for checks, and
33
+ nothing deploys before the protected `main` push accepts that checked SHA.
34
+
35
+ ## Merge floor (#3167 — non-negotiable, applies to every step below)
36
+
37
+ - **Never squash-merge a tagged commit.** A squash re-mints the SHA, so the tag's commit vanishes from the
38
+ branch's lineage and every tag-anchored check (coverage trailers, misalignment guard, version probes)
39
+ re-flags or mis-reads forever after. Any merge that carries an already-tagged commit — the Step 5
40
+ `main → development` roll-forward, any alignment PR — lands as a **true merge** (`--merge`), never squash.
41
+ - **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt below — authority
42
+ probe, coverage guard, untolerated conflict, required checks, stray tag — ends the run. The recovery is
43
+ always to fix the cause and re-run the train; it is never hand-resolving on `main`, bare-pushing a train
44
+ branch, acking to save time, or bypassing a check.
45
+ - **Version probes anchor on the latest Release tag, not a branch.** `next-version.mjs` and the fold derive
46
+ the next version from tags; a branch manifest or a stale local tag is not a version source. (This is why a
47
+ stray tag must be deleted locally too — a surviving local tag silently mints the *next* version, Step 3.)
48
+
49
+ ## Step 0 — confirm + probe
50
+
51
+ Confirm the human holding train authority for THIS repo authorized a prod release this turn. Probe:
52
+ ```bash
53
+ mmi-cli oracle org access role {owner}/{repo} --json # Hub-verified: { role, train }
54
+ ```
55
+ `train: false` — or any error (fail closed) → stop: a product repo's train belongs to that repo's
56
+ project-admin or the master; the Hub train is master-only. **Three exceptions to fail-closed here, and all
57
+ mean "unverified", never an authority verdict:**
58
+
59
+ - A `repository … does not exist or is not visible to the org App` error (HTTP 404) means the probe target
60
+ is not a resolvable repo — a package name, a typo, a repo never created (#4778). It is UNKNOWN, not a
61
+ denial, and retrying will never change it: fix the `{owner}/{repo}` argument, then re-probe. Before the
62
+ fix this string came back `role: master, train: true, verified: true` for anything owner/name-shaped.
63
+
64
+ - An `unknown command 'oracle'` error is a STALE CLI — the house-prefixed namespaces ship in newer
65
+ trains, so an old installed CLI can't even parse the probe. Jump to Step 0a, heal the CLI, then re-run
66
+ this probe; never conclude the command form is wrong from a pre-heal failure (#3150). The mirror-image
67
+ failure is a Wave-3 flat alias on a CURRENT CLI: the un-housed `org …`, `secrets …`, and
68
+ `release …` forms were removed in #4316 and fail closed with a house-prefix error — rewrite to the
69
+ house form (`mmi-cli oracle org …`, `mmi-cli vault secrets …`, `mmi-cli devops release …`); that is
70
+ never a stale CLI and never an authority verdict.
71
+ - A **TIMEOUT or network error** (`operation aborted due to timeout`, DNS/socket failures) is **not** a
72
+ `train: false` denial (#3321). Read the probe's own `verified` field: `verified: false` means the verdict
73
+ could not be established, so the `train: false` beside it carries NO authority meaning. The client
74
+ already retries transient failures internally, so a *persistent* timeout points at the local network
75
+ rather than the Hub — resolve the Hub API host first (fails in seconds and rules out the whole
76
+ local-network class), then re-run the probe. Conclude "no train authority" ONLY from a `verified: true`
77
+ response. Aborting a legitimate release on an unverified probe is the failure this carve-out exists to
78
+ prevent.
79
+
80
+ Then
81
+ preconditions: clean tree; full-track repos run from `rc` (or from `development` with `--dev`), while
82
+ direct-track repos run from `development`. Ordinary `/release` takes its bump intent from the bare
83
+ argument (`/release minor|major|patch` → `MMI_BUMP_INTENT`, exported for the whole run); unstated, the
84
+ train defaults to `patch` (#4929). An invalid intent fails `--apply` closed at intent resolution
85
+ (`MMI_BUMP_INTENT must be one of major|minor|patch`), before any merge, tag, or fold.
86
+
87
+ **Run from the checkout that already has that branch — never a fresh isolated worktree (#2770).** This is
88
+ a shared-branch train operation (merge/tag/push against origin's protected `main`/`rc`), not an isolated
89
+ feature-branch edit. A background-job harness that isolates every task into a fresh worktree by default
90
+ breaks here: the new worktree's branch is never literally named `development`/`rc`, and `git checkout
91
+ development` inside it fails outright when `development` is already checked out in the primary checkout (git
92
+ worktrees cannot have the same branch checked out twice). If you are in such a worktree, exit it first and
93
+ run the release from the primary checkout.
94
+ The clean-tree check rejects UNTRACKED scratch too, not just modified tracked files. When `--apply` or
95
+ `--resume` stops with `working tree must be clean before …`, run `git status --porcelain` on the paths it
96
+ named and read BOTH status columns before touching anything (#1472, #4004):
97
+
98
+ - `??` — untracked scratch. Gitignore it, or move it to a gitignored path like `tmp/`, then retry.
99
+ - exactly ` M` (blank staged column) — the only state that can be churn rather than work. Test it with
100
+ `git diff HEAD --numstat -- <paths>`: non-empty is a real edit to commit or stash; empty means the
101
+ worktree normalizes straight back to HEAD (an LF↔CRLF rewrite, say), and `git checkout -- <paths>`
102
+ then clears the refusal without discarding anything.
103
+ - anything else — `M `, `MM`, `A`, `D`, `R`, `T`, or any `U` conflict — is real work or an exceptional
104
+ index state. Commit, stash or resolve it; do not try to classify it.
105
+
106
+ Do not substitute plain `git diff --numstat` for the `HEAD` form. It compares the worktree against the
107
+ INDEX, so a change that is merely staged prints nothing at all, and reading that emptiness as
108
+ "line-ending churn" throws the change away. The remedy is the index form of `git checkout` for the same
109
+ reason: `git checkout HEAD -- <paths>` would overwrite a staged edit that was misread as churn.
110
+
111
+ Full-track repos:
112
+ ```bash
113
+ git fetch origin
114
+ git rev-list --count origin/main..origin/rc
115
+ ```
116
+
117
+ Direct-track repos (e.g. MMI-Hub):
118
+ ```bash
119
+ git fetch origin
120
+ git rev-list --count origin/main..origin/development
121
+ ```
122
+ `0` → stop ("nothing to release").
123
+
124
+ ## Step 0a — stale CLI preflight (#1410)
125
+
126
+ Before local gates or `release --apply`, ensure the repo-local / PATH `mmi-cli` matches the released train
127
+ path — a stale checkout (e.g. 2.32.0 while 2.32.4 is released) fails release gates with opaque errors.
128
+ Self-heal in place without repo writes (heals run by default since #3975; the flag suppresses only
129
+ mutations of the working tree you are about to release from):
130
+ ```bash
131
+ mmi-cli doctor --no-repo-writes
132
+ ```
133
+ **Read the checks, not the exit code.** `doctor` exits non-zero on any failed check it could have healed —
134
+ not just a version gap — so a red exit here does not by itself mean the train is blocked (#2962). The
135
+ inverse is also true: a run can exit **0** while printing ✗ lines (report-only rows, #3485). Neither
136
+ direction is a gate. Read the rows.
137
+
138
+ - **Blocking:** a red `mmi-cli` / `Claude plugin` version line, or a reported `minClientVersion` gap.
139
+ `--no-repo-writes` does not block the env heals — the step above self-updates the npm global CLI
140
+ (#3272) and reinstalls the plugin (#3282), suppressing only repo-file writes — so the re-run should clear
141
+ it. If a gap persists, fix it — `mmi-cli doctor`, or `npm install -g @mutmutco/cli` — before
142
+ continuing. Do **not** proceed to Step 0b while the installed CLI is behind the Hub's `minClientVersion`.
143
+ - **Not blocking:** every other red check is hygiene, not a train gate. Stale branches and scratch heal with
144
+ a plain `mmi-cli doctor` (after the release); `plugin cache` needs a different verb,
145
+ `mmi-cli plugin prune --apply` — saying doctor cleared it was wrong. Both still exit 1 until you run the
146
+ right verb. Read them, then proceed.
147
+
148
+ Treating doctor's exit code as the gate halts a healthy train on cosmetic drift — or, worse, teaches an
149
+ operator to ignore doctor's exit code entirely.
150
+
151
+ **A local `npm ci` failure during the fold on a lockfile CI already accepted is an npm-major mismatch, not a
152
+ bad lockfile (#4578).** Before touching the lockfile, compare toolchains:
153
+ ```bash
154
+ npm -v
155
+ gh api repos/{owner}/{repo}/actions/runs --jq '.workflow_runs[0].id' # or read the gate log's
156
+ # `node -v && npm -v` toolchain-preflight line (#3446) for the npm major CI ran with
157
+ ```
158
+ A different npm MAJOR (e.g. local 12 vs. CI's 11) resolves the same lockfile differently and throws `Missing:
159
+ <pkg> from lock file` on a lockfile that is not actually broken. Align the local npm major
160
+ (`npm install -g npm@<CI major>`) and re-run `npm ci` before rewriting or regenerating the lockfile.
161
+
162
+ **A `npm ci was killed (signal SIGTERM)` failure during the fold is a cold npm cache, not a lockfile or
163
+ toolchain problem (#4841).** The signature is the kill, not error text: no `Missing:` line, no lockfile
164
+ complaint at all — the fold's child-process budget expired while a cold cache installed the full tree.
165
+ The npm-major note above is inert here (majors match, the lockfile is fine), and following it tempts the
166
+ one thing it warns against — regenerating the lockfile for a problem that has nothing to do with it.
167
+ Warm the cache with a plain `npm ci` in the release checkout, then rerun the identical train command; the
168
+ rollback the failed fold already ran makes the rerun clean. Never regenerate or rewrite the lockfile in
169
+ response to a SIGTERM kill.
170
+
171
+ **Then check the version yourself — do not rely on doctor having printed a row (#3674).** "Read the rows"
172
+ resolves to "proceed" when the row is *absent*, and doctor's version row is conditional. On 2026-07-27 this
173
+ step reported healthy with no version line while the installed CLI was 3.70.0 against a released 3.71.0;
174
+ `release --apply` refused one command later. Two lines, no dependency on another command's reporting
175
+ completeness:
176
+ ```bash
177
+ mmi-cli --version
178
+ npm view @mutmutco/cli version # public package; a bare view is trustworthy here
179
+ ```
180
+ Mismatch → `npm install -g @mutmutco/cli@<released>` before Step 0b, **regardless of what doctor's rows
181
+ said**. This is the check whose absence Step 0a's own title promises to cover, so it is not optional.
182
+
183
+ ## Step 0b — registry + main secret-name preflight
184
+
185
+ Resolve the project META first; its `deployModel` decides the deploy path. `tenant-container` repos use the
186
+ central tenant deployer and therefore need DEPLOY# coords. `hub-serverless` (MMI-Hub), `serverless`,
187
+ `registry-publish`, and `solo-container` repos deploy from their own branch/release-triggered or model
188
+ workflow, so do **not** dispatch `tenant-deploy.yml` for them.
189
+ Verify META + required SSM secret names before touching `main`:
190
+ ```bash
191
+ mmi-cli oracle org project get {owner}/{repo}
192
+ mmi-cli vault secrets preflight --stage main --repo {owner}/{repo}
193
+ ```
194
+ An enumerated missing META row or missing secret name → stop and repair the registry/secrets first. An
195
+ HTTP 5xx, timeout, DNS, socket, or other transport failure is **unverified**, not evidence that a name is
196
+ missing: retry the read/preflight and repair connectivity if it persists. Never provision or rename a
197
+ secret from a transport-error response.
198
+
199
+ ## Step 0c — hotfix-coverage guard (fail closed, #839, #958)
200
+
201
+ Full-track repos only. Direct-track repos skip this specific guard — not because they are exposed to no
202
+ risk, but because the risk this guard checks structurally cannot arise there: a direct-track `/hotfix`
203
+ cherry-picks straight from `development` onto `main`, so every hotfix trailer's source sha is *by
204
+ construction* an ancestor of `origin/development` — a trailer-ancestry check would be permanently green and
205
+ add no signal (#2786 — a literal port was considered and rejected as a false-confidence guard).
206
+
207
+ Hotfixes are cherry-picked from `development` to `main` with **no back-merge** (see `/hotfix`), so a
208
+ candidate cut *before* a fix landed on `development` would silently revert that hotfix in prod. The guard
209
+ proves every main-only commit is in the candidate before the `rc → main` merge.
210
+
211
+ **Direct-track repos are not risk-free, though — they carry a different, real failure mode:** a hotfix
212
+ freezes a snapshot of the files it cherry-picks (e.g. docs, a test assertion) while `development` keeps
213
+ evolving those same files afterward. Step 1's `development → main` merge preflight (untolerated-path
214
+ conflict check) is what actually catches this — a genuine content conflict on the **next** release is
215
+ expected when a hotfix's cherry-picked commits touched files `development` went on to rewrite
216
+ incompatibly. That is routine, working-as-designed friction, not a fluke or a misconfiguration (incident:
217
+ hotfix v3.16.1 #2765 cherry-picked #2762/#2768's doc sweep onto `main`; `development` then rewrote the same
218
+ files further via #2767/#2771; `/release` correctly stopped on 4 conflicted files, resolved by confirming
219
+ `development`'s side already represented the correct, current content and landing alignment PR #2784 — see
220
+ #2786). When Step 1 stops this way: run `git merge-tree` (or read the conflict content directly) to confirm
221
+ which side carries the correct current content, then land a true-merge alignment PR (never hand-resolve on
222
+ `main`) before rerunning release.
223
+
224
+ It runs **automatically inside `mmi-cli devops release --apply`** (Step 1+ below) — built into the CLI so it
225
+ works in every product repo with no repo-local script. You do not invoke it separately.
226
+
227
+ Per main-only commit it accepts: the `(cherry picked from commit <sha>)` trailer with that dev SHA an
228
+ ancestor of `origin/rc` (immune to conflict-resolved ports); a matching `git patch-id` on the rc side
229
+ (trailer-less picks); or a distribution-manifest-only bump (exempt — rc carries its own). Anything else
230
+ **fails the release closed** → **stop**: the right fix is a re-cut `/rcand` from `development`. Only when
231
+ the authorized human has manually verified the content is in the candidate, rerun with
232
+ `mmi-cli devops release --apply --ack <sha>[,<sha>…]` — the ack is recorded in the verdict. Never ack to save time.
233
+
234
+ ## Step 0d — no docs-keeping on the release train (Hub#4164)
235
+
236
+ Scheduled docs-janitor / wiki-keeper / living-docs freshness passes are **retired** (#4117/#4119).
237
+ The release train does **not** open docs-only PRs, wait on README/architecture auto-merges, or
238
+ treat prose refresh as a gate.
239
+
240
+ Current state is compute-at-read + the estate repo-index (`mmi-cli oracle repo-index search`,
241
+ `mmi-cli oracle org project`, board/schedules verbs). Durable WHY stays in `docs/decisions/`.
242
+ If shipping code made a hand-written surface wrong, fix it on `development` as an ordinary PR
243
+ **outside** the train — never as a release Step.
244
+
245
+ ## Step 1 — merge to main (never force)
246
+
247
+ Full-track repos:
248
+
249
+ ```bash
250
+ git checkout main
251
+ git pull --ff-only origin main
252
+ git merge rc --no-edit
253
+ ```
254
+
255
+ Direct-track repos (e.g. MMI-Hub):
256
+ ```bash
257
+ git checkout main
258
+ git pull --ff-only origin main
259
+ git merge development --no-edit
260
+ ```
261
+ Conflict → abort + stop (the train is misaligned — investigate; don't hand-resolve on `main`). On a
262
+ direct-track repo, a conflict here right after a hotfix is expected friction, not a fluke — see Step 0c for
263
+ why (a hotfix freezes a snapshot that `development` can keep rewriting).
264
+ Alignment PRs are the exception to the org's squash default: land them with a true merge —
265
+ `mmi-cli devops pr merge <n> --auto --merge` (not squash; a squash discards the merge parentage, so the
266
+ misalignment guard re-flags the same divergence on the next run). `--auto` clears the checks the PR
267
+ triggers, which block an immediate merge right after a release.
268
+
269
+ **Exception — version-manifest and `.gitignore` paths.** `mmi-cli devops release --apply` tolerates
270
+ conflicts confined to the version-fold paths (Step 1b) and `.gitignore` (#1037 — a
271
+ repo bootstrapped before the managed-gitignore era still carries the legacy file on `main`; the candidate
272
+ carries the Hub-managed copy with project-local entries preserved). The org spine is no longer delivered per
273
+ repo (hub-v3 WS4), so a spine-path conflict is treated like any other untolerated path. A hotfix bumps the manifests +
274
+ committed CLI bundle on `main` only, so the next merge re-conflicts there even when the train is healthy —
275
+ and the fold rewrites those exact paths right after the merge. For all tolerated paths the CLI takes the
276
+ incoming side deterministically and continues. Any other conflicted path → abort + stop as above.
277
+
278
+ ## Step 1b — version fold (automatic, inside `mmi-cli devops release --apply`)
279
+
280
+ Every release folds the version bump into the release itself (#976): after the merge onto local `main` and
281
+ before the tag, the CLI bumps the version manifests to the release version and commits — **unconditionally,
282
+ changed or not** — then the tag-first push (Step 3) earns that fresh commit its required checks. There is
283
+ no separate bump PR, no `development` prep cycle, and `development` never sits ahead of the published
284
+ version (the back-merge in Step 5 carries the bump back).
285
+
286
+ What the fold bumps, by repo:
287
+ - **Hub (`hub-serverless`):** the full locked distribution set via `scripts/release-distribution.mjs
288
+ prepare` — spine dogfood (`scripts/spine-dogfood.mjs`: verify docs/surfaces + the managed `.gitignore` block),
289
+ every registry-declared version holder, adapter payload synchronization, build output, and the public
290
+ artifact bill of materials — then verifies the set (`verify --skip-npm-view`). Publication and staging
291
+ both derive from `surfaces.json`; there is no second package list. Claude, Codex, Kimi, Cursor, and
292
+ Kilo are active.
293
+ - **App-style repos with a root `package.json`** (most products): the manifest + lockfile version via
294
+ `npm version --no-git-tag-version`, kept in lockstep with the release tag.
295
+ - **Repos with neither:** nothing to fold — the tag is the version.
296
+
297
+ Nothing to do by hand; the `--apply` result reports the fold outcome (`versionFold`).
298
+
299
+ ## Step 2 — tag the release
300
+
301
+ Full-track repos drop the `-rc.N` suffix from the open cycle. Direct-track repos use the next cycle directly
302
+ because they have no rc tag — `cycle` mode reads `MMI_BUMP_INTENT` (the `/release` argument; unset →
303
+ `patch`, #4929). The Step 0 export is what covers the apply run — the inline prefix below only resolves
304
+ the tag for display; `mmi-cli devops release --apply` re-derives intent itself and fail-closes before any
305
+ merge, tag, or fold when the value is invalid:
306
+ ```bash
307
+ TAG=$(node scripts/next-version.mjs release) # full-track repos -> vX.Y.0
308
+ TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle) # direct-track repos -> vX.Y.0
309
+ git tag "$TAG"
310
+ ```
311
+
312
+ ## Step 3 — push tag, wait for the REQUIRED checks, then push main (the gate)
313
+
314
+ `mmi-cli devops release --apply`'s exit code is **not** the release verdict: exit `2` can mean the release
315
+ shipped successfully while protected-branch alignment remains pending, and exit `1` can mean a
316
+ post-release follow-up failed after promotion — **or that the follow-up is merely unresolved** (#4939):
317
+ a verdict of `releaseStatus=succeeded` with `followUpStatus=pending` (the release-triggered runs — e.g.
318
+ the Hub's `deploy.yml` + `publish.yml` — had only just auto-fired) also exits nonzero, and a
319
+ background-task harness surfacing exit codes misreads it as "release failed" when the release actually
320
+ shipped. Classify from the verdict fields, never the exit code alone: `succeeded` + `pending` means
321
+ shipped with the follow-up unresolved — watch the enumerated `workflowRuns` to conclusion; only a
322
+ resolved failure there is a failed follow-up. Read the live release verdict and verify these four
323
+ facts instead: the `main..development` count, the tag on `origin`, `gh release view`, and the runs on
324
+ the release SHA.
325
+
326
+ Required status checks are **per-repo branch protection, not a fixed list** — MMI-Hub's `main` requires
327
+ `cli` · `infra` · `docs`, but a product repo may require different contexts or none at all (#1045). The
328
+ release SHA is always fresh (the Step 1b fold commits on local `main`), so when checks ARE required,
329
+ pushing the branch *first* is structurally rejected until CI catches up. Push the **tag first** (it lands
330
+ the SHA and triggers the repo's CI — in MMI-Hub `gate.yml` runs on `v*` tags — without touching the
331
+ protected ref), then probe what `main` actually requires and wait only for those contexts:
332
+
333
+ ```bash
334
+ git push origin "vX.Y.0" # lands the SHA + triggers the repo's CI
335
+ SHA=$(git rev-parse main)
336
+ # discover the REQUIRED contexts on main (classic protection + rulesets; 404 = none from that source):
337
+ gh api repos/{owner}/{repo}/branches/main/protection/required_status_checks --jq '[.contexts[]]'
338
+ gh api repos/{owner}/{repo}/rules/branches/main \
339
+ --jq '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]'
340
+ # ZERO required contexts -> push main immediately (the GitHub push gate is the backstop).
341
+ # Otherwise poll until every required context is "success" on $SHA — never a hard-coded list, and bound
342
+ # the wait (~10 min): on timeout, stop with a clear failure naming the pending/failed contexts.
343
+ gh api repos/{owner}/{repo}/commits/$SHA/check-runs \
344
+ --jq '[.check_runs[]|{name:.name,conclusion:.conclusion}]'
345
+ git push origin main
346
+ ```
347
+ (`mmi-cli devops release --apply` performs this discovery + bounded wait itself.) A required context can only ever
348
+ resolve on a tag SHA if its workflow runs on `push: tags` (or is otherwise SHA-addressable) — a PR/issue-event
349
+ job (e.g. a stale hand-added `add-to-project` / `mark-merged-pr-done` board-automation check) structurally
350
+ never produces a check-run there. The train recognizes that fixed pair by name and, after a short grace
351
+ window, treats it as satisfied-for-the-tag instead of burning the full ~10-minute budget on a check-run that
352
+ can never appear (#2404). Any OTHER required context still waits the full budget and, on timeout, names
353
+ exactly which contexts never materialized vs. which were merely pending. If your repo's ruleset requires a
354
+ PR-only job outside that pair, fix the ruleset (drop it, or replace with a real gate) — don't rely on the
355
+ grace window for anything not on the allowlist.
356
+ Rejected → stop (nothing released).
357
+
358
+ **Halted on a FAILED required check (not merely pending)?** That is a **stray unreleased tag** (#2734): the
359
+ tag is pushed but `main` was not, no GitHub Release exists, and nothing deployed/published off it — so it can
360
+ never go forward (a failed check will not pass on that SHA). This is the **opposite** of a *resumable* partial
361
+ state (checks passing/pending, only the branch push / Release / deploy left, where the recovery is to finish
362
+ forward and you must NOT delete the tag). Here: fix the cause on `development` via a CI-gated PR, then **delete
363
+ the stray tag** — `git push origin --delete vX.Y.0` **and** `git tag -d vX.Y.0` (delete the local tag too; a
364
+ surviving local tag makes `next-version cycle` silently mint the *next* version) — and re-run `mmi-cli devops release
365
+ --apply` (the fold re-tags the same version on the fixed HEAD). **Tag deletion is deletion-class → it needs the
366
+ authorized human's explicit per-turn go.** Exception: if a publish/deploy DID run off the stray tag, do **not**
367
+ delete — mint the next version with `MMI_RELEASE_VERSION` instead. The CLI already classifies this
368
+ (`isStrayUnreleasedTag`, `cli/src/train-apply.ts`); this note surfaces the doctrine so a halted operator does
369
+ not re-derive it from source.
370
+
371
+ ## Step 4 — GitHub Release + start prod deploy (non-blocking)
372
+
373
+ For `tenant-container` repos, publish the GitHub Release and dispatch the central tenant deploy.
374
+ ```bash
375
+ gh release create "vX.Y.0" --target main --generate-notes --latest
376
+ gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub \
377
+ -f slug={slug} -f repo={owner}/{repo} -f ref=main -f stage=main
378
+ gh run watch "$(gh run list --workflow tenant-deploy.yml --limit 1 --json databaseId -q '.[0].databaseId')" \
379
+ --exit-status # the central prod-deploy run — run this in the BACKGROUND (Bash run_in_background)
380
+ ```
381
+
382
+ For `hub-serverless` (MMI-Hub), publish the GitHub Release but do **not** dispatch `tenant-deploy.yml`:
383
+ the release event auto-fires `deploy.yml` for prod and `publish.yml` for the
384
+ plugin/CLI package. A missing `DEPLOY#main` registry row for MMI-Hub is expected, not a tenant stack repair
385
+ task. Watch/report the release-triggered `deploy.yml` and `publish.yml` runs instead.
386
+
387
+ For other direct-track repos, the train dispatches nothing centrally: a `registry-publish` repo's release
388
+ event fires its own `publish.yml` (npm / plugin marketplace); a `solo-container` repo deploys via its own
389
+ workflow. Publish the GitHub Release, then watch/report that repo's own release-triggered run.
390
+
391
+ `mmi-cli devops release --apply --json` returns the relevant run id/url data with `deployStatus`; `--watch` blocks
392
+ on the run(s) and resolves `deployStatus` to `success`/`failure`. For tenant-container repos, that is the
393
+ dispatched `tenant-deploy.yml` run. For Hub serverless, that is the auto-fired release `deploy.yml` and
394
+ `publish.yml` workflow pair — watched on `mutmutco/MMI-Hub`. For `registry-publish`, that is the target
395
+ repo's own release-triggered `publish.yml` run — watched on that repo, never dispatched, and never the
396
+ central `tenant-publish.yml` (#2428: dispatching both is a guaranteed npm E409 double-publish, since the
397
+ repo's own release-triggered publish always wins the version race).
398
+ `promoted: true` stays set even on a failed deploy — promotion and deploy are separate outcomes.
399
+
400
+ **`deployStatus: 'pending'` is NEVER a terminal, reportable state (#3322).** A release that returns
401
+ `pending` has an UNRESOLVED deploy, not a clean one. The report now prints `deploy: UNVERIFIED` instead of
402
+ omitting the deploy line entirely — which is exactly how a failed deploy once hid behind a "successful"
403
+ release (Jerv-PowerTools v1.22.0: the release-triggered `publish.yml` went green, a separate `push: main`
404
+ SAM deploy FAILED, and the train returned `deployStatus: pending`, so the run read as clean). Resolve it,
405
+ or report it as unverified-RED. Never as success.
406
+
407
+ **Read the `--apply` result's `workflowRuns` array — the train already enumerates every run on the release
408
+ SHA (#3699).** That enumeration includes `push: main` infra deploys (`deploy.yml`, SAM) the deploy model does
409
+ not name, and `deployStatus` is held at `pending` until they resolve. Any non-success entry — including
410
+ `pending` — keeps the release unverified: watch those listed runs to conclusion:
411
+ ```bash
412
+ gh run watch <run-id> --repo {owner}/{repo} --exit-status
413
+ ```
414
+ The one remaining caveat: `--watch` blocks only on deploy-model-named runs, not extra push-triggered
415
+ deploys — those you conclude from the `workflowRuns` list yourself. Any `failure` there is a failed release
416
+ deploy even when the train reported success — flag it loudly and fix it before calling the release healthy.
417
+
418
+ **Hub releases announce to Slack (#883).** Hub scope is **only** `mutmutco/MMI-Hub`: the commits/PRs on
419
+ `origin/main..origin/development`, this repo's `deploy.yml` + `publish.yml`, and Hub tooling (`mmi-cli`,
420
+ skills, plugin, registry, central workflows). Do **not** read another repo's board (`mmi-cli oracle board` / `/mmi`
421
+ on a product), watch or dispatch `ds-propagate.yml`, run `design-system` / `doctor` design-system heals for
422
+ consumers, or treat a product's deploy state as part of this release — those belong to that product's own
423
+ `/release` or `/rcand`, never a Hub train.
424
+
425
+ Before running `--apply` for MMI-Hub, resolve the real tag first:
426
+ `TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle)` — always print `$TAG` (e.g.
427
+ `v2.43.0`) in summaries, Slack, chat, and the final report; never a placeholder like `vX.Y.0` or
428
+ `v0.x.0`.
429
+
430
+ Write a curated summary — 3-6 very short plain lines, one change per line, dev-readable, no PR-dump —
431
+ to a fresh temp file (`f=$(mktemp tmp/release-summary.XXXXXX)`, so a stale prior summary is never reused).
432
+ Source from **Hub PR titles only** (`origin/main..origin/development` on `mutmutco/MMI-Hub`), but **rewrite**
433
+ each line in neutral Hub-subsystem terms (CLI, skills, plugin, workflows, registry, deploy hub) — **never**
434
+ a product or brand name (FoFu, Katip, etc.) anywhere in the summary file, Slack post, chat, or release
435
+ report. Product names are allowed only when releasing **that product's repo**. Then pass the file through:
436
+ `mmi-cli devops release --apply --announce-summary-file "$f"`. After the GitHub Release publishes, the CLI posts
437
+ the summary to the org alerts channel as the MMI-Future Slack app (token + channel from SSM at run time).
438
+ For a new MMI-Hub `--apply`, the CLI refuses before promotion when the file is missing, unreadable, or does
439
+ not contain 3–6 non-empty lines; generated-note fallback is not an agent release path. `--resume` never
440
+ requires or republishes a summary, and non-Hub repos skip the announcement automatically. After a valid
441
+ summary is accepted, Slack delivery remains best-effort: a transport failure is reported in the result and
442
+ never rolls back an otherwise completed release.
443
+
444
+ **Don't block on the deploy.** Start the watch as a background task and proceed to Steps 4b–5 (docs, project
445
+ info, branch alignment) while prod deploys. The verdict is collected in Step 6 — verification is not
446
+ skipped, only un-blocked. Deploy failure → report plainly, then **retry the existing promoted ref by deploy
447
+ model** once the runtime is repaired — never a re-tag or republish (`main` is already at the release, which is
448
+ correct). Read the model from the registry (`mmi-cli oracle org project get {owner}/{repo}` → `deployModel`):
449
+ - **tenant-container** — the central tenant deployer owns the redeploy: `mmi-cli devops runtime tenant redeploy
450
+ {owner}/{repo} main --watch`.
451
+ - **repository-owned serverless / registry-publish** (e.g. Jerv-PowerTools' `Deploy Jerv Memory`) — there is
452
+ no `runtime tenant redeploy` for these; re-run the repo's OWN declared `workflow_dispatch` deploy workflow at
453
+ the promoted-equivalent ref: `gh workflow run <deploy-workflow> --repo {owner}/{repo} --ref <ref>`.
454
+
455
+ Name the sanctioned dispatch mechanism you used in the train report.
456
+
457
+ ## Step 4b — re-sync project info (no in-repo docs pass)
458
+
459
+ In-repo README/architecture freshness is **not** a train step (Step 0d retired, Hub#4164).
460
+ `mmi-cli devops release --apply` re-syncs the GitHub **Project** short description + thin README from the
461
+ released commit and the registry member list through the command ladder. For a manual repair or preview use:
462
+ ```bash
463
+ mmi-cli oracle org project sync-info --apply # omit --apply for the read-only plan
464
+ ```
465
+
466
+ ## Step 5 — roll development forward
467
+
468
+ - Keep branches aligned: `mmi-cli devops release --apply` back-merges the released `main` (incl. the version
469
+ fold) into `development` and reports it as `devRollForward`. When `development` has no required checks it
470
+ pushes directly (`status: pushed`). When `development` *requires* checks (e.g. MMI-Hub needs
471
+ `cli`/`infra`/`docs`), the fresh merge commit carries no passing checks, so a direct push is structurally
472
+ rejected — the train instead **opens an alignment PR** `main → development` (`status: pr-pending`) and the
473
+ release report prints the exact land command. The release itself has already shipped; **land the alignment
474
+ PR with a true merge** — `mmi-cli devops pr merge <number> --auto --merge` (never squash — a squash drops the merge
475
+ parentage and the misalignment guard re-flags the divergence). `--auto` is what makes it land right after a
476
+ release: the alignment PR's own `cli`/`infra`/`docs` checks are still running, so a plain immediate merge is
477
+ policy-blocked — `--auto` merges once they pass. Never force.
478
+ - Full-track repos: `mmi-cli devops release --apply` already aligned `rc` to the released `main` (#1036 — the
479
+ push runs inside the authority-gated train; the result reports it as `rcAlignment`). No manual `rc`
480
+ push — if the result reports a failed alignment, investigate and rerun via the train, never bare-push.
481
+ - **Local branches never left stale (#2582).** A release advances `main`/`development`/`rc` on **origin**;
482
+ `mmi-cli devops release --apply` also fast-forwards the LOCAL train branches to match, so the checkout you
483
+ released from does not lag what you just pushed. The train restores + fast-forwards the start branch
484
+ (`checkout`, #2340) and fast-forwards the others (`localSync`) — non-destructively: only a true
485
+ fast-forward moves a branch; a diverged local branch is reported and left untouched (never a force).
486
+ The one branch it **cannot** sync at release time is a check-gated `development` whose roll-forward is a
487
+ `pr-pending` alignment PR (Hub) — that PR merges on origin *after* the release returns. That case is
488
+ healed automatically by the **next session**: `mmi-cli plugin session-start` (invoked by the resume
489
+ channel — #3630 took SessionStart off the hook surface) runs the
490
+ same fast-forward first thing, so a stale local `development`/`main`/`rc` self-heals to origin before you
491
+ work — no manual `git pull`. Nothing here ever blocks or fails the release.
492
+ - **Read the `checkout:` clause — it does not always say "returned" (#4006).** The report always prints
493
+ one, and restoration is deliberately skipped rather than forced when the repo is not in a fit state.
494
+ A release leaves the repo on `main` until the train moves it back, so a non-`returned` outcome means
495
+ you are still there and must return by hand once the cause is cleared:
496
+ - `checkout restoration skipped: working tree changed after release` — the tree went dirty DURING the
497
+ train, so the train declined to move you. Diagnose those paths with the Step 0 clean-tree rules,
498
+ then `git checkout <start branch>` yourself. The release itself already shipped; this is a
499
+ deliberate, harmless decline, not a failed release.
500
+ - `checkout restoration failed while returning to …` / `origin/<branch> fast-forward failed` — the
501
+ checkout or the pull errored. Read the appended git message; the release is unaffected.
502
+
503
+ `--resume` prints no `checkout:` clause at all, so after a resumed release check `git branch
504
+ --show-current` rather than assuming you were moved back.
505
+
506
+ ## Step 6 — collect deploy verdict + report
507
+
508
+ The `release --apply` exit code is **not** the release verdict. A nonzero exit with
509
+ `releaseStatus=succeeded` and `followUpStatus=pending` is the unresolved-follow-up case from Step 3 —
510
+ the release shipped; the runs enumerated in `workflowRuns` are the outstanding verdict, so watch them
511
+ to conclusion here rather than re-running or alarming. Report the live release verdict from
512
+ the four checks that matter: the `main..development` count, the tag on `origin`, `gh release view`,
513
+ and the runs on the release SHA; an alignment PR is normal follow-up work when those checks confirm
514
+ the release shipped.
515
+
516
+ Collect the backgrounded prod-deploy watch from Step 4 (it has typically finished by now). Confirm prod is
517
+ healthy (the central deploy workflow smoke step / a health check); **red** → report the failure prominently and flag
518
+ that the release shipped on a failed deploy (re-run just the deploy — `main` is already correct).
519
+
520
+ **This distribution verifier is Hub-only.** It assumes the Hub release fold has already committed its
521
+ generated artifacts. Non-Hub `registry-publish` repositories must not run `release-distribution.mjs
522
+ verify`: use their own release-workflow evidence and the publish-visibility contract below.
523
+
524
+ Hub releases always carry a distribution bump (the Step 1b fold), so the **publish workflow**
525
+ (`publish.yml`) ships every registry-declared public npm artifact on the GitHub Release from Step 4 —
526
+ don't publish by hand.
527
+ Watch it, then confirm npm caught up:
528
+ ```bash
529
+ gh run watch "$(gh run list --workflow publish.yml --event release --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status
530
+ node scripts/release-distribution.mjs verify "vX.Y.0" # asserts registry versions, BOM identities, and published npm artifacts
531
+ ```
532
+ Run that verify from a checkout **at the tag** (`main` right after the release merge, `git checkout
533
+ "vX.Y.0"`, or a worktree at the tag) — verify checks the working tree and refuses any other commit once
534
+ the tag exists, so a stale `development`/`rc` checkout can't masquerade as a broken release.
535
+ Release-blocking for Hub tooling changes: every public artifact must match the registry version and BOM
536
+ identity. Manual fallback if CI can't publish: `node scripts/release-distribution.mjs publish "vX.Y.0"`
537
+ from a machine with npm auth.
538
+
539
+ **Private packages (#2405) — never trust a bare `npm view` to mean "not published".** An unauthenticated
540
+ `npm view <pkg>` returns a hard 404 on any package published with `publishConfig.access=restricted`
541
+ (private) — indistinguishable from "not published" — because npm masks private packages from anonymous
542
+ reads. Before verifying ANY npm publish (Hub's own, a `registry-publish` product repo's own `publish.yml`,
543
+ or a `publishRequired` product repo's companion `tenant-publish.yml`), check that surface's
544
+ `publishVisibility` in the target repo's `surfaces.json`
545
+ (`node -e "console.log(require('./surfaces.json').surfaces.find(s=>s.id==='<id>').publishVisibility)"`, or
546
+ the repo-agnostic reader `scripts/release-distribution.mjs` exports as `npmVerifyModeFor`):
547
+ - `public` → an unauthenticated `npm view <pkg>@<version> version` is a trustworthy verify signal.
548
+ - `private` → do **not** run a bare `npm view`. Verify from an authenticated source instead: the
549
+ release-triggered run's conclusion (`gh run watch … --exit-status` — the repo's own `publish.yml` for
550
+ `registry-publish`, #2428; the central `tenant-publish.yml` for a `publishRequired` companion publish) and
551
+ its logged `+ pkg@ver` / `"already on npm — nothing to do"` / `"already on npm ... treating as idempotent
552
+ success"` output (`gh run view <id> --log`), or `npm view` with a registry auth token in env. A 404 here is
553
+ "unverifiable from here", never "not published" — report it as such, don't block or alarm on it.
554
+ - `n/a` → nothing publishes to npm for that surface; skip.
555
+ Hub's registry-declared npm surfaces are public today, so `release-distribution.mjs verify` uses a bare
556
+ `npm view` for them — this changes only if a package's declared visibility changes.
557
+
558
+ **tenant-publish.yml E409 belt-and-braces (#2428).** The central `tenant-publish.yml` lane (the
559
+ `publishRequired` companion publish only — never dispatched for `registry-publish`, see above) already
560
+ treats a lost `npm publish` race as idempotent inside the run (a re-check `npm view` after a failed publish;
561
+ see the workflow file). `mmi-cli devops release --apply --watch` mirrors that at the CLI layer: a watched
562
+ `tenant-publish.yml` run that concludes `failure` is re-checked against that SAME run's own log (bounded
563
+ retries, visibility-agnostic — never a bare `npm view`) for the idempotent-success marker before the CLI
564
+ reports the publish as failed. A run whose log never shows the version landed stays a loud failure.
565
+
566
+ Report: Release `$TAG` (the resolved tag from Step 2 — never a placeholder) + the GitHub Release URL · prod
567
+ deploy run + URL + **green/red** · branch-alignment note · npm publish run + CLI version (Hub releases).
568
+
569
+ ## Notes
570
+
571
+ - `/hotfix` stays the cherry-pick promotion lane (a hotfix always skips rc — it cherry-picks
572
+ `development → main` directly), but a `/release patch` from development is a legitimate ordinary
573
+ release (#4929). Never force-push `main`.
574
+ - **`MMI_BUMP_INTENT=major|minor|patch` comes from the bare `/release` argument; unstated it defaults to
575
+ `patch`** (#4929). Export it for the whole run, not a one-off prefix on `next-version.mjs cycle`:
576
+ `mmi-cli devops release --apply` (and `rcand --apply`) resolve intent themselves **before any merge,
577
+ tag, or fold** — direct-track included — defaulting to `patch` when the env is absent and failing closed
578
+ there with `MMI_BUMP_INTENT must be one of major|minor|patch` on an invalid value, so the tree stays
579
+ clean on the start branch. `MMI_RELEASE_VERSION`
580
+ below is a different knob: it overrides the computed version; it does not replace the intent export
581
+ unless you set that exact-target env instead.
582
+ - **`--dev` (full-track only):** releases `development → main` skipping rc, with the same fold/tag/Release/
583
+ deploy machinery plus the post-release rc retirement and rc alignment. Refuses (fail closed) when
584
+ `origin/rc` carries commits not in `development`; on direct-track repos it's a no-op (they already
585
+ release from `development`).
586
+ - **MAJOR / exact-target release:** to ship a version the tag math can't derive (a MAJOR like `2.0.0`, or
587
+ skipping a version already on npm), product repos export `MMI_RELEASE_VERSION=X.Y.Z` for **both** `/rcand`
588
+ and `/release`; MMI-Hub exports it for `/release` only. Steps 1b/2 then fold and tag exactly that
589
+ version. Unset, the train uses the declared `MMI_BUMP_INTENT` increment, defaulting to `patch` when
590
+ neither the argument nor the env declares one (#4929).
591
+
592
+ ## Retro — one check before you finish
593
+ Before your final report, answer one question honestly: did **this skill's own instructions** misfire
594
+ this run — ambiguous wording, a misleading message, or an environment failure it should have warned
595
+ about? (Process only — never the user's code or task; e.g. a misleading authority or gate message, or an
596
+ ambiguous version-fold or back-merge step.) If yes, file **one** lesson and move on; a clean run is silent
597
+ (hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR — never
598
+ edit the skill live; the retro is advisory, so if the call fails, note it and continue:
599
+ `mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"`