The Docker workflow built both images and never started a container,
and never parsed a Compose file. A green run therefore only meant the
Dockerfile compiled. Two defects that break the very first documented
command shipped past it: `docker-compose.yml` carried a bare
`environment:` key that made Compose reject the file outright (#2188),
and `_embed_texts` handed chromadb `np.float32` scalars so `mine`
aborted on the first drawer (#2187).
Add `scripts/docker-smoke.sh`, which exercises what the README tells
users to run:
1. `compose config` on docker-compose.yml and the server compose file
2. entrypoint dispatch for both `cli ...` and bare passthrough
3. `mine` a mounted directory, asserting a drawer is filed
4. `search` from a *separate* container, asserting the stored text
comes back verbatim — this is the assertion that matters, since
storing user words exactly is the promise the palace makes
5. a real MCP stdio JSON-RPC handshake: initialize, tools/list, and a
mempalace_search call whose result must contain the drawer
It asserts on returned content, not just exit codes, and lives in a
script rather than inline YAML so it runs identically on a laptop:
`scripts/docker-smoke.sh <image>`.
The new `smoke` job builds amd64 natively with `load: true` (buildx
cannot load a multi-arch manifest) and reads the publish job's cache
while writing its own scope, so an amd64-only export never lands on top
of the multi-arch one. `build` now needs it, so a failing smoke test
blocks publication rather than being noticed afterwards.
Verified by reintroducing each defect against a real build: the compose
regression fails at step 1, the embedding regression at step 3, and the
current tree passes all five. Failure output is clipped to 500 columns
because a rejected embedding batch otherwise prints a whole 384-dim
vector on one line and buries the message.