Skip to content

feat(26): Council map presence — 12 synced NPCs on map#16

Merged
moyunzero merged 4 commits into
mainfrom
phase/26-council-map-presence
Jul 1, 2026
Merged

feat(26): Council map presence — 12 synced NPCs on map#16
moyunzero merged 4 commits into
mainfrom
phase/26-council-map-presence

Conversation

@moyunzero

@moyunzero moyunzero commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • MapSchema v2 (breaking): All 12 council members appear on the authoritative map via shuffleCouncilSpawnAssignments; removes legacy single-NPC + background villager tier.
  • Cross-layer: game-server bridge/ambient tick, Phaser full-roster render, speak path for all seats, worker REL-08 leaningDrift, and npc-memory migration 0010_npc_leaning_drift.
  • Ship gate: pnpm verify:phase26 (real LLM, ~10–12 min). E2E regression fixes for Phase 26 fallout: uat:phase7:reset-snap (council spawn coords) and verify:phase16 (no bg-villager tier).

Commits

  1. fix(26-uat): remove stale mainNpcGrid cleanup crashing ChatPage
  2. feat(26): council map presence — MapSchema v2, verify gate, E2E alignment

Test plan

  • pnpm agent:verify (pre-push hook)
  • pnpm --filter @aetherlife/game-server test — 271 passed
  • cd workers/agent-worker && LLM_MOCK=1 uv run pytest -q — 339 passed
  • pnpm verify:phase26 — ship gate (real LLM)
  • pnpm verify:phase13, pnpm verify:phase16, pnpm uat:phase7:reset-snap
  • pnpm verify:phase20 — advisory gap (firstTextMs>8000 LLM latency SLA, not Phase 26 regression)

Notes

  • DB: run pnpm --filter @aetherlife/npc-memory db:migrate for leaning drift column.
  • Existing rooms: optional node scripts/migrate-room-council-npcs.mjs for persisted rooms.
  • npc-asset/ left untracked (local PNG, not part of phase).

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Council NPC presence updated to 12 seats with deterministic, room-wide spawn distribution and normalized council-state handling.
    • Added NPC speak-phase tracking (thinking/speaking/idle) end-to-end, with improved web UI status syncing and speak “halo” visuals.
    • Introduced leaning-drift–driven world-vote behavior plus new Phase 26 verification/UAT flows.
  • Bug Fixes

    • Improved ambient NPC ticking/movement rules (max-radius + soft leash) and stabilized council placement walkability checks.
    • More robust speak-intent fallback behavior and safer speak-job contention checks.
    • Removed outdated spawn configurations (e.g., village plaza NPC spawns) and aligned room/chat validation to council NPCs.
  • Documentation / Tests

    • Updated contracts/invariants and expanded automated test coverage for Phase 26.

moyunzero and others added 2 commits June 30, 2026 12:33
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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e07a6648-dce7-4924-a71e-a815a0202b5f

📥 Commits

Reviewing files that changed from the base of the PR and between 6291a16 and b1dc964.

📒 Files selected for processing (2)
  • apps/web/src/index.css
  • scripts/verify-phase26.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/index.css
  • scripts/verify-phase26.mjs

📝 Walkthrough

Walkthrough

Phase 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.

Changes

Phase 26 Council Map Presence

Layer / File(s) Summary
Council spawn data model
apps/game-server/data/world/beginning-fields@v1/spawns.json, apps/game-server/data/world/village-plaza@v1/spawns.json, packages/shared/src/worldRegion.ts, packages/shared/src/index.ts, docs/BEGINNING-FIELDS.md
councilSpawns replaces the old NPC/background spawn shape, and world-region parsing now requires 12 council entries for the beginning-fields region.
Shared council room construction and migration
packages/shared/src/council/spawn.ts, packages/shared/src/council/migrate.ts, packages/shared/src/room.ts, packages/shared/src/homeMap.ts, packages/shared/src/index.ts, packages/shared/src/council/migrate.test.ts, packages/shared/src/room.test.ts, packages/shared/src/pathfind.test.ts, scripts/lib/home-spawn.mjs, scripts/lib/council-spawn.mjs, scripts/migrate-room-council-npcs.mjs, apps/game-server/src/room/council-migrate.ts, apps/game-server/src/room/store.ts, apps/game-server/src/room/store.test.ts, apps/game-server/src/room/executor.test.ts
Shared helpers build shuffled council rooms, migrate legacy rooms into 12-council form, normalize stored room state, and expose the new council spawn/home helpers and migration commands.
Colyseus council NPC map
apps/game-server/src/colyseus/schema.ts, apps/game-server/src/colyseus/bridge.ts, apps/game-server/src/colyseus/bridge.test.ts, apps/game-server/src/ambient/schema.test.ts
GameRoomState now stores NPCs in MapSchema<NpcEntityState>, and the bridge syncs NPC fields by id while removing stale entries.
NPC speak-phase wiring
apps/game-server/src/colyseus/speak-schema.ts, apps/game-server/src/colyseus/GameRoom.ts, apps/game-server/src/sse/hub.ts, apps/game-server/src/sse/hub.colyseus.test.ts, apps/game-server/src/routes/chat.ts
Speak jobs now update NPC speak state through setNpcSpeakPhase, and SSE/job routing carries thinking/speaking state into Colyseus.
Council ambient tick and chat validation
apps/game-server/src/ambient/tick.ts, apps/game-server/src/ambient/intent-fallback.ts, apps/game-server/src/ambient/tick.test.ts, apps/game-server/src/ambient/intent-fallback.test.ts, apps/game-server/src/ambient/schedule.test.ts, apps/game-server/src/world/region-walkability.test.ts, apps/game-server/src/colyseus/npc-chat.ts, apps/game-server/src/colyseus/npc-chat.test.ts
Ambient movement is limited to council NPCs with bucket gating and soft-leash movement, and chat validation is restricted to council ids.
Leaning drift and vote defaults
packages/npc-memory/migrations/0010_npc_leaning_drift.sql, packages/npc-memory/migrations/meta/_journal.json, packages/npc-memory/src/schema.ts, workers/agent-worker/src/council/leaning_drift.py, workers/agent-worker/src/graph/npc_loop.py, workers/agent-worker/src/graph/world_vote.py, workers/agent-worker/src/graph/ambient_intent.py, workers/agent-worker/src/graph/nodes/llm_social_turn.py, workers/agent-worker/src/config.py, workers/agent-worker/tests/test_leaning_drift.py, workers/agent-worker/tests/test_world_vote.py, workers/agent-worker/tests/test_ambient_intent.py, workers/agent-worker/tests/test_fetch_state_and_memory.py, workers/agent-worker/tests/test_persona_runtime.py, workers/agent-worker/tests/test_social_stream_extract.py
Adds leaning-drift storage and application, room-aware vote defaults, fallback text expansion, and salvaged social-turn replies from partial JSON.
Web council state pipeline and UI updates
apps/web/src/lib/colyseusAmbientSnapshot.ts, apps/web/src/lib/colyseusAmbientSnapshot.test.ts, apps/web/src/hooks/useColyseusRoom.ts, apps/web/src/ChatPage.tsx, apps/web/src/components/PhaserGame.tsx, apps/web/src/game/roomSceneSync.ts, apps/web/src/game/roomSceneInput.ts, apps/web/src/game/roomSceneTypes.ts, apps/web/src/components/CouncilRosterPanel.tsx, apps/web/src/components/CouncilRosterPanel.test.ts, apps/web/src/components/DialogueBar.tsx, apps/web/src/components/DialogueOverlay.tsx, apps/web/src/index.css
The web client now consumes roomNpcs, propagates speaking state into Phaser, updates room-scene sync, and adds council-specific debug hooks and UI copy.
Phase 26 verification and UAT scripts
package.json, scripts/verify-phase26.mjs, scripts/uat-phase26-playwright.mjs, scripts/lib/council-spawn.mjs, scripts/lib/dialogue-engage.mjs, scripts/uat-phase7-reset-snap.mjs, scripts/uat-phase6-move-flash.mjs, scripts/verify-phase13.mjs, scripts/verify-phase16.mjs
Adds the Phase 26 verification runners and supporting helpers, and updates earlier scripts to use the council spawn and home SSOTs.
Phase 26 documentation updates
docs/CONTRACTS.md, docs/DEVELOPMENT-HISTORY.md, docs/DEVELOPMENT-HISTORY.zh-CN.md, docs/INVARIANTS-MULTIPLAYER.md, docs/ISSUE-LOG.md
Updates the phase history, contracts, invariants, and issue log to reflect the council map, drift, and verification changes.

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")
Loading
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]
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • moyunzero/AetherLife#3: Both PRs modify the ambient NPC pipeline, including tick and intent fallback behavior.
  • moyunzero/AetherLife#5: Both PRs touch the game-server speak-job lifecycle and room-level speak handling around GameRoom.ts.
  • moyunzero/AetherLife#6: Both PRs extend the same web scene-sync and debug-hook code paths in apps/web/src/game/roomSceneSync.ts.

Poem

🐇 Twelve council seats now line the lane,
With soft-leashed hops and drift in brain.
The rabbit drums a tiny paw,
At sync and votes and schema law.
No bg villagers in sight today —
Just council starlight on its way ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main change: Phase 26 council map presence with 12 synced NPCs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase/26-council-map-presence

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (2)
apps/game-server/src/room/store.test.ts (1)

19-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert 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_IDS equality 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 win

Use Math.imul for the shuffle PRNG step seed * 1103515245 + 12345 can drift from 32-bit LCG semantics because JS uses floating-point multiplication, so the low bits that drive the shuffle can change. Math.imul keeps 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

📥 Commits

Reviewing files that changed from the base of the PR and between f457ccd and 18e0b13.

📒 Files selected for processing (73)
  • apps/game-server/data/world/beginning-fields@v1/spawns.json
  • apps/game-server/data/world/village-plaza@v1/spawns.json
  • apps/game-server/src/ambient/intent-fallback.test.ts
  • apps/game-server/src/ambient/intent-fallback.ts
  • apps/game-server/src/ambient/schedule.test.ts
  • apps/game-server/src/ambient/schema.test.ts
  • apps/game-server/src/ambient/tick.test.ts
  • apps/game-server/src/ambient/tick.ts
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/colyseus/bridge.test.ts
  • apps/game-server/src/colyseus/bridge.ts
  • apps/game-server/src/colyseus/npc-chat.test.ts
  • apps/game-server/src/colyseus/npc-chat.ts
  • apps/game-server/src/colyseus/schema.ts
  • apps/game-server/src/colyseus/speak-schema.ts
  • apps/game-server/src/index.test.ts
  • apps/game-server/src/room/council-migrate.ts
  • apps/game-server/src/room/executor.test.ts
  • apps/game-server/src/room/store.test.ts
  • apps/game-server/src/room/store.ts
  • apps/game-server/src/sse/hub.colyseus.test.ts
  • apps/game-server/src/sse/hub.ts
  • apps/game-server/src/world/region-walkability.test.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/CouncilRosterPanel.test.ts
  • apps/web/src/components/CouncilRosterPanel.tsx
  • apps/web/src/components/DialogueBar.tsx
  • apps/web/src/components/DialogueOverlay.tsx
  • apps/web/src/components/PhaserGame.tsx
  • apps/web/src/game/roomSceneInput.ts
  • apps/web/src/game/roomSceneSync.ts
  • apps/web/src/game/roomSceneTypes.ts
  • apps/web/src/hooks/useColyseusRoom.ts
  • apps/web/src/lib/colyseusAmbientSnapshot.test.ts
  • apps/web/src/lib/colyseusAmbientSnapshot.ts
  • docs/BEGINNING-FIELDS.md
  • docs/CONTRACTS.md
  • docs/DEVELOPMENT-HISTORY.md
  • docs/DEVELOPMENT-HISTORY.zh-CN.md
  • docs/INVARIANTS-MULTIPLAYER.md
  • docs/ISSUE-LOG.md
  • package.json
  • packages/npc-memory/migrations/0010_npc_leaning_drift.sql
  • packages/npc-memory/migrations/meta/_journal.json
  • packages/npc-memory/src/schema.ts
  • packages/shared/src/council/migrate.test.ts
  • packages/shared/src/council/migrate.ts
  • packages/shared/src/council/spawn.ts
  • packages/shared/src/homeMap.ts
  • packages/shared/src/index.ts
  • packages/shared/src/pathfind.test.ts
  • packages/shared/src/room.test.ts
  • packages/shared/src/room.ts
  • packages/shared/src/worldRegion.ts
  • scripts/lib/council-spawn.mjs
  • scripts/lib/dialogue-engage.mjs
  • scripts/lib/home-spawn.mjs
  • scripts/migrate-room-council-npcs.mjs
  • scripts/uat-phase26-playwright.mjs
  • scripts/uat-phase6-move-flash.mjs
  • scripts/uat-phase7-reset-snap.mjs
  • scripts/verify-phase13.mjs
  • scripts/verify-phase16.mjs
  • scripts/verify-phase26.mjs
  • workers/agent-worker/src/council/leaning_drift.py
  • workers/agent-worker/src/graph/ambient_intent.py
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/agent-worker/src/graph/world_vote.py
  • workers/agent-worker/tests/test_ambient_intent.py
  • workers/agent-worker/tests/test_fetch_state_and_memory.py
  • workers/agent-worker/tests/test_leaning_drift.py
  • workers/agent-worker/tests/test_persona_runtime.py
  • workers/agent-worker/tests/test_world_vote.py
💤 Files with no reviewable changes (1)
  • apps/game-server/src/ambient/schedule.test.ts

Comment thread apps/game-server/src/ambient/tick.test.ts
Comment thread apps/game-server/src/ambient/tick.test.ts
Comment thread apps/game-server/src/ambient/tick.ts
Comment thread apps/game-server/src/colyseus/GameRoom.ts Outdated
Comment thread apps/web/src/ChatPage.tsx
Comment thread scripts/verify-phase26.mjs
Comment thread scripts/verify-phase26.mjs Outdated
Comment thread scripts/verify-phase26.mjs
Comment thread workers/agent-worker/src/council/leaning_drift.py
Comment thread workers/agent-worker/src/graph/npc_loop.py
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
scripts/verify-phase26.mjs (1)

223-226: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compute extra from all NPC IDs, not the already-filtered council IDs.

councilIds only contains canonical IDs, so extra is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 18e0b13 and 6291a16.

📒 Files selected for processing (22)
  • apps/game-server/src/ambient/tick.test.ts
  • apps/game-server/src/ambient/tick.ts
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/room/store.test.ts
  • apps/game-server/src/routes/chat.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/game/roomSceneInput.ts
  • apps/web/src/game/roomSceneSync.ts
  • apps/web/src/index.css
  • docs/ISSUE-LOG.md
  • packages/shared/src/council/spawn.ts
  • packages/shared/src/worldRegion.ts
  • scripts/lib/council-spawn.mjs
  • scripts/lib/dialogue-engage.mjs
  • scripts/uat-phase26-playwright.mjs
  • scripts/verify-phase16.mjs
  • scripts/verify-phase26.mjs
  • workers/agent-worker/src/config.py
  • workers/agent-worker/src/council/leaning_drift.py
  • workers/agent-worker/src/graph/nodes/llm_social_turn.py
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/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

Comment thread apps/web/src/index.css Outdated
Comment thread scripts/verify-phase26.mjs Outdated
Comment thread scripts/verify-phase26.mjs
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>
@moyunzero moyunzero merged commit e5ece3a into main Jul 1, 2026
2 checks passed
moyunzero added a commit that referenced this pull request Jul 1, 2026
main already shipped Phase 26 via #16; keep LPC/CELL_PX=32 deltas in
ISSUE-LOG guardrails #106–108 and verify-phase16 scaled nameplate gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant