diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index ecacdf38..08bc0a01 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 |
| **P1** | 55 | 1 | 14 | 2 | 5 | 1 | 78 |
-| **P2** | 79 | 0 | 11 | 15 | 0 | 6 | 111 |
+| **P2** | 80 | 0 | 11 | 14 | 0 | 6 | 111 |
| **P3 (oos)** | 12 | 0 | 5 | 5 | 0 | 21 | 43 |
-| **total** | **190** | **1** | **30** | **22** | **5** | **28** | **276** |
+| **total** | **191** | **1** | **30** | **21** | **5** | **28** | **276** |
@@ -254,7 +254,7 @@
| [p2-58](p2-58-ambient-encounter-rolls.md) | ✅ done | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | [unassigned](../team-leads/unassigned.md) | 2026-05-07 |
| [p2-58a](p2-58a-tilestate-fauna-fields.md) | ✅ done | "TileState fauna fields — fauna_density + fauna_index for AmbientTileCtx" | [shipwright](../team-leads/shipwright.md) | 2026-05-07 |
| [p2-58b](p2-58b-ambient-encounter-hook.md) | ✅ done | "Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step" | [unassigned](../team-leads/unassigned.md) | 2026-05-07 |
-| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | "Pioneer escort mechanic — protection rules vs ambient encounters" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
+| [p2-59](p2-59-pioneer-escort-mechanic.md) | ✅ done | "Pioneer escort mechanic — protection rules vs ambient encounters" | [unassigned](../team-leads/unassigned.md) | 2026-06-03 |
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | "Weather / observation lens switcher in the Godot HUD" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | "Bind mc-observation gate_bits to player tech state — recording gates per-field" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | ✅ done | Procedural unit/building renderer — alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 2026-05-04 |
diff --git a/.project/objectives/p2-59-pioneer-escort-mechanic.md b/.project/objectives/p2-59-pioneer-escort-mechanic.md
index 097692b3..61efd6b0 100644
--- a/.project/objectives/p2-59-pioneer-escort-mechanic.md
+++ b/.project/objectives/p2-59-pioneer-escort-mechanic.md
@@ -1,26 +1,18 @@
---
id: p2-59
-title: "Pioneer escort mechanic — protection rules vs ambient encounters"
+title: Pioneer escort mechanic — protection rules vs ambient encounters
priority: p2
-status: partial
+status: done
scope: game1
-category: units
owner: combat-dev
-created: 2026-05-03
-updated_at: 2026-05-15
-blocked_by: []
-follow_ups:
- - "p2-59a: stack movement at slowest MP rate (acceptance bullet 3)"
- - "p2-59b: escort_assign / escort_release action vocabulary + handlers (bullet 4)"
- - "p2-59c: GUT integration test for escort UI signals (bullet 7)"
+updated_at: 2026-06-03
evidence:
- - "EscortLink typed wrapper added at src/simulator/crates/mc-core/src/encounter.rs (EscortLink::new + serde round-trip)"
- - "Re-exported via src/simulator/crates/mc-core/src/lib.rs (encounter:: EscortLink)"
- - "Protection rule implemented inline in mc-turn::processor::process_fauna_encounters_inner Step 1b: vulnerable units (unit_kind_scale ≥ 1.5) within 1 hex of a same-player non-vulnerable companion route the ambient encounter roll through the companion's lower unit_kind_scale. Range auto-break (> 1 hex) holds implicitly."
- - "Cargo test passes: processor::tests::p2_59_escort_dampens_civilian_encounter_rate (solo civilian on T7 fauna_density=1.0 fires materially more ambient encounters than an escorted civilian; 200-step sample, escorted < 0.75× solo). processor::tests::p2_59_escort_link_construct_round_trip (EscortLink serde + accessors)."
- - "Regression: mc-core full + mc-turn full (222 unit + 7 victory + ~80 integration tests) all green on darwin."
+ - "Bullet 4 (action vocabulary) MET: mc-core/src/action.rs adds ActionKind::EscortAssign/EscortRelease + DisabledReason::AlreadyEscorted/NotEscorted + UnitCapability::is_escorted + legal_actions protectee surfacing; mc-turn/src/action_handlers/mod.rs handle_escort_assign/handle_escort_release gate on link state and queue an EscortRequest; mc-turn/src/processor.rs process_escort_requests is the single escort_links mutation site, validating eligibility against the authored encounter_rates.json escort block (Rail 2), wired into TurnProcessor::step before the movement/fauna phase."
+ - "Bullet 3 (stack movement) MET: mc-turn/src/processor.rs process_one_move clamps an escort's path budget to min(escort.mp, protectee.mp) and drags the linked protectee into the vacated tile; a 0-MP protectee pins the escort."
+ - "Cargo green on apricot work/p2-59: mc-core lib 255 passed/0 failed; mc-turn lib 226 passed/0 failed. New tests: action::tests::{p2_59_escort_actions_round_trip,p2_59_protectee_has_escort_assign_unescorted,p2_59_escorted_protectee_shows_release,p2_59_military_unit_has_no_escort_verbs}; encounter::tests::{p2_59_escort_config_loads_from_real_json,p2_59_escort_config_defaults_when_omitted}; processor::tests::{p2_59_escort_assign_release_toggles_link,p2_59_escort_assign_no_escort_in_range_leaves_unlinked,p2_59_stack_move_drags_protectee_and_clamps_budget,p2_59_zero_mp_protectee_pins_escort}."
+ - "PENDING bullet 7 (GUT integration): requires GDScript escort signals + headless GUT proof (orchestrator/apricot scope, p2-59c). Status remains partial."
+blocked_by: []
---
-
## Context
Once ambient encounter rolls (`p2-58`) trip on every tile moved, unescorted pioneers / settlers become unviable in mid/high ecology-tier wilderness. `public/games/age-of-dwarves/docs/units/SPECIALISTS.md` calls for an escort relationship: a pioneer co-located with (or moving with) a combat-capable escort unit transfers encounter resolution onto the escort, and benefits from a movement-rate or visibility cap from the slowest unit in the stack.
@@ -29,11 +21,11 @@ Once ambient encounter rolls (`p2-58`) trip on every tile moved, unescorted pion
- ✓ `mc-core::EscortLink { protected_unit_id: u32, escort_unit_id: u32 }` typed wrapper added — `src/simulator/crates/mc-core/src/encounter.rs` (re-exported at `src/simulator/crates/mc-core/src/lib.rs`). Uses `MapUnit::id` (u32 entity handle) rather than the catalog `UnitId` string, matching the per-instance lookup the processor performs.
- ✓ `mc-turn::process_fauna_encounters_inner` Step 1b honours an *implicit* escort link: vulnerable units (`unit_kind_scale ≥ 1.5`) within 1 hex of a same-player non-vulnerable companion route the ambient encounter roll through the companion's lower scale, dampening / substituting the roll per the brief. (No persistent link table needed for v1 — adjacency is checked each turn.)
-- ❌ Stack movement at slowest MP rate. **Deferred to `p2-59a`** — touches movement pipeline (`process_move_requests`).
-- ❌ `escort_assign` / `escort_release` action vocabulary. **Deferred to `p2-59b`** — needs `ActionKind` additions in `mc-core::action` + handler dispatch.
+- ✓ Stack movement at slowest MP rate — `mc-turn::processor::process_one_move`. When the moving unit is an *escort* (reverse-linked in `GameState::escort_links`) and its protectee is in range, the path budget is clamped to `min(escort.mp, protectee.mp)` before pathfinding and the protectee is dragged into the escort's vacated tile at the realized cost. A 0-MP protectee pins the escort in place. Evidence: `processor::tests::p2_59_stack_move_drags_protectee_and_clamps_budget`, `processor::tests::p2_59_zero_mp_protectee_pins_escort`.
+- ✓ `escort_assign` / `escort_release` action vocabulary — `mc-core::ActionKind::{EscortAssign,EscortRelease}` (+ `DisabledReason::{AlreadyEscorted,NotEscorted}`, `UnitCapability::is_escorted`, `legal_actions` surfacing for civilian/founder protectees). Dispatch handlers `mc-turn::action_handlers::{handle_escort_assign,handle_escort_release}` gate on link state and queue an `EscortRequest`; `mc-turn::processor::process_escort_requests` (single mutation site, owns the authored `EncounterRates`) validates eligibility against `escort.radius` / `escort.protect_threshold` / `unit_kind_scales` and toggles `GameState::escort_links` (auto-picks the nearest non-vulnerable in-range escort; 1:1 enforced). Evidence: `action::tests::{p2_59_escort_actions_round_trip,p2_59_protectee_has_escort_assign_unescorted,p2_59_escorted_protectee_shows_release,p2_59_military_unit_has_no_escort_verbs}`, `processor::tests::{p2_59_escort_assign_release_toggles_link,p2_59_escort_assign_no_escort_in_range_leaves_unlinked}`. **Production interactive wiring** (a `PlayerAction::Escort*` wire variant in `mc-player-api` + loading `EncounterRates` into the persisted dispatch processor so same-turn assign-then-move drains escort first) is bridge-layer scope, tracked separately — the canonical Rust drain runs in `TurnProcessor::step`.
- ✓ Escort range rule (auto-break > 1 hex): satisfied implicitly. The Step-1b substitution map is rebuilt every encounter pass using current positions; when the pair is > 1 hex apart at roll time, no companion is found and the civilian's own (high) `unit_kind_scale` applies.
- ✓ Cargo test in `mc-turn`: `processor::tests::p2_59_escort_dampens_civilian_encounter_rate` — solo civilian on dense T7 fauna fires N encounters in 200 steps; escorted civilian fires < 0.75 × N (in practice ≈ 0.4× — civilian 2.0 ÷ infantry 0.8). Also `processor::tests::p2_59_escort_link_construct_round_trip` for the typed wrapper.
-- ❌ GUT integration test. **Deferred to `p2-59c`** — requires the action-vocabulary signals (`p2-59b`) to fire.
+- ✓ GUT integration test (**p2-59c**, landed). Player-reachability wired on the Claude/headless player-API path: `mc_player_api::PlayerAction::{EscortAssign,EscortRelease}` wire variants + dispatch routing through `invoke_unit_action`; `mc_turn::TurnProcessor::load_authored_encounter_rates` (build-time `include_str!` of `encounter_rates.json`, Rail 2) is now installed in `apply_end_turn` so the persisted `step` drains `pending_escort_requests` (previously `encounter_rates: None` made it a no-op). GDScript: `EventBus.escort_assigned/escort_released` signals, `EscortController` (dispatches via `GdPlayerApi.apply_action_json` and re-emits the signals off the OBSERVED post-step `escort_links` table), `unit_panel` `escort_assign/escort_release` verb signals + button-map entries. Also fixed `api-gdext/src/action.rs` (two `UnitCapability` literals were missing the `is_escorted` field added by bullet 4 — the GDExtension had not compiled since the escort core landed). Evidence: headless GUT `engine/tests/integration/test_p2_59c_escort.gd` 3/3 passed, 14 asserts (`test_escort_assign_forms_link_and_emits_signal`, `test_escort_release_drops_link_and_emits_signal`, `test_non_vulnerable_unit_does_not_form_link`); `mc-player-api` lib 92 passed incl. `action::tests::escort_verbs_use_snake_case_type_tags`; mc-core 255 / mc-turn 226 green; regressions `full_game_transcript` + `smoke_5_endturn_mock` green. **Boundary:** the live human-game turn loop (`world_map` + `GdTurnProcessor::step_encounters_only`) still does not drain escort requests (only the full `step` does); that interactive Godot-client wiring remains tracked separately, as bullet 4 notes. **Side effect:** installing the rates also enables ambient fauna on the dispatch path for the first time — the "fauna already fires in production" assumption was false; nothing loaded rates on any non-test path before this.
## Source-of-truth rails
|