# 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 /// /// A6.P3 issue #98 (2026-05-23) — focused probe INSIDE AdjustOffset /// revealing which branch was taken and the Z gain per call. Pair with /// [step-walk] site=after-adjust at the call site to triangulate /// where the projection ends up. Caller MUST guard with /// if (!ProbeStepWalkEnabled) return; before calling. /// 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.