I found six problems in the range 2ae5923..HEAD (cdce7c76d) and none of them blocks. The one to fix before merge is the sandbox adoption at `qm-stack.ts:106`. When a sandbox create call fails, it can delete containers and volumes that belong to other QM stacks on the same host. The fix is one narrower filter.

gstack: ok 1.91.366.0 require=claude,codex,patch (pins: skipped (no project); behind=3 (latest: 1.91.369.0)). That is the result of the `--check` run on this shared machine, which is above the 1.91.364.0 floor. This review did not run any gstack workflow.

**Scope.** I read in full all non-fixture source that changed: `harnesses/qm/gstack/{qm-stack,s1-stage,smoke}.ts`, the gstack `README.md` and `CHANGELOG.md`. I reviewed the fixtures and tests in **summary mode only**. `tests/qm-gstack-smoke.test.ts` gained 39 lines and four tests: a successful import after a failed register reports no refusal, `lockHost` refuses a lock held by a live process and takes over a dead one, `receiptOut` keeps the committed receipt only for its own gstack ref, and a transport failure comes back as http 0 instead of throwing. I did not pull any payload bytes. With a scrubbed environment, `bun test tests/qm-gstack-smoke.test.ts` passed 16 of 16 and `bunx tsc --noEmit` was clean.

**Findings**

1. **A failed sandbox create adopts sandboxes from other stacks** (`qm-stack.ts:106-108`, `145-149`). `sandboxNames()` lists every container on the host with `label=qm.sandbox=1`. When the create call fails, or is cut off by `qmClient`'s 300 s timeout, `adoptNew()` takes ownership of every such container that appeared during that window. `down()` then runs `docker rm -f` on each one, plus `volume rm -f qm-home-*` and `network rm`. A live trial's QM stack uses the same label (`capture.sh:20`), so one failed s1 create during a trial deletes that trial's sandbox and its home volume. The lock does not help, because it only excludes other qm-gstack runs. The interface comment and the new README line ("never another stack's") claim more than the code does. Fix: add `--filter ancestor=<sandboxImage>` to the adoption query. s1's image `harness-eval/qm-sandbox:gstack-<ref8>` is unique to it, and the project lock already rules out a second s1. Alternatively, filter on whatever label QM puts on sandboxes to tie them to their core. FIXABLE. Severity: Medium. Blocks: no, because it needs a concurrent QM stack, which the README already says not to run.

2. **One bad container name skips the whole teardown** (`qm-stack.ts:105`, `108`, `123`). `own()` throws for any name that is not `qm-sbx-*`. `adoptNew()` calls it inside `down()`'s `finally` before `removeSandboxes()`, `dc down` and `unlock()`. One labelled container with an unexpected name aborts all three steps. Because `stopping` is already true, nothing retries, so pg, core and the owned sandboxes stay running. The same throw inside `createSandbox`'s catch leaves `creating` set, so the same failure repeats at teardown. Fix: skip names that don't match in `adoptNew` instead of throwing, or put each teardown step in its own `try`. FIXABLE. Severity: Low. Blocks: no.

3. **A sandbox created after the call is cut off leaks** (`qm-stack.ts:108`, `147`). If the client times out while QM is still creating the container, `adoptNew()` takes its snapshot and sets `creating = null` before the container exists. Nothing tracks it afterwards, and `compose down --remove-orphans` doesn't remove it either, because core created it through the Docker socket and not through compose. Fix: keep the pre-create baseline until `down()`, and diff against it after `dc down` has stopped core. FIXABLE. Severity: Low. Blocks: no.

4. **The host lock is not atomic** (`qm-stack.ts:66-76`). The lock code reads the file, checks the pid and then writes, with no `O_EXCL`. Two runs launched close together can both take the lock. The second run's opening `dc down -v --remove-orphans` then removes the first run's stack, which is the exact case the lock exists to stop. `release()` also deletes the file without checking it still holds its own pid, so the run that lost the race clears the winner's lock. Two smaller gaps:
   - The lock directory comes from `tmpdir()`, so a run with `TMPDIR` set and one without lock in different places.
   - A reused pid makes a dead lock look alive.

   Fix: create the file with `writeFileSync(path, pid, { flag: 'wx' })`. On `EEXIST`, check whether the holder is alive, then unlink and retry `wx` once. Release only if the file still holds this process's pid. Use a fixed directory. FIXABLE. Severity: Low. Blocks: no, since this is a manual tool.

5. **A failed s1 run still overwrites the committed router file** (`s1-stage.ts:104`, `117`). The receipt now goes to `--work` on failure, but `routerOut` is still derived from `out` and written at line 117, before the stack runs. At the committed ref, a failed or aborted run overwrites `receipts/s1-router-21277291.md`. The content comes from `gstack-harness ... --print` run under the caller's `HOME`, so it may not be byte-identical, and the committed receipt's `router_sha256` would then no longer match the file. Fix: decide the destination for the router the same way as for the receipt, or write it after `ok` is known. FIXABLE. Severity: Low. Blocks: no.

6. **`receiptOut` treats an unreadable receipt as permission to overwrite it** (`qm-stack.ts:156-160`). A committed receipt that is missing, has bad JSON, or lacks `gstack.ref` gives `committedRef === null`. Any default run at any ref then overwrites the committed path. Today both committed receipts carry the full sha `21277291c850...` under `gstack.ref`, which matches the newly normalised `ref`, so this is latent. INVESTIGATE: decide whether a parse failure should go to `work` instead. Severity: Low. Blocks: no.

I checked these and found them sound: the lock now runs before the opening `compose down`; the signed `whoami` check (the committed smoke receipt shows `/v1/admin/whoami` answers 200 at this QM pin); running `prepare.sh` before the lock (it only creates directories and prints values); `refusal` being null after a successful import; `--ref` now resolving to a full commit sha; and the CHANGELOG count (61 skills plus `connect-chrome` plus the root file makes 63).

Recommendation: Fix the sandbox adoption filter at qm-stack.ts:106 before merge because a failed s1 create runs `docker rm -f` and `volume rm -f` on every new `qm.sandbox=1` container on the host, including a live trial's; the change is one filter and everything else can ship as-is.