@mutmutco/codex-plugin 4.6.26 → 4.6.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.6.26",
3
+ "version": "4.6.27",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -28179,10 +28179,10 @@ var rollout_plan_default = {
28179
28179
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
28180
28180
  },
28181
28181
  baseline: {
28182
- version: "4.6.26",
28183
- tag: "v4.6.26",
28184
- commit: "bab0b3157d41",
28185
- npm: "@mutmutco/cli@4.6.26"
28182
+ version: "4.6.27",
28183
+ tag: "v4.6.27",
28184
+ commit: "b83c0c39cb1f",
28185
+ npm: "@mutmutco/cli@4.6.27"
28186
28186
  },
28187
28187
  exitCriterion: "fleet-n-of-n",
28188
28188
  hubOnlyShortcut: "forbidden",
@@ -28199,14 +28199,14 @@ var rollout_plan_default = {
28199
28199
  repo: "mutmutco/mmi-hub",
28200
28200
  role: "canary",
28201
28201
  schedule: "train",
28202
- v3Target: "v4.6.26"
28202
+ v3Target: "v4.6.27"
28203
28203
  }
28204
28204
  ],
28205
28205
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
28206
28206
  rollback: {
28207
28207
  independent: true,
28208
- mechanism: "npm dist-tag latest -> 4.6.26 and redeploy the Hub Lambda from tag v4.6.26 (bab0b3157d41); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28209
- v3Target: "v4.6.26 (@mutmutco/cli@4.6.26, tag commit bab0b3157d41 \u2014 last known-good release carrying the repo-index v4-only contract)"
28208
+ mechanism: "npm dist-tag latest -> 4.6.27 and redeploy the Hub Lambda from tag v4.6.27 (b83c0c39cb1f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28209
+ v3Target: "v4.6.27 (@mutmutco/cli@4.6.27, tag commit b83c0c39cb1f \u2014 last known-good release carrying the repo-index v4-only contract)"
28210
28210
  }
28211
28211
  },
28212
28212
  {
@@ -43931,6 +43931,20 @@ ${list}
43931
43931
  Co-authored-by: GitHub <noreply@github.com>
43932
43932
  `;
43933
43933
  }
43934
+ function checkQueueSquashOverride(title, pr, messages, root) {
43935
+ if (messages.length <= 1 || !messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
43936
+ ${message2.body}`)) || readMessageOverride2(githubQueueSquashMessage(title, pr, messages), root)) return void 0;
43937
+ return {
43938
+ mergeStatus: "failed",
43939
+ preparation: {
43940
+ status: "preparation-needed",
43941
+ reason: "queue-message-override",
43942
+ issueTargets: [],
43943
+ nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. First, add one commit on this same branch whose message ends with a trailer block carrying the Test-Policy-Override waiver, push (no force-push), then rerun pr land on the same PR. This makes the waiver the final trailer block of the LAST commit. As a fallback, put the change in ONE commit whose message ends with a trailer block carrying the waiver: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one, then rerun pr land. Keep repository settings unchanged."
43944
+ },
43945
+ error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
43946
+ };
43947
+ }
43934
43948
  async function inspectQueuePreparation(pr, repo, options) {
43935
43949
  const read = options.read ?? readApi;
43936
43950
  const queueRead = options.read ?? actorReadApi;
@@ -44044,21 +44058,9 @@ ${pull.body}`;
44044
44058
  ])];
44045
44059
  return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\` remedy.`) };
44046
44060
  }
44047
- if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && messages.length > 1 && options.bodyText == null && messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
44048
- ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
44049
- return {
44050
- kind: "refused",
44051
- result: {
44052
- mergeStatus: "failed",
44053
- preparation: {
44054
- status: "preparation-needed",
44055
- reason: "queue-message-override",
44056
- issueTargets: [],
44057
- nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
44058
- },
44059
- error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
44060
- }
44061
- };
44061
+ if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && options.bodyText == null) {
44062
+ const refusal = checkQueueSquashOverride(pull.title, pr, messages, process.cwd());
44063
+ if (refusal) return { kind: "refused", result: refusal };
44062
44064
  }
44063
44065
  const requiredBody = options.bodyText ?? composedBody;
44064
44066
  const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
@@ -47947,6 +47949,16 @@ function evaluateFilelessComposeGuard(input) {
47947
47949
  }
47948
47950
  return { ok: true };
47949
47951
  }
47952
+ function positionalOrRepoFlagTarget(commandName, positional, repoFlag) {
47953
+ const pos = positional?.trim() || void 0;
47954
+ const flag = repoFlag?.trim() || void 0;
47955
+ if (!pos || !flag) return pos ?? flag;
47956
+ const [a, b] = [pos.toLowerCase(), flag.toLowerCase()];
47957
+ const tail = (s) => s.split("/").pop();
47958
+ const same2 = a === b || (!a.includes("/") || !b.includes("/")) && tail(a) === tail(b);
47959
+ if (!same2) throw new Error(`${commandName}: positional target ${pos} and --repo ${flag} name different repos; pass one of them`);
47960
+ return pos;
47961
+ }
47950
47962
  function requireProjectTarget(commandName, explicitTarget, currentRepo) {
47951
47963
  const target = explicitTarget?.trim() || currentRepo?.trim();
47952
47964
  if (!target) {
@@ -56273,6 +56285,7 @@ function githubCustodyStore(repo, baseSha, api = defaultGitHubClient()) {
56273
56285
  }
56274
56286
  };
56275
56287
  }
56288
+ var DISPATCH_ABSENT_AFTER_MS = 18e4;
56276
56289
  var ReleaseCustodySession = class _ReleaseCustodySession {
56277
56290
  constructor(store, sha, state) {
56278
56291
  this.store = store;
@@ -56371,6 +56384,13 @@ var ReleaseCustodySession = class _ReleaseCustodySession {
56371
56384
  async reconcile(observer) {
56372
56385
  for (const effect of Object.values(this.state.effects).filter((e) => e.state === "intent")) {
56373
56386
  const observation = await observer(effect);
56387
+ if (!observation.observed && observation.absent) {
56388
+ await this.change((state) => {
56389
+ const { [effect.id]: _dropped, ...effects } = state.effects;
56390
+ return { ...state, effects };
56391
+ });
56392
+ continue;
56393
+ }
56374
56394
  if (!observation.observed) throw new ReleaseCustodyError(`effect ${effect.kind} (${effect.id}) has no proven outcome; observe it before retrying`);
56375
56395
  await this.change((state) => ({ ...state, effects: {
56376
56396
  ...state.effects,
@@ -56541,7 +56561,8 @@ function effectObserver(api) {
56541
56561
  const fields = i.fields;
56542
56562
  const title = effect.kind === "tenant-deploy" ? `tenant-deploy ${i.slug} ${i.stage} @ ${i.ref}` : workflow === "get-publish.yml" ? `get-publish (${fields.product} ${fields.mode || "full"})` : workflow === "tenant-deploy.yml" ? `tenant-deploy ${fields.slug} ${fields.stage} @ ${fields.ref}` : null;
56543
56563
  const matches = runs2.workflow_runs.filter((run) => title && run.display_title === title && run.event === "workflow_dispatch" && run.head_sha === i.sha && !i.beforeRunIds?.includes(run.id) && Date.parse(run.created_at) >= Date.parse(effect.startedAt) - 1e3 && Date.parse(run.created_at) <= Date.parse(effect.startedAt) + 12e4);
56544
- return matches.length === 1 ? { observed: true, result: effect.kind === "tenant-deploy" ? null : "" } : { observed: false };
56564
+ if (matches.length === 1) return { observed: true, result: effect.kind === "tenant-deploy" ? null : "" };
56565
+ return matches.length === 0 && Date.now() > Date.parse(effect.startedAt) + DISPATCH_ABSENT_AFTER_MS ? { observed: false, absent: true } : { observed: false };
56545
56566
  }
56546
56567
  return { observed: false };
56547
56568
  };
@@ -60828,6 +60849,25 @@ function createPreparationDeps(input, io = {}) {
60828
60849
  if (!await docs.refResolves(candidate.root, candidate.headSha) || !await docs.refResolves(candidate.root, candidate.baseSha)) {
60829
60850
  return result("skipped", "exact remote candidate objects are not available locally; committed test policy was not checked");
60830
60851
  }
60852
+ const log = (await exec3("git", ["-C", candidate.root, "log", "--reverse", "--format=%B%x00", `${candidate.baseSha}..${candidate.headSha}`], { timeout: GIT_TIMEOUT_MS })).stdout;
60853
+ const messages = log.split("\0").filter((message2) => message2.trim()).map((message2) => {
60854
+ const [headline, ...body] = message2.trim().split("\n");
60855
+ return { headline, body: body.join("\n") };
60856
+ });
60857
+ const refusal = checkQueueSquashOverride("Candidate PR", "0", messages, candidate.root);
60858
+ if (refusal) {
60859
+ const [owner, name] = candidate.repo.split("/");
60860
+ const metadata = await client.graphql(
60861
+ "query($owner:String!,$name:String!,$base:String!) { repository(owner:$owner,name:$name) { squashMergeCommitMessage mergeQueue(branch:$base) { configuration { mergeMethod } } } }",
60862
+ { owner, name, base: candidate.base }
60863
+ );
60864
+ const rules = await client.restPaginate(`repos/${candidate.repo}/rules/branches/${encodeURIComponent(candidate.base)}?per_page=100`);
60865
+ const methods = [...new Set(rules.filter((rule) => rule.type === "merge_queue").map((rule) => rule.parameters?.merge_method))];
60866
+ const method = methods.length === 1 ? methods[0] : !methods.length ? metadata.repository.mergeQueue?.configuration?.mergeMethod : void 0;
60867
+ if (method === "SQUASH" && metadata.repository.squashMergeCommitMessage === "COMMIT_MESSAGES") {
60868
+ return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
60869
+ }
60870
+ }
60831
60871
  const policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
60832
60872
  return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
60833
60873
  }
@@ -73549,13 +73589,13 @@ project.command("sync-info [owner/repo]").description("synchronize the owning Gi
73549
73589
  }
73550
73590
  });
73551
73591
  async function runProjectDeployGet(repoOrSlug, o) {
73552
- const cfg = await loadConfig();
73553
73592
  let target;
73554
73593
  try {
73555
- target = await projectTarget("org project deploy get", repoOrSlug);
73594
+ target = await projectTarget("org project deploy get", positionalOrRepoFlagTarget("org project deploy get", repoOrSlug, o.repo));
73556
73595
  } catch (e) {
73557
73596
  return fail(e.message);
73558
73597
  }
73598
+ const cfg = await loadConfig();
73559
73599
  const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
73560
73600
  if (!out) return failGraceful(`org project deploy get: Hub deploy facts read failed for ${target}`);
73561
73601
  const stage = o.stage?.trim();
@@ -73563,8 +73603,12 @@ async function runProjectDeployGet(repoOrSlug, o) {
73563
73603
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
73564
73604
  console.log(JSON.stringify(payload, null, 2));
73565
73605
  }
73566
- var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)").argument("[ownerRepo]", "shorthand for `get <owner/repo>`").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (ownerRepo, o) => runProjectDeployGet(ownerRepo, o));
73567
- projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => runProjectDeployGet(repoOrSlug, { stage: projectDeploy.opts().stage, json: o.json }));
73606
+ var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)").argument("[ownerRepo]", "shorthand for `get <owner/repo>`").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (ownerRepo, o) => runProjectDeployGet(ownerRepo, o));
73607
+ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => runProjectDeployGet(repoOrSlug, {
73608
+ stage: projectDeploy.opts().stage,
73609
+ json: o.json,
73610
+ repo: projectDeploy.opts().repo ?? o.repo
73611
+ }));
73568
73612
  projectDeploy.command("doctor").description("read-only estate port scan; --box lists its DEPLOY# owners and suggests free 8xxx ports (master-only)").option("--box <name>", "resolve box from the live Hetzner inventory and inspect its ports").option("--json", "machine-readable output").action(async (o) => {
73569
73613
  const cfg = await loadConfig();
73570
73614
  const report = await fetchDeployPortCollisions(registryClientDeps(cfg));
@@ -73593,13 +73637,14 @@ projectDeploy.command("doctor").description("read-only estate port scan; --box l
73593
73637
  if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
73594
73638
  if (!report.ok && !report.error) process.exitCode = 1;
73595
73639
  });
73596
- projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--decommissioned", "main only (#8148): retire a tenant-container main row after `runtime tenant control <repo> main retire`; the box must show main gone").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
73640
+ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--decommissioned", "main only (#8148): retire a tenant-container main row after `runtime tenant control <repo> main retire`; the box must show main gone").option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
73597
73641
  const stage = projectDeploy.opts().stage;
73598
73642
  if (!stage) return fail("org project deploy retire: --stage is required (dev | rc | main)");
73599
73643
  const cfg = await loadConfig();
73600
73644
  let target;
73601
73645
  try {
73602
- target = await projectTarget("org project deploy retire", repoOrSlug);
73646
+ const repoFlag = projectDeploy.opts().repo ?? o.repo;
73647
+ target = await projectTarget("org project deploy retire", positionalOrRepoFlagTarget("org project deploy retire", repoOrSlug, repoFlag));
73603
73648
  } catch (e) {
73604
73649
  return fail(e.message);
73605
73650
  }
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.26",
3
+ "version": "4.6.27",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.6.26",
3
+ "version": "4.6.27",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -172,11 +172,15 @@ jobs:
172
172
  # MMI-Hub#5871: declare the npm MAJOR rather than inheriting whatever the runner's node bundles.
173
173
  # A bare major, never an exact patch (it goes stale and then prescribes downgrading operator
174
174
  # hosts toward it, #5862) and never `latest` (it takes the next major silently, #4782).
175
+ # MMI-Hub#9850: this single gate job is the designated hosted npm cache writer (auto saves only on a
176
+ # default-branch push), keyed on the one lockfile its install uses.
175
177
  - if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'node') }}
176
178
  uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
177
179
  with:
178
180
  node-version: 24
179
181
  npm-version: '12'
182
+ hosted-npm-cache: auto
183
+ hosted-npm-cache-lockfiles: {{GATE_CACHE_DEP_PATH}}
180
184
  - if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
181
185
  uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
182
186
  with: { python-version: '{{GATE_PY_VERSION}}' }
@@ -37,11 +37,17 @@ jobs:
37
37
  - name: Isolate npm cache
38
38
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
39
39
  run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
40
+ # MMI-Hub#9850: the single gate job is the designated hosted npm cache writer (auto saves only on a
41
+ # default-branch push), keyed on both lockfiles its two installs below use.
40
42
  - uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
41
43
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
42
44
  with:
43
45
  node-version: 24
44
46
  npm-version: '12'
47
+ hosted-npm-cache: auto
48
+ hosted-npm-cache-lockfiles: |
49
+ package-lock.json
50
+ module/package-lock.json
45
51
  - run: npm ci --no-audit --no-fund
46
52
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
47
53
  - run: npm ci --prefix module --no-audit --no-fund
@@ -56,6 +56,11 @@ design work follows one harmonized **Front-Loaded Impeccable v4.7.0 + MM Artifac
56
56
  before choosing pieces; `exists` reports whether the requested guide exists. Never guess
57
57
  repo-root `node_modules/@mutmutco/mmwork-ds/docs/components/<Piece>.md`; component guides
58
58
  live in `<Piece>/guide.md`. No install/cache means capability remains unknown.
59
+ **Loading state comes from the content (#9851).** A page or component renders its loading state
60
+ from the same component tree as its real content: the kit Page `busy` or the component's own
61
+ loading state, drawing the real rows inert or hidden. Never hand-author a separate placeholder
62
+ skeleton kept in sync by hand. Every UI change checks the loading and loaded DOM structure match,
63
+ so an indicator cannot drift from the content it fronts.
59
64
 
60
65
  How the five pillars fit together (owner, 2026-10-25):
61
66
 
@@ -161,6 +161,8 @@ Production changes require explicit authorization from the authorized human.
161
161
  Read the selected release skill before operating the train. Let it choose versions, run gates,
162
162
  publish, deploy, and verify the resulting release. A refusal requires fixing its cause; do not
163
163
  finish by hand, bypass checks, or start a duplicate release.
164
+ Finish the board before a release cut: every item of this cycle is merged and `Done` first; a queued
165
+ lane is not a landed one.
164
166
  Versions: patch is the default for fixes and small compatible additions. Minor is a deliberate call
165
167
  for a real feature, with a one-line reason. Major is a new start or direction and needs the owner's
166
168
  fresh approval before tagging. A breaking change refuses until it is shimmed or made an approved
@@ -15,6 +15,8 @@ Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([
15
15
 
16
16
  ## Preflight
17
17
 
18
+ Board first: `mmi-cli oracle board read --repo <owner/repo>`; cut only once every in-flight item of this cycle is merged and `Done` ([train-common.md](train-common.md#board-first)).
19
+
18
20
  `mmi-cli devops train doctor --lane release --heal --json`; read it per [train-common.md](train-common.md#doctor).
19
21
 
20
22
  ## Apply
@@ -1,7 +1,11 @@
1
- # Train skills: shared doctor, recovery, merge floor, report and retro
1
+ # Train skills: shared board-first, doctor, recovery, merge floor, report and retro
2
2
 
3
3
  `/release`, `/rcand` and `/hotfix` read this one file (from a sibling skill: `../release/train-common.md`). Each skill keeps only what differs by lane.
4
4
 
5
+ ## Board first
6
+
7
+ Before any release cut, read the repo's own board: `mmi-cli oracle board read --repo <owner/repo>`. Every in-flight item of this cycle — claimed, in review, queued in the autolander, or awaiting landing — must be merged and `Done` first. A queued lane is not a landed one: wait for its landing to report merge and board `Done`. The cut is the last step of a board cycle, never interleaved with pending landings; otherwise the version ships without them and needs a second cut. Standing backlog nobody is working is not in flight and does not block.
8
+
5
9
  ## Doctor
6
10
 
7
11
  The preflight JSON is on stdout alone: a stale CLI first prints its self-update notice on stderr and re-runs itself, so parse stdout only, never `2>&1`. 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. The apply step (`release --apply`, `rcand --apply`, `hotfix start`, `hotfix release`) re-runs the doctor first, with heal on.