# A6.P3 issue #98 — cellar-up fix plan (diagnostic-first)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Close issue #98 (sphere stuck on cottage cellar ramp) by identifying the EXACT failure point through a focused diagnostic probe, then writing a minimal evidence-driven fix. The user can walk up and out of a Holtburg cottage cellar in the live client.

**Architecture:** Three phases. Phase 0 re-reads the existing capture for everything the divergence comparison missed — no code. Phase 1 adds ONE focused probe at the only site where existing instrumentation is silent (inside `AdjustOffset`). Phase 2 runs the probe and decides which of three branches the fix must take. Phase 3 writes the fix against the replay harness as TDD oracle.

**Tech Stack:** C# .NET 10. The deterministic replay loop ([Issue98CellarUpReplayTests.cs](../../tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs), <200ms) is the inner test loop. The cdb capture script ([tools/cdb/issue98-runner.ps1](../../tools/cdb/issue98-runner.ps1)) is the outer ground-truth loop.

**Risk:** Four prior sessions guessed-and-shipped (10+ variants). The pattern: convinced of diagnosis → ship fix → user reports "still can't pass." This plan refuses to ship code until the diagnostic data NAMES the failure site.

---

## What the existing log already proves (Phase 0 already partially done)

The slice 7 handoff and the divergence comparison both claimed *"our sphere oscillates at world Z ≈ 92.01 with no altitude gain."* That framing is **wrong**. Scanning the 2,589 `[step-walk]` lines in [a6-issue98-negpoly-20260523-135032.out.log](../../a6-issue98-negpoly-20260523-135032.out.log) shows:

| What the log shows | Implication |
|---|---|
| Sphere `cur` Z climbs **90.00 → 92.79** across the capture (2.79 m gain). | The climb works for most of the ramp. Z gain per `[step-walk] site=after-adjust` is +0.197 to +0.227 when offset points INTO the ramp normal. |
| Climb **caps at world Z ≈ 92.79**, then descends back. | Something at the top of the ramp prevents the last ~1.21 m of climb to cottage floor (world Z=94). |
| At the peak (line 17458–17485 in the log): `before-insert` has `check=(141.58, 7.18, 92.79)`, `walkPoly=True`. `after-insert` has same position but **`walkPoly=False`** + `winterp=-0.0000`. | `DoStepDown` at the peak burned `WalkInterp` from 1.0 → 0 in one tick and the sphere ended up off the walkable polygon. |
| `cell=0xA9B40147->0xA9B40147` for every line. | Our sphere never transitions to a cottage cell (`0xA9B40143` / `0xA9B40146`) during the climb. Retail's BPA capture shows queries against cottage cells starting at BPA hit#430. |
| ContactPlane normal is `(0, +0.719, +0.695)` in our log. The divergence doc says retail's ramp normal is `(0, -0.719, +0.695)` (sign-flipped) — but retail's BPE NEVER writes the ramp as ContactPlane at all (only flat cellar floor or flat cottage floor). | Retail does not use the ramp as a ContactPlane. Our code does. This is a SHAPE-of-the-fix question for later; do not act on it before the diagnostic confirms it matters. |

**Revised hypothesis (informed by the data, not the divergence doc):**

The climb works while the sphere is mid-ramp. It **fails at the top of the ramp** where the polygon ends. Three mutually-exclusive failure modes are plausible:

1. **Geometric cap.** The ramp polygon physically ends at world Z ≈ 92.79; there is no further walkable surface within the step-up reach. Retail's sphere reaches the cottage floor by a transition we never trigger.
2. **Cell-set divergence.** At world Z ≈ 92.79 the sphere overlaps cottage cell volumes, but our `CheckOtherCells` either doesn't query the cottage cell, or queries it with the wrong sphere reference (foot vs lifted). Retail's BPA hit#430 / hit#434 show queries against TWO different leaves at SIGNIFICANTLY different sphere positions (cell A: local 0.48, cell B: local 0.63) — that's the retail two-step-up pattern at work, which we may not be doing.
3. **WalkInterp depletion before forward motion applies.** The peak `[step-walk]` line at 17485 shows `winterp=-0.0000` after `DoStepDown`. If `DoStepDown` consumed all WalkInterp on the lift (the slice 7 handoff's reading), the next tick starts WalkInterp at 1.0 again — so this isn't the inter-tick blocker the handoff thought. But INTRA-tick, the next call's `AdjustOffset` runs with no remaining WalkInterp, which could legitimately gate further motion.

**The point of Phase 1 is to disambiguate between (1), (2), and (3).**

---

## Phase 1 — focused diagnostic (no code logic changes)

Add ONE probe site inside `AdjustOffset` and rerun. The existing `[step-walk]` probe instruments the call from the outside (req → adj across the call) but never reveals which **branch** AdjustOffset took. The new probe reveals the branch; that's the missing signal.

### Task 1.1: Add `[step-walk-adjust]` probe call inside AdjustOffset

**Files:**
- Modify: `src/AcDream.Core/Physics/PhysicsDiagnostics.cs` (add one new helper)
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:2635-2739` (add 4 probe calls inside AdjustOffset's branches)

- [ ] **Step 1: Add the diagnostic emitter to PhysicsDiagnostics.cs**

Add this method right after `LogStepWalk` (currently ending around line 700). It is intentionally separate from `LogStepWalk` so the line format stays short and greppable.

```csharp
/// <summary>
/// A6.P3 issue #98 (2026-05-23) — focused probe INSIDE AdjustOffset
/// revealing which branch was taken and the Z gain per call. Pair with
/// <c>[step-walk] site=after-adjust</c> at the call site to triangulate
/// where the projection ends up. Caller MUST guard with
/// <c>if (!ProbeStepWalkEnabled) return;</c> before calling.
/// </summary>
public static void LogStepWalkAdjust(
    string  branch,
    Vector3 input,
    Vector3 output,
    Plane?  contactPlane,
    bool    slidingValid,
    Vector3 slidingNormal,
    float   collisionAngle,
    float   walkInterp)
{
    var culture = System.Globalization.CultureInfo.InvariantCulture;

    string cpDesc = contactPlane is { } cp
        ? string.Format(culture,
            "n=({0:F4},{1:F4},{2:F4}) d={3:F4}",
            cp.Normal.X, cp.Normal.Y, cp.Normal.Z, cp.D)
        : "n/a";

    string slideDesc = slidingValid
        ? string.Format(culture,
            "({0:F4},{1:F4},{2:F4})",
            slidingNormal.X, slidingNormal.Y, slidingNormal.Z)
        : "n/a";

    Console.WriteLine(string.Format(culture,
        "[step-walk-adjust] branch={0} input=({1:F4},{2:F4},{3:F4}) " +
        "output=({4:F4},{5:F4},{6:F4}) zGain={7:F4} " +
        "cp={8} slide={9} colAngle={10:F4} winterp={11:F4}",
        branch,
        input.X, input.Y, input.Z,
        output.X, output.Y, output.Z,
        output.Z - input.Z,
        cpDesc, slideDesc, collisionAngle, walkInterp));
}
```

- [ ] **Step 2: Wire the probe at the four AdjustOffset branches**

Open [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) at line 2635 (`private Vector3 AdjustOffset(Vector3 offset)`). The body currently has FOUR exit/branch points:

1. **no-cp** path (line 2654–2659): `if (!ci.ContactPlaneValid) return result;`
2. **slide** path (lines 2665–2676): `if (checkSlide) { ... result = ... }`
3. **into-plane** path (lines 2677–2681): `else if (collisionAngle <= 0f) { result -= ... }`
4. **away-plane** path (lines 2682–2687): `else { result -= ... }` — same arithmetic as `into-plane`, kept distinct for the probe.

At the FINAL return (line 2738 `return result;`), emit ONE probe line that captures the branch taken and the final result.

Replace lines 2635–2739 with a body that tracks the branch token (initialize to `"unknown"`) and assigns it at each branch point. Concretely:

```csharp
private Vector3 AdjustOffset(Vector3 offset)
{
    var sp = SpherePath;
    var ci = CollisionInfo;

    Vector3 result    = offset;
    bool    checkSlide = false;
    string  branch    = "init";                       // ← new

    // Check if we should apply sliding.
    float slidingAngle = Vector3.Dot(result, ci.SlidingNormal);
    if (ci.SlidingNormalValid)
    {
        if (slidingAngle < 0f)
            checkSlide = true;
        else
            ci.SlidingNormalValid = false;
    }

    // No contact plane — simple slide projection.
    if (!ci.ContactPlaneValid)
    {
        if (checkSlide)
        {
            result -= ci.SlidingNormal * slidingAngle;
            branch = "no-cp-slide";                   // ← new
        }
        else
        {
            branch = "no-cp";                         // ← new
        }

        if (PhysicsDiagnostics.ProbeStepWalkEnabled)  // ← new
            PhysicsDiagnostics.LogStepWalkAdjust(
                branch, offset, result,
                contactPlane: null,
                slidingValid: ci.SlidingNormalValid,
                slidingNormal: ci.SlidingNormal,
                collisionAngle: 0f,
                walkInterp: sp.WalkInterp);

        return result;
    }

    // Have a contact plane — project movement onto the contact surface.
    float   collisionAngle = Vector3.Dot(result, ci.ContactPlane.Normal);
    Vector3 slideOffset    = Vector3.Cross(ci.ContactPlane.Normal, ci.SlidingNormal);

    if (checkSlide)
    {
        // Project movement along the crease between contact and slide planes.
        float slideLen = slideOffset.Length();
        if (slideLen < PhysicsGlobals.EPSILON)
        {
            result = Vector3.Zero;
            branch = "slide-degenerate";              // ← new
        }
        else
        {
            slideOffset /= slideLen;
            result = Vector3.Dot(slideOffset, result) * slideOffset;
            branch = "slide-crease";                  // ← new
        }
    }
    else if (collisionAngle <= 0f)
    {
        // Moving into the contact plane: remove component into the plane.
        result -= ci.ContactPlane.Normal * collisionAngle;
        branch = "into-plane";                        // ← new
    }
    else
    {
        // Moving away from contact plane: snap to plane surface.
        result -= ci.ContactPlane.Normal * collisionAngle;
        branch = "away-plane";                        // ← new
    }

    // ── (Existing safety check unchanged — keep lines 2689–2736 verbatim) ──
    if (ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater)
    {
        Vector3 globCenter = sp.GlobalSphere[0].Origin;
        float   radius     = sp.GlobalSphere[0].Radius;

        float dist = Vector3.Dot(globCenter, ci.ContactPlane.Normal)
                     + ci.ContactPlane.D;

        float naturalRestingDist = radius * ci.ContactPlane.Normal.Z;

        if (dist < naturalRestingDist - PhysicsGlobals.EPSILON)
        {
            float zDist = (naturalRestingDist - dist) / ci.ContactPlane.Normal.Z;
            if (radius > MathF.Abs(zDist))
            {
                sp.AddOffsetToCheckPos(new Vector3(0f, 0f, zDist));
                branch += "+safety-push";              // ← new — branch annotation
            }
        }
    }

    if (PhysicsDiagnostics.ProbeStepWalkEnabled)       // ← new
        PhysicsDiagnostics.LogStepWalkAdjust(
            branch, offset, result,
            contactPlane: ci.ContactPlane,
            slidingValid: ci.SlidingNormalValid,
            slidingNormal: ci.SlidingNormal,
            collisionAngle: collisionAngle,
            walkInterp: sp.WalkInterp);

    return result;
}
```

**What this adds:** ONE log line per AdjustOffset call (probe-gated) naming the branch and showing Z gain. Nothing else changes — the math is identical.

- [ ] **Step 3: Run dotnet build to confirm the edit compiles**

```powershell
dotnet build src\AcDream.Core\AcDream.Core.csproj -c Debug
```

Expected: green build. The diff is additive (one new diagnostic helper + four single-line probe gates inside an existing method).

- [ ] **Step 4: Run dotnet test to confirm the apparatus tests stay green**

```powershell
dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Issue98CellarUpReplayTests"
```

Expected: all 7 Issue98CellarUpReplayTests pass (they document the bug; the failing-frame assertions still pin the current behavior). No new failures elsewhere.

- [ ] **Step 5: Commit the probe-only change**

```powershell
git add src\AcDream.Core\Physics\PhysicsDiagnostics.cs src\AcDream.Core\Physics\TransitionTypes.cs
git commit -m "diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset

Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK)
naming the branch taken (no-cp / no-cp-slide / slide-degenerate /
slide-crease / into-plane / away-plane, optionally +safety-push) plus
zGain = output.Z - input.Z.

No math changes — pure observability so the next capture can disambiguate
the three failure-mode hypotheses for the cellar-ramp climb cap at
world Z ≈ 92.79."
```

---

### Task 1.2: Capture with the new probe

- [ ] **Step 1: Confirm ACE is up and the test character is in the cottage cellar**

The character is `+Acdream` (server guid `0x5000000A`). Stand on the cellar ramp, facing the top.

- [ ] **Step 2: Launch the client with the step-walk probe enabled**

```powershell
$env:ACDREAM_DAT_DIR   = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE      = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_PROBE_STEP_WALK = "1"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Tee-Object -FilePath "a6-issue98-stepwalkadjust-$timestamp.out.log"
```

- [ ] **Step 3: User walks SLOWLY up the cellar ramp until stuck — no 180° turn-around**

The previous capture (negpoly log) polluted the trajectory because the user turned around and walked back. This capture must be a clean monotone climb.

- [ ] **Step 4: User closes the client (graceful — Alt-F4 / window close, not Ctrl-C)**

Per the logout-before-reconnect rules in CLAUDE.md — hard kill costs 3 minutes of ACE session recovery.

- [ ] **Step 5: Snapshot the capture to docs/research**

```powershell
$captureDir = "docs\research\2026-05-23-a6-captures\stepwalkadjust"
New-Item -ItemType Directory -Force $captureDir | Out-Null
Copy-Item "a6-issue98-stepwalkadjust-$timestamp.out.log" "$captureDir\acdream.log"
```

---

### Task 1.3: Analyze the new probe data

- [ ] **Step 1: Extract the [step-walk-adjust] lines from the climb portion only**

```powershell
Select-String "step-walk-adjust" "docs\research\2026-05-23-a6-captures\stepwalkadjust\acdream.log" |
    Select-Object -ExpandProperty Line |
    Set-Content "docs\research\2026-05-23-a6-captures\stepwalkadjust\adjust-only.log"
```

- [ ] **Step 2: Cross-correlate [step-walk-adjust] with [step-walk] site=after-adjust by line position**

For each ramp-climb tick, you should see the pattern:
```
[step-walk] site=after-adjust  cur=(...) req=(rX,rY,0)   adj=(aX,aY,aZ)  ...
[step-walk-adjust] branch=...  input=(rX,rY,0) output=(aX,aY,aZ) zGain=aZ  cp=(...) ...
```

- [ ] **Step 3: Classify the climb by branch token**

Walk forward through the lines from the start of the climb to the peak (world Z ≈ 92.79). Build a histogram:

```
branch=into-plane       — how many calls? Avg zGain?
branch=away-plane       — how many? Avg zGain?
branch=slide-crease     — how many? Avg zGain?
branch=no-cp            — how many? (means ContactPlaneValid cleared mid-climb)
+safety-push annotation — how often? At what zGain?
```

- [ ] **Step 4: Decide which Phase 2 branch the fix takes**

Decision tree (read the histogram + the climb-cap moment together):

| Observation | Implies fix target | Phase 2 branch |
|---|---|---|
| `into-plane` dominates the climb, +zGain ~0.2/call, then at peak the branch flips to `no-cp` or `away-plane` with zero zGain | **Target A: ContactPlane is being cleared / replaced at the ramp top.** Fix: investigate why the ramp's CP is dropping when the climb is incomplete. | **Branch A** |
| `into-plane` continues at peak but zGain becomes near-zero (offset.Y trends to zero) | **Target B: forward motion is being consumed elsewhere.** Either by step-up burning WalkInterp before AdjustOffset gets the offset, or by the offset being slid horizontally by a wall hit. Look at `[step-walk] site=before-insert` for a Y-collapsing pattern. | **Branch B** |
| `into-plane` at peak with correct zGain but CurPos doesn't advance | **Target C: the offset is computed correctly but never committed.** Look at `TransitionalInsert` / `Insert` for a path that returns before commit. | **Branch C** |
| Any branch with frequent `slide-crease` mid-climb | **Side-finding: a SlidingNormal is being set against the ramp.** Should not happen on a clean slope — points at a wall poly being mis-classified. | **Branch D (side-investigation, not the fix)** |

- [ ] **Step 5: Write a 1-page findings note**

Save to `docs/research/2026-05-23-a6-stepwalkadjust-findings.md`. Format:

```markdown
# A6.P3 #98 — [step-walk-adjust] capture analysis (YYYY-MM-DD)

## Climb branch histogram (90.00 → peak)
- into-plane: X calls, avg zGain=Y
- away-plane: ...
- ...

## At the climb cap (world Z ≈ ZZZ):
- Branch flipped from X to Y at log line NNN
- ContactPlane normal at cap: (...)
- WalkInterp at cap: ...

## Conclusion: Fix target is Branch A / B / C / D.

## Reason (one paragraph)
...
```

- [ ] **Step 6: Commit the findings note**

```powershell
git add docs\research\2026-05-23-a6-stepwalkadjust-findings.md docs\research\2026-05-23-a6-captures\stepwalkadjust\
git commit -m "research(phys): A6.P3 #98 — [step-walk-adjust] capture + findings

Identifies fix target Branch [A/B/C/D] for the cellar-up climb cap."
```

---

## Phase 2 — branch to fix target (data-driven)

**Do NOT execute Phase 2 until Phase 1's findings note exists and the user has reviewed it.** This is the explicit lesson from the 4-session failure pattern: shipping speculative fixes wastes a session each time.

Per CLAUDE.md (no-workarounds rule) the fix must address the root cause. Each branch below names the LIKELY fix shape. The actual code lands as a separate plan once Phase 1 confirms the branch.

### Branch A — ContactPlane clearing / replacement at ramp top

If the climb works while CP=ramp and FAILS when CP changes mid-climb:

**Investigate first (no code):**
- Where in `Transition.FindEnvCollisions` / `Transition.SetContactPlane` is the ramp CP dropped? Likely candidates: `[TransitionTypes.cs:1814](../../src/AcDream.Core/Physics/TransitionTypes.cs:1814)` (FindEnvCollisions write), `[TransitionTypes.cs:1837](../../src/AcDream.Core/Physics/TransitionTypes.cs:1837)` (post-stepdown write).
- Compare against retail's BPE pattern: retail's CP toggles cellar-floor → cottage-floor with **no intermediate** (per the divergence comparison table). It never sets CP to the ramp. Our code DOES. The fix MAY be making our ramp-CP behavior match retail (never set CP=ramp; set CP=cellar-floor while climbing, then transition CP=cottage-floor when the sphere is above cottage floor) — but this is a SHAPE-of-the-fix question; verify it's actually needed before changing the contract.

**Probable fix file:** [src/AcDream.Core/Physics/TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) around `FindEnvCollisions` (line 1700–1900).

**Acceptance:** Phase 1's findings note's branch histogram shows the failure point. The fix must produce a log where `cur` Z monotonically increases from 90.00 to ≥ 94.00 across the climb.

### Branch B — forward motion consumed before AdjustOffset

If `into-plane` continues at peak but zGain → 0 because input offset Y → 0:

**Investigate first:**
- Look at `[step-walk] site=before-insert` in the existing log around line 17458 at the peak. Compare the `req` value to the next tick's `req`. If `req.Y` decays to zero across consecutive ticks, the motion source itself is being cut. Trace back to where the per-tick offset is generated — `[PlayerMovementController](../../src/AcDream.Core/Physics/PlayerMovementController.cs)` and the input → velocity → offset chain.
- Compare against retail's BPF (adjust_sphere_to_plane) cadence — 431 hits over 35K BPs suggests retail re-projects relatively frequently but not in a way that zeros out motion. Our code might be over-projecting.

**Probable fix file:** [src/AcDream.Core/Physics/PlayerMovementController.cs](../../src/AcDream.Core/Physics/PlayerMovementController.cs) or the velocity-source upstream of the physics tick.

**Acceptance:** Same as Branch A.

### Branch C — offset computed but never committed (CurPos doesn't advance)

If `[step-walk-adjust] zGain` is correct per call but `[step-walk] cur` doesn't accumulate across calls:

**Investigate first:**
- `TransitionalInsert` in [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) — find the path that returns without committing `sp.CurPos += sp.GlobalOffset`. Likely guard: a collision state that retreats.
- The `[step-walk] delta=(0,0,0)` pattern across consecutive lines is the smoking gun if it persists at the peak.

**Probable fix file:** `TransitionTypes.cs`, the per-step commit at the bottom of the step loop (around line 689–760).

**Acceptance:** Same as Branch A.

### Branch D — sliding-normal mis-classification (side-investigation only)

If `slide-crease` appears mid-climb, the ramp polygon is being treated as a wall by some upstream call. This is unlikely to be the fix for #98 — slide is the right answer for walls, the question is which polygon triggered it.

**Investigate:** which polygon's normal got installed as `SlidingNormal`? Add a one-line probe at `ci.SlidingNormal = ...` write sites and capture again.

---

## Phase 3 — write the fix (TDD against replay tests)

Once Phase 1 names the branch and the findings note is committed, write a fresh plan for the fix:

`docs/superpowers/plans/2026-MM-DD-a6-p3-issue98-cellar-up-fix-impl.md`

Use [superpowers:test-driven-development](../../.claude/superpowers/skills/test-driven-development/SKILL.md) discipline:

1. **Flip the replay test assertions FIRST** so they document the FIXED behavior. Two assertions to invert:
   - `FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` — at minimum one of `OverlapsSphere` / `InsideEdges` must become `true`.
   - `FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` — at least one neighbour cell must accept.
2. Run `dotnet test`; the inverted assertions fail (red).
3. Implement the minimal fix in the file Phase 2 identified.
4. Run `dotnet test`; the inverted assertions pass (green).
5. Run the live client; the user verifies they can walk up out of the cottage cellar.

---

## Acceptance for Phase 1 (the only Phase landing in this plan)

- [ ] `dotnet build` green
- [ ] `dotnet test --filter "FullyQualifiedName~Issue98CellarUpReplayTests"` — 7 passing (no regressions)
- [ ] `dotnet test` overall — baseline 1167 + 8 maintained (no new failures)
- [ ] Diagnostic commit landed (one commit, additive only — no math/control-flow changes)
- [ ] Capture commit landed (acdream.log + findings note)
- [ ] Findings note names ONE branch (A / B / C / D) as the Phase 2 fix target

**Acceptance for Phase 3 (the eventual fix, captured here for the next session):**

- [ ] `Issue98CellarUpReplayTests.FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` — assertion inverted to require acceptance; test passes.
- [ ] `Issue98CellarUpReplayTests.FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` — assertion inverted to require sphere-overlap; test passes.
- [ ] `dotnet build` green.
- [ ] `dotnet test` baseline 1167 + 8 (or better) maintained.
- [ ] User walks up out of the Holtburg cottage cellar in the live client — **visual verification required**.

---

## What this plan does NOT do (per CLAUDE.md no-workarounds rule)

- Does not reattempt placement-insert bypasses in `BSPQuery.FindCollisions`, `Transition.FindEnvCollisions`, or `Transition.DoStepDown`. Six variants already tried; none worked.
- Does not reattempt cell-resolver tiebreaker changes in `PhysicsEngine.ResolveCellId`. Slice 3 already shipped a stickiness fix; the bug persists.
- Does not reattempt negative-side polygon handling. Reverted in `35b37df`; the neg-poly branch fired zero times in the failing log.
- Does not reattempt the bldg-check / IsLandblockBuilding flag propagation. Reverted in `35b37df`.
- Does not add suppression flags, grace periods, retry loops, or `if (problematicState) return` symptom guards.

If at any point during Phase 1 implementation you feel certain about the fix without having seen the new probe's branch histogram, **STOP**. The 4-session pattern was: convinced of diagnosis → ship fix → user reports "still can't pass." Verifying via the probe is what breaks the pattern.

---

## Self-review (writing-plans skill discipline)

**Spec coverage.** The user's prompt asks for: state altitudes (done in handoff), diagnostic-first verification (Task 1.1–1.3), branch-to-fix decision (Phase 2), TDD against replay tests (Phase 3), forbidden shortcut list (final section). ✓

**Placeholder scan.** No "TBD" / "add error handling" / "similar to Task N" left in the plan. Exact file paths and line numbers throughout. ✓

**Type consistency.** `LogStepWalkAdjust` matches `LogStepWalk`'s signature pattern. Branch tokens are stringly-typed but enumerated explicitly. `[step-walk-adjust]` prefix is distinct from `[step-walk]`. ✓

**Realism.** Phase 1 ships one diagnostic commit + one capture commit. ~30 minutes if everything goes smoothly. Phase 2 is research (no code). Phase 3 is the actual fix — separate plan. The whole arc fits in 1–2 working days, broken at the user-review checkpoint after Phase 1's findings note.

