Part of #16 — major nation changes, P4a: nation renames ("LLM proposes, player confirms").
The LLM can now propose that a nation be renamed during a turn. Unlike stat / relation /
territory changes (applied automatically), a rename is stored as a pending proposal and only
takes effect when the player confirms it (optionally editing the new name) or is dropped on reject. The NationOp contract is stabilized now: split and merge are declared in the same
union but are accepted-and-stored, not applied until P4b.
What changed
shared/protocol.ts — new NationOp discriminated union (rename / split / merge), each
naming its target via a nationRef the model may give as a nation id or name. TurnResult gains a requirednationOps: NationOp[] (the model's proposals this turn). New request/response types: NationOpConfirmRequest{ index, edits?: { name? } }, NationOpRejectRequest{ index }, response { game }.
shared/types.ts — GameState.pendingNationOps: NationOp[] (type-only import, no runtime cycle).
A turn appends to it (never cleared on commit), so a proposal persists across turns until resolved.
Legacy SQLite rows and new games default it to [] (server/db.ts).
server/engine.ts (all pure) —
resolveNationRef(state, ref): exact id first, else a case-insensitive unique-name match, else null (unknown/ambiguous never throws).
validateNationOps: drops invalid ops defensively.
applyTurnResult: keeps existing pending ops and appends this turn's valid ones; a rename-only turn is isMeaningful (survives retry/nudge).
applyNationOp(state, op, edits?): mutates a clone (rename → Nation.name = edited value or the proposed to; id / territory / relations / playerNationId stay valid, so a renamed player is re-named in place; split/merge are no-ops for now).
confirmNationOp(state, index, edits?) / rejectNationOp(state, index): splice by index + bump version once; return null for an invalid index.
System prompt (tool-mode + plain-mode) gains the nationOps JSON field and the rename-proposal rule.
server/index.ts — POST /api/games/:id/nation-op/confirm and .../reject (404 unknown game, 400 bad index), each returning { game } and broadcasting a version-bumped SSE state — mirrors the existing queue add/remove routes. Fresh games start with pendingNationOps: [].
Client — src/lib/api.tsconfirmNationOp / rejectNationOp; new src/components/NationOpDialog.tsx (editable rename + Confirm/Reject, Dismiss for split/merge); wired into the "Your move" panel (ActionPanel.tsx) via a "Nation proposals (N)" trigger and App.tsx (opBusy flag + version-guarded applyState).
server/stub.ts — returns nationOps: []; the stub never proposes a nation op, so the e2e suite is unaffected (rename is covered by unit tests only, mirroring the P3 no-man's-land approach).
AGENTS.md / CHANGELOG.md — refreshed for P4a; version bumped to 0.23.0.
Verification
npm run typecheck, npm run lint, npm test (151 unit tests, up from 127), and npm run build all pass locally.
e2e (CI, LLM_STUB=1) has no nation-op assertions and stays green with the unchanged stub.
Deploy: manual docker compose up -d --build after merge (no down -v — GameState only gains a defaulted field, so existing saves stay compatible).
Related: #16 (major nation changes — this is P4a; P4b will implement split/merge).
Part of #16 — **major nation changes, P4a: nation renames ("LLM proposes, player confirms").**
The LLM can now propose that a nation be **renamed** during a turn. Unlike stat / relation /
territory changes (applied automatically), a rename is stored as a *pending proposal* and only
takes effect when the player **confirms** it (optionally editing the new name) or is dropped on
**reject**. The `NationOp` contract is stabilized now: `split` and `merge` are declared in the same
union but are *accepted-and-stored, not applied* until P4b.
## What changed
- **`shared/protocol.ts`** — new `NationOp` discriminated union (`rename` / `split` / `merge`), each
naming its target via a `nationRef` the model may give as a nation **id or name**. `TurnResult` gains a
**required** `nationOps: NationOp[]` (the model's proposals this turn). New request/response types:
`NationOpConfirmRequest` `{ index, edits?: { name? } }`, `NationOpRejectRequest` `{ index }`, response `{ game }`.
- **`shared/types.ts`** — `GameState.pendingNationOps: NationOp[]` (type-only import, no runtime cycle).
A turn **appends** to it (never cleared on commit), so a proposal persists across turns until resolved.
Legacy SQLite rows and new games default it to `[]` (`server/db.ts`).
- **`server/engine.ts`** (all pure) —
- `resolveNationRef(state, ref)`: exact id first, else a case-insensitive unique-name match, else `null` (unknown/ambiguous never throws).
- `validateNationOps`: drops invalid ops defensively.
- `applyTurnResult`: keeps existing pending ops and appends this turn's valid ones; a rename-only turn is `isMeaningful` (survives retry/nudge).
- `applyNationOp(state, op, edits?)`: mutates a clone (rename → `Nation.name` = edited value or the proposed `to`; id / territory / relations / `playerNationId` stay valid, so a renamed player is re-named in place; split/merge are no-ops for now).
- `confirmNationOp(state, index, edits?)` / `rejectNationOp(state, index)`: splice by index + bump `version` once; return `null` for an invalid index.
- System prompt (tool-mode + plain-mode) gains the `nationOps` JSON field and the rename-proposal rule.
- **`server/index.ts`** — `POST /api/games/:id/nation-op/confirm` and `.../reject` (404 unknown game, 400 bad index), each returning `{ game }` and broadcasting a version-bumped SSE `state` — mirrors the existing queue add/remove routes. Fresh games start with `pendingNationOps: []`.
- **Client** — `src/lib/api.ts` `confirmNationOp` / `rejectNationOp`; new `src/components/NationOpDialog.tsx` (editable rename + Confirm/Reject, Dismiss for split/merge); wired into the "Your move" panel (`ActionPanel.tsx`) via a "Nation proposals (N)" trigger and `App.tsx` (`opBusy` flag + version-guarded `applyState`).
- **`server/stub.ts`** — returns `nationOps: []`; the stub **never** proposes a nation op, so the e2e suite is unaffected (rename is covered by unit tests only, mirroring the P3 no-man's-land approach).
- **`AGENTS.md` / `CHANGELOG.md`** — refreshed for P4a; version bumped to **0.23.0**.
## Verification
- `npm run typecheck`, `npm run lint`, `npm test` (**151** unit tests, up from 127), and `npm run build` all pass locally.
- e2e (CI, `LLM_STUB=1`) has no nation-op assertions and stays green with the unchanged stub.
- Deploy: manual `docker compose up -d --build` after merge (no `down -v` — `GameState` only gains a defaulted field, so existing saves stay compatible).
Related: #16 (major nation changes — this is P4a; P4b will implement split/merge).
Wire the 'LLM proposes, player confirms' nation-rename flow end to end:
- server: POST /api/games/:id/nation-op/confirm|reject (404/400, version-bumped SSE state)
- client: confirmNationOp/rejectNationOp api helpers + NationOpDialog (editable rename) wired into the Action panel via a 'Nation proposals' trigger
- AGENTS.md: document the pendingNationOps mechanism, routes, nationRef resolution, stub-never-proposes
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #16 — major nation changes, P4a: nation renames ("LLM proposes, player confirms").
The LLM can now propose that a nation be renamed during a turn. Unlike stat / relation /
territory changes (applied automatically), a rename is stored as a pending proposal and only
takes effect when the player confirms it (optionally editing the new name) or is dropped on
reject. The
NationOpcontract is stabilized now:splitandmergeare declared in the sameunion but are accepted-and-stored, not applied until P4b.
What changed
shared/protocol.ts— newNationOpdiscriminated union (rename/split/merge), eachnaming its target via a
nationRefthe model may give as a nation id or name.TurnResultgains arequired
nationOps: NationOp[](the model's proposals this turn). New request/response types:NationOpConfirmRequest{ index, edits?: { name? } },NationOpRejectRequest{ index }, response{ game }.shared/types.ts—GameState.pendingNationOps: NationOp[](type-only import, no runtime cycle).A turn appends to it (never cleared on commit), so a proposal persists across turns until resolved.
Legacy SQLite rows and new games default it to
[](server/db.ts).server/engine.ts(all pure) —resolveNationRef(state, ref): exact id first, else a case-insensitive unique-name match, elsenull(unknown/ambiguous never throws).validateNationOps: drops invalid ops defensively.applyTurnResult: keeps existing pending ops and appends this turn's valid ones; a rename-only turn isisMeaningful(survives retry/nudge).applyNationOp(state, op, edits?): mutates a clone (rename →Nation.name= edited value or the proposedto; id / territory / relations /playerNationIdstay valid, so a renamed player is re-named in place; split/merge are no-ops for now).confirmNationOp(state, index, edits?)/rejectNationOp(state, index): splice by index + bumpversiononce; returnnullfor an invalid index.nationOpsJSON field and the rename-proposal rule.server/index.ts—POST /api/games/:id/nation-op/confirmand.../reject(404 unknown game, 400 bad index), each returning{ game }and broadcasting a version-bumped SSEstate— mirrors the existing queue add/remove routes. Fresh games start withpendingNationOps: [].src/lib/api.tsconfirmNationOp/rejectNationOp; newsrc/components/NationOpDialog.tsx(editable rename + Confirm/Reject, Dismiss for split/merge); wired into the "Your move" panel (ActionPanel.tsx) via a "Nation proposals (N)" trigger andApp.tsx(opBusyflag + version-guardedapplyState).server/stub.ts— returnsnationOps: []; the stub never proposes a nation op, so the e2e suite is unaffected (rename is covered by unit tests only, mirroring the P3 no-man's-land approach).AGENTS.md/CHANGELOG.md— refreshed for P4a; version bumped to 0.23.0.Verification
npm run typecheck,npm run lint,npm test(151 unit tests, up from 127), andnpm run buildall pass locally.LLM_STUB=1) has no nation-op assertions and stays green with the unchanged stub.docker compose up -d --buildafter merge (nodown -v—GameStateonly gains a defaulted field, so existing saves stay compatible).Related: #16 (major nation changes — this is P4a; P4b will implement split/merge).