A credential-free post-publish job must assert the PUBLISHED image's CLI carries every command the shipped docs name — today `sandbox-published` logs in and reads only `--version`
Repo: motir-core. One PR. Carved out of MOTIR-2611 by its 2026-08-10 re-plan — it was that card's AC 5, and it is the one deliverable of that card the correction leaves entirely intact.
The gap, measured on origin/main @ 816eb501
Three checks already guard the sandbox release. None of them reads what is INSIDE the published artifact on a stranger's behalf:
| lane / job | what it asks | credential | blind to |
|---|---|---|---|
sandbox-images.yml → sandbox-published (:255) | pull every digest, run motir --version | docker/login-action (:261) | asks as the PUBLISHER; --version is a number, not a command set |
sandbox-images.yml → sandbox-public (:320) | can a stranger pull it at all | none — by construction | says nothing about CONTENT |
sandbox-staleness.yml (MOTIR-2131) | is main far ahead of the newest cli-v* | none (reads git) | never pulls anything |
So the question "does the command the docs tell a reader to run exist in the image they are told to pull?" is asked by nothing. That is not hypothetical: MOTIR-2131 shipped a :claude whose CLI predated motir login while /docs/sandbox promised it, and it stayed green for five days.
The source of truth already exists and is machine-readable
lib/apiDocs/sandbox.ts annotates each step with the commands that step tells the reader to run — cliCommands: ['login'] (:267), ['link'] (:289), ['doctor'] (:310). That array is the expected set; deriving the assertion from it means a future step that names a new command is covered the day it is written, with no second list to keep in sync.
Do this
- Add
packages/cli/sandbox/smoke/assert-commands.mjs— same shape as its two siblings (assert-public.mjs,assert-current.mjs): zero dependencies, runnable from any shell, a header comment stating why it exists. Given an image ref, it resolves the manifest, runs the image'smotir help, and asserts every command in the expected set appears as a top-level command. Take the expected set fromSANDBOX_GUIDE'scliCommandsso the docs remain the single source. - Add a
sandbox-commandsjob to.github/workflows/sandbox-images.yml, gated onpublishlikesandbox-publicis, that runs it against the just-published tags. It holds NO credential: nodocker/login-action, nopackages:permission, noAuthorizationheader — the anonymity is the point, andassert-public.mjs's header explains why at length. Model the job onsandbox-public, not onsandbox-published. - Run it control-first, exactly as
assert-public.mjsdoes: the probe must be shown able to FAIL (a known-absent command name) before its pass means anything. - Unit-test the script (
packages/cli/test/sandboxCommands.test.ts, mirroringsandboxCurrent.test.ts) and extend the workflow-wiring guard (packages/cli/test/sandboxCi.test.ts) to assert the new job carries no login step and nopackages:scope — the guard is what stops a later edit from quietly re-crediting it.
Acceptance criteria
packages/cli/sandbox/smoke/assert-commands.mjsexists, exits 0 when the image'smotir helplists every expected command, non-zero when one is missing, and sends noAuthorizationheader on any request.- The expected command set is DERIVED from
lib/apiDocs/sandbox.ts'scliCommands, not restated — adding acliCommandsentry to a guide step changes what the job asserts, proven by a test. - A
sandbox-commandsjob exists in.github/workflows/sandbox-images.yml, runs only whenpublishis true, and contains nodocker/login-actionstep and nopackages:permission — asserted bypackages/cli/test/sandboxCi.test.ts, not only by reading the YAML. - The script runs its control (a command name known to be absent) and reports INDETERMINATE rather than PASS if the control does not fail.
packages/cli/test/sandboxCommands.test.tscovers: all-present, one-missing, control-broken, and malformedmotir helpoutput.- The PR body shows the script run against the CURRENTLY published
ghcr.io/moooon-b-v/motir-sandbox:claudewith its output quoted — it must PASS today (0.2.0 haslogin), which is what makes it a regression guard rather than a bug report.
Context refs
.github/workflows/sandbox-images.yml—sandbox-published(:255, logged in) andsandbox-public(:320, credential-free); the latter is the model.packages/cli/sandbox/smoke/assert-public.mjs— the header explains the publisher-vs-consumer distinction and the control-first discipline; copy both.packages/cli/sandbox/smoke/assert-current.mjs+.github/workflows/sandbox-staleness.yml— the script-plus-thin-caller arrangement to mirror.packages/cli/test/sandboxCurrent.test.ts,packages/cli/test/sandboxCi.test.ts— the unit-test and wiring-guard patterns.lib/apiDocs/sandbox.ts—cliCommandsat :267 / :289 / :310.
Out of scope
Cutting any tag — that is MOTIR-2611's sibling release card. Changing the guide's prose — that is MOTIR-2611 itself.