diff --git a/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs b/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs
new file mode 100644
index 0000000..1a940c6
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Numerics;
+using System.Text.RegularExpressions;
+
+namespace AcDream.Core.Tests.Conformance;
+
+///
+/// A single retail find_cell_list pick captured via cdb (golden oracle).
+/// is the seed/current cell at find_cell_list
+/// entry; is the chosen containing cell
+/// (retail *arg5 in CObjCell::find_cell_list @ 0x52b4e0 pc:308742).
+///
+public sealed record RetailCellPick(uint SeedCellId, Vector3 Position, uint PickedCellId);
+
+/// Parser for the find-cell-list-capture.cdb log format.
+public static class RetailTrace
+{
+ private static readonly Regex Fcl = new(
+ @"^\[fcl\]\s+seed=0x(?[0-9A-Fa-f]{1,8})\s+" +
+ @"px=(?-?\d+(\.\d+)?)\s+py=(?-?\d+(\.\d+)?)\s+pz=(?-?\d+(\.\d+)?)\s+" +
+ @"picked=0x(?[0-9A-Fa-f]{1,8})\s*$",
+ RegexOptions.Compiled);
+
+ public static RetailCellPick? ParseFindCellList(string line)
+ {
+ if (string.IsNullOrEmpty(line)) return null;
+ var m = Fcl.Match(line);
+ if (!m.Success) return null;
+ var ci = CultureInfo.InvariantCulture;
+ return new RetailCellPick(
+ SeedCellId: Convert.ToUInt32(m.Groups["seed"].Value, 16),
+ Position: new Vector3(
+ float.Parse(m.Groups["px"].Value, ci),
+ float.Parse(m.Groups["py"].Value, ci),
+ float.Parse(m.Groups["pz"].Value, ci)),
+ PickedCellId: Convert.ToUInt32(m.Groups["picked"].Value, 16));
+ }
+
+ /// Parse a log, skipping every non-matching line (noise/banner/other BPs).
+ public static IReadOnlyList ParseAll(IEnumerable lines)
+ {
+ var list = new List();
+ foreach (var line in lines)
+ {
+ var rec = ParseFindCellList(line);
+ if (rec is not null) list.Add(rec);
+ }
+ return list;
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs b/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs
new file mode 100644
index 0000000..420e3ce
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs
@@ -0,0 +1,56 @@
+using System.Numerics;
+using Xunit;
+
+namespace AcDream.Core.Tests.Conformance;
+
+/// P0 Task 5 — TDD for the retail find_cell_list trace parser.
+public class RetailTraceTests
+{
+ [Fact]
+ public void Parse_FindCellListLine_YieldsSeedPosAndPicked()
+ {
+ const string line =
+ "[fcl] seed=0xA9B40170 px=141.5000 py=7.2200 pz=92.7400 picked=0xA9B40171";
+ var rec = RetailTrace.ParseFindCellList(line);
+ Assert.NotNull(rec);
+ Assert.Equal(0xA9B40170u, rec!.SeedCellId);
+ Assert.Equal(new Vector3(141.5f, 7.22f, 92.74f), rec.Position);
+ Assert.Equal(0xA9B40171u, rec.PickedCellId);
+ }
+
+ [Fact]
+ public void Parse_NegativeCoordinatesAndLowercaseHex_Ok()
+ {
+ const string line =
+ "[fcl] seed=0xa9b40031 px=-12.5 py=0 pz=-3.25 picked=0xa9b40170";
+ var rec = RetailTrace.ParseFindCellList(line);
+ Assert.NotNull(rec);
+ Assert.Equal(0xA9B40031u, rec!.SeedCellId);
+ Assert.Equal(new Vector3(-12.5f, 0f, -3.25f), rec.Position);
+ Assert.Equal(0xA9B40170u, rec.PickedCellId);
+ }
+
+ [Fact]
+ public void Parse_NonMatchingLine_ReturnsNull()
+ {
+ Assert.Null(RetailTrace.ParseFindCellList("[BP4] find_collisions hit#10170 collide=0"));
+ Assert.Null(RetailTrace.ParseFindCellList(""));
+ Assert.Null(RetailTrace.ParseFindCellList("[fcl] seed=0xA9B40170 px=1 py=2")); // truncated
+ }
+
+ [Fact]
+ public void ParseFile_SkipsNoiseAndYieldsOnlyPicks()
+ {
+ var lines = new[]
+ {
+ "armed; walk now",
+ "[fcl] seed=0xA9B40031 px=160.0 py=10.0 pz=94.0 picked=0xA9B40170",
+ "[BP] noise",
+ "[fcl] seed=0xA9B40170 px=158.0 py=12.0 pz=95.0 picked=0xA9B40171",
+ };
+ var picks = RetailTrace.ParseAll(lines);
+ Assert.Equal(2, picks.Count);
+ Assert.Equal(0xA9B40170u, picks[0].PickedCellId);
+ Assert.Equal(0xA9B40171u, picks[1].PickedCellId);
+ }
+}
diff --git a/tools/cdb/README-find-cell-list-capture.md b/tools/cdb/README-find-cell-list-capture.md
new file mode 100644
index 0000000..88833e8
--- /dev/null
+++ b/tools/cdb/README-find-cell-list-capture.md
@@ -0,0 +1,75 @@
+# find-cell-list-capture — operator runbook
+
+Captures retail's **accepted membership sequence** at the cottage doorway so P0 can pin
+acdream's `CellTransit.FindCellList` against the real client (P0 Task 6 — the P1 gate).
+This is the one user-gated step of P0. Everything else (parser, fixture loader, goldens) is
+already headless + green.
+
+> **Try the autonomous path first.** Before running this, P0 Task 6 greps the *already-committed*
+> retail traces under `docs/research/2026-05-21-a6-captures/` for a usable membership pick. Only
+> run this capture if those traces don't yield one.
+
+## What you get
+
+A log of lines in the golden format the parser (`RetailTrace.ParseFindCellList`) reads:
+
+```
+[fcl] seed=0xA9B40031 px=0x43200000 py=0x41200000 pz=0x42BC0000 picked=0xA9B40170
+```
+
+`seed` = the cell the player was in; `picked` = the cell retail committed; `px/py/pz` = the player
+world origin as **raw IEEE-754 hex** (decode with `decode_retail_hex.py`). One line per accepted
+cell change, so a doorway walk yields a short clean sequence (e.g. `0031 → 0170 → 0171`).
+
+## Prerequisites
+
+1. **PDB matches the binary** (CLAUDE.md retail-debugger toolchain):
+ ```
+ py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe"
+ ```
+ Expect `=== MATCH ===`.
+2. **Retail in-world at the Holtburg cottage doorway** (the building at world ≈ (161.9, 7.5, 94),
+ outdoor landcell `0xA9B40031`, vestibule `0xA9B40170`, room `0xA9B40171` — see
+ `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`). Stand just outside the door.
+3. `cdb.exe` (x86) at `C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe`.
+
+## One-time: verify the struct offsets (the script ships with placeholders)
+
+Launch cdb attached, let it break once, then dump the layouts and edit `$t4/$t5/$t6` in the
+`.cdb`:
+
+```
+dt acclient!CPhysicsObj @ecx # note m_position (origin x,y,z) offset -> POS_OFF ($t6)
+ # note the current-cell pointer field -> CELLPTR_OFF ($t5)
+dt acclient!CObjCell poi(@esp+4) # note cell_id offset -> CELLID_OFF ($t4)
+```
+
+`change_cell` is `thiscall`: `this` (CPhysicsObj*) is `@ecx`; the new cell arg is `poi(@esp+4)`.
+The defaults in the script (`0x18 / 0x08 / 0x1C`) are best-guesses from `acclient.h` — **confirm
+them** before trusting a capture.
+
+## Run
+
+```powershell
+& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" `
+ -pn acclient.exe -cf tools\cdb\find-cell-list-capture.cdb *>&1 |
+ Tee-Object -FilePath "find-cell-list-capture.console.log"
+```
+
+Then, in retail, **walk slowly in and out of the doorway 5–10 times** (out → vestibule → room →
+back). The script auto-detaches (`qd`) after 400 cell changes; retail keeps running. Do **not**
+`Stop-Process` cdb — that kills retail (CLAUDE.md watchout).
+
+## Fold into the golden
+
+1. Decode the hex floats to decimals:
+ ```
+ py tools/cdb/decode_retail_hex.py find-cell-list-capture.log > find-cell-list-threshold.log
+ ```
+ (or hand-decode; each `0xHHHHHHHH` is a little-endian float). Result lines must read
+ `[fcl] seed=0xA9B40031 px=160.0 py=10.0 pz=94.0 picked=0xA9B40170`.
+2. Place the decoded file at
+ `tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log`.
+3. Run `dotnet test … --filter FindCellList_DoorwayThreshold_MatchesRetailTrace`. GREEN = acdream
+ already matches retail at the threshold. RED = a real divergence → that is the **P1** work,
+ left as a documents-the-bug conformance test (no weakening the assertion — master plan §4).
diff --git a/tools/cdb/find-cell-list-capture.cdb b/tools/cdb/find-cell-list-capture.cdb
new file mode 100644
index 0000000..be1f922
--- /dev/null
+++ b/tools/cdb/find-cell-list-capture.cdb
@@ -0,0 +1,42 @@
+$$ ============================================================================
+$$ find-cell-list-capture.cdb — P0 conformance retail golden capture
+$$ ----------------------------------------------------------------------------
+$$ Captures retail's ACCEPTED membership decision at the cottage doorway so we
+$$ can pin acdream's CellTransit.FindCellList against it (P0 Task 6, the P1 gate).
+$$
+$$ Emits one line per accepted cell change in the golden format the parser reads
+$$ (tests/AcDream.Core.Tests/Conformance/RetailTrace.cs):
+$$ [fcl] seed=0xHHHHHHHH px=0x py=0x pz=0x picked=0xHHHHHHHH
+$$ where seed = the cell the player was in (old), picked = the cell committed (new),
+$$ and px/py/pz = the player world origin (raw IEEE-754 hex; decode offline with
+$$ tools/cdb/decode_retail_hex.py, then write decimals into the golden fixture).
+$$
+$$ Target: CPhysicsObj::change_cell @ 0x00513390 (pc:281192) — commit-on-diff.
+$$ It fires ONLY on an accepted cell transition, so the doorway crossing yields a
+$$ short, clean sequence (e.g. 0031 -> 0170 -> 0171), not a per-tick flood.
+$$
+$$ thiscall: this (CPhysicsObj*) = @ecx ; arg new_cell (CObjCell*) = poi(@esp+4)
+$$
+$$ OFFSETS BELOW ARE PLACEHOLDERS — VERIFY LIVE (see README). At the first break:
+$$ dt acclient!CPhysicsObj @ecx $$ find m_position (Frame/Position) + cell
+$$ dt acclient!CObjCell poi(@esp+4) $$ find cell_id offset
+$$ dt acclient!Position $$ find origin (x,y,z) float offsets
+$$ then edit CELLID_OFF / CELLPTR_OFF / POS_OFF and re-run.
+$$ ============================================================================
+
+.logopen C:\Users\erikn\source\repos\acdream\find-cell-list-capture.log
+.sympath C:\Users\erikn\source\repos\acdream\refs
+.symopt+ 0x40
+.reload /f acclient.exe
+
+$$ ---- EDIT THESE after the dt dumps (hex byte offsets) ----------------------
+r $t4 = 0x18 $$ CELLID_OFF : CObjCell.cell_id (acclient.h:30938 — verify)
+r $t5 = 0x08 $$ CELLPTR_OFF : CPhysicsObj.cell (current cell ptr — verify)
+r $t6 = 0x1C $$ POS_OFF : CPhysicsObj.m_position.origin.x (verify; y=+4, z=+8)
+$$ ---------------------------------------------------------------------------
+
+r $t0 = 0
+bp acclient!CPhysicsObj::change_cell "r $t0 = @$t0 + 1; r $t1 = poi(@ecx+@$t5); r $t2 = poi(@esp+4); .printf /D \"[fcl] seed=0x%08x px=0x%08x py=0x%08x pz=0x%08x picked=0x%08x\\n\", poi(@$t1+@$t4), poi(@ecx+@$t6), poi(@ecx+@$t6+4), poi(@ecx+@$t6+8), poi(@$t2+@$t4); .if (@$t0 >= 400) { .printf /D \"=== DETACH after %d cell changes ===\\n\", @$t0; qd } .else { gc }"
+
+.printf \"find-cell-list capture armed (change_cell). Walk SLOWLY in/out of the cottage doorway now.\\n\"
+g