Implements issue #57 — Espionage & Hybrid War: a new TurnResult.hybrid = { ops: HybridOp[] } lever (required, default { ops: [] }) that adds a deterministic, non-kinetic covert-operations spectrum alongside the kinetic regionChanges lever.
What changed
shared/protocol.ts — HybridOpType (7 types), HYBRID_OP_TYPES, HybridOp = { type, from, target, detail?, outcome? }, and the required TurnResult.hybrid field (between wars and markers).
server/engine.ts
validateHybridOps (lenient: accepts {ops:[...]} or a bare array; keeps outcome: 'exposed' only; field-length caps) wired into validateTurnResult.
Deterministic per-type effects (no randomness): sabotage economy −3, economic economy −5, cyber military −3, assassinate stability −5, information/proxy one-step relation nudge (capped at hostile / rival), spy informational only.
from/target resolve via the shared resolveNationRef (id first, then case-insensitive unique name); a non-nation target is treated as a region key and hits that region's sole owner (0 or 2+ owners, or a self-target, is dropped — never thrown).
A region-targeted dip lands on the single regionStats[regionKey] entry; a nation-targeted dip lands on every region the nation owns; afterwards the target's derived stats is re-derived (0-region / no-entry guards), so the effect persists. Hybrid ops never move territory — the sole-ownership invariant is untouched.
Exposed backfire: an op with outcome: 'exposed' does NOT apply its intended effect; instead the from↔target relation moves one step toward hostile (capped; war is never created or escalated) and a diplomatic/failed event is auto-appended to the turn's events.
The apply block runs inside applyTurnResult after territory + wars, before markers; isMeaningful counts hybrid ops; the system prompt documents the field (tool and plain modes); buildUserPrompt gained an 8th, backward-compatible relations param (the user payload now carries the player's relations map); the "unclear" fallback returns hybrid: { ops: [] }.
## Summary
Implements issue #57 — Espionage & Hybrid War: a new `TurnResult.hybrid = { ops: HybridOp[] }` lever (required, default `{ ops: [] }`) that adds a deterministic, non-kinetic covert-operations spectrum alongside the kinetic `regionChanges` lever.
## What changed
- **`shared/protocol.ts`** — `HybridOpType` (7 types), `HYBRID_OP_TYPES`, `HybridOp = { type, from, target, detail?, outcome? }`, and the required `TurnResult.hybrid` field (between `wars` and `markers`).
- **`server/engine.ts`**
- `validateHybridOps` (lenient: accepts `{ops:[...]}` or a bare array; keeps `outcome: 'exposed'` only; field-length caps) wired into `validateTurnResult`.
- Deterministic per-type effects (no randomness): `sabotage` economy −3, `economic` economy −5, `cyber` military −3, `assassinate` stability −5, `information`/`proxy` one-step relation nudge (capped at `hostile` / `rival`), `spy` informational only.
- `from`/`target` resolve via the shared `resolveNationRef` (id first, then case-insensitive unique name); a non-nation `target` is treated as a region key and hits that region's **sole** owner (0 or 2+ owners, or a self-target, is dropped — never thrown).
- A region-targeted dip lands on the single `regionStats[regionKey]` entry; a nation-targeted dip lands on every region the nation owns; afterwards the target's derived `stats` is re-derived (0-region / no-entry guards), so the effect persists. Hybrid ops **never move territory** — the sole-ownership invariant is untouched.
- **Exposed backfire**: an op with `outcome: 'exposed'` does NOT apply its intended effect; instead the from↔target relation moves one step toward `hostile` (capped; `war` is never created or escalated) and a diplomatic/failed event is auto-appended to the turn's `events`.
- The apply block runs inside `applyTurnResult` after territory + wars, before markers; `isMeaningful` counts hybrid ops; the system prompt documents the field (tool and plain modes); `buildUserPrompt` gained an 8th, backward-compatible `relations` param (the user payload now carries the player's relations map); the "unclear" fallback returns `hybrid: { ops: [] }`.
- **`server/stub.ts`** — the stub always returns `hybrid: { ops: [] }` (deterministic; e2e unaffected).
- **Additive only** — no `GameState` field, no `db.ts` change.
- **Tests** — ~30 new unit tests in `server/engine.test.ts` (all 48 raw `TurnResult` literals updated with the required field).
Local verification: typecheck ✅ · lint ✅ · test (386) ✅ · build ✅ — e2e runs in CI (`LLM_STUB=1`, the stub is hybrid-free).
Fixes #57
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.
Summary
Implements issue #57 — Espionage & Hybrid War: a new
TurnResult.hybrid = { ops: HybridOp[] }lever (required, default{ ops: [] }) that adds a deterministic, non-kinetic covert-operations spectrum alongside the kineticregionChangeslever.What changed
shared/protocol.ts—HybridOpType(7 types),HYBRID_OP_TYPES,HybridOp = { type, from, target, detail?, outcome? }, and the requiredTurnResult.hybridfield (betweenwarsandmarkers).server/engine.tsvalidateHybridOps(lenient: accepts{ops:[...]}or a bare array; keepsoutcome: 'exposed'only; field-length caps) wired intovalidateTurnResult.sabotageeconomy −3,economiceconomy −5,cybermilitary −3,assassinatestability −5,information/proxyone-step relation nudge (capped athostile/rival),spyinformational only.from/targetresolve via the sharedresolveNationRef(id first, then case-insensitive unique name); a non-nationtargetis treated as a region key and hits that region's sole owner (0 or 2+ owners, or a self-target, is dropped — never thrown).regionStats[regionKey]entry; a nation-targeted dip lands on every region the nation owns; afterwards the target's derivedstatsis re-derived (0-region / no-entry guards), so the effect persists. Hybrid ops never move territory — the sole-ownership invariant is untouched.outcome: 'exposed'does NOT apply its intended effect; instead the from↔target relation moves one step towardhostile(capped;waris never created or escalated) and a diplomatic/failed event is auto-appended to the turn'sevents.applyTurnResultafter territory + wars, before markers;isMeaningfulcounts hybrid ops; the system prompt documents the field (tool and plain modes);buildUserPromptgained an 8th, backward-compatiblerelationsparam (the user payload now carries the player's relations map); the "unclear" fallback returnshybrid: { ops: [] }.server/stub.ts— the stub always returnshybrid: { ops: [] }(deterministic; e2e unaffected).GameStatefield, nodb.tschange.server/engine.test.ts(all 48 rawTurnResultliterals updated with the required field).Local verification: typecheck ✅ · lint ✅ · test (386) ✅ · build ✅ — e2e runs in CI (
LLM_STUB=1, the stub is hybrid-free).Fixes #57