feat(26): Council map presence — 12 synced NPCs on map#16
Conversation
MapSchema migration dropped mainNpcGridById/bgNpcGridById state but leave cleanup still called the removed setters, crashing React on room unmount. Replace with setRoomNpcs([]). Add verify:phase26 screenshots + uat script. Co-authored-by: Cursor <cursoragent@cursor.com>
…erify gate Place all 12 council members on the authoritative map via shuffleCouncilSpawnAssignments and breaking MapSchema v2. Web renders the full roster in Phaser; speak/ambient cover all seats; REL-08 leaningDrift in worker. Ship gate verify:phase26 plus E2E regression fixes for phase7 reset-snap and phase16 after background NPC removal. Co-authored-by: Cursor <cursoragent@cursor.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughPhase 26 replaces the legacy NPC slot/background model with a 12-seat council NPC system. The change spans spawn data, shared room construction and migration, Colyseus state, ambient behavior, worker drift/vote logic, web client state flow, verification scripts, and documentation. ChangesPhase 26 Council Map Presence
Sequence Diagram(s)sequenceDiagram
participant Player as Player Client
participant GameRoom as GameRoom
participant Speak as speak-schema
participant Colyseus as GameRoomState.npcs
participant SSE as SSE Hub
Player->>GameRoom: speak message
GameRoom->>Speak: setNpcSpeakPhase(npcId, "thinking")
Speak->>Colyseus: syncNpcSpeakFlagsToColyseus
SSE->>Speak: thinking / speakPartial event
Speak->>Colyseus: update isThinking / isSpeaking
GameRoom->>Speak: setNpcSpeakPhase(npcId, "idle")
flowchart TD
A[runAmbientTick] --> B{council NPC?}
B -- no --> X[skip]
B -- yes --> C{bucket matches minute?}
C -- no --> X
C -- yes --> D{maxRadius == 0?}
D -- yes --> X
D -- no --> E[applySoftLeashTarget]
E --> F[stepNpcTowardTarget]
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (2)
apps/game-server/src/room/store.test.ts (1)
19-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the canonical council id set in this migration test too.
This still passes if migration returns 12 entries with a duplicate or missing council seat. Add the same
COUNCIL_NPC_IDSequality check used in the adjacent tests.Proposed diff
const migrated = getOrCreate(roomId); expect(migrated.state.npcs).toHaveLength(12); + expect(migrated.state.npcs.map((n) => n.id)).toEqual([...COUNCIL_NPC_IDS]); expect(migrated.state.npcs.find((n) => n.id === "npc-1")?.x).toBe(23); expect(migrated.state.npcs.some((n) => n.id.startsWith("bg-villager"))).toBe(false);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/game-server/src/room/store.test.ts` around lines 19 - 41, The migration test only checks the NPC count and a few spot values, so it can still miss duplicate or missing council seats. Update the getOrCreate migration test in store.test.ts to assert the migrated state’s council NPC IDs exactly match the canonical COUNCIL_NPC_IDS set, using the same equality check pattern as the neighboring tests, while keeping the existing assertions for preserved NPC data and removal of background NPCs.packages/shared/src/council/spawn.ts (1)
51-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse
Math.imulfor the shuffle PRNG stepseed * 1103515245 + 12345can drift from 32-bit LCG semantics because JS uses floating-point multiplication, so the low bits that drive the shuffle can change.Math.imulkeeps the update in exact 32-bit integer math.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/council/spawn.ts` around lines 51 - 56, The shuffle PRNG step in `spawnCouncil` is using normal সংখ্যা multiplication, which can deviate from 32-bit LCG behavior and affect slot ordering. Update the seed update logic in the `spawnCouncil` loop to use `Math.imul` for the `seed * 1103515245 + 12345` calculation so the shuffle stays in exact 32-bit integer math and preserves deterministic behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/game-server/src/ambient/tick.test.ts`:
- Around line 238-261: The bucket-gating test in tick.test.ts should force at
least one NPC to move so a fully frozen ambient tick cannot still pass; update
the test around runAmbientTick, createDefaultRoom, and hashNpcBucket to set up
or mock conditions so at least one council NPC is expected to move, then assert
that this movement happened and still verify any moved NPC matches the current
minute bucket.
- Around line 263-284: Update the ambient tick test to explicitly set at least
one council seat NPC’s maxRadius to 0 before calling runAmbientTick, so the case
actually exercises the stationary branch. Use the existing test setup around
createDefaultRoom, map.npcs, and the loop before the before-position snapshot to
mutate the relevant NPC(s) directly, then keep the assertions that their
positions remain unchanged.
In `@apps/game-server/src/ambient/tick.ts`:
- Around line 51-64: applySoftLeashTarget currently checks only the NPC’s
current position, so it can still allow a target outside the leash radius and
let stepNpcTowardTarget overshoot for one tick. Update applySoftLeashTarget in
tick.ts to clamp based on the requested target relative to
npcHomeCell/npc.maxRadius, not just chebyshev(npc.x, npc.y, ...), so any
resolved target beyond the home radius is redirected before movement.
In `@apps/game-server/src/colyseus/GameRoom.ts`:
- Around line 455-458: The HTTP chat path in GameRoom.acquireNpcSpeakJob and the
related helper code never updates lastSpeakInitiatorByNpc, so clear the stale
entry here or, better, pass the HTTP playerId into the acquisition flow and
store it before setting the NPC to "thinking". Make sure the same NPC’s entry is
refreshed for HTTP /chat turns so clearSpeakInFlight() and the speak_end
follow-up use the correct initiator instead of reusing the previous Colyseus
speaker.
In `@apps/web/src/ChatPage.tsx`:
- Around line 299-307: The stale draft issue happens because only engageNpc
clears draft when switching NPCs, while the proximity auto-select path also
updates activeNpcId without resetting it. Update the proximity-driven NPC change
logic in ChatPage so it clears draft whenever the selected NPC changes, reusing
the same behavior as engageNpc. Make sure the fix is applied in the activeNpcId
transition path that runs while the overlay is closed, and keep engageNpc and
the auto-select handler consistent.
In `@apps/web/src/game/roomSceneInput.ts`:
- Around line 375-378: The Shift+click grid-debug guard in roomSceneInput should
short-circuit before any NPC hit-testing so diagnostic clicks do not also select
an NPC and record a spawn pick. Move the `isGridDebugEnabled()` and
`pointer.event.shiftKey` return in `handlePointerDown`/the pointer selection
flow to run before the NPC selection block, keeping the existing NPC hit logic
untouched for normal clicks.
In `@apps/web/src/game/roomSceneSync.ts`:
- Around line 311-334: The NPC halo and thinking effects are both animating
ent.ring.alpha, causing flicker when isNpcSpeaking and isNpcThinking overlap.
Update the logic in roomSceneSync so the speakHaloTween created for
ent.speakHaloTween only animates scale (or otherwise avoids alpha), and make
sure any separate thinking pulse is paused or suppressed while speaking. Use the
existing isNpcSpeaking, isNpcThinking, and ent.speakHaloTween branches to keep
the speaking state from writing ring alpha at the same time as the thinking
animation.
In `@docs/ISSUE-LOG.md`:
- Around line 187-188: The guardrail entry for councilSpawns is contradicting
ISSUE-099 in the same document, so update the wording to a single consistent
spawn policy. Make the phase 26 guidance in the ISSUE-099 / `#105` section match
the accepted dispersed layout already documented there, and ensure the
references to region-walkability.test.ts and BEGINNING-FIELDS.md describe the
same bounds so future spawn changes are not guided in opposite directions.
In `@packages/shared/src/worldRegion.ts`:
- Around line 247-254: The BEGINNING_FIELDS_ID validation in worldRegion.ts only
checks councilSpawns when the array exists, so a spawns.json with only
defaultPlayerSpawn can still load successfully. Update loadWorldRegistry() / the
councilSpawns validation block to require councilSpawns for BEGINNING_FIELDS_ID
and throw immediately if it is missing, then keep the existing length and slot
validation for the world registry path.
In `@scripts/lib/dialogue-engage.mjs`:
- Around line 70-74: The early return in engageNpcDialogue is too broad because
it exits whenever any dialogue bar is visible, even if a different NPC is
requested. Update engageNpcDialogue to only short-circuit when the currently
active dialogue already matches the requested npcId, and otherwise continue to
select the correct avatar (for example before the sendSpeakOverlay flow in
scripts/uat-phase26-playwright.mjs). Use the existing engageNpcDialogue and
dialogue-bar handling to detect the active NPC before returning.
In `@scripts/uat-phase26-playwright.mjs`:
- Around line 81-90: The movement-idle wait is being swallowed inside
waitMoveIdle(), so moveNearNpc() can proceed as if the player has settled even
when the timeout fires or pathing is still in progress. Update waitMoveIdle()
and the call site in moveNearNpc() to surface a timeout/failed idle state
instead of converting it to success, and only continue to the proximity/dialogue
checks when the idle condition actually resolves. Use the existing
window.__aetherlife_moveDebug probe and the moveNearNpc flow to preserve the
intended ship-gate sequencing.
- Around line 51-63: The runCmd helper only rejects on child close, so spawn
failures can crash the process before the test/report flow handles them. Update
runCmd in scripts/uat-phase26-playwright.mjs to attach a child.on("error", ...)
handler alongside the existing close listener, and make it reject the same
Promise with a clear label/error message while preserving the current
close-based success path.
In `@scripts/verify-phase16.mjs`:
- Around line 37-39: The council roster check is too loose because
`assertCouncilRoomAndSpeakGuards()` and `fetchRoomCouncilNpcs()` only validate
ids with `COUNCIL_ID_RE`, so any 12 `npc-*` entries can pass even if the real
council seat ids are missing. Replace the regex-based acceptance with an
authoritative list of the 12 expected council ids and have both functions
compare the room’s NPCs against that exact roster before passing P16-11/P16-10,
while keeping `COUNCIL_NPC_COUNT` and related nameplate checks aligned with the
same source of truth.
- Around line 300-310: The proximity check is too loose because
movePlayerNearCouncilNpc() accepts any council nameplate instead of the selected
NPC’s nameplate. Thread the expected targetCouncil.id/npcId through the probe
path used by waitForCouncilNameplate() and readCouncilNameplateProbe(), and
update the visibleCouncilNameplates matching logic so success only occurs when
the expected NPC id is present. Apply the same targeted assertion anywhere this
probe is reused in the P16-10 flow.
In `@scripts/verify-phase26.mjs`:
- Around line 92-132: The internal HTTP helpers in verify-phase26.mjs are
missing per-request timeouts, so stalled requests can hang the phase gate
indefinitely. Update fetchCollectiveState, fetchWorldVoteContext, and
triggerWorldVote to use a timeout-bound request pattern (for example via
AbortController or a shared timeout wrapper) and ensure each fetch fails fast
within the configured phase timeout while preserving the existing error handling
and logging.
- Around line 331-349: The runLeaningDriftPytest helper is weakening the ship
gate by forcing LLM_MOCK=1 and treating a failing leaning_drift test as
non-fatal. Update runLeaningDriftPytest in verify-phase26.mjs so it runs with
the normal subprocess environment, removes the mock override, and propagates a
non-zero exit (or otherwise fails the phase) when pytest fails instead of only
logging a warning.
- Around line 210-230: The council NPC gate in
readNpcSnapshot/assertCouncilMapPresence is too permissive because it only
counts ids matching npc-\d+ instead of verifying the exact 12 expected council
ids. Tighten the check by comparing against the full canonical council id set
and ensuring none are missing or replaced by unexpected ids, while still keeping
the sprite count validation. Update the snapshot object to report missing/extra
ids if helpful, and make the failure message in assertCouncilMapPresence reflect
exact-id validation rather than a regex-based count.
In `@workers/agent-worker/src/council/leaning_drift.py`:
- Around line 60-74: The fallback in _database_url and _use_in_memory currently
hides persistence/configuration failures by silently switching REL-08 to the
in-memory dict. Update the logic in _database_url, _use_in_memory, and the
caller in leaning_drift to fail fast when DATABASE_URL is missing or
get_settings() cannot resolve it, except when LLM_MOCK is explicitly enabled for
tests. Keep the room-scoped persisted path as the default so world_vote
continues to read durable state instead of ephemeral process-local data.
In `@workers/agent-worker/src/graph/npc_loop.py`:
- Around line 1180-1183: The NPC sentiment update path in npc_loop.py is
incorrectly defaulting a missing npc_id to npc-1, which can write drift to the
wrong (room_id, npc_id) row. Update the logic around the state lookup in the NPC
loop so it does not remap absent npc_id values; instead, detect when npc_id is
missing, skip the update and log it, or ensure the caller always supplies npc_id
before this code runs. Use the existing state["room_id"], state.get("npc_id"),
and room_snapshot/gameMinute flow to locate and adjust the update gate.
---
Nitpick comments:
In `@apps/game-server/src/room/store.test.ts`:
- Around line 19-41: The migration test only checks the NPC count and a few spot
values, so it can still miss duplicate or missing council seats. Update the
getOrCreate migration test in store.test.ts to assert the migrated state’s
council NPC IDs exactly match the canonical COUNCIL_NPC_IDS set, using the same
equality check pattern as the neighboring tests, while keeping the existing
assertions for preserved NPC data and removal of background NPCs.
In `@packages/shared/src/council/spawn.ts`:
- Around line 51-56: The shuffle PRNG step in `spawnCouncil` is using normal
সংখ্যা multiplication, which can deviate from 32-bit LCG behavior and affect
slot ordering. Update the seed update logic in the `spawnCouncil` loop to use
`Math.imul` for the `seed * 1103515245 + 12345` calculation so the shuffle stays
in exact 32-bit integer math and preserves deterministic behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a2b3361d-56a3-45c6-bd9c-1fe432be605d
📒 Files selected for processing (73)
apps/game-server/data/world/beginning-fields@v1/spawns.jsonapps/game-server/data/world/village-plaza@v1/spawns.jsonapps/game-server/src/ambient/intent-fallback.test.tsapps/game-server/src/ambient/intent-fallback.tsapps/game-server/src/ambient/schedule.test.tsapps/game-server/src/ambient/schema.test.tsapps/game-server/src/ambient/tick.test.tsapps/game-server/src/ambient/tick.tsapps/game-server/src/colyseus/GameRoom.tsapps/game-server/src/colyseus/bridge.test.tsapps/game-server/src/colyseus/bridge.tsapps/game-server/src/colyseus/npc-chat.test.tsapps/game-server/src/colyseus/npc-chat.tsapps/game-server/src/colyseus/schema.tsapps/game-server/src/colyseus/speak-schema.tsapps/game-server/src/index.test.tsapps/game-server/src/room/council-migrate.tsapps/game-server/src/room/executor.test.tsapps/game-server/src/room/store.test.tsapps/game-server/src/room/store.tsapps/game-server/src/sse/hub.colyseus.test.tsapps/game-server/src/sse/hub.tsapps/game-server/src/world/region-walkability.test.tsapps/web/src/ChatPage.tsxapps/web/src/components/CouncilRosterPanel.test.tsapps/web/src/components/CouncilRosterPanel.tsxapps/web/src/components/DialogueBar.tsxapps/web/src/components/DialogueOverlay.tsxapps/web/src/components/PhaserGame.tsxapps/web/src/game/roomSceneInput.tsapps/web/src/game/roomSceneSync.tsapps/web/src/game/roomSceneTypes.tsapps/web/src/hooks/useColyseusRoom.tsapps/web/src/lib/colyseusAmbientSnapshot.test.tsapps/web/src/lib/colyseusAmbientSnapshot.tsdocs/BEGINNING-FIELDS.mddocs/CONTRACTS.mddocs/DEVELOPMENT-HISTORY.mddocs/DEVELOPMENT-HISTORY.zh-CN.mddocs/INVARIANTS-MULTIPLAYER.mddocs/ISSUE-LOG.mdpackage.jsonpackages/npc-memory/migrations/0010_npc_leaning_drift.sqlpackages/npc-memory/migrations/meta/_journal.jsonpackages/npc-memory/src/schema.tspackages/shared/src/council/migrate.test.tspackages/shared/src/council/migrate.tspackages/shared/src/council/spawn.tspackages/shared/src/homeMap.tspackages/shared/src/index.tspackages/shared/src/pathfind.test.tspackages/shared/src/room.test.tspackages/shared/src/room.tspackages/shared/src/worldRegion.tsscripts/lib/council-spawn.mjsscripts/lib/dialogue-engage.mjsscripts/lib/home-spawn.mjsscripts/migrate-room-council-npcs.mjsscripts/uat-phase26-playwright.mjsscripts/uat-phase6-move-flash.mjsscripts/uat-phase7-reset-snap.mjsscripts/verify-phase13.mjsscripts/verify-phase16.mjsscripts/verify-phase26.mjsworkers/agent-worker/src/council/leaning_drift.pyworkers/agent-worker/src/graph/ambient_intent.pyworkers/agent-worker/src/graph/npc_loop.pyworkers/agent-worker/src/graph/world_vote.pyworkers/agent-worker/tests/test_ambient_intent.pyworkers/agent-worker/tests/test_fetch_state_and_memory.pyworkers/agent-worker/tests/test_leaning_drift.pyworkers/agent-worker/tests/test_persona_runtime.pyworkers/agent-worker/tests/test_world_vote.py
💤 Files with no reviewable changes (1)
- apps/game-server/src/ambient/schedule.test.ts
Record HTTP speak initiator in GameRoom, align phase16/26 scripts with COUNCIL_NPC_IDS, fix ambient leash and bucket tests, and stop truncating dialogue overlay text while salvaging partial social JSON from the worker. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
scripts/verify-phase26.mjs (1)
223-226: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCompute
extrafrom all NPC IDs, not the already-filtered council IDs.
councilIdsonly contains canonical IDs, soextrais always empty. A room with all 12 council NPCs plus a legacy/background NPC still passes this Phase 26 gate.Proposed fix
- const councilIds = npcs.map((n) => n.id).filter((id) => canonicalIds.includes(id)); + const npcIds = npcs.map((n) => n.id); + const canonicalSet = new Set(canonicalIds); + const councilIds = npcIds.filter((id) => canonicalSet.has(id)); const missing = canonicalIds.filter((id) => !councilIds.includes(id)); - const extra = councilIds.filter((id) => !canonicalIds.includes(id)); + const extra = npcIds.filter((id) => !canonicalSet.has(id));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/verify-phase26.mjs` around lines 223 - 226, The roster check in the Phase 26 verifier is computing extra NPCs from councilIds, which is already filtered to canonical IDs, so non-canonical NPCs are never detected. Update the logic around the councilIds/missing/extra/rosterOk calculation to derive extra from all NPC IDs in npcs, while still using councilIds (or a canonical-only set) for missing canonical IDs. Keep the existing gate behavior in verify-phase26.mjs, but ensure rosterOk fails when any non-canonical NPC is present alongside the required council NPCs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/index.css`:
- Line 212: The CSS rule in the index stylesheet is using the deprecated
word-break: break-word value. Update the affected text-wrapping style to use
overflow-wrap for long-token wrapping behavior, and keep word-break limited to
supported values if still needed. Locate the existing word-break declaration in
the stylesheet and replace it so the wrapping behavior remains the same without
relying on the deprecated keyword.
In `@scripts/verify-phase26.mjs`:
- Around line 53-57: The fetchWithTimeout helper still creates an unused
destructuring alias for timeoutMs, which can trigger lint failures in the
ship-gate script. Update fetchWithTimeout to remove the extra binding while
still excluding timeoutMs from the options passed to fetch, keeping the
AbortSignal.timeout behavior intact. Use the fetchWithTimeout symbol to locate
the destructuring of options and simplify it so only the needed values remain.
- Line 31: The import in verify-phase26.mjs includes
assertCanonicalCouncilRoster, but that symbol is unused and triggers ESLint.
Remove assertCanonicalCouncilRoster from the import unless you intend to call it
in the script’s phase verification flow; keep COUNCIL_NPC_IDS if it is still
used, and verify the remaining references in the module still resolve.
---
Duplicate comments:
In `@scripts/verify-phase26.mjs`:
- Around line 223-226: The roster check in the Phase 26 verifier is computing
extra NPCs from councilIds, which is already filtered to canonical IDs, so
non-canonical NPCs are never detected. Update the logic around the
councilIds/missing/extra/rosterOk calculation to derive extra from all NPC IDs
in npcs, while still using councilIds (or a canonical-only set) for missing
canonical IDs. Keep the existing gate behavior in verify-phase26.mjs, but ensure
rosterOk fails when any non-canonical NPC is present alongside the required
council NPCs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9393a907-b0e8-40c7-9f06-2f3ace0aa141
📒 Files selected for processing (22)
apps/game-server/src/ambient/tick.test.tsapps/game-server/src/ambient/tick.tsapps/game-server/src/colyseus/GameRoom.tsapps/game-server/src/room/store.test.tsapps/game-server/src/routes/chat.tsapps/web/src/ChatPage.tsxapps/web/src/game/roomSceneInput.tsapps/web/src/game/roomSceneSync.tsapps/web/src/index.cssdocs/ISSUE-LOG.mdpackages/shared/src/council/spawn.tspackages/shared/src/worldRegion.tsscripts/lib/council-spawn.mjsscripts/lib/dialogue-engage.mjsscripts/uat-phase26-playwright.mjsscripts/verify-phase16.mjsscripts/verify-phase26.mjsworkers/agent-worker/src/config.pyworkers/agent-worker/src/council/leaning_drift.pyworkers/agent-worker/src/graph/nodes/llm_social_turn.pyworkers/agent-worker/src/graph/npc_loop.pyworkers/agent-worker/tests/test_social_stream_extract.py
✅ Files skipped from review due to trivial changes (1)
- docs/ISSUE-LOG.md
🚧 Files skipped from review as they are similar to previous changes (11)
- scripts/lib/dialogue-engage.mjs
- packages/shared/src/council/spawn.ts
- apps/game-server/src/ambient/tick.test.ts
- workers/agent-worker/src/graph/npc_loop.py
- apps/game-server/src/room/store.test.ts
- apps/web/src/game/roomSceneInput.ts
- apps/web/src/ChatPage.tsx
- workers/agent-worker/src/council/leaning_drift.py
- apps/game-server/src/ambient/tick.ts
- scripts/verify-phase16.mjs
- apps/web/src/game/roomSceneSync.ts
Detect non-canonical NPCs in MAP-05 snapshot checks, fail verify:phase26 when leaning_drift pytest fails, and clean up ship-gate script lint/CSS. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
shuffleCouncilSpawnAssignments; removes legacy single-NPC + background villager tier.leaningDrift, andnpc-memorymigration0010_npc_leaning_drift.pnpm verify:phase26(real LLM, ~10–12 min). E2E regression fixes for Phase 26 fallout:uat:phase7:reset-snap(council spawn coords) andverify:phase16(no bg-villager tier).Commits
fix(26-uat):remove stalemainNpcGridcleanup crashing ChatPagefeat(26):council map presence — MapSchema v2, verify gate, E2E alignmentTest plan
pnpm agent:verify(pre-push hook)pnpm --filter @aetherlife/game-server test— 271 passedcd workers/agent-worker && LLM_MOCK=1 uv run pytest -q— 339 passedpnpm verify:phase26— ship gate (real LLM)pnpm verify:phase13,pnpm verify:phase16,pnpm uat:phase7:reset-snappnpm verify:phase20— advisory gap (firstTextMs>8000LLM latency SLA, not Phase 26 regression)Notes
pnpm --filter @aetherlife/npc-memory db:migratefor leaning drift column.node scripts/migrate-room-council-npcs.mjsfor persisted rooms.npc-asset/left untracked (local PNG, not part of phase).Made with Cursor
Summary by CodeRabbit
New Features
Bug Fixes
Documentation / Tests