@mutmutco/codex-plugin 4.3.5 → 4.3.7

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.
@@ -5,964 +5,83 @@ description: Ship rc or direct-track development to main and production.
5
5
 
6
6
  **Host-native invocation:** Claude `/mmi:release` · Codex `$mmi:release` · Cursor `/release` · jervcode `/release` · Hermes skill_view. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
7
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.
8
+ **Argument:** `/release minor`, `/release major`, or `/release patch` — a bare bump word, never a `--flag` or env var. No argument → `patch`; a `/release patch` from development is a legitimate ordinary release, `/hotfix` stays the cherry-pick promotion lane.
12
9
 
13
10
  # /release — ship to main + prod
14
11
 
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.
12
+ Full-track repos ship exactly what is on `rc` (never `development`): merge `rc → main`, tag `vX.Y.0`, publish the GitHub Release, deploy prod, roll `development` forward. Direct-track repos ship `development → main`, and the release event fires the repo's own deploy/publish workflow (MMI-Hub: `deploy.yml` + `publish.yml`). `rc` is ephemeral: `/rcand` creates its runtime, `/release` retires it after a confirmed prod deploy (`rcRetirement`). A prod release needs the authorized human's explicit go in the current turn — an agent never self-initiates it. The board needs no touch: items reached `Done` when their PRs merged to `development`.
29
13
 
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.
14
+ ## Lane
34
15
 
35
- ## Merge floor (#3167 non-negotiable, applies to every step below)
16
+ Read META and name the lane before describing or executing anything:
36
17
 
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
- `--wait` does not relax this: use `mmi-cli devops pr merge <n> --wait --merge` (or the policy-gated
42
- `--auto --merge`). `--wait` alone still defaults the CLI merge method to squash and must not land an
43
- alignment / tagged-commit-carrying PR.
44
- - **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt below — authority
45
- probe, coverage guard, untolerated conflict, required checks, stray tag — ends the run. The recovery is
46
- always to fix the cause and re-run the train; it is never hand-resolving on `main`, bare-pushing a train
47
- branch, acking to save time, or bypassing a check.
48
- - **Version probes anchor on the latest Release tag, not a branch.** `next-version.mjs` and the fold derive
49
- the next version from tags; a branch manifest or a stale local tag is not a version source. (This is why a
50
- stray tag must be deleted locally too — a surviving local tag silently mints the *next* version, Step 3.)
51
- - **One `/release` at a time on a repo (#5380).** Two overlapping `--apply` runs race the shared tag namespace:
52
- the slower train can tag a SHA that never got a fold commit, while the other sees that tag mid-flight and
53
- mints the *next* version (a skip). Publish then fails verify (`root package and lockfile versions must
54
- equal …`) and the broken GitHub Release can briefly hold **Latest**. `mmi-cli devops release --apply`
55
- fail-closes post-fold / pre-tag when the computed tag (or a newer release tag) appears on origin since
56
- the plan-phase probe — stop, do not hand-finish, do not start a second train. Recovery is below (Step 3).
57
-
58
- ## Step 0 — confirm + probe
59
-
60
- Before describing or executing any release lane, read the project META and name the lane:
61
18
  ```bash
62
19
  mmi-cli oracle org project get {owner}/{repo} --json
63
20
  ```
64
- `releaseTrack: direct` means `development → main` and **no `/rcand`**. An absent `releaseTrack` means full-track
65
- (`rc → main`) except for MMI-Hub itself, whose Hub-control special case is direct. Stop on an unreadable META record;
66
- never infer the lane from branch names, release history, or the existence of an `rc` branch.
67
-
68
- Then confirm the human holding train authority for THIS repo authorized a prod release this turn. Probe:
69
- ```bash
70
- mmi-cli oracle org access role {owner}/{repo} --json # Hub-verified: { role, train }
71
- ```
72
- `train: false` — or any error (fail closed) → stop: a product repo's train belongs to that repo's
73
- project-admin or the master; the Hub train is master-only. **Three exceptions to fail-closed here, and all
74
- mean "unverified", never an authority verdict:**
75
-
76
- - A `repository … does not exist or is not visible to the org App` error (HTTP 404) means the probe target
77
- is not a resolvable repo — a package name, a typo, a repo never created (#4778). It is UNKNOWN, not a
78
- denial, and retrying will never change it: fix the `{owner}/{repo}` argument, then re-probe. Before the
79
- fix this string came back `role: master, train: true, verified: true` for anything owner/name-shaped.
80
-
81
- - An `unknown command 'oracle'` error is a STALE CLI — the house-prefixed namespaces ship in newer
82
- trains, so an old installed CLI can't even parse the probe. Jump to Step 0a, heal the CLI, then re-run
83
- this probe; never conclude the command form is wrong from a pre-heal failure (#3150). The mirror-image
84
- failure is a Wave-3 flat alias on a CURRENT CLI: the un-housed `org …`, `secrets …`, and
85
- `release …` forms were removed in #4316 and fail closed with a house-prefix error — rewrite to the
86
- house form (`mmi-cli oracle org …`, `mmi-cli vault secrets …`, `mmi-cli devops release …`); that is
87
- never a stale CLI and never an authority verdict.
88
- - A **TIMEOUT or network error** (`operation aborted due to timeout`, DNS/socket failures) is **not** a
89
- `train: false` denial (#3321). Read the probe's own `verified` field: `verified: false` means the verdict
90
- could not be established, so the `train: false` beside it carries NO authority meaning. The client
91
- already retries transient failures internally, so a *persistent* timeout points at the local network
92
- rather than the Hub — resolve the Hub API host first (fails in seconds and rules out the whole
93
- local-network class), then re-run the probe. Conclude "no train authority" ONLY from a `verified: true`
94
- response. Aborting a legitimate release on an unverified probe is the failure this carve-out exists to
95
- prevent.
96
-
97
- Then
98
- preconditions: clean tree; full-track repos run from `rc` (or from `development` with `--dev`), while
99
- direct-track repos run from `development`. Ordinary `/release` takes its bump intent from the bare
100
- argument (`/release minor|major|patch` → `MMI_BUMP_INTENT`, exported for the whole run); unstated, the
101
- train defaults to `patch` (#4929). An invalid intent fails `--apply` closed at intent resolution
102
- (`MMI_BUMP_INTENT must be one of major|minor|patch`), before any merge, tag, or fold.
103
-
104
- **Run from the checkout that already has that branch — never a fresh isolated worktree (#2770).** This is
105
- a shared-branch train operation (merge/tag/push against origin's protected `main`/`rc`), not an isolated
106
- feature-branch edit. A background-job harness that isolates every task into a fresh worktree by default
107
- breaks here: the new worktree's branch is never literally named `development`/`rc`, and `git checkout
108
- development` inside it fails outright when `development` is already checked out in the primary checkout (git
109
- worktrees cannot have the same branch checked out twice). If you are in such a worktree, exit it first and
110
- run the release from the primary checkout.
111
-
112
- The clean-tree check rejects UNTRACKED scratch too, not just modified tracked files. When `--apply` or
113
- `--resume` stops with `working tree must be clean before …`, run `git status --porcelain` on the paths it
114
- named and read BOTH status columns before touching anything (#1472, #4004):
115
-
116
- - `??` — untracked scratch. For `?? .jerv/` / `?? .pi/` (org-managed agent scratch), run
117
- `mmi-cli doctor --apply` to write the managed `.gitignore` block — do not hand-edit `.gitignore`
118
- (#5409). The train CLI already ignores those live-session dirs (#5397); doctor is the durable heal.
119
- For other `??` paths, gitignore them, or move them to a gitignored path like `tmp/`, then retry.
120
- - exactly ` M` (blank staged column) — the only state that can be churn rather than work. Test it with
121
- `git diff HEAD --numstat -- <paths>`: non-empty is a real edit to commit or stash; empty means the
122
- worktree normalizes straight back to HEAD (an LF↔CRLF rewrite, say), and `git checkout -- <paths>`
123
- then clears the refusal without discarding anything.
124
- - anything else — `M `, `MM`, `A`, `D`, `R`, `T`, or any `U` conflict — is real work or an exceptional
125
- index state. Commit, stash or resolve it; do not try to classify it.
126
-
127
- Do not substitute plain `git diff --numstat` for the `HEAD` form. It compares the worktree against the
128
- INDEX, so a change that is merely staged prints nothing at all, and reading that emptiness as
129
- "line-ending churn" throws the change away. The remedy is the index form of `git checkout` for the same
130
- reason: `git checkout HEAD -- <paths>` would overwrite a staged edit that was misread as churn.
131
-
132
- Full-track repos:
133
- ```bash
134
- git fetch origin
135
- git rev-list --count origin/main..origin/rc
136
- ```
137
-
138
- Direct-track repos (e.g. MMI-Hub):
139
- ```bash
140
- git fetch origin
141
- git rev-list --count origin/main..origin/development
142
- ```
143
- `0` → stop ("nothing to release").
144
-
145
- **Checkout topology — verify the lane's refs exist locally before apply (#5624).** A fresh single-branch
146
- `development` clone has no local `main`; the train then fails or rolls back at `git checkout main`. Before
147
- any merge or `--apply`, confirm every ref the lane will touch is present as a local tracking branch:
148
-
149
- ```bash
150
- git branch --list development main # direct-track (and full-track --dev)
151
- git branch --list rc main # full-track rc → main
152
- ```
153
-
154
- A missing ref is a topology gap, not a train failure — materialize it from origin, never force past it:
155
-
156
- ```bash
157
- git fetch origin main:main # (or development:development / rc:rc) — creates the local ref
158
- ```
159
-
160
- Re-run the `--list` until every ref the lane touches is present, then proceed.
161
-
162
- ## Step 0a — stale CLI preflight (#1410)
163
-
164
- Before local gates or `release --apply`, ensure the repo-local / PATH `mmi-cli` matches the released train
165
- path — a stale checkout (e.g. 2.32.0 while 2.32.4 is released) fails release gates with opaque errors.
166
- Read the gap on the compatibility-safe no-repository-write lane:
167
- ```bash
168
- mmi-cli doctor --no-repo-writes
169
- ```
170
- **Read the checks, not the exit code.** `doctor` exits non-zero on any failed check it could have healed —
171
- not just a version gap — so a red exit here does not by itself mean the train is blocked (#2962). The
172
- inverse is also true: a run can exit **0** while printing ✗ lines (report-only rows, #3485). Neither
173
- direction is a gate. Read the rows.
174
-
175
- - **Blocking:** a red `mmi-cli` / `Claude plugin` version line, or a reported `minClientVersion` gap.
176
- **Doctor does not update the CLI (#4954).** `mmi-hub` owns release-gated version convergence; doctor
177
- reports lag and last-run evidence only. The remedy is `mmi-hub update`, not a doctor re-run or a bare
178
- CLI install. Plain doctor/`--no-repo-writes` may safely repair active plugin wiring but never repository
179
- state. Do **not**
180
- proceed to Step 0b while the installed CLI is behind the Hub's `minClientVersion`.
181
- - **Not blocking:** every other red check is hygiene, not a train gate. Stale branches and scratch require
182
- explicit `mmi-cli doctor --apply` after the release; `plugin cache` needs a different verb,
183
- `mmi-cli plugin prune --apply` — saying doctor cleared it was wrong. Both still exit 1 until you run the
184
- right verb. Read them, then proceed.
185
-
186
- Treating doctor's exit code as the gate halts a healthy train on cosmetic drift — or, worse, teaches an
187
- operator to ignore doctor's exit code entirely.
188
-
189
- **`mmi-cli devops release --apply` and `mmi-cli devops rcand --apply` now run an active npm-major preflight
190
- before any branch/tag mutation (#5666).** They read the repo's expected CI npm, in this order (#5866,
191
- #5893): an explicit `npm-version:` on the gate's `runner-node-toolchain` step; else an explicit
192
- `npm install -g npm@<major>` step in gate.yml; else the infra gate's exact node pin through the
193
- bundled-npm map (only for an estate that still carries a retired pre-#5870 pin); else the publish
194
- workflow's own npm pin (`npm install -g npm@12`, the #5870 retirement shape). Local `npm -v`
195
- must match that npm **major**; a
196
- mismatch refuses `--apply` before any repository write.
197
-
198
- **Both gate shapes are a declaration; only a gate that declares NEITHER falls through (#5893).** A lane
199
- that installs its own npm has stated its npm as plainly as an `npm-version:` input — the shape differs, the
200
- intent does not. Reading only `npm-version:` skipped Jerv-JervCode's gate (floating `node-version: 24`
201
- plus `npm install -g npm@12`), fell through to publish.yml's stale npm 11, and prescribed a downgrade the
202
- repo had already retired (#2845, MMI-Hub#5871); the publish smoke then failed under npm 11 (`Cannot find
203
- module jiti/package.json` — a packed global install materializes no dependency closure under npm 11 once
204
- the root declares `bundleDependencies`), shipping v1.57.7 to main with a Release but no npm publish.
205
- **When gate.yml and publish.yml disagree on the npm major, that is a repo defect** — one lane will run the
206
- wrong toolchain whichever the preflight believes. The preflight names it and proceeds; fix it with a PR
207
- moving both pins together, never by silently trusting publish.yml.
208
-
209
- **Check which side is wrong before repairing (#5862).** The receipt names one direction — move the machine
210
- to the pin — and that is only right when the pin is current. Compare it against the registry first:
211
-
212
- ```bash
213
- npm view npm version # what npm is actually at
214
- ```
215
-
216
- A pin **behind** latest is fixed by a PR moving the pin, not by a machine-global downgrade that every
217
- release host then inherits. The pin is coordinated, so move all of it together: on Jerv-Hub the same
218
- version lives in `publish.yml`, `DISTRIBUTION_NPM_VERSION`, `package.json` `packageManager`, and a
219
- workflow-contract test. Prefer declaring a bare **major** (`npm@12`) over an exact patch — an exact pin
220
- goes stale and then prescribes downgrades, and `latest` silently takes the next major.
221
21
 
222
- **Bumping the pin can break workflow code that reads npm output (#5862).** `npm pack --json` changed shape
223
- between majors (npm ≤10 a top-level array, npm ≥11 a name-keyed object) and npm 12 prints the `prepare`
224
- lifecycle script's stdout ahead of the document, so `JSON.parse(stdout)` throws. An inline
225
- `const [entry] = JSON.parse(pack.json)` in a workflow step is the hazard — on Jerv-Hub it failed **after**
226
- `npm publish` had already succeeded, leaving the GitHub Release assets and the Gateway deploy undone. Repo
227
- scripts that already tolerate both shapes (`scripts/distribution-assembly.mjs`, #4741) are the fix; audit
228
- inline workflow parsing before bumping.
22
+ `releaseTrack: direct` `development main`, no `/rcand`. Absent full (`rc → main`), except MMI-Hub, which is direct by the Hub-control special case. Full-track `--dev` ships `development main` skipping rc and fails closed when `origin/rc` carries content not in `development`. Unreadable META → stop; never infer the lane from branch names, tags or release history.
229
23
 
230
- When the machine genuinely is the wrong side, the receipt's repair (`npm install -g npm@<CI major>`, then
231
- `node -v && npm -v`, then rerun the same release command) preserves Node and avoids routing a
232
- machine-global mutation through the train.
24
+ ## Preflight
233
25
 
234
- **A local `npm ci` failure during the fold on a lockfile CI already accepted is an npm-major mismatch, not a
235
- bad lockfile (#4578).** Before touching the lockfile, compare toolchains:
236
26
  ```bash
237
- npm -v
238
- gh api repos/{owner}/{repo}/actions/runs --jq '.workflow_runs[0].id' # or read the gate log's
239
- # `node -v && npm -v` toolchain-preflight line (#3446) for the npm major CI ran with
240
- # Read the gate's OWN declared npm first — `npm-version:` or `npm install -g npm@X` (#5893);
241
- # only a gate that declares neither falls back to the publish workflow npm pin (#5666).
27
+ mmi-cli devops train doctor --lane release --heal --json
242
28
  ```
243
- A different npm MAJOR (e.g. local 12 vs. CI's 11) resolves the same lockfile differently and throws `Missing:
244
- <pkg> from lock file` on a lockfile that is not actually broken. Align the local npm major
245
- (`npm install -g npm@<CI major>`) and re-run `npm ci` before rewriting or regenerating the lockfile.
246
29
 
247
- **A pre-tag `npm publish --dry-run` failure whose stack trace is inside npm internals e.g.
248
- libnpmpublish cannot find its declared sigstore dependency (`MODULE_NOT_FOUND`) — is toolchain
249
- corruption / an npm-major mismatch, not a malformed publish surface (#5616).** Compare toolchains
250
- BEFORE inspecting package contents (deleted dir, malformed `package.json`, missing `files`):
251
- ```bash
252
- npm -v
253
- # green gate npm from the gate log's `node -v && npm -v` toolchain-preflight line (#3446),
254
- # the gate's declared `npm-version:` or its `npm install -g npm@X` step (#5893), or an exact
255
- # node pin that maps to a bundled npm, or the publish workflow npm pin when the gate declares
256
- # no npm of its own (#5666)
257
- ```
258
- When local npm differs from the green gate (e.g. local npm 11 vs CI's 12), align local npm
259
- (`npm install -g npm@<gate npm>`) and rerun the train — do not rewrite the publish surface for an
260
- npm-internal missing module.
30
+ Read `ready`, then `findings[]`: each carries `code`, `severity` (`blocker` · `warning` · `healed` · `info`), `source` (`local` · `origin`), `remedy` and `anchor`. `ready: false` → stop and quote every blocker's `code` and `remedy`; an unread origin is never green. `--apply` re-runs this doctor as its step 0 with heal on.
261
31
 
262
- **A `npm ci was killed (signal SIGTERM)` failure during the fold is a cold npm cache, not a lockfile or
263
- toolchain problem (#4841).** The signature is the kill, not error text: no `Missing:` line, no lockfile
264
- complaint at all — the fold's child-process budget expired while a cold cache installed the full tree.
265
- The npm-major note above is inert here (majors match, the lockfile is fine), and following it tempts the
266
- one thing it warns against — regenerating the lockfile for a problem that has nothing to do with it.
267
- Warm the cache with a plain `npm ci` in the release checkout, then rerun the identical train command; the
268
- rollback the failed fold already ran makes the rerun clean. Never regenerate or rewrite the lockfile in
269
- response to a SIGTERM kill.
32
+ ## Apply
270
33
 
271
- **Repeated `EPERM` or `EBUSY` unlink errors during the fold's npm install on Windows are one race window,
272
- not a lockfile failure (#4982).** Rename `node_modules` aside once into a gitignored `tmp/` path, then rerun
273
- the identical train command. `npm ci` extracts into the now-empty tree and performs no unlinks; do not retry
274
- this workaround repeatedly.
34
+ Run from the primary checkout on the lane's start branch (`rc`; `development` for direct-track and `--dev`). Export the bump intent once from the bare argument `MMI_BUMP_INTENT=minor|major|patch`, unset `patch`; `MMI_RELEASE_VERSION=X.Y.Z` only for an exact target the tag math cannot derive.
275
35
 
276
- **A fold failure outside `npm ci` is often the repo's own npm lifecycle script, not a toolchain or lockfile
277
- problem (#5365).** App-style folds run `npm version`, which fires `preversion` / `version` / `postversion`
278
- (e.g. sync-workspace-version + refresh-distribution-bom); Hub folds run `release-distribution.mjs prepare`,
279
- which likewise executes repo scripts. When those fail, the train surfaces only
280
- `fold failed; local main was reset` — the real error is one layer down. Open the newest npm debug log
281
- (path printed by npm, typically under the npm cache `_logs/`) and read the verbose title / argv: if it
282
- names `npm version …` (or another non-`ci` command) and a `runScript` / lifecycle command failed, run that
283
- script standalone (e.g. `node scripts/refresh-distribution-bom.mjs`) to get the real error in seconds.
284
- Do this **before** suspecting npm major (#4578), cold cache (#4841), EPERM/EBUSY (#4982), or regenerating
285
- the lockfile — those classes apply to install failures, not to a script that already had `node_modules`.
36
+ MMI-Hub only: write a fresh 3–6 line neutral summary to `f=$(mktemp tmp/release-summary.XXXXXX)` sourced from Hub PR titles only (`origin/main..origin/development`), rewritten in Hub-subsystem terms, never a product or brand name. Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state.
286
37
 
287
- **Then check the version yourself — do not rely on doctor having printed a row (#3674).** "Read the rows"
288
- resolves to "proceed" when the row is *absent*, and doctor's version row is conditional. On 2026-07-27 this
289
- step reported healthy with no version line while the installed CLI was 3.70.0 against a released 3.71.0;
290
- `release --apply` refused one command later. Two lines, no dependency on another command's reporting
291
- completeness:
292
38
  ```bash
293
- mmi-cli --version
294
- npm view @mutmutco/cli version # public package; a bare view is trustworthy here
39
+ r=$(mktemp tmp/release-receipt.XXXXXX)
40
+ mmi-cli devops release --apply --json --out "$r" # product repos
41
+ mmi-cli devops release --apply --announce-summary-file "$f" --json --out "$r" # MMI-Hub
295
42
  ```
296
- Mismatch → run `mmi-hub update`, then repeat both reads before Step 0b, **regardless of what doctor's
297
- rows said**. This is the check whose absence Step 0a's own title promises to cover, so it is not optional.
298
-
299
- ## Step 0b — registry + main secret-name preflight
300
-
301
- Resolve the project META first; its `deployModel` decides the deploy path. `tenant-container` repos use the
302
- central tenant deployer and therefore need DEPLOY# coords. `hub-serverless` (MMI-Hub), `serverless`,
303
- `registry-publish`, and `solo-container` repos deploy from their own branch/release-triggered or model
304
- workflow, so do **not** dispatch `tenant-deploy.yml` for them.
305
- Verify META + required SSM secret names before touching `main`:
306
- ```bash
307
- mmi-cli oracle org project get {owner}/{repo}
308
- mmi-cli vault secrets preflight --stage main --repo {owner}/{repo}
309
- ```
310
- An enumerated missing META row or missing secret name → stop and repair the registry/secrets first. An
311
- HTTP 5xx, timeout, DNS, socket, or other transport failure is **unverified**, not evidence that a name is
312
- missing: retry the read/preflight and repair connectivity if it persists. Never provision or rename a
313
- secret from a transport-error response.
314
-
315
- **GitHub Actions billing/spending (#5604).** Those checks do not prove a *hosted* Actions job can start.
316
- The train dispatches `actions-job-start-canary.yml` on MMI-Hub (Linux hosted, not `windows-latest`) and
317
- refuses to mint a tag if the canary job never starts (empty steps / billing refusal). If a tag is already
318
- on origin, do **not** recut: `mmi-cli devops release --retry-publish <run-id> --apply` retries that exact
319
- failed or cancelled run. A failed run retries failed jobs; a cancelled run reruns the whole idempotent
320
- publish workflow because GitHub does not classify cancelled jobs as failed (#5797).
321
-
322
- ## Step 0c — hotfix-coverage guard (fail closed, #839, #958)
323
-
324
- Full-track repos only. Direct-track repos skip this specific guard — not because they are exposed to no
325
- risk, but because the risk this guard checks structurally cannot arise there: a direct-track `/hotfix`
326
- cherry-picks straight from `development` onto `main`, so every hotfix trailer's source sha is *by
327
- construction* an ancestor of `origin/development` — a trailer-ancestry check would be permanently green and
328
- add no signal (#2786 — a literal port was considered and rejected as a false-confidence guard).
329
-
330
- Hotfixes are cherry-picked from `development` to `main` with **no back-merge** (see `/hotfix`), so a
331
- candidate cut *before* a fix landed on `development` would silently revert that hotfix in prod. The guard
332
- proves every main-only commit is in the candidate before the `rc → main` merge.
333
-
334
- **Direct-track repos are not risk-free, though — they carry a different, real failure mode:** a hotfix
335
- freezes a snapshot of the files it cherry-picks (e.g. docs, a test assertion) while `development` keeps
336
- evolving those same files afterward. Step 1's `development → main` merge preflight (untolerated-path
337
- conflict check) is what actually catches this — a genuine content conflict on the **next** release is
338
- expected when a hotfix's cherry-picked commits touched files `development` went on to rewrite
339
- incompatibly. That is routine, working-as-designed friction, not a fluke or a misconfiguration (incident:
340
- hotfix v3.16.1 #2765 cherry-picked #2762/#2768's doc sweep onto `main`; `development` then rewrote the same
341
- files further via #2767/#2771; `/release` correctly stopped on 4 conflicted files, resolved by confirming
342
- `development`'s side already represented the correct, current content and landing alignment PR #2784 — see
343
- #2786). When Step 1 stops this way: run `git merge-tree` (or read the conflict content directly) to confirm
344
- which side carries the correct current content, then land a true-merge alignment PR (never hand-resolve on
345
- `main`) before rerunning release.
346
-
347
- It runs **automatically inside `mmi-cli devops release --apply`** (Step 1+ below) — built into the CLI so it
348
- works in every product repo with no repo-local script. You do not invoke it separately.
349
-
350
- Per main-only commit it accepts: the `(cherry picked from commit <sha>)` trailer with that dev SHA an
351
- ancestor of `origin/rc` (immune to conflict-resolved ports); a matching `git patch-id` on the rc side
352
- (trailer-less picks); or a distribution-manifest-only bump (exempt — rc carries its own). Anything else
353
- **fails the release closed** → **stop**: the right fix is a re-cut `/rcand` from `development`. Only when
354
- the authorized human has manually verified the content is in the candidate, rerun with
355
- `mmi-cli devops release --apply --ack <sha>[,<sha>…]` — the ack is recorded in the verdict. Never ack to save time.
356
-
357
- ## Step 0d — no docs-keeping on the release train (Hub#4164)
358
-
359
- Scheduled docs-janitor / wiki-keeper / living-docs freshness passes are **retired** (#4117/#4119).
360
- The release train does **not** open docs-only PRs, wait on README/architecture auto-merges, or
361
- treat prose refresh as a gate.
362
-
363
- Current state comes from code and live CLI verbs. Durable WHY stays in `docs/decisions/`.
364
- If shipping code made a hand-written surface wrong, fix it on `development` as an ordinary PR
365
- **outside** the train — never as a release Step.
366
-
367
- ## Step 1 — merge to main (never force)
368
-
369
- Full-track repos:
370
-
371
- ```bash
372
- git checkout main
373
- git pull --ff-only origin main
374
- git merge rc --no-edit
375
- ```
376
-
377
- Direct-track repos (e.g. MMI-Hub):
378
- ```bash
379
- git checkout main
380
- git pull --ff-only origin main
381
- git merge development --no-edit
382
- ```
383
- Conflict → abort + stop (the train is misaligned — investigate; don't hand-resolve on `main`). On a
384
- direct-track repo, a conflict here right after a hotfix is expected friction, not a fluke — see Step 0c for
385
- why (a hotfix freezes a snapshot that `development` can keep rewriting).
386
- Alignment PRs are the exception to the org's squash default: land them with a true merge —
387
- `mmi-cli devops pr merge <n> --auto --merge` (not squash; a squash discards the merge parentage, so the
388
- misalignment guard re-flags the same divergence on the next run). `--auto` clears the checks the PR
389
- triggers, which block an immediate merge right after a release. When awaiting checks inline, keep
390
- `--merge`: `mmi-cli devops pr merge <n> --wait --merge` — `--wait` alone is not enough (merge floor above).
391
-
392
- **Exception — version-manifest and `.gitignore` paths.** `mmi-cli devops release --apply` tolerates
393
- conflicts confined to the version-fold paths (Step 1b) and `.gitignore` (#1037 — a
394
- repo bootstrapped before the managed-gitignore era still carries the legacy file on `main`; the candidate
395
- carries the Hub-managed copy with project-local entries preserved). The org spine is no longer delivered per
396
- repo (hub-v3 WS4), so a spine-path conflict is treated like any other untolerated path. A hotfix bumps the manifests +
397
- committed CLI bundle on `main` only, so the next merge re-conflicts there even when the train is healthy —
398
- and the fold rewrites those exact paths right after the merge. For all tolerated paths the CLI takes the
399
- incoming side deterministically and continues. Any other conflicted path → abort + stop as above.
400
-
401
- ## Step 1b — version fold (automatic, inside `mmi-cli devops release --apply`)
402
43
 
403
- Every release folds the version bump into the release itself (#976): after the merge onto local `main` and
404
- before the tag, the CLI bumps the version manifests to the release version and commits — **unconditionally,
405
- changed or not** — then the tag-first push (Step 3) earns that fresh commit its required checks. There is
406
- no separate bump PR, no `development` prep cycle, and `development` never sits ahead of the published
407
- version (the back-merge in Step 5 carries the bump back).
44
+ Add `--dev` for a full-track dev → main release and `--watch` to block on the deploy/publish runs. `mmi-cli devops release --apply --ack <sha>[,<sha>…]` only when the authorized human has verified that a main-only commit the hotfix-coverage guard refused is already in the candidate — never ack to save time. The one command merges, runs the version fold (automatic, inside `mmi-cli devops release --apply`) that bumps the manifests unconditionally before the tag, pushes the tag first (`gate.yml` runs on `v*` tags, so the fold commit earns its required checks), waits for the required contexts, pushes `main`, publishes the Release, dispatches or correlates the deploy (`tenant-deploy.yml` only for tenant-container), announces (Hub), re-syncs project info, and rolls `development` and `rc` forward. Nothing here is done by hand.
408
45
 
409
- What the fold bumps, by repo:
410
- - **Hub (`hub-serverless`):** the full locked distribution set via `scripts/release-distribution.mjs
411
- prepare` — spine dogfood (`scripts/spine-dogfood.mjs`: verify docs/surfaces + the managed `.gitignore` block),
412
- every registry-declared version holder, adapter payload synchronization, build output, and the public
413
- artifact bill of materials — then verifies the set (`verify --skip-npm-view`). That prepare path runs
414
- `scripts/check-hook-contract.mjs`, which executes host adapter commands as a native argv without a
415
- shell (#5605), so a checkout path that contains spaces is supported; the spaced-path argv regression
416
- is pinned in `infra/plugin-set.test.mjs`. Publication and staging both derive from `surfaces.json`;
417
- there is no second package list. Claude, Codex, Cursor, jervcode, and Hermes are active.
418
- - **App-style repos with a root `package.json`** (most products): the manifest + lockfile version via
419
- `npm version --no-git-tag-version`, kept in lockstep with the release tag.
420
- - **Repos with neither:** nothing to fold — the tag is the version.
46
+ ## Verify
421
47
 
422
- Nothing to do by hand; the `--apply` result reports the fold outcome (`versionFold`).
48
+ Read the receipt at `$r`, never the exit code (exit `2` = shipped with a follow-up pending; exit `1` can also mean a follow-up failed after promotion):
423
49
 
424
- When the fold fails here, classify with Step 0a's fold-failure notes before retrying or touching the
425
- lockfile. If the failure is outside `npm ci`, read the npm debug log's verbose title/argv and run the
426
- named lifecycle script standalone (#5365) do not stop at `fold failed; local main was reset`.
50
+ - `releaseVerdict.releaseStatus` + `followUpStatus` `succeeded` + `pending` is shipped with the follow-up unresolved: neither failed nor done.
51
+ - `deployStatus` `pending` is never terminal; `promoted: true` holds even when a deploy failed.
52
+ - `workflowRuns` every run on the release SHA (deploy-model runs, any `push: main` deploy, the repo's own `gate`). Watch each non-success entry to conclusion: `gh run watch <run-id> --repo {owner}/{repo} --exit-status`. Name which run is red: `deploy.yml` / `publish.yml` / `tenant-deploy.yml` / `jerv-gateway` is a deploy or publish verdict; a `gate` push run is ordinary CI sharing the SHA.
53
+ - `announceNote` (Hub) — `announced`, `skipped`, or the failure note.
54
+ - `devRollForward` / `rcAlignment` — `pushed`, or `pr-pending` with the alignment PR to land by true merge (Merge floor).
55
+ - `versionFold`, `rcRetirement`, `checkout` — `returned`, or the named reason you are still on `main`.
427
56
 
428
- ## Step 2 tag the release
57
+ Then the bounded Latest Release read; its `tagName` must equal the resolved tag:
429
58
 
430
- Full-track repos drop the `-rc.N` suffix from the open cycle. Direct-track repos use the next cycle directly
431
- because they have no rc tag — `cycle` mode reads `MMI_BUMP_INTENT` (the `/release` argument; unset →
432
- `patch`, #4929). The Step 0 export is what covers the apply run — the inline prefix below only resolves
433
- the tag for display; `mmi-cli devops release --apply` re-derives intent itself and fail-closes before any
434
- merge, tag, or fold when the value is invalid:
435
- ```bash
436
- TAG=$(node scripts/next-version.mjs release) # full-track repos -> vX.Y.0
437
- TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle) # direct-track repos -> vX.Y.0
438
- git tag "$TAG"
439
- ```
440
-
441
- ## Step 3 — push tag, wait for the REQUIRED checks, then push main (the gate)
442
-
443
- `mmi-cli devops release --apply`'s exit code is **not** the release verdict: exit `2` can mean the release
444
- shipped successfully while protected-branch alignment remains pending, and exit `1` can mean a
445
- post-release follow-up failed after promotion — **or that the follow-up is merely unresolved** (#4939):
446
- a verdict of `releaseStatus=succeeded` with `followUpStatus=pending` (the release-triggered runs — e.g.
447
- the Hub's `deploy.yml` + `publish.yml` — had only just auto-fired) also exits nonzero, and a
448
- background-task harness surfacing exit codes misreads it as "release failed" when the release actually
449
- shipped. Classify from the verdict fields, never the exit code alone: `succeeded` + `pending` means
450
- shipped with the follow-up unresolved — watch the enumerated `workflowRuns` to conclusion; only a
451
- resolved failure there is a failed follow-up. Read the live release verdict and verify these four
452
- facts instead: the `main..development` count, the tag on `origin`, the bounded Latest Release read in
453
- Step 6, and the runs on the release SHA.
454
-
455
- Required status checks are **per-repo branch protection, not a fixed list** — MMI-Hub's `main` requires
456
- `cli` · `infra` · `docs`, but a product repo may require different contexts or none at all (#1045). The
457
- release SHA is always fresh (the Step 1b fold commits on local `main`), so when checks ARE required,
458
- pushing the branch *first* is structurally rejected until CI catches up. Push the **tag first** (it lands
459
- the SHA and triggers the repo's CI — in MMI-Hub `gate.yml` runs on `v*` tags — without touching the
460
- protected ref), then probe what `main` actually requires and wait only for those contexts:
461
-
462
- ```bash
463
- git push origin "vX.Y.0" # lands the SHA + triggers the repo's CI
464
- SHA=$(git rev-parse main)
465
- # discover the REQUIRED contexts on main (classic protection + rulesets; 404 = none from that source):
466
- gh api repos/{owner}/{repo}/branches/main/protection/required_status_checks --jq '[.contexts[]]'
467
- gh api repos/{owner}/{repo}/rules/branches/main \
468
- --jq '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]'
469
- # ZERO required contexts -> push main immediately (the GitHub push gate is the backstop).
470
- # Otherwise poll until every required context is "success" on $SHA — never a hard-coded list, and bound
471
- # the wait (~10 min): on timeout, stop with a clear failure naming the pending/failed contexts.
472
- gh api repos/{owner}/{repo}/commits/$SHA/check-runs \
473
- --jq '[.check_runs[]|{name:.name,conclusion:.conclusion}]'
474
- git push origin main
475
- ```
476
- (`mmi-cli devops release --apply` performs this discovery + bounded wait itself.) A required context can only ever
477
- resolve on a tag SHA if its workflow runs on `push: tags` (or is otherwise SHA-addressable) — a PR/issue-event
478
- job (e.g. a stale hand-added `add-to-project` / `mark-merged-pr-done` board-automation check) structurally
479
- never produces a check-run there. The train recognizes that fixed pair by name and, after a short grace
480
- window, treats it as satisfied-for-the-tag instead of burning the full ~10-minute budget on a check-run that
481
- can never appear (#2404). Any OTHER required context still waits the full budget and, on timeout, names
482
- exactly which contexts never materialized vs. which were merely pending. If your repo's ruleset requires a
483
- PR-only job outside that pair, fix the ruleset (drop it, or replace with a real gate) — don't rely on the
484
- grace window for anything not on the allowlist.
485
- Rejected → stop (nothing released).
486
-
487
- **Halted on a FAILED required check (not merely pending)?** That is a **stray unreleased tag** (#2734): the
488
- tag is pushed but `main` was not, no GitHub Release exists, and nothing deployed/published off it — so it can
489
- never go forward (a failed check will not pass on that SHA). This is the **opposite** of a *resumable* partial
490
- state (checks passing/pending, only the branch push / Release / deploy left, where the recovery is to finish
491
- forward and you must NOT delete the tag). Here: fix the cause on `development` via a CI-gated PR, then **delete
492
- the stray tag** — `git push origin --delete vX.Y.0` **and** `git tag -d vX.Y.0` (delete the local tag too; a
493
- surviving local tag makes `next-version cycle` silently mint the *next* version) — and re-run `mmi-cli devops release
494
- --apply` (the fold re-tags the same version on the fixed HEAD). **Tag deletion is deletion-class → it needs the
495
- authorized human's explicit per-turn go.** Exception: if a publish/deploy DID run off the stray tag, do **not**
496
- delete — mint the next version with `MMI_RELEASE_VERSION` instead. The CLI already classifies this
497
- (`isStrayUnreleasedTag`, `cli/src/train-apply.ts`); this note surfaces the doctrine so a halted operator does
498
- not re-derive it from source.
499
-
500
- **Halted by a concurrent-train refusal, or a publish verify that says package/lockfile versions ≠ the tag
501
- (#5380)?** Same deletion class as a stray tag, plus the GitHub Release if one was created (it may still show
502
- as **Latest** while broken). Evidence shape: two tags one patch apart from overlapping `/release` runs (e.g.
503
- `v3.1.11` on a no-fold SHA + `v3.1.12` minted because that tag landed mid-flight), and verify text like
504
- `root package and lockfile versions must equal 3.1.11`. Do **not** leave the skip-version tag and continue —
505
- with the authorized human's go: delete the broken Release (`gh release delete vX.Y.Z --yes`), delete the
506
- stray tag locally and on origin, confirm `next-version` would re-derive the *same* version (not the skip),
507
- then re-run a **single** `mmi-cli devops release --apply`. If the newer skip-version already published
508
- cleanly, keep it and only remove the broken older Release/tag.
509
-
510
- **Resuming a resumable partial state? The recovery form is `--resume --watch --json`, and nothing
511
- else (#5815).** `--resume` and `--apply` are **mutually exclusive** and the CLI refuses the pair:
512
- `--apply cuts the NEXT version, --resume finishes the immutable tag already on origin`
513
- (`cli/src/index.ts`, the #3851 guard). A continuation that reaches for
514
- `--resume --apply --watch` is asking the train to mint a new version while finishing an old one —
515
- it refuses before any mutation, so nothing is lost, but the operator has spent a turn on a form that
516
- can never run. `--apply` belongs to a **new** release cut, never to recovery.
517
- ```bash
518
- mmi-cli devops release --resume --watch --json --repo <owner/repo>
519
- ```
520
-
521
- ## Step 4 — GitHub Release + start prod deploy (non-blocking)
522
-
523
- For `tenant-container` repos, publish the GitHub Release and dispatch the central tenant deploy.
524
- ```bash
525
- gh release create "vX.Y.0" --target main --generate-notes --latest
526
- gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub \
527
- -f slug={slug} -f repo={owner}/{repo} -f ref=main -f stage=main
528
- ```
529
-
530
- On PowerShell, read the run as a structured receipt and filter it natively; do not nest a
531
- `gh run list` query inside `gh run watch`:
532
- ```powershell
533
- $runs = gh run list --workflow tenant-deploy.yml --limit 10 --json databaseId,createdAt,url,status,conclusion | ConvertFrom-Json
534
- $run = $runs |
535
- Where-Object { $_.databaseId -and $_.createdAt } |
536
- Sort-Object { [DateTimeOffset]$_.createdAt } -Descending |
537
- Select-Object -First 1
538
- if (-not $run) { throw 'tenant-deploy workflow run was not found' }
539
- $run | Select-Object databaseId,createdAt,url,status,conclusion | ConvertTo-Json -Compress
540
- # Run this in the BACKGROUND (PowerShell Start-Job / task runner).
541
- gh run watch $run.databaseId --exit-status
542
- ```
543
-
544
- When several run receipts need collecting, assign the loop output before serializing it; a top-level
545
- `foreach (...) { ... } | ConvertTo-Json` is a PowerShell parser trap:
546
- ```powershell
547
- $out = foreach ($run in $runs) {
548
- gh run view $run.databaseId --json databaseId,url,status,conclusion,createdAt | ConvertFrom-Json
549
- }
550
- $out | ConvertTo-Json -Compress
551
- ```
552
-
553
- For `hub-serverless` (MMI-Hub), publish the GitHub Release but do **not** dispatch `tenant-deploy.yml`:
554
- the release event auto-fires `deploy.yml` for prod and `publish.yml` for the
555
- plugin/CLI package. A missing `DEPLOY#main` registry row for MMI-Hub is expected, not a tenant stack repair
556
- task. Watch/report the release-triggered `deploy.yml` and `publish.yml` runs instead.
557
-
558
- For other direct-track repos, the train dispatches nothing centrally: a `registry-publish` repo's release
559
- event fires its own `publish.yml` (npm / plugin marketplace); a `solo-container` repo deploys via its own
560
- workflow. Publish the GitHub Release, then watch/report that repo's own release-triggered run.
561
-
562
- **Jerv-Hub has one additional fail-closed operator-host phase (#5699).** Only for exact repository
563
- `mutmutco/Jerv-Hub`, after its release-triggered `publish.yml` is proven green, the train runs
564
- `scripts/jerv-gateway-release-deploy.sh --tag <resolved-tag>`. That repository helper keeps root SSH in
565
- Jerv Vault custody, deploys the exact tagged commit to jerv-central, and requires systemd, `/livez`, and
566
- `/readyz` health. Its validated receipt is appended to `workflowRuns` as `jerv-gateway`; a missing helper,
567
- refusal, malformed receipt, unhealthy activation, or failed rollback sets `deployStatus: failure`. An
568
- unwatched/pending publish defers Gateway deployment and keeps `deployStatus: pending` — it never runs the
569
- host mutation before publication is proven. `release --resume --watch` re-derives the historical publish
570
- verdict and invokes the idempotent helper once it is green; it never re-tags or republishes the release.
571
- Do not move this phase into GitHub Actions: no supported Actions identity reaches personal jerv-central
572
- without moving root SSH custody out of its owning vault.
573
-
574
- `mmi-cli devops release --apply --json` returns the relevant run id/url data with `deployStatus`; `--watch` blocks
575
- on the run(s) and resolves `deployStatus` to `success`/`failure`. For tenant-container repos, that is the
576
- dispatched `tenant-deploy.yml` run. For Hub serverless, that is the auto-fired release `deploy.yml` and
577
- `publish.yml` workflow pair — watched on `mutmutco/MMI-Hub`. For `registry-publish`, that is the target
578
- repo's own release-triggered `publish.yml` run — watched on that repo, never dispatched, and never the
579
- central `tenant-publish.yml` (#2428: dispatching both is a guaranteed npm E409 double-publish, since the
580
- repo's own release-triggered publish always wins the version race).
581
- `promoted: true` stays set even on a failed deploy — promotion and deploy are separate outcomes.
582
-
583
- **`deployStatus: 'pending'` is NEVER a terminal, reportable state (#3322).** A release that returns
584
- `pending` has an UNRESOLVED deploy, not a clean one. The report now prints `deploy: UNVERIFIED` instead of
585
- omitting the deploy line entirely — which is exactly how a failed deploy once hid behind a "successful"
586
- release (Jerv-PowerTools v1.22.0: the release-triggered `publish.yml` went green, a separate `push: main`
587
- SAM deploy FAILED, and the train returned `deployStatus: pending`, so the run read as clean). Resolve it,
588
- or report it as unverified-RED. Never as success.
589
-
590
- **Read the `--apply` result's `workflowRuns` array — the train already enumerates every run on the release
591
- SHA (#3699).** That enumeration includes `push: main` infra deploys (`deploy.yml`, SAM) the deploy model does
592
- not name, and `deployStatus` is held at `pending` until they resolve. Any non-success entry — including
593
- `pending` — keeps the release unverified: watch those listed runs to conclusion:
594
- ```bash
595
- gh run watch <run-id> --repo {owner}/{repo} --exit-status
596
- ```
597
- The one remaining caveat: `--watch` blocks only on deploy-model-named runs, not extra push-triggered
598
- deploys — those you conclude from the `workflowRuns` list yourself. Any `failure` there is a failed release
599
- deploy even when the train reported success — flag it loudly and fix it before calling the release healthy.
600
-
601
- **Identify WHICH run is red before believing the wording (#5872).** The release pushes the tag and the
602
- branch seconds apart, so an ordinary repo CI gate lands on the release SHA too — it is enumerated
603
- alongside the deploy and publish runs, and a red one reads out as
604
- `deploy/publish on the release SHA FAILED`. On Jerv-Hub v3.4.4 that sentence appeared while `publish.yml`
605
- was green and npm was already serving 3.4.4: the red entry was a `gate` push run that had raced the
606
- shared runner's npm cache (the same SHA's tag-push gate passed, and a re-run passed). A red build gate
607
- **holds the release unverified but is not a publish failure** — and on Jerv-Hub it defers the fail-closed
608
- Gateway phase (#5699) until the gate is green, so it must be re-run and the release resumed, not
609
- re-tagged. Read the run NAMES in `workflowRuns` first:
610
-
611
- - `deploy.yml` / `publish.yml` / `tenant-deploy.yml` / `jerv-gateway` — a real deploy or publish verdict.
612
- - `gate` (or any repo CI workflow) on a `push` event — ordinary CI that happens to share the SHA. Re-run
613
- it; nothing about the published artifact is in question.
614
-
615
- **Hub releases announce to Slack (#883).** Hub scope is **only** `mutmutco/MMI-Hub`: the commits/PRs on
616
- `origin/main..origin/development`, this repo's `deploy.yml` + `publish.yml`, and Hub tooling (`mmi-cli`,
617
- skills, plugin, registry, central workflows). Do **not** read another repo's board (`mmi-cli oracle board` / `/mmi`
618
- on a product), watch or dispatch `ds-propagate.yml`, run `design-system` / `doctor` design-system heals for
619
- consumers, or treat a product's deploy state as part of this release — those belong to that product's own
620
- `/release` or `/rcand`, never a Hub train.
621
-
622
- Before running `--apply` for MMI-Hub, resolve the real tag first:
623
- `TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle)` — always print `$TAG` (e.g.
624
- `v2.43.0`) in summaries, Slack, chat, and the final report; never a placeholder like `vX.Y.0` or
625
- `v0.x.0`.
626
-
627
- **Write the announcement from the actual release evidence** — summarize the user-visible change, required action, and recovery path before posting. Key rules:
628
-
629
- - **Audience is the whole org** — no issue/PR numbers, repo names, commit SHAs, or internal tool jargon.
630
- - **Scale length and depth by release type** (major > minor > patch > hotfix): a major is a broad,
631
- migration-focused announcement; a minor is scannable grouped bullets; a patch is factual and minimal;
632
- a hotfix is incident-style (impact/status/workaround) with a concise durable note.
633
- - **Front-load**: one lead sentence stating the most important thing, then grouped bold sections;
634
- breaking changes and required actions get their own visible section.
635
- - **Benefits, active voice, addressed to the reader** — no hype, no internal identifiers.
636
-
637
- Write the curated summary to a fresh temp file (`f=$(mktemp tmp/release-summary.XXXXXX)`, so a stale prior
638
- summary is never reused) and point `--out` at an equally fresh result receipt
639
- (`r=$(mktemp tmp/release-receipt.XXXXXX)`, #5983). Source from **Hub PR titles only** (`origin/main..origin/development` on
640
- `mutmutco/MMI-Hub`), but **rewrite** each line in neutral Hub-subsystem terms (CLI, skills, plugin,
641
- workflows, registry, deploy hub) and to the release-type depth the guide demands — **never** a
642
- product or brand name (FoFu, Katip, etc.) anywhere in the summary file, Slack post, chat, or release
643
- report. Product names are allowed only when releasing **that product's repo**. Then pass the file through:
644
- `mmi-cli devops release --apply --announce-summary-file "$f" --json --out "$r"`. After the GitHub Release publishes, the CLI posts
645
- the summary to the org alerts channel as the MMI-Future Slack app (token + channel from SSM at run time).
646
- For a new MMI-Hub `--apply`, the CLI refuses before promotion when the file is missing, unreadable, or does
647
- not contain 3–6 non-empty lines; generated-note fallback is not an agent release path. `--resume` never
648
- requires or republishes a summary, and non-Hub repos skip the announcement automatically. After a valid
649
- summary is accepted, Slack delivery remains best-effort: a transport failure is reported in the result and
650
- never rolls back an otherwise completed release.
651
-
652
- **Write the result receipt before bounded monitoring (#5983).** A background-task harness that bounds its
653
- output retains only the tail of the apply run — exactly where the accepted announcement outcome
654
- (`announceNote`) can be dropped. With `--out <path>`, the CLI itself writes the full apply result to the
655
- file as BOM-free UTF-8 (never a shell `>` redirect — see #5802), so the receipt survives any monitor
656
- bound. `--resume` proves the Release and deploy but does not and should not re-announce, so this receipt
657
- is the only durable record of the accepted Slack delivery — verify it in Step 6.
658
-
659
- **Don't block on the deploy.** Start the watch as a background task and proceed to Steps 4b–5 (docs, project
660
- info, branch alignment) while prod deploys. The verdict is collected in Step 6 — verification is not
661
- skipped, only un-blocked. Deploy failure → report plainly, then **retry the existing promoted ref by deploy
662
- model** once the runtime is repaired — never a re-tag or republish (`main` is already at the release, which is
663
- correct). Read the model from the registry (`mmi-cli oracle org project get {owner}/{repo}` → `deployModel`):
664
- - **tenant-container** — the central tenant deployer owns the redeploy: `mmi-cli devops runtime tenant redeploy
665
- {owner}/{repo} main --watch`.
666
- - **repository-owned serverless / registry-publish** (e.g. Jerv-PowerTools' `Deploy Jerv Memory`) — there is
667
- no `runtime tenant redeploy` for these; re-run the repo's OWN declared `workflow_dispatch` deploy workflow at
668
- the promoted-equivalent ref: `gh workflow run <deploy-workflow> --repo {owner}/{repo} --ref <ref>`.
669
- - **hub-serverless** (MMI-Hub) — repair the named deploy failure, then rerun the failed deploy run. Do not re-tag.
670
-
671
- Name the sanctioned dispatch mechanism you used in the train report.
672
-
673
- **Cut over after hook changes.** If this release changes or retires plugin hooks, stop this seat here.
674
- Complete the host's declared session/workspace reload or process restart, resume the train in that fresh
675
- seat, and run read-only `mmi-hub status` before any Step 4b shell/git command.
676
-
677
- ## Step 4b — re-sync project info (no in-repo docs pass)
678
-
679
- In-repo README/architecture freshness is **not** a train step (Step 0d retired, Hub#4164).
680
- `mmi-cli devops release --apply` re-syncs the GitHub **Project** short description + thin README from the
681
- released commit and the registry member list through the command ladder. For a manual repair or preview use:
682
- ```bash
683
- mmi-cli oracle org project sync-info --apply # omit --apply for the read-only plan
684
- ```
685
-
686
- ## Step 5 — roll development forward
687
-
688
- - Keep branches aligned: `mmi-cli devops release --apply` back-merges the released `main` (incl. the version
689
- fold) into `development` and reports it as `devRollForward`. When `development` has no required checks it
690
- pushes directly (`status: pushed`). When `development` *requires* checks (e.g. MMI-Hub needs
691
- `cli`/`infra`/`docs`), the fresh merge commit carries no passing checks, so a direct push is structurally
692
- rejected — the train instead **opens an alignment PR** `main → development` (`status: pr-pending`) and the
693
- release report prints the exact land command. The release itself has already shipped; **land the alignment
694
- PR with a true merge** — `mmi-cli devops pr merge <number> --auto --merge` (never squash — a squash drops the merge
695
- parentage and the misalignment guard re-flags the divergence). `--auto` is what makes it land right after a
696
- release: the alignment PR's own `cli`/`infra`/`docs` checks are still running, so a plain immediate merge is
697
- policy-blocked — `--auto` merges once they pass. If you await with `--wait` instead, still pass `--merge`
698
- (`mmi-cli devops pr merge <number> --wait --merge`); `--wait` alone must not land the alignment PR. Never force.
699
- A pre-#5903 CLI can true-merge this PR and still exit 1 with `mergeStatus: partial-cleanup` after GitHub
700
- refuses to delete its protected `main` head. Classify that legacy receipt from the live PR: `MERGED` with
701
- merge method `MERGE` means alignment succeeded and protected-main retention is expected. Never delete
702
- `main`, retry the release, or call the alignment failed. Current CLIs report the same retention as
703
- `remoteBranch.status: retained-protected` with `mergeStatus: merged`.
704
- - Full-track repos: `mmi-cli devops release --apply` already aligned `rc` to the released `main` (#1036 — the
705
- push runs inside the authority-gated train; the result reports it as `rcAlignment`). No manual `rc`
706
- push — if the result reports a failed alignment, investigate and rerun via the train, never bare-push.
707
- - **Local branches never left stale (#2582).** A release advances `main`/`development`/`rc` on **origin**;
708
- `mmi-cli devops release --apply` also fast-forwards the LOCAL train branches to match, so the checkout you
709
- released from does not lag what you just pushed. The train restores + fast-forwards the start branch
710
- (`checkout`, #2340) and fast-forwards the others (`localSync`) — non-destructively: only a true
711
- fast-forward moves a branch; a diverged local branch is reported and left untouched (never a force).
712
- The one branch it **cannot** sync at release time is a check-gated `development` whose roll-forward is a
713
- `pr-pending` alignment PR (Hub) — that PR merges on origin *after* the release returns. Once that PR
714
- lands, fast-forward the local branch yourself (`git pull --ff-only` on `development`) — #5908 retired
715
- the session-start self-heal along with the rest of the hook-era plumbing. Nothing here ever blocks or
716
- fails the release.
717
- - **Read the `checkout:` clause — it does not always say "returned" (#4006).** The report always prints
718
- one, and restoration is deliberately skipped rather than forced when the repo is not in a fit state.
719
- A release leaves the repo on `main` until the train moves it back, so a non-`returned` outcome means
720
- you are still there and must return by hand once the cause is cleared:
721
- - `checkout restoration skipped: working tree changed after release` — the tree went dirty DURING the
722
- train, so the train declined to move you. Diagnose those paths with the Step 0 clean-tree rules,
723
- then `git checkout <start branch>` yourself. The release itself already shipped; this is a
724
- deliberate, harmless decline, not a failed release.
725
- - `checkout restoration failed while returning to …` / `origin/<branch> fast-forward failed` — the
726
- checkout or the pull errored. Read the appended git message; the release is unaffected.
727
-
728
- `--resume` uses the same `checkout:` clause after every branch-mutating terminal path. Direct-track
729
- resumes return to `development`; full-track resumes return to `rc`. A skipped restoration names the
730
- dirty tree, checkout failure, or fast-forward failure and still leaves the shipped release intact.
731
-
732
- ## Step 6 — collect deploy verdict + report
733
-
734
- Verify the Slack announcement from the receipt, not from memory (#5983). The `--out` receipt written at
735
- Step 4 is the durable record of the initial apply result; read its `announceNote` field before the final
736
- report and state the delivery verdict (`announced`, `skipped`, or the failure note). A monitor that kept
737
- only the apply output's tail may have dropped that line — the receipt is what keeps that from losing the
738
- outcome.
739
-
740
- The `release --apply` exit code is **not** the release verdict. A nonzero exit with
741
- `releaseStatus=succeeded` and `followUpStatus=pending` is the unresolved-follow-up case from Step 3 —
742
- the release shipped; the runs enumerated in `workflowRuns` are the outstanding verdict, so watch them
743
- to conclusion here rather than re-running or alarming. Report the live release verdict from
744
- the four checks that matter: the `main..development` count, the tag on `origin`, the bounded Latest
745
- Release read, and the runs on the release SHA; an alignment PR is normal follow-up work when those
746
- checks confirm the release shipped.
747
-
748
- Use only this bounded Latest Release read:
749
59
  ```bash
750
60
  gh api repos/{owner}/{repo}/releases/latest --jq '{tagName:.tag_name,targetCommitish:.target_commitish,publishedAt:.published_at,url:.html_url}'
751
61
  ```
752
- Require its `tagName` to equal the expected `$TAG`; a mismatch means Latest does not identify this
753
- release and is unverified.
754
62
 
755
- **`gh release view` has no `isLatest` field (#6019).** Never request Latest status through
756
- `gh release view`: the field is not in its JSON schema, so gh rejects the whole read and lists its
757
- supported fields — the check errors instead of answering, and Latest stays unverified. This bounded
758
- read is the supported Latest probe; `gh release list --json <fields>,isLatest` is the supported form
759
- only when a list receipt is genuinely the right shape.
63
+ Four facts close the verdict: the `origin/main..origin/development` count, the tag on origin, Latest, and green runs on the release SHA.
760
64
 
761
- Collect the backgrounded prod-deploy watch from Step 4 (it has typically finished by now). Confirm prod is
762
- healthy (the central deploy workflow smoke step / a health check); **red** → report the failure prominently and flag
763
- that the release shipped on a failed deploy (re-run just the deploy — `main` is already correct).
65
+ ## Recovery
764
66
 
765
- **Say which run is red, never just "deploy/publish failed" (#5872).** `--resume` reports a single
766
- `deployStatus` for every run on the release SHA, so a flaky repo CI gate that shares that SHA is reported
767
- in the language of a failed publication — the operator is told publication failed while npm is serving
768
- the version. Before writing that into a report, name the workflow: a red `deploy.yml` / `publish.yml` /
769
- `tenant-deploy.yml` / `jerv-gateway` is a real deploy or publish failure; a red `gate` on a `push` event
770
- is ordinary CI. Both hold the release unverified and both are fixed by re-running that run and resuming —
771
- neither is ever fixed by re-tagging — but only the first means anything about the artifact.
67
+ Every refusal and every doctor blocker names its `code` and `anchor`: open `docs/Guides/train-troubleshooting.md#<code>` and follow that entry, nothing else. A partial release (tag on origin, later phases pending or failed) continues with `mmi-cli devops release --resume --watch --json --out <fresh-receipt>` — the only continuation form: `--resume` never re-tags, republishes or re-announces, and `--apply` belongs to a new cut only. A refused train is a stop (Merge floor): fix the cause on `development` through a normal PR, then rerun.
772
68
 
773
- **This distribution verifier is Hub-only.** It assumes the Hub release fold has already committed its
774
- generated artifacts. Non-Hub `registry-publish` repositories must not run `release-distribution.mjs
775
- verify`: use their own release-workflow evidence and the publish-visibility contract below.
69
+ ## Merge floor
776
70
 
777
- Hub releases always carry a distribution bump (the Step 1b fold), so the **publish workflow**
778
- (`publish.yml`) ships every registry-declared public npm artifact on the GitHub Release from Step 4
779
- don't publish by hand.
780
- Watch the release-triggered `publish.yml` receipt with the same structured PowerShell probe above
781
- (`--workflow publish.yml --event release`), then confirm npm caught up:
782
- ```powershell
783
- $runs = gh run list --workflow publish.yml --event release --limit 10 --json databaseId,createdAt,url,status,conclusion | ConvertFrom-Json
784
- $run = $runs |
785
- Where-Object { $_.databaseId -and $_.createdAt } |
786
- Sort-Object { [DateTimeOffset]$_.createdAt } -Descending |
787
- Select-Object -First 1
788
- if (-not $run) { throw 'release-triggered publish workflow run was not found' }
789
- $run | Select-Object databaseId,createdAt,url,status,conclusion | ConvertTo-Json -Compress
790
- gh run watch $run.databaseId --exit-status
791
- node scripts/release-distribution.mjs verify "X.Y.0" # bare semver — asserts registry versions, BOM identities, and published npm artifacts
792
- ```
793
- **verify takes bare semver (#5379).** Pass `X.Y.Z`, not the git-tag form `vX.Y.Z`. Checkout still
794
- uses the tag (`git checkout "vX.Y.0"`); the distribution script's version argument is strict semver
795
- without a leading `v`.
796
- Run that verify from a checkout **at the tag** (`main` right after the release merge, `git checkout
797
- "vX.Y.0"`, or a worktree at the tag) — verify checks the working tree and refuses any other commit once
798
- the tag exists, so a stale `development`/`rc` checkout can't masquerade as a broken release.
71
+ - **Never squash-merge a tagged commit (#3167).** A squash re-mints the SHA and orphans the tag from the branch lineage, so every tag-anchored check mis-reads forever after. Any merge that carries an already-tagged commit — the `main → development` roll-forward, an alignment PR, a hotfix fold PR — lands as a true merge: `mmi-cli devops pr merge <number> --auto --merge` (or `--wait --merge`); `--wait` alone still squashes. The hotfix main-base PR itself may squash: its tag lands after, on the merged `main` HEAD, and the `-x` trailer must survive the squash message.
72
+ - **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt doctor blocker, authority probe, coverage guard, untolerated conflict, required checks, stray tag, ledger — ends the run. Fix the cause on `development` through a normal PR and rerun the train; never hand-resolve on `main`, hand-cherry-pick, bare-push a train branch, ack to save time, or bypass a check.
73
+ - **Version probes anchor on the latest Release tag, not a branch (#2734).** A branch manifest or a stale local tag is not a version source; a surviving local tag silently mints the next version.
74
+ - **One train at a time on a repo (#5380).** Two overlapping runs race the shared tag namespace; the ledger refuses the second never start one to help the first.
75
+ - **Tag and Release deletion are deletion-class:** the authorized human's explicit per-turn go, named in the guide entry that prescribes it.
799
76
 
800
- **Build the tag checkout before verifying, or it refuses deterministically (#5814).** `verify` (without
801
- `--skip-npm-view`) runs a live `npm pack` per public surface, and `npm pack` refuses a package whose
802
- declared `bin` target is absent — e.g. `updater publishes bin jerv-hub -> dist/index.cjs, which npm pack
803
- does not ship — run npm run build`. Whether that bites depends on the repo: a build output that is
804
- **tracked** (MMI-Hub commits `updater/dist/index.cjs`) is present in a clean tag checkout, one that is
805
- **gitignored** is not, and a fresh clone has no `node_modules` either way. So provision the tag worktree
806
- first, and check the declared bin targets rather than assuming:
807
- ```bash
808
- git worktree add "$PROOF" "vX.Y.0" # $PROOF outside the repo — see the scratch note below
809
- cd "$PROOF" && npm ci && npm run build # build only if a declared bin target is missing from the checkout
810
- node scripts/release-distribution.mjs verify "X.Y.0"
811
- ```
812
- Removing that proof tree afterwards: `npm ci` writes `node_modules`, which `git status --porcelain`
813
- never shows and which makes `git worktree remove` refuse with `Directory not empty`. Delete
814
- `node_modules` first, then remove the worktree. On Windows a global install made from a worktree leaves
815
- a junction there, so delete the directory rather than following it. In a guarded agent shell pass the
816
- **literal absolute** proof path to `git worktree remove` — the checkout guard refuses a variable-held
817
- target (`Blocked git worktree remove with an unanalyzable variable-held target`, #2492 / #6060); only an
818
- interactive shell may pass `"$PROOF"`.
819
- Release-blocking for Hub tooling changes: every public artifact must match the registry version and BOM
820
- identity. Manual fallback if CI can't publish: `node scripts/release-distribution.mjs publish "X.Y.0"`
821
- from a machine with npm auth.
77
+ ## Report
822
78
 
823
- **Windows WebAuthn/passkey exception (#5920).** Normal publishing remains non-interactive and token-based;
824
- headless and CI hosts keep using automation tokens. When any npm write requires a human desktop passkey on
825
- Windows, **never run it from the agent shell**. Write the package loop to a durable `.cmd` outside disposable
826
- worktrees (for example, `C:\Users\<you>\Documents\mmi-npm-write.cmd`):
827
- ```bat
828
- @echo off
829
- set "failures=%~dpn0.failures.txt"
830
- del "%failures%" 2>nul
831
- for %%P in (@scope/package-a @scope/package-b) do (
832
- call npm deprecate "%%P@*" "replacement message"
833
- if errorlevel 1 >>"%failures%" echo %%P
834
- )
835
- if exist "%failures%" type "%failures%"
836
- ```
837
- Launch it visibly and leave the window open, then let the human tap once for each npm invocation:
838
- ```powershell
839
- Start-Process cmd -ArgumentList '/k', 'C:\Users\<you>\Documents\mmi-npm-write.cmd'
840
- ```
841
- Ignore any EOTP browser URL npm prints; it does not complete this passkey flow. Treat the failure file as the
842
- per-package receipt and verify the corresponding registry state, for example
843
- `npm view <pkg> deprecated --prefer-online` after `npm deprecate`.
79
+ Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), and every workflow run row with its conclusion and evidence — use its URL when emitted, otherwise the receipt's explicit no-run-URL note — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
844
80
 
845
- **npm tarball identity is packer-sensitive (#5366 / #5251).** `verify` without `--skip-npm-view`
846
- compares the registry's served `dist.integrity` (SSRI) against a live `npm pack` on THIS machine
847
- (`packedIntegrityBySurface`). A tarball SSRI covers file modes and packer (npm version) behavior, so
848
- that identity step is authoritative **only** when run in the publish environment (`publish.yml` on the
849
- self-hosted Linux runner) — or on an operator machine with the **same OS and npm major** as that
850
- runner. Elsewhere (Windows, a clean Linux container with a different packer, a mismatched npm major) a
851
- `tarball identity mismatch` can false-fail even when the published bytes match the tag: three different
852
- SSRIs (registry / Windows pack / container pack) with content-identical unpacked trees is the expected
853
- cross-environment shape for surfaces with no publish-time staged payload, not a substituted artifact.
854
- Off the publish environment, either:
855
- - fall back to an **unpacked-content** proof — download the registry tarball (`npm pack <pkg>@X.Y.Z`
856
- from the registry, or `npm view … dist.tarball`), `npm pack` from the tag checkout, unpack both, and
857
- compare the trees (do not alarm on SSRI alone). On Windows unpack with the native
858
- `%SystemRoot%\System32\tar.exe` named explicitly — a bare `tar.exe` can resolve to Git's GNU tar
859
- through PATH, which reads a drive-letter path as a remote host (`Cannot connect to C: resolve
860
- failed`, #6059) — and run `npm pack` from the package directory (`cd` there; no positional path
861
- and no `--pack-destination` under a path with spaces), since routing npm through `cmd.exe /c`
862
- reparses quoted paths (#6058). Hub host-plugin npm surfaces stage the canonical
863
- `skills/` tree and declared hook-policy `scripts/` into the package at publish time via
864
- `assembleDeclaredPayloads` (#5471); those paths are gitignored and absent from a plain tag checkout,
865
- so a local `npm pack` without staging cannot reproduce them and an empty `diff -r` is an unreachable
866
- bar (#5677), not evidence of substitution. For those surfaces, prove identity in two parts:
867
- 1. **Committed package content** — `diff -r` the unpacked trees while excluding each publish-time
868
- staged path relative to the package root (for `@mutmutco/claude-plugin`:
869
- `diff -r -x skills -x scripts …`; list all staged targets with
870
- `node --input-type=module -e "import { generatedPayloadPaths } from './scripts/distribution-assembly.mjs'; console.log(generatedPayloadPaths().join('\\n'))"` and strip each surface's `assembly.rootPath` prefix). Empty diff ⇒ the committed package matches.
871
- 2. **Staged payload** — for each excluded path, compare the registry unpacked copy against the tag
872
- checkout repo source named in that surface's `surfaces.json` assembly sync (`directories` sync:
873
- `diff -r reg/package/skills skills/`; `files` sync: `cmp` each `include` entry against
874
- `<sourcePath>/<name>`). Alternatively, run
875
- `node --input-type=module -e "import { assembleDeclaredPayloads } from './scripts/distribution-assembly.mjs'; assembleDeclaredPayloads()"` on the tag checkout before the local `npm pack` so a plain `diff -r` compares the full publish payload.
876
- Surfaces with no publish-time staged payload still use a plain `diff -r` (empty diff ⇒ identity OK), or
877
- - skip the registry identity read with
878
- `node scripts/release-distribution.mjs verify "X.Y.0" --skip-npm-view` and verify published content
879
- by hand the same way (including the staged-payload split above when applicable).
81
+ ## Retro
880
82
 
881
- **Unpacked proof scratch must not pollute the checkout (#5702).** npm unpack copies full package trees
882
- including staged `skills/` into whatever directory you choose. Those copies are **not** registered skill
883
- payloads — if they remain anywhere scannable under the repo root when the next release fold runs
884
- `check-skill-payload`, the gate fails with `unregistered authored or copied skills`. Prefer unpacking
885
- **outside** the repository:
886
- ```bash
887
- PROOF=$(mktemp -d "${TMPDIR:-/tmp}/mmi-release-proof.XXXXXX")
888
- # unpack both tarballs under $PROOF, diff -r, then:
889
- rm -rf "$PROOF"
890
- ```
891
- If you must unpack inside the checkout, use the org-managed gitignored scratch at repo-root `tmp/`
892
- (`mktemp -d tmp/mmi-release-proof.XXXXXX`) — **not** `.jerv/tmp/` (agent session scratch still lives on
893
- disk and must not carry npm proof trees between releases). Remove the proof tree **immediately** after the
894
- diff succeeds; do not leave it for the next `--apply`.
83
+ Before the final report, answer one question honestly: did this skill's own instructions misfire this run — ambiguous wording, a misleading message, or an environment failure it should have warned about? Process only, never the user's code or task. If yes, file one lesson and move on; a clean run is silent (hard cap: one per run). It lands on the Hub board, deduplicated, and is fixed only through a reviewed PR — never edit the skill live. The retro is advisory: if the call fails, note it and continue.
895
84
 
896
- Before the next `release --apply`, confirm no leftover proof copies remain outside org scratch:
897
85
  ```bash
898
- node scripts/check-skill-payload.mjs --contract
86
+ mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"
899
87
  ```
900
- A failure naming `unregistered authored or copied skills` means a proof tree (or other rogue copy) is
901
- still on disk — delete it and rerun the contract check before retrying the train.
902
-
903
- Do not treat an off-runner SSRI mismatch as a broken release, and do not change how the runner publishes
904
- to paper over a local packer difference.
905
-
906
- **Private packages (#2405) — never trust a bare `npm view` to mean "not published".** An unauthenticated
907
- `npm view <pkg>` returns a hard 404 on any package published with `publishConfig.access=restricted`
908
- (private) — indistinguishable from "not published" — because npm masks private packages from anonymous
909
- reads. Before verifying ANY npm publish (Hub's own, a `registry-publish` product repo's own `publish.yml`,
910
- or a `publishRequired` product repo's companion `tenant-publish.yml`), check that surface's
911
- `publishVisibility` in the target repo's `surfaces.json`
912
- (`node -e "console.log(require('./surfaces.json').surfaces.find(s=>s.id==='<id>').publishVisibility)"`, or
913
- the repo-agnostic reader `scripts/release-distribution.mjs` exports as `npmVerifyModeFor`):
914
- - `public` → an unauthenticated `npm view <pkg>@<version> version` is a trustworthy verify signal.
915
- - `private` → do **not** run a bare `npm view`. Verify from an authenticated source instead: the
916
- release-triggered run's conclusion (`gh run watch … --exit-status` — the repo's own `publish.yml` for
917
- `registry-publish`, #2428; the central `tenant-publish.yml` for a `publishRequired` companion publish) and
918
- its logged `+ pkg@ver` / `"already on npm — nothing to do"` / `"already on npm ... treating as idempotent
919
- success"` output (`gh run view <id> --log`), or `npm view` with a registry auth token in env. A 404 here is
920
- "unverifiable from here", never "not published" — report it as such, don't block or alarm on it.
921
- - `n/a` → nothing publishes to npm for that surface; skip.
922
- Hub's registry-declared npm surfaces are public today, so `release-distribution.mjs verify` uses a bare
923
- `npm view` for them — this changes only if a package's declared visibility changes.
924
-
925
- **tenant-publish.yml E409 belt-and-braces (#2428).** The central `tenant-publish.yml` lane (the
926
- `publishRequired` companion publish only — never dispatched for `registry-publish`, see above) already
927
- treats a lost `npm publish` race as idempotent inside the run (a re-check `npm view` after a failed publish;
928
- see the workflow file). `mmi-cli devops release --apply --watch` mirrors that at the CLI layer: a watched
929
- `tenant-publish.yml` run that concludes `failure` is re-checked against that SAME run's own log (bounded
930
- retries, visibility-agnostic — never a bare `npm view`) for the idempotent-success marker before the CLI
931
- reports the publish as failed. A run whose log never shows the version landed stays a loud failure.
932
-
933
- Report: Release `$TAG` (the resolved tag from Step 2 — never a placeholder) + the GitHub Release URL · prod
934
- deploy run + URL + **green/red** · branch-alignment note · npm publish run + CLI version (Hub releases).
935
- For Jerv-Hub, also report the `jerv-gateway` exact-commit deployment and health verdict; npm green without
936
- Gateway green is a failed follow-up, never a complete Jerv-Hub release.
937
-
938
- ## Notes
939
-
940
- - `/hotfix` stays the cherry-pick promotion lane (a hotfix always skips rc — it cherry-picks
941
- `development → main` directly), but a `/release patch` from development is a legitimate ordinary
942
- release (#4929). Never force-push `main`.
943
- - **`MMI_BUMP_INTENT=major|minor|patch` comes from the bare `/release` argument; unstated it defaults to
944
- `patch`** (#4929). Export it for the whole run, not a one-off prefix on `next-version.mjs cycle`:
945
- `mmi-cli devops release --apply` (and `rcand --apply`) resolve intent themselves **before any merge,
946
- tag, or fold** — direct-track included — defaulting to `patch` when the env is absent and failing closed
947
- there with `MMI_BUMP_INTENT must be one of major|minor|patch` on an invalid value, so the tree stays
948
- clean on the start branch. `MMI_RELEASE_VERSION`
949
- below is a different knob: it overrides the computed version; it does not replace the intent export
950
- unless you set that exact-target env instead.
951
- - **`--dev` (full-track only):** releases `development → main` skipping rc, with the same fold/tag/Release/
952
- deploy machinery plus the post-release rc retirement and rc alignment. Refuses (fail closed) when
953
- `origin/rc` carries commits not in `development`; on direct-track repos it's a no-op (they already
954
- release from `development`).
955
- - **MAJOR / exact-target release:** to ship a version the tag math can't derive (a MAJOR like `2.0.0`, or
956
- skipping a version already on npm), product repos export `MMI_RELEASE_VERSION=X.Y.Z` for **both** `/rcand`
957
- and `/release`; MMI-Hub exports it for `/release` only. Steps 1b/2 then fold and tag exactly that
958
- version. Unset, the train uses the declared `MMI_BUMP_INTENT` increment, defaulting to `patch` when
959
- neither the argument nor the env declares one (#4929).
960
-
961
- ## Retro — one check before you finish
962
- Before your final report, answer one question honestly: did **this skill's own instructions** misfire
963
- this run — ambiguous wording, a misleading message, or an environment failure it should have warned
964
- about? (Process only — never the user's code or task; e.g. a misleading authority or gate message, or an
965
- ambiguous version-fold or back-merge step.) If yes, file **one** lesson and move on; a clean run is silent
966
- (hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR — never
967
- edit the skill live; the retro is advisory, so if the call fails, note it and continue:
968
- `mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"`