docs(plan): Phase L.3.1+L.3.2 PositionManager + retail-faithful jump plan
6-task plan with subagent dispatch on Tasks 1, 3, 5: - Task 1: PositionManager class + 6 unit tests (subagent) - Task 2: Plumb IsGrounded through EntityPositionUpdate (parent, ~5 lines) - Task 3: Retail-faithful per-frame remote tick (subagent — biggest: RemoteMotion.Position field + OnLivePositionUpdated rewrite [airborne no-op + landing transition + grounded routing] + TickAnimations rewrite [PositionManager.ComputeOffset + UpdatePhysicsInternal]) - Task 4: USER GATE (visual verification with retail observer) - Task 5: Cleanup commit (subagent, parallel with 6) - Task 6: Roadmap + spec status update (parent, parallel with 5) Each task has TDD-style steps with exact file paths, code blocks, and commit messages. Spec atc4446e7lists L.3.1's already-shipped 6 commits; this plan picks up from the revert at1641d6e. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c4446e76fb
commit
d063ac884d
1 changed files with 785 additions and 0 deletions
785
docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md
Normal file
785
docs/superpowers/plans/2026-05-02-l3-positionmanager-jump.md
Normal file
|
|
@ -0,0 +1,785 @@
|
|||
# Phase L.3.1+L.3.2 Combined — PositionManager + Retail-Faithful Remote Tick
|
||||
|
||||
> **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:** Add the PositionManager combiner (animation root motion + InterpolationManager corrections) that was originally deferred to L.3.2, plumb `IsGrounded` through `EntityPositionUpdate`, and rewrite the per-frame remote tick + `OnLivePositionUpdated` env-var-on branches to match retail's `MoveOrTeleport` semantics. This eliminates the 1-Hz chop and endless-jump bugs surfaced during Task 7 visual verification.
|
||||
|
||||
**Architecture:** Pure-data `PositionManager.ComputeOffset(dt, body.Position, seqVel, ori, interp, maxSpeed) → Vector3` returns the per-frame world-space delta to add to body.Position. Combines (a) animation root motion = `seqVel * dt` rotated by body orientation with (b) `InterpolationManager.AdjustOffset` correction. Per-frame tick always runs all steps (matches retail `UpdateObjectInternal`). `OnLivePositionUpdated` routes per `MoveOrTeleport`: airborne → no-op; landing transition → snap + clear flags; grounded → enqueue or slide-snap. Server is authoritative for airborne arcs (no local prediction fights gravity).
|
||||
|
||||
**Tech Stack:** C# / .NET 10 / xUnit. No new NuGet deps. Tests at `tests/AcDream.Core.Tests/Physics/*Tests.cs`.
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`](../specs/2026-05-02-l3-remote-entity-motion-design.md) (committed `c4446e7`).
|
||||
|
||||
**Already shipped (do NOT rebuild):**
|
||||
- `f43f168` + `927636e` Task 1 — InterpolationManager
|
||||
- `9c5634a` + `5b26d28` Task 2 — MotionInterpreter.GetMaxSpeed
|
||||
- `517a3ce` Task 3 — RemoteMotion.Interp field
|
||||
- `062e19f` Task 4 — OnLivePositionUpdated env-var routing v1
|
||||
- `ae79e34` Task 5 — Per-frame Interp.AdjustOffset v1
|
||||
- `e08accf` Task 6 — VectorUpdate.Omega
|
||||
- `1641d6e` revert of band-aids
|
||||
- `c4446e7` spec revision
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/AcDream.Core/Physics/PositionManager.cs` | **CREATE** | Pure-function combiner: animation root motion + Interp correction. ~50 lines including XML docs. |
|
||||
| `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` | **CREATE** | 6 unit tests against pure `ComputeOffset`. |
|
||||
| `src/AcDream.Core.Net/Messages/UpdatePosition.cs` | **MODIFY** | Add `IsGrounded` to `Parsed` record, populate from `flags & PositionFlags.IsGrounded`. ~3 lines. |
|
||||
| `src/AcDream.Core.Net/WorldSession.cs` | **MODIFY** | Add `IsGrounded` to `EntityPositionUpdate` record, pass through in PositionUpdated invoke. ~2 lines. |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | (a) `RemoteMotion` gains `Position` field; (b) rewrite `OnLivePositionUpdated` env-var-on branch (airborne no-op + landing transition + grounded routing); (c) rewrite `TickAnimations` env-var-on branch (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). |
|
||||
| (cleanup commit) `src/AcDream.App/Rendering/GameWindow.cs` | **MODIFY** | Delete env-var dual paths; delete `RemoteMotion` soft-snap residual fields. |
|
||||
| `docs/plans/2026-04-11-roadmap.md` | **MODIFY** (cleanup phase) | Update Phase L.3 entry to reflect L.3.1+L.3.2 combined. |
|
||||
| `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` | **MODIFY** (cleanup phase) | Mark L.3.1+L.3.2 as SHIPPED. |
|
||||
|
||||
---
|
||||
|
||||
## Task Decomposition Overview
|
||||
|
||||
```
|
||||
Task 1 — PositionManager class + 6 tests (subagent)
|
||||
↓
|
||||
Task 2 — Plumb IsGrounded through EntityPositionUpdate (parent, 2 files, ~5 lines)
|
||||
↓
|
||||
Task 3 — Retail-faithful per-frame remote tick (subagent — biggest change)
|
||||
↓
|
||||
Task 4 — USER GATE: visual verification with retail observer
|
||||
↓ (after sign-off)
|
||||
┌─ DISPATCH IN PARALLEL ──────────────────┐
|
||||
│ Task 5: Cleanup commit (subagent) │
|
||||
│ Task 6: Roadmap + spec status (parent) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — PositionManager class + 6 unit tests
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose).
|
||||
|
||||
**Files:**
|
||||
- Create: `src/AcDream.Core/Physics/PositionManager.cs`
|
||||
- Create: `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs`
|
||||
|
||||
**Subagent dispatch prompt** (use `general-purpose` agent type, Sonnet):
|
||||
|
||||
> You are implementing Task 1 of Phase L.3.1+L.3.2 in the acdream codebase. Read the spec at `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` section "L.3.2 architecture" → "New file — `src/AcDream.Core/Physics/PositionManager.cs`".
|
||||
>
|
||||
> **What to build:**
|
||||
>
|
||||
> Create `src/AcDream.Core/Physics/PositionManager.cs`:
|
||||
>
|
||||
> ```csharp
|
||||
> using System.Numerics;
|
||||
>
|
||||
> namespace AcDream.Core.Physics;
|
||||
>
|
||||
> /// <summary>
|
||||
> /// Per-frame combiner for remote-entity motion: animation root motion
|
||||
> /// + InterpolationManager catch-up correction. Pure function — no
|
||||
> /// side effects, no hidden state.
|
||||
> ///
|
||||
> /// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730):
|
||||
> /// rootOffset = CPartArray::Update(dt) // animation
|
||||
> /// PositionManager::adjust_offset(rootOffset) // adds correction
|
||||
> /// frame.origin += rootOffset
|
||||
> ///
|
||||
> /// In acdream the animation root motion is sourced from
|
||||
> /// AnimationSequencer.CurrentVelocity (body-local velocity from the
|
||||
> /// active locomotion cycle). We rotate that by the body's orientation
|
||||
> /// to get a world-space delta, then add the InterpolationManager's
|
||||
> /// world-space correction.
|
||||
> /// </summary>
|
||||
> public sealed class PositionManager
|
||||
> {
|
||||
> /// <summary>
|
||||
> /// Compute the per-frame world-space delta to add to body.Position.
|
||||
> /// </summary>
|
||||
> /// <param name="dt">Per-frame delta time, seconds.</param>
|
||||
> /// <param name="currentBodyPosition">Body's current world-space position.</param>
|
||||
> /// <param name="seqVel">
|
||||
> /// Body-local velocity from the active animation cycle
|
||||
> /// (from <c>AnimationSequencer.CurrentVelocity</c>); pass
|
||||
> /// <c>Vector3.Zero</c> if the entity has no sequencer or is on a
|
||||
> /// non-locomotion cycle.
|
||||
> /// </param>
|
||||
> /// <param name="ori">Body orientation; used to rotate seqVel from body-local to world.</param>
|
||||
> /// <param name="interp">The remote's InterpolationManager (for AdjustOffset call).</param>
|
||||
> /// <param name="maxSpeed">From <c>MotionInterpreter.GetMaxSpeed()</c> — passed to AdjustOffset for the catch-up clamp.</param>
|
||||
> public Vector3 ComputeOffset(
|
||||
> double dt,
|
||||
> Vector3 currentBodyPosition,
|
||||
> Vector3 seqVel,
|
||||
> Quaternion ori,
|
||||
> InterpolationManager interp,
|
||||
> float maxSpeed)
|
||||
> {
|
||||
> // Step 1: animation root motion (body-local → world).
|
||||
> Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
> Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
|
||||
>
|
||||
> // Step 2: interpolation correction (world-space already).
|
||||
> Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
|
||||
>
|
||||
> // Step 3: combined delta.
|
||||
> return rootMotionWorld + correction;
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Create `tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs` with EXACTLY these 6 test names (these are the contract):
|
||||
>
|
||||
> 1. `ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion`
|
||||
> - seqVel = Vector3.Zero, no enqueued nodes in interp
|
||||
> - Assert: returned offset == Vector3.Zero
|
||||
>
|
||||
> 2. `ComputeOffset_AnimationOnly_Forward_BodyAdvances`
|
||||
> - seqVel = (0, 4, 0) (4 m/s forward), ori = Quaternion.Identity, dt = 0.1
|
||||
> - Assert: returned offset == (0, 0.4, 0) (forward 0.4m)
|
||||
>
|
||||
> 3. `ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth`
|
||||
> - seqVel = (0, 4, 0), ori = quaternion rotating +Y → -Y (180° around Z), dt = 0.1
|
||||
> - Assert: returned offset.Y ≈ -0.4 (south)
|
||||
>
|
||||
> 4. `ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue`
|
||||
> - seqVel = Vector3.Zero, interp has 1 enqueued node 1m ahead, dt = 0.1, maxSpeed = 4f
|
||||
> - Expected: AdjustOffset returns the catch-up step (≤ 1m, clamped); ComputeOffset returns same
|
||||
>
|
||||
> 5. `ComputeOffset_BothActive_Combined`
|
||||
> - seqVel = (0, 4, 0) — root motion (0, 0.4, 0)
|
||||
> - interp has node 1m ahead — AdjustOffset returns ~Vector3.UnitY * step
|
||||
> - Assert: returned offset == rootMotion + correction
|
||||
>
|
||||
> 6. `ComputeOffset_LocalToWorldRotation_Yaw90`
|
||||
> - seqVel = (0, 1, 0) (forward 1 m/s in body frame)
|
||||
> - ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f) (yaw +90°)
|
||||
> - dt = 1
|
||||
> - Verify the rotation is applied correctly. With yaw +90° around Z, body-local +Y rotates to world... compute the expected and assert with precision: 4.
|
||||
>
|
||||
> Use xUnit, `namespace AcDream.Core.Tests.Physics;`, file-private fakes via `file sealed class` if needed. Read `tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs` for the existing pattern.
|
||||
>
|
||||
> Note: Tests #4 and #5 need a real `InterpolationManager` (not a fake) because PositionManager calls AdjustOffset directly. Construct one inline in each test, Enqueue what you need, and call ComputeOffset.
|
||||
>
|
||||
> **Build + test:**
|
||||
>
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug --nologo
|
||||
> dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager"
|
||||
> ```
|
||||
>
|
||||
> Both green. 6 tests pass.
|
||||
>
|
||||
> **Commit:**
|
||||
>
|
||||
> ```bash
|
||||
> git add src/AcDream.Core/Physics/PositionManager.cs tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> feat(physics): PositionManager combiner class + 6 unit tests (L.3.2)
|
||||
>
|
||||
> Pure-function ComputeOffset(dt, pos, seqVel, ori, interp, maxSpeed) →
|
||||
> Vector3. Combines animation root motion (seqVel × dt rotated by body
|
||||
> orientation) with InterpolationManager.AdjustOffset world-space
|
||||
> correction. Mirrors retail CPhysicsObj::UpdateObjectInternal
|
||||
> (acclient @ 0x00513730).
|
||||
>
|
||||
> Composed into RemoteMotion in subsequent task (L.3.1+L.3.2 Task 3);
|
||||
> not yet consumed.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Self-review checklist:**
|
||||
> - [ ] `PositionManager` is public sealed class
|
||||
> - [ ] `ComputeOffset` is the only public method (no other API)
|
||||
> - [ ] All 6 tests have the exact names listed
|
||||
> - [ ] Tests #4 and #5 use a real `InterpolationManager`
|
||||
> - [ ] No game/window/sequencer dependencies — only `System.Numerics` + `AcDream.Core.Physics.InterpolationManager`
|
||||
> - [ ] Build clean, all 6 tests pass
|
||||
> - [ ] Commit references "L.3.2"
|
||||
>
|
||||
> **Report:**
|
||||
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
> - What you built (1-2 sentences)
|
||||
> - Test results (count, any deviations)
|
||||
> - Files changed
|
||||
> - Concerns (if any)
|
||||
|
||||
**Steps for the parent (controller):**
|
||||
|
||||
- [ ] **Step 1.1: Dispatch the implementer subagent** using the prompt above.
|
||||
- [ ] **Step 1.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.Core/Physics/PositionManager.cs
|
||||
```
|
||||
Expected: commit message starts with `feat(physics): PositionManager combiner class`.
|
||||
- [ ] **Step 1.3: Re-run tests in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build --nologo --filter "FullyQualifiedName~PositionManager"
|
||||
```
|
||||
Expected: 6 tests pass.
|
||||
- [ ] **Step 1.4: Dispatch spec compliance reviewer** (use `general-purpose`, Sonnet). Verify the 6 tests have the EXACT names listed and verify `ComputeOffset` algorithm matches the spec's pseudocode.
|
||||
- [ ] **Step 1.5: Dispatch code quality reviewer** (use `superpowers:code-reviewer`). Check for: API surface (only ComputeOffset public), test quality, no superfluous deps.
|
||||
- [ ] **Step 1.6: Address review issues if any.** If issues found, dispatch fix subagent. Re-review.
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Plumb `IsGrounded` through `EntityPositionUpdate`
|
||||
|
||||
**Owner:** Parent. Mechanical edit, ~5 lines across 2 files.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:62-69` (add `IsGrounded` to `Parsed` record)
|
||||
- Modify: `src/AcDream.Core.Net/Messages/UpdatePosition.cs:166` (populate `IsGrounded` in the constructor call)
|
||||
- Modify: `src/AcDream.Core.Net/WorldSession.cs:110-113` (add `IsGrounded` to `EntityPositionUpdate` record)
|
||||
- Modify: `src/AcDream.Core.Net/WorldSession.cs:711-714` (pass `posUpdate.Value.IsGrounded` through)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 2.1: Read existing `UpdatePosition.Parsed` record + TryParse return**
|
||||
```bash
|
||||
grep -n "public readonly record struct Parsed\|return new Parsed" "C:/Users/erikn/source/repos/acdream/src/AcDream.Core.Net/Messages/UpdatePosition.cs"
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Add `IsGrounded` field to `UpdatePosition.Parsed`**
|
||||
|
||||
Edit `src/AcDream.Core.Net/Messages/UpdatePosition.cs` (~line 62):
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
uint? PlacementId,
|
||||
ushort InstanceSequence = 0,
|
||||
ushort TeleportSequence = 0,
|
||||
ushort ForcePositionSequence = 0);
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
uint? PlacementId,
|
||||
bool IsGrounded,
|
||||
ushort InstanceSequence = 0,
|
||||
ushort TeleportSequence = 0,
|
||||
ushort ForcePositionSequence = 0);
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3: Populate `IsGrounded` in the `Parsed` constructor call (~line 166)**
|
||||
|
||||
Find the line `return new Parsed(guid, serverPos, velocity, placementId,` (~line 166) and change to pass `(flags & PositionFlags.IsGrounded) != 0` as the new IsGrounded argument. Looks roughly like:
|
||||
|
||||
```csharp
|
||||
return new Parsed(guid, serverPos, velocity, placementId,
|
||||
(flags & PositionFlags.IsGrounded) != 0,
|
||||
instSeq, teleSeq, forceSeq);
|
||||
```
|
||||
|
||||
(Verify the trailing-arg layout against what's actually there; preserve any existing trailing arguments.)
|
||||
|
||||
- [ ] **Step 2.4: Add `IsGrounded` field to `WorldSession.EntityPositionUpdate`**
|
||||
|
||||
Edit `src/AcDream.Core.Net/WorldSession.cs:110`:
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
public readonly record struct EntityPositionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity);
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
public readonly record struct EntityPositionUpdate(
|
||||
uint Guid,
|
||||
CreateObject.ServerPosition Position,
|
||||
System.Numerics.Vector3? Velocity,
|
||||
bool IsGrounded);
|
||||
```
|
||||
|
||||
- [ ] **Step 2.5: Pass `IsGrounded` through in PositionUpdated invoke (~line 711)**
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||
posUpdate.Value.Guid,
|
||||
posUpdate.Value.Position,
|
||||
posUpdate.Value.Velocity));
|
||||
```
|
||||
To:
|
||||
```csharp
|
||||
PositionUpdated?.Invoke(new EntityPositionUpdate(
|
||||
posUpdate.Value.Guid,
|
||||
posUpdate.Value.Position,
|
||||
posUpdate.Value.Velocity,
|
||||
posUpdate.Value.IsGrounded));
|
||||
```
|
||||
|
||||
- [ ] **Step 2.6: Build + test**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: 0 build errors. Same 4 pre-existing test failures, no new failures.
|
||||
|
||||
- [ ] **Step 2.7: Commit**
|
||||
```bash
|
||||
git add src/AcDream.Core.Net/Messages/UpdatePosition.cs src/AcDream.Core.Net/WorldSession.cs
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(net): plumb IsGrounded through EntityPositionUpdate (L.3.2)
|
||||
|
||||
PositionFlags.IsGrounded (0x04) was already parsed by UpdatePosition
|
||||
but not exposed through Parsed record or EntityPositionUpdate.
|
||||
Adds the bool field to both records so OnLivePositionUpdated can
|
||||
consume it for retail-faithful MoveOrTeleport routing
|
||||
(acclient @ 0x00516330: has_contact=false → no-op during airborne arc).
|
||||
|
||||
Consumed in subsequent task (L.3.1+L.3.2 Task 3).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Retail-faithful per-frame remote tick
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Largest task — touches 3 distinct sites in `GameWindow.cs`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (RemoteMotion class line ~224 + OnLivePositionUpdated env-var branch + TickAnimations env-var branch)
|
||||
|
||||
**Subagent dispatch prompt:**
|
||||
|
||||
> You are implementing Task 3 of Phase L.3.1+L.3.2 in the acdream codebase. This task rewrites two env-var-gated branches in `src/AcDream.App/Rendering/GameWindow.cs` to consume the new PositionManager (Task 1) and IsGrounded plumbing (Task 2).
|
||||
>
|
||||
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main branch — direct-to-main per CLAUDE.md.
|
||||
>
|
||||
> **Spec:** `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` "L.3.2 architecture" sections.
|
||||
>
|
||||
> **Three changes in `GameWindow.cs`:**
|
||||
>
|
||||
> ### Change 1: `RemoteMotion` class gains `Position` field
|
||||
>
|
||||
> Find the existing `Interp` field (added in commit `517a3ce`). Right after it, add:
|
||||
>
|
||||
> ```csharp
|
||||
> /// <summary>
|
||||
> /// Per-frame combiner for animation root motion + InterpolationManager
|
||||
> /// correction (Phase L.3.2). Consumed in TickAnimations to compute the
|
||||
> /// per-frame body.Position delta.
|
||||
> /// </summary>
|
||||
> public AcDream.Core.Physics.PositionManager Position { get; } =
|
||||
> new AcDream.Core.Physics.PositionManager();
|
||||
> ```
|
||||
>
|
||||
> ### Change 2: Rewrite `OnLivePositionUpdated` env-var-on branch
|
||||
>
|
||||
> Find the existing env-var-on block in `OnLivePositionUpdated` (was added at commit `062e19f`). It currently looks roughly like:
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> rmState.Body.Orientation = rot;
|
||||
> // teleport check, dist check, etc.
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Replace the env-var-on body with this new logic:
|
||||
>
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> // Orientation always snaps on receipt — the InterpolationManager
|
||||
> // walks position only; heading would otherwise lag the queue.
|
||||
> rmState.Body.Orientation = rot;
|
||||
>
|
||||
> // ── AIRBORNE NO-OP ────────────────────────────────────────────
|
||||
> // Mirrors retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
|
||||
> // when has_contact==0, return false (don't touch body, don't queue).
|
||||
> // body.Velocity (set once by OnLiveVectorUpdated at jump start) keeps
|
||||
> // integrating gravity via per-frame UpdatePhysicsInternal. Server is
|
||||
> // authoritative for the arc; we don't predict it locally.
|
||||
> if (!update.IsGrounded)
|
||||
> return;
|
||||
>
|
||||
> // ── LANDING TRANSITION ─────────────────────────────────────────
|
||||
> // First IsGrounded=true UP after rmState.Airborne signals landed.
|
||||
> // Clear airborne flags, hard-snap to authoritative landing position,
|
||||
> // clear interpolation queue (any pre-jump waypoints are stale).
|
||||
> if (rmState.Airborne)
|
||||
> {
|
||||
> rmState.Airborne = false;
|
||||
> rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
> rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
> rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
> | AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
> rmState.Interp.Clear();
|
||||
> rmState.Body.Position = worldPos;
|
||||
> return;
|
||||
> }
|
||||
>
|
||||
> // ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ────────────
|
||||
> const float MaxPhysicsDistance = 96f;
|
||||
> var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||
> float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos);
|
||||
>
|
||||
> if (dist > MaxPhysicsDistance)
|
||||
> {
|
||||
> // Beyond view bubble: SetPositionSimple slide-snap. Clear queue.
|
||||
> rmState.Interp.Clear();
|
||||
> rmState.Body.Position = worldPos;
|
||||
> }
|
||||
> else
|
||||
> {
|
||||
> // Within view bubble: enqueue waypoint for adjust_offset to walk to.
|
||||
> // PositionManager (called per-frame in TickAnimations) handles the
|
||||
> // actual body advancement — mix of animation root motion + queue
|
||||
> // correction.
|
||||
> float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||
> rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
||||
> }
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> The legacy `else` branch (env-var unset) STAYS UNCHANGED.
|
||||
>
|
||||
> If `ExtractYawFromQuaternion` doesn't exist anymore (it might have been removed in the revert), re-add it near the original location (search for it in commit `062e19f`'s diff). The body is:
|
||||
> ```csharp
|
||||
> private static float ExtractYawFromQuaternion(System.Numerics.Quaternion q)
|
||||
> {
|
||||
> // Standard z-up yaw extraction: atan2(2(wz + xy), 1 - 2(y² + z²))
|
||||
> return MathF.Atan2(2f * (q.W * q.Z + q.X * q.Y),
|
||||
> 1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> ### Change 3: Rewrite `TickAnimations` env-var-on branch
|
||||
>
|
||||
> Find the existing env-var-on block in the per-frame remote tick (added at commit `ae79e34`). It currently looks roughly like:
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> if (rm.Interp.IsActive) {
|
||||
> float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
> Vector3 delta = rm.Interp.AdjustOffset((double)dt, rm.Body.Position, maxSpeed);
|
||||
> rm.Body.Position += delta;
|
||||
> }
|
||||
> rm.Body.UpdatePhysicsInternal(dt);
|
||||
> // entity write-back
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Replace with PositionManager call:
|
||||
>
|
||||
> ```csharp
|
||||
> if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
> {
|
||||
> // Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal
|
||||
> // (acclient @ 0x00513730):
|
||||
> // 1+2. animation root motion + interpolation correction (combined)
|
||||
> // 3. physics integration (gravity for airborne; no-op for grounded)
|
||||
> System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity
|
||||
> ?? System.Numerics.Vector3.Zero;
|
||||
> float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
> System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
|
||||
> dt: (double)dt,
|
||||
> currentBodyPosition: rm.Body.Position,
|
||||
> seqVel: seqVel,
|
||||
> ori: rm.Body.Orientation,
|
||||
> interp: rm.Interp,
|
||||
> maxSpeed: maxSpeed);
|
||||
> rm.Body.Position += offset;
|
||||
> rm.Body.UpdatePhysicsInternal(dt);
|
||||
> // KEEP whatever entity write-back lines were here (ae.Entity.Position = ..., etc.)
|
||||
> }
|
||||
> else
|
||||
> {
|
||||
> // EXISTING legacy path UNCHANGED
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> The `else` branch (legacy path) stays UNCHANGED.
|
||||
>
|
||||
> **Build + test:**
|
||||
>
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
> dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
> ```
|
||||
>
|
||||
> Expected: 0 build errors. Same 4 pre-existing failures (`DispatcherToMovementIntegrationTests` + `BSPStepUpTests` — these are not related to L.3 work). No NEW failures.
|
||||
>
|
||||
> **Commit:**
|
||||
>
|
||||
> ```bash
|
||||
> git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2)
|
||||
>
|
||||
> Combines PositionManager (Task 1) + IsGrounded plumbing (Task 2) into
|
||||
> the per-frame remote motion path. Three changes in GameWindow.cs,
|
||||
> all gated behind ACDREAM_INTERP_MANAGER=1:
|
||||
>
|
||||
> 1. RemoteMotion gains Position field (PositionManager instance).
|
||||
>
|
||||
> 2. OnLivePositionUpdated env-var branch rewritten to mirror retail
|
||||
> CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330):
|
||||
> - orientation snap-on-receipt (PositionManager handles position only)
|
||||
> - airborne (!IsGrounded) → no-op (server is authoritative for arc;
|
||||
> body.Velocity from VectorUpdate integrates gravity locally)
|
||||
> - landing transition (first IsGrounded=true after Airborne) →
|
||||
> clear airborne flags, hard-snap to landing pos, clear queue
|
||||
> - grounded routing: dist > 96m → slide-snap; dist ≤ 96m → enqueue
|
||||
>
|
||||
> 3. TickAnimations env-var branch rewritten to use PositionManager:
|
||||
> body.Position += PositionManager.ComputeOffset(dt, pos, seqVel,
|
||||
> ori, interp, maxSpeed); body.UpdatePhysicsInternal(dt) for gravity.
|
||||
>
|
||||
> Replaces the L.3.1-only AdjustOffset-only path. Legacy (env-var off)
|
||||
> path unchanged.
|
||||
>
|
||||
> Cleanup commit (next sub-task) deletes the env-var dual paths after
|
||||
> visual verification.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Self-review checklist:**
|
||||
> - [ ] `RemoteMotion.Position` field added (alongside existing `Interp`)
|
||||
> - [ ] `OnLivePositionUpdated` env-var branch has 3 sub-branches: airborne return, landing transition, grounded routing (snap or enqueue)
|
||||
> - [ ] `OnLivePositionUpdated` legacy `else` branch UNCHANGED
|
||||
> - [ ] `TickAnimations` env-var branch uses `PositionManager.ComputeOffset` exclusively (no direct `AdjustOffset` call)
|
||||
> - [ ] `TickAnimations` legacy `else` branch UNCHANGED
|
||||
> - [ ] `ExtractYawFromQuaternion` helper present (re-add if missing)
|
||||
> - [ ] `OnLiveVectorUpdated` UNTOUCHED (it already does the right thing)
|
||||
> - [ ] Build clean, same 4 pre-existing failures
|
||||
>
|
||||
> **Report:**
|
||||
> - Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||
> - Lines changed (with file:line refs)
|
||||
> - Test count
|
||||
> - Concerns (if any)
|
||||
>
|
||||
> If the existing legacy `else` path is so tangled that you can't safely rewrite the env-var branch without disturbing it, REPORT BLOCKED with specifics.
|
||||
|
||||
**Steps for the parent:**
|
||||
|
||||
- [ ] **Step 3.1: Dispatch the implementer subagent** using the prompt above.
|
||||
- [ ] **Step 3.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat src/AcDream.App/Rendering/GameWindow.cs
|
||||
```
|
||||
- [ ] **Step 3.3: Build + test in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo && dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: 0 build errors. Same 4 pre-existing failures.
|
||||
- [ ] **Step 3.4: Spec compliance review** (general-purpose subagent). Verify the rewrite matches the spec's pseudocode exactly. Verify legacy `else` paths are byte-for-byte unchanged.
|
||||
- [ ] **Step 3.5: Code quality review** (`superpowers:code-reviewer`). Specifically check: orientation snap is in ALL routing paths; airborne no-op is the FIRST gate; landing transition resets all the right flags; ExtractYawFromQuaternion is correct.
|
||||
- [ ] **Step 3.6: Address review issues if any.** Fix subagent + re-review.
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — USER GATE: visual verification
|
||||
|
||||
**Owner:** User. Cannot be automated.
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 4.1: Kill any running acdream**
|
||||
```powershell
|
||||
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 8
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Launch acdream with `ACDREAM_INTERP_MANAGER=1`**
|
||||
```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_INTERP_MANAGER = "1"
|
||||
dotnet run --project C:\Users\erikn\source\repos\acdream\src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "C:\Users\erikn\source\repos\acdream\.claude\worktrees\jovial-blackburn-773942\launch.log"
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Visual test matrix** with parallel retail observer of `+Acdream`. On the retail side, walk + run + jump + turn the toon and verify:
|
||||
|
||||
| Scenario | Expected |
|
||||
|---|---|
|
||||
| Walk forward 5 sec | acdream observer sees smooth glide, NO 1-Hz popping |
|
||||
| Walk backward 5 sec | smooth glide backward (regression check vs commit `17a9ff1`) |
|
||||
| Strafe left/right 5 sec each | smooth glide sideways |
|
||||
| Stop, then run forward 5 sec | smooth glide at run speed |
|
||||
| Jump from standstill 2-3× | curved arc, lands cleanly, NO endless rise |
|
||||
| Jump while running 2-3× | arc preserves forward motion, lands cleanly |
|
||||
| Turn quickly while running | heading tracks smoothly (not stuck at login direction) |
|
||||
|
||||
- [ ] **Step 4.4: User signs off OR files a regression**
|
||||
- If smooth + jumps land + turning works → proceed to Tasks 5+6.
|
||||
- If anything regresses → describe the symptom; parent dispatches a fix subagent or unsets the env-var for instant rollback.
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — Cleanup commit (parallel with Task 6)
|
||||
|
||||
**Owner:** Sonnet subagent (general-purpose). Independent of Task 6.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (delete env-var dual paths + soft-snap fields)
|
||||
|
||||
**Subagent dispatch prompt:**
|
||||
|
||||
> You are implementing Task 5 of Phase L.3.1+L.3.2: cleanup. The user has visually verified that `ACDREAM_INTERP_MANAGER=1` works correctly. Now collapse the dual-path scaffolding.
|
||||
>
|
||||
> **Repo:** `C:/Users/erikn/source/repos/acdream` — main — direct-to-main per CLAUDE.md.
|
||||
>
|
||||
> **What to do in `src/AcDream.App/Rendering/GameWindow.cs`:**
|
||||
>
|
||||
> 1. **In `OnLivePositionUpdated`**: delete the `if (Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { ... return; }` wrapper. Keep ONLY the new logic inside it. Delete the legacy hard-snap path that came after.
|
||||
>
|
||||
> 2. **In `TickAnimations` (per-frame remote tick)**: delete the `if/else` env-var gate. Keep ONLY the new path (`PositionManager.ComputeOffset` + `UpdatePhysicsInternal`). Delete the legacy `apply_current_movement` + `force-OnWalkable` + Euler-extrapolate code in the `else` branch.
|
||||
>
|
||||
> 3. **In the `RemoteMotion` class** (~line 224): delete `SnapResidualDecayRate` and any soft-snap residual fields. Search for `_snapResidual`, `SnapResidualDecayRate`, `SoftSnap`. Also delete any related code in the call sites.
|
||||
>
|
||||
> 4. **Search for any remaining `ACDREAM_INTERP_MANAGER` references** in the codebase and confirm zero remain:
|
||||
> ```bash
|
||||
> grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
|
||||
> ```
|
||||
> Expected: no output.
|
||||
>
|
||||
> **Build + test:**
|
||||
> ```bash
|
||||
> cd C:/Users/erikn/source/repos/acdream
|
||||
> dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
||||
> dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
> ```
|
||||
> Expected: 0 build errors. Same 4 pre-existing failures, no new ones.
|
||||
>
|
||||
> **Commit:**
|
||||
> ```bash
|
||||
> git add src/AcDream.App/Rendering/GameWindow.cs
|
||||
> git commit -m "$(cat <<'EOF'
|
||||
> chore(motion): remove ACDREAM_INTERP_MANAGER flag + dead legacy paths (L.3.1+L.3.2 cleanup)
|
||||
>
|
||||
> User has visually verified the new PositionManager + IsGrounded
|
||||
> routing path works correctly. Collapses the env-var dual-path:
|
||||
> deletes legacy hard-snap + apply_current_movement + Euler-extrapolate
|
||||
> code from OnLivePositionUpdated and the per-frame remote tick.
|
||||
> Deletes SnapResidualDecayRate + soft-snap residual fields from
|
||||
> RemoteMotion.
|
||||
>
|
||||
> Single retail-faithful path remains. ~80 lines net deletion.
|
||||
>
|
||||
> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
> EOF
|
||||
> )"
|
||||
> ```
|
||||
>
|
||||
> **Report:**
|
||||
> - Status, line counts deleted, files touched, test results.
|
||||
|
||||
**Steps for the parent:**
|
||||
|
||||
- [ ] **Step 5.1: Dispatch the cleanup subagent in parallel with Task 6** (one message, two Agent tool calls).
|
||||
- [ ] **Step 5.2: Verify the commit landed**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && git log -1 --stat
|
||||
```
|
||||
- [ ] **Step 5.3: Confirm zero env-var references remain**
|
||||
```bash
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" "C:/Users/erikn/source/repos/acdream/src/" 2>&1
|
||||
```
|
||||
Expected: no output.
|
||||
- [ ] **Step 5.4: Re-run all tests in parent**
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream && dotnet test --no-build --nologo 2>&1 | tail -6
|
||||
```
|
||||
Expected: same baseline.
|
||||
|
||||
---
|
||||
|
||||
## Task 6 — Roadmap + spec status update (parallel with Task 5)
|
||||
|
||||
**Owner:** Parent. Mechanical doc updates.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-04-11-roadmap.md` (Phase L.3 entry — mark L.3.1+L.3.2 SHIPPED)
|
||||
- Modify: `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md` (add SHIPPED status banner)
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] **Step 6.1: Find the Phase L.3 entry in the roadmap**
|
||||
```bash
|
||||
grep -n "Phase L.3\|L.3.1\|L.3.2\|L.3.3" "C:/Users/erikn/source/repos/acdream/docs/plans/2026-04-11-roadmap.md"
|
||||
```
|
||||
If the roadmap doesn't yet have a Phase L.3 entry, add one between L.2 and M with the L.3.1+L.3.2 combined status = SHIPPED, L.3.3 status = PLANNED.
|
||||
|
||||
- [ ] **Step 6.2: Update the spec doc's status**
|
||||
|
||||
In `docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md`, near the top (after the title / methodology), add or update a status line:
|
||||
|
||||
```markdown
|
||||
**Status:** L.3.1+L.3.2 SHIPPED 2026-05-02. L.3.3 PLANNED.
|
||||
```
|
||||
|
||||
- [ ] **Step 6.3: Commit (combined doc update)**
|
||||
```bash
|
||||
git add docs/plans/2026-04-11-roadmap.md docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs(roadmap+spec): Phase L.3.1+L.3.2 shipped (L.3.3 pending)
|
||||
|
||||
Roadmap Phase L.3 entry updated. Spec status banner reflects the
|
||||
combined L.3.1+L.3.2 deliverable as shipped after visual verification.
|
||||
L.3.3 (MoveToManager) remains a separate sub-lane to be specced and
|
||||
scheduled.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
End-to-end smoke test after Task 6:
|
||||
|
||||
```bash
|
||||
cd C:/Users/erikn/source/repos/acdream
|
||||
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo # green
|
||||
dotnet test --no-build --nologo # 4 pre-existing failures only
|
||||
git log --oneline -10 # see commits in order
|
||||
grep -rn "ACDREAM_INTERP_MANAGER" src/ # zero hits (cleanup confirmed)
|
||||
grep -rn "SnapResidualDecayRate" src/ # zero hits (deleted)
|
||||
```
|
||||
|
||||
User can re-run the visual test matrix WITHOUT setting `ACDREAM_INTERP_MANAGER` (default behavior is now the new path) and confirm parity.
|
||||
|
||||
If everything's green → Phase L.3.1+L.3.2 done; brainstorm L.3.3 (MoveToManager) as the next sub-lane.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** every section of the spec maps to a task here. PositionManager → Task 1; IsGrounded plumbing → Task 2; per-frame tick rewrite + RemoteMotion field + OnLivePositionUpdated rewrite → Task 3; cleanup → Task 5; doc updates → Task 6.
|
||||
- **Already-shipped commits NOT rebuilt.** L.3.1's first 6 commits (f43f168 → e08accf) already provide InterpolationManager + GetMaxSpeed + Interp field + v1 routing + v1 tick + Omega.
|
||||
- **Reverted commits** (5154a3e + f199a6a) were band-aids; their replacements live in Task 3.
|
||||
- **Subagent failure handling:** if a subagent reports BLOCKED on Task 3 (the largest), break it into smaller pieces (3a: RemoteMotion field; 3b: OnLivePositionUpdated rewrite; 3c: TickAnimations rewrite) and dispatch sequentially. Don't let a confused subagent leave broken code in main.
|
||||
- **Task 4's visual verification is the gate.** Tasks 5+6 only fire after user sign-off. If visual fails, dispatch a fix subagent before Tasks 5+6.
|
||||
Loading…
Add table
Add a link
Reference in a new issue