I found three INFORMATIONAL issues and nothing CRITICAL. All three are in the fix commit `cdce7c76d`, which fixes the earlier findings. The new code behind each one is the lock, the sandbox adoption and the receipt-overwrite gate.

**Fixture coverage was reduced.** I read `tests/qm-gstack-smoke.test.ts` in summary mode only (+39/-1). It adds four tests: the outcome success path, `lockHost`, `receiptOut`, and the http 0 transport failure. No test covers `createSandbox`, `adoptNew` or the teardown order in `down()`.

**Where the QM facts come from.** QM's container naming and labels come from the QM source copy at `~/.capy/work/local-sandbox.ts`. I assumed it matches the b6786d67 pin.

**gstack check on this machine:** `gstack: ok 1.91.366.0 require=claude,codex,patch` (3 behind the latest, 1.91.369.0; above the 1.91.364.0 floor).

```json
{"severity":"INFORMATIONAL","confidence":7,"path":"harnesses/qm/gstack/qm-stack.ts","line":108,"category":"red-team","summary":"adoptNew lists qm.sandbox=1 containers host-wide with no qm.org or qm.scope filter, and calls own(), which throws on any name outside qm-sbx-*. QM's scratch sandboxes (local-sandbox.ts:314-323, runContainer(qm-scratch-<key>, undefined, false)) carry the same qm.sandbox=1 label. Failure 1: if any qm-scratch-* container appears on the host while a create call fails or returns no machineId, own() throws partway through the loop and `creating` is never reset. down() then calls adoptNew() first in its finally, so it throws again and skips removeSandboxes, compose down, unlock and the signal-handler removal. That brings back the teardown bypass outside-voice#3 reported, now triggered by another process's container. Failure 2: a live trial's freshly created qm-sbx-* sandbox (also org harness-eval) that appears in the same window, up to the 300 s client timeout, is adopted and removed with docker rm -f, its qm-home volume and its qm-net network. This contradicts the updated README line 'never another stack's' and brings back a narrower version of the label sweep that was dropped to protect live trials.","fix":"Narrow the diff to this call: filter on label=qm.org=harness-eval plus label=qm.scope=<the scope being created>. QM names the container deterministically from the scope (localContainerName = qm-sbx-<slug(scopeId)>), so an exact name or scope match is possible. In adoptNew, skip names that fail the regex instead of throwing, and reset `creating` in a finally. In down(), wrap adoptNew in its own try so removeSandboxes, compose down and unlock always run. Add a fake-docker test where a qm-scratch-* name appears during a failed create, and assert that compose down still runs.","fingerprint":"harnesses/qm/gstack/qm-stack.ts:108:red-team","specialist":"red-team"}
{"severity":"INFORMATIONAL","confidence":7,"path":"harnesses/qm/gstack/qm-stack.ts","line":102,"category":"red-team","summary":"The host lock is taken inside upStack, after the caller has already modified the shared default --work directory. Both scripts default to fixed work directories (~/.capy/work/qm-gstack-smoke and qm-gstack-s1). Before reaching the lock, a second run truncates fake-requests.ndjson (smoke.ts:114, s1-stage.ts:135). s1 has also already run stage(), which does rmSync(work/context) and rewrites it (s1-stage.ts:114), rewritten the router, and re-tagged the overlay image. The run that holds the lock then reads the clobbered state after its turn. smoke's fake_model_requests counts lines in the truncated log. s1's stagedText() reads work/context/gstack/..., which is missing or from another ref if the second run failed after its rmSync, so the first run crashes with no receipt or records the wrong comparison. The lock only refuses the second run after this damage is done. The lock itself is also not atomic: it reads, checks liveness and then writes without O_EXCL, so two runs started together can both take it. It also lives under tmpdir(), so runs launched with different TMPDIR values (env -i or not) never see each other's locks.","fix":"Take the lock at the top of main(), before touching --work, and key it on the resolved work directory as well as the project and ports. Create each lock file with writeFileSync(path, pid, { flag: 'wx' }), and take over a dead owner's lock only through unlink-and-retry. Use a fixed lock directory such as /tmp or the work root, not tmpdir().","fingerprint":"harnesses/qm/gstack/qm-stack.ts:102:red-team","specialist":"red-team"}
{"severity":"INFORMATIONAL","confidence":6,"path":"harnesses/qm/gstack/qm-stack.ts","line":156,"category":"red-team","summary":"receiptOut lets a default run overwrite the committed receipt whenever the gstack ref matches, and nothing else is checked. The smoke's success gate is only result.registered (smoke.ts:154). Case 1: `smoke.ts --core-url ... --secret-file ...` at 21277291 replaces receipts/smoke-b6786d67.json with qm.commit null and an unchecked external core, while the file name and CHANGELOG still claim QM b6786d67. Case 2: a default run at the same ref where core can't reach GitHub, or QM's refusal changes, still registers the pack. It then rewrites the published refusal_kind bytes_cap receipt as clone_timeout or other and exits 0, so the README and CHANGELOG cite numbers the committed receipt no longer shows. s1 has the same gap: `ok` checks only the complete read, so a run whose over-cap read (the cited 16,384 cut) fails or changes still overwrites. Also, if pin.json later moves QM, the b6786d67-named file is overwritten with a receipt from the new QM build.","fix":"Overwrite the committed path only when the run's qm.commit equals the pinned commit (refuse when it is null, as with --core-url) and the outcome fields the docs cite (smoke: refusal_kind and refusal; s1: complete_read.identical and over_cap_read.cut) match the committed receipt. Otherwise write to <work>/ and print the difference. Or simply require an explicit --out to touch the committed receipts.","fingerprint":"harnesses/qm/gstack/qm-stack.ts:156:red-team","specialist":"red-team"}
```