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 or control-flow changes — pure observability so the next capture can disambiguate the three failure-mode hypotheses for the cellar-ramp climb cap. Re-reading the existing capture (a6-issue98-negpoly-...log) showed the sphere DOES climb 90.00 -> 92.79 (2.79 m gain), then caps, contradicting the divergence comparison's "no altitude gain" framing. The real question is what stops the climb at world Z ~= 92.79 with the cottage floor still 1.21 m higher. Existing [step-walk] probes wrap AdjustOffset; this new probe reveals which branch the projection takes. Fix plan with the four-branch decision tree at docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md. Test baseline maintained: 1167 + 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 KiB
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, <200ms) is the inner test loop. The cdb capture script (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 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:
- 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.
- Cell-set divergence. At world Z ≈ 92.79 the sphere overlaps cottage cell volumes, but our
CheckOtherCellseither 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. - WalkInterp depletion before forward motion applies. The peak
[step-walk]line at 17485 showswinterp=-0.0000afterDoStepDown. IfDoStepDownconsumed 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'sAdjustOffsetruns 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.
/// <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 at line 2635 (private Vector3 AdjustOffset(Vector3 offset)). The body currently has FOUR exit/branch points:
- no-cp path (line 2654–2659):
if (!ci.ContactPlaneValid) return result; - slide path (lines 2665–2676):
if (checkSlide) { ... result = ... } - into-plane path (lines 2677–2681):
else if (collisionAngle <= 0f) { result -= ... } - away-plane path (lines 2682–2687):
else { result -= ... }— same arithmetic asinto-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:
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
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
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
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
$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
$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
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:
# 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
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.SetContactPlaneis 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 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-insertin the existing log around line 17458 at the peak. Compare thereqvalue to the next tick'sreq. Ifreq.Ydecays 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 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:
TransitionalInsertin TransitionTypes.cs — find the path that returns without committingsp.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 discipline:
- Flip the replay test assertions FIRST so they document the FIXED behavior. Two assertions to invert:
FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges— at minimum one ofOverlapsSphere/InsideEdgesmust becometrue.FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable— at least one neighbour cell must accept.
- Run
dotnet test; the inverted assertions fail (red). - Implement the minimal fix in the file Phase 2 identified.
- Run
dotnet test; the inverted assertions pass (green). - 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 buildgreendotnet test --filter "FullyQualifiedName~Issue98CellarUpReplayTests"— 7 passing (no regressions)dotnet testoverall — 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 buildgreen.dotnet testbaseline 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, orTransition.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) returnsymptom 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.