AllianceFormOp = { a, b } — two nation refs (id first, then case-insensitive unique name via shared resolveNationRef).
AllianceBreakOp = { a, b } — same shape, ends an existing pact.
GameState field alliances: Alliance[] (durable, persists in SQLite):
Alliance = { id: al1|al2|..., parties: [a, b] (sorted resolved ids), formedTurn, formedDate } — at most one entry per pair.
Effects (applied in applyTurnResult, in ABILITIES order):
break first: remove matching alliance entry; set a<->b relation to neutral (an active war still overrides via syncWarRelations).
form: create the alliance entry (fresh alN id); set a<->b relation to ally. Self-alliance, duplicates, and breaking a non-existent pact are no-ops.
Because ally is now a formal state behind the relation, wars mutual-defense drag-in already reads the ally relation — no change needed.
Constraints: at most 1-2 per turn, each narrated as a diplomatic event. Unknown/ambiguous/self refs dropped, never thrown.
Payload: always emits alliances (like sea), so the model knows existing pacts.
Durable: yes -> stateFields: [alliances]; db migration automatic (ABILITIES stateFields loop defaults listed fields to [] on legacy rows). New Alliance type in shared/types.ts.
shared/types.ts — GameState.alliances + Alliance type
server/abilities/alliances.ts — new ability module
server/abilities/index.ts — import + append to ABILITIES + re-export helpers
server/abilities/alliances.test.ts — unit tests
Regenerate goldens if system prompt changes (REGEN_GOLDENS=1 npx vitest run server/abilities/registry.test.ts)
Convention
Follow the existing ability pattern exactly (see server/abilities/sea.ts, server/abilities/migration.ts). New abilities are APPENDED last to ABILITIES — never reorder.
Fixes: none pre-filed; this issue is the tracker.
Add a new modular LLM ability: alliances — formal mutual-defense pacts between two nations.
## Design
TurnResult field alliances (required, default { form: [], break: [] }):
- AllianceFormOp = { a, b } — two nation refs (id first, then case-insensitive unique name via shared resolveNationRef).
- AllianceBreakOp = { a, b } — same shape, ends an existing pact.
GameState field alliances: Alliance[] (durable, persists in SQLite):
- Alliance = { id: al1|al2|..., parties: [a, b] (sorted resolved ids), formedTurn, formedDate } — at most one entry per pair.
Effects (applied in applyTurnResult, in ABILITIES order):
1. break first: remove matching alliance entry; set a<->b relation to neutral (an active war still overrides via syncWarRelations).
2. form: create the alliance entry (fresh alN id); set a<->b relation to ally. Self-alliance, duplicates, and breaking a non-existent pact are no-ops.
3. Because ally is now a formal state behind the relation, wars mutual-defense drag-in already reads the ally relation — no change needed.
Constraints: at most 1-2 per turn, each narrated as a diplomatic event. Unknown/ambiguous/self refs dropped, never thrown.
Payload: always emits alliances (like sea), so the model knows existing pacts.
Durable: yes -> stateFields: [alliances]; db migration automatic (ABILITIES stateFields loop defaults listed fields to [] on legacy rows). New Alliance type in shared/types.ts.
## Files to touch
- shared/protocol.ts — TurnResult.alliances + AllianceFormOp/AllianceBreakOp shapes
- shared/types.ts — GameState.alliances + Alliance type
- server/abilities/alliances.ts — new ability module
- server/abilities/index.ts — import + append to ABILITIES + re-export helpers
- server/abilities/alliances.test.ts — unit tests
- Regenerate goldens if system prompt changes (REGEN_GOLDENS=1 npx vitest run server/abilities/registry.test.ts)
## Convention
Follow the existing ability pattern exactly (see server/abilities/sea.ts, server/abilities/migration.ts). New abilities are APPENDED last to ABILITIES — never reorder.
Fixes: none pre-filed; this issue is the tracker.
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.
Add a new modular LLM ability: alliances — formal mutual-defense pacts between two nations.
Design
TurnResult field alliances (required, default { form: [], break: [] }):
GameState field alliances: Alliance[] (durable, persists in SQLite):
Effects (applied in applyTurnResult, in ABILITIES order):
Constraints: at most 1-2 per turn, each narrated as a diplomatic event. Unknown/ambiguous/self refs dropped, never thrown.
Payload: always emits alliances (like sea), so the model knows existing pacts.
Durable: yes -> stateFields: [alliances]; db migration automatic (ABILITIES stateFields loop defaults listed fields to [] on legacy rows). New Alliance type in shared/types.ts.
Files to touch
Convention
Follow the existing ability pattern exactly (see server/abilities/sea.ts, server/abilities/migration.ts). New abilities are APPENDED last to ABILITIES — never reorder.
Fixes: none pre-filed; this issue is the tracker.