diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md
index 77fb8084..0408dbf7 100644
--- a/.project/objectives/DASHBOARD_CATEGORIES.md
+++ b/.project/objectives/DASHBOARD_CATEGORIES.md
@@ -269,6 +269,7 @@
| [p2-38](p2-38-unit-audio-cues-stubs.md) | ✅ done | P2 | Unit audio_cues stub strings — selection/move/attack lines for the dwarven roster | [asset-audio](../team-leads/asset-audio.md) | 🟢 |
| [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | ✅ done | P2 | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | — | 🟢 |
| [p2-43](p2-43-culture-research-completion-event.md) | 🟡 partial | P2 | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | 🟢 |
+| [p2-43a](p2-43a-rust-port-culture-pick.md) | 🔴 stub | P3 | Rail-1 port — `_pick_culture_tradition` → mc-ai::tactical::culture_pick | — | 🟢 |
| [p2-44](p2-44-ai-promotion-selection.md) | 🟡 partial | P2 | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | 🟢 |
| [p2-45](p2-45-elimination-reconciliation.md) | ✅ done | P2 | Player elimination reconciliation — emit `player_eliminated` on every transition | — | 🟢 |
| [p2-46](p2-46-past-games-archive-replay-viewer.md) | 🟡 partial | P2 | Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 🟢 |
@@ -311,6 +312,7 @@
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | P2 | Bind mc-observation gate_bits to player tech state — recording gates per-field | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | ✅ done | P2 | Procedural unit/building renderer — alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 🟢 |
| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | 🔴 stub | P2 | mc-flora generation: migrate biome filter to substrate_climate-aware path | [unassigned](../team-leads/unassigned.md) | 🟢 |
+| [p2-64](p2-64-apricot-async-batch-protocol.md) | 🟡 partial | P2 | Apricot async batch protocol — launch / status / fetch decoupling | [simulator-infra](../team-leads/simulator-infra.md) | 🟢 |
| [p3-01](p3-01-courier-diplomacy.md) | ✅ done | P3 | Courier-gated diplomacy — open borders + shared maps via tech-tiered courier units | [envoy](../team-leads/envoy.md) | 🟢 |
| [p3-03](p3-03-courier-route-resolver.md) | ✅ done | P3 | Courier route resolver — real hex pathfinding, per-tier movement, severable infrastructure | [envoy](../team-leads/envoy.md) | 🟢 |
| [p3-04](p3-04-per-hex-improvement-layer.md) | ✅ done | P3 | Per-hex improvement layer in `mc-core` / `mc-turn` — anchor improvements at (col,row) | [envoy](../team-leads/envoy.md) | 🟢 |
@@ -322,7 +324,7 @@
| [p3-06](p3-06-civic-anarchy-and-axis-switching.md) | 🟡 partial | P3 | Civic anarchy — 5-turn anarchy on axis switch | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p3-07a](p3-07a-cv-wealth-and-authority-amplifier.md) | 🔴 stub | P3 | CV-of-wealth + Authority amplifier → inequality stat | [unassigned](../team-leads/unassigned.md) | 🔒 p3-05b |
| [p3-07b](p3-07b-four-damage-channels.md) | 🔴 stub | P3 | Four damage channels — Land/Water/Magic/Air emission from inequality | [unassigned](../team-leads/unassigned.md) | 🔒 p3-07a |
-| [p3-10a](p3-10a-lair-assault-mode.md) | 🔴 stub | P3 | Lair assault mode — enter-and-clear | [unassigned](../team-leads/unassigned.md) | 🟢 |
+| [p3-10a](p3-10a-lair-assault-mode.md) | 🟡 partial | P3 | Lair assault mode — enter-and-clear | [unassigned](../team-leads/unassigned.md) | 🟢 |
| [p3-10b](p3-10b-lair-siege-mode.md) | 🔴 stub | P3 | Lair siege mode — multi-turn pressure from adjacent | [unassigned](../team-leads/unassigned.md) | 🔒 p3-10a |
| [p3-10c](p3-10c-lair-raid-mode.md) | 🔴 stub | P3 | Lair raid mode — grab-and-exit | [unassigned](../team-leads/unassigned.md) | 🔒 p3-10a |
| [p3-11](p3-11-pioneer-engineer-action-points.md) | 🟡 partial | P3 | Pioneer & Engineer action-point pool | [unassigned](../team-leads/unassigned.md) | 🟢 |
diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 44c739ad..6a04a28d 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 |
| **P1** | 1 | 13 | 2 | 6 | 1 | 50 | 73 |
-| **P2** | 0 | 11 | 11 | 0 | 6 | 58 | 86 |
-| **P3 (oos)** | 0 | 6 | 10 | 0 | 21 | 5 | 42 |
-| **total** | **1** | **30** | **23** | **6** | **28** | **157** | **245** |
+| **P2** | 0 | 12 | 11 | 0 | 6 | 58 | 87 |
+| **P3 (oos)** | 0 | 7 | 10 | 0 | 21 | 5 | 43 |
+| **total** | **1** | **32** | **23** | **6** | **28** | **157** | **247** |
@@ -29,7 +29,7 @@
| [unassigned](../team-leads/unassigned.md) | 26 |
| [asset-sprite](../team-leads/asset-sprite.md) | 6 |
| [shipwright](../team-leads/shipwright.md) | 5 |
-| [simulator-infra](../team-leads/simulator-infra.md) | 3 |
+| [simulator-infra](../team-leads/simulator-infra.md) | 4 |
| [testwright](../team-leads/testwright.md) | 3 |
| [combat-dev](../team-leads/combat-dev.md) | 2 |
| [warcouncil](../team-leads/warcouncil.md) | 2 |
@@ -79,7 +79,7 @@
|---|---|---|---|---|---|---|
| [p2-10](p2-10-regression-ci-gate.md) | 🟡 partial | Automated regression CI gate on every push to main | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked |
| [p2-18](p2-18-guide-public-deployment.md) | 🟡 partial | Guide web app — public hosting + deploy pipeline | — | — | 2026-04-17 | 🟢 unblocked |
-| [p2-43](p2-43-culture-research-completion-event.md) | 🟡 partial | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | — | 2026-05-04 | 🟢 unblocked |
+| [p2-43](p2-43-culture-research-completion-event.md) | 🟡 partial | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | — | 2026-05-05 | 🟢 unblocked |
| [p2-44](p2-44-ai-promotion-selection.md) | 🟡 partial | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | — | 2026-05-04 | 🟢 unblocked |
| [p2-46](p2-46-past-games-archive-replay-viewer.md) | 🟡 partial | Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback | — | [shipwright](../team-leads/shipwright.md) | 2026-05-05 | 🟢 unblocked |
| [p2-47](p2-47-in-game-statistics-screens.md) | 🟡 partial | In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked |
@@ -88,6 +88,7 @@
| [p2-56c](p2-56c-master-grandmaster-auras.md) | 🟡 partial | Master / Grandmaster auras — adjacent-slot yield propagation | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked |
| [p2-57a](p2-57a-typed-resource-stockpile.md) | 🟡 partial | Typed resource stockpile — raw vs processed taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked |
| [p2-58](p2-58-ambient-encounter-rolls.md) | 🟡 partial | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | — | [unassigned](../team-leads/unassigned.md) | 2026-05-05 | 🟢 unblocked |
+| [p2-64](p2-64-apricot-async-batch-protocol.md) | 🟡 partial | Apricot async batch protocol — launch / status / fetch decoupling | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-05 | 🟢 unblocked |
| [p2-10k](p2-10k-gdlint-cleanup.md) | 🔴 stub | CI: fix 51 gdlint violations so Stage 3 is hard-green | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked |
| [p2-10l](p2-10l-gut-regression-triage.md) | 🔴 stub | CI: fix 15 GUT regressions so Stage 5 is hard-green | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked |
| [p2-55d](p2-55d-ai-ransom-decision-hook.md) | 🔴 stub | AI ransom accept/refuse hook in mc-turn start-of-turn | — | — | 2026-05-03 | 🟢 unblocked |
diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json
index 5a4949ea..4a83a1b7 100644
--- a/.project/objectives/objectives.json
+++ b/.project/objectives/objectives.json
@@ -1,13 +1,13 @@
{
- "generated_at": "2026-05-05T16:06:46Z",
+ "generated_at": "2026-05-05T18:13:24Z",
"totals": {
"done": 157,
"in_progress": 1,
- "partial": 30,
+ "partial": 32,
"stub": 23,
"missing": 6,
"oos": 28,
- "total": 245
+ "total": 247
},
"objectives": [
{
@@ -1760,7 +1760,7 @@
"priority": "p2",
"status": "partial",
"scope": "game1",
- "updated_at": "2026-05-04",
+ "updated_at": "2026-05-05",
"blocked_by": [],
"summary": "`EventBus.culture_researched(tradition_id, player_index)` is defined and\n**every downstream consumer is wired** (AudioManager handler, manifest\nentry `culture_researched`, the asset shipped at\n`public/resources/audio/sfx/ui/culture_researched.ogg`). What's missing\nturned out to be deeper than the original framing of this objective: the\n**entire per-turn culture-research path doesn't run in the live game**.\n\n### Trace\n\n- `turn_manager.gd:246` calls `_process_culture(player, game_map)`\n- `turn_processor.gd:360 _process_culture` only handles **border\n expansion** via `city.process_culture_with_modifier()` — no\n tradition-research accumulator\n- `processor.rs:604 process_culture_research` (Rust mc-turn) **does**\n drive tradition completion via `mc_culture::CultureResearchResult`,\n but it lives in the bench / legacy-headless path, not in the\n GDScript-driven live-game per-turn\n- Tech has a Rust GDExt method `tech_web.process_research(player_dict,\n yields, mult) → {new_progress, new_researching, completed_tech}` that\n GDScript calls in `turn_processor.gd::_process_research` — **no\n equivalent exists for culture**\n\nSo in the playable game today: `culture_research_progress` never\nincrements, `researched_traditions` never grows, no completion event\never fires. `p1-28` shipped the UI and the data graph but not the\nruntime accumulator."
},
@@ -2229,6 +2229,17 @@
"blocked_by": [],
"summary": "Authored flora JSON files have migrated from a top-level `biomes: [...]`\narray to the `substrate_climate` ontology\n(see `p2-52-substrate-flora-cover-ontology-split.md`). The biome-filter\nloop in `mc-flora/src/generation.rs` was not updated, so the\n`AuthoredSpeciesFile.biomes` field is now empty for every authored file\nand the candidate-pool query returns nothing.\n\nTwo pre-existing regression tests in `mc-flora` document the gap:\n\n- `load_authored_returns_species_for_known_biome` (generation.rs:645)\n- `generate_flora_for_biome_more_species_with_authored_files`\n (generation.rs:695)\n\nBoth fail because the biome filter at `generation.rs:508-510` still\nchecks `raw.biomes.iter().any(|b| b == biome_id)` while the JSON now\nencodes biome eligibility through `substrate_climate` blocks the loader\ndoes not currently inspect."
},
+ {
+ "id": "p2-64",
+ "title": "Apricot async batch protocol — launch / status / fetch decoupling",
+ "priority": "p2",
+ "status": "partial",
+ "scope": "game1",
+ "owner": "simulator-infra",
+ "updated_at": "2026-05-05",
+ "blocked_by": [],
+ "summary": ""
+ },
{
"id": "g2-01",
"title": "Ley lines — Game 2 (Age of Kzzykt)",
@@ -2446,6 +2457,16 @@
"blocked_by": [],
"summary": "Persistent trade-route units (caravans, traders) that travel between owned cities OR between own-city and foreign-city, generate per-turn gold/resource yields tied to distance and city-pair characteristics, and can be plundered by enemy units. Distinct from p1-01's instantaneous luxury-for-gold trade modal."
},
+ {
+ "id": "p2-43a",
+ "title": "Rail-1 port — `_pick_culture_tradition` → mc-ai::tactical::culture_pick",
+ "priority": "p3",
+ "status": "stub",
+ "scope": "game1",
+ "updated_at": "2026-05-03",
+ "blocked_by": [],
+ "summary": "Phase A of `p2-43` landed the AI culture-tradition picker as GDScript in\n`auto_play.gd::_pick_culture_tradition`. This violates Rail-1 (Rust is\nthe simulation source of truth) and is filed here as the explicit\nport-back follow-up.\n\nMirror the shape of `mc-ai::tactical::pick_promotion`:\n\n- New module `src/simulator/crates/mc-ai/src/tactical/culture_pick.rs`\n with `pub fn pick_culture_tradition(state: &PlayerState, available: &[TraditionId], priors: &PersonalityPriors) -> Option`.\n- Extend `PersonalityPriors` (in `policy.rs`) with `culture_pillar_weights: BTreeMap` and a single\n `culture_cost_bias: f32` knob — no parallel structs, no stringly maps.\n- Bridge through `GdAiController::pick_culture_tradition(player_dict, available_array)` in\n `api-gdext/src/ai.rs` (alongside the existing promotion bridge).\n- Replace the `_pick_culture_tradition` body in `auto_play.gd` with a\n one-liner delegating to the bridge. Delete the local scoring code —\n Zero-Tech-Debt rail forbids leaving the GDScript shadow.\n- GUT test asserts the bridge returns the same id sequence the Phase A\n GDScript would have, using a fixed personality + tradition graph.\n- `cargo test -p mc-ai test_culture_pick_personality_weighting` green."
+ },
{
"id": "p2-55f",
"title": "Read ransom_offer_duration_turns from combat_balance.json",
@@ -2596,10 +2617,10 @@
"id": "p3-10a",
"title": "Lair assault mode — enter-and-clear",
"priority": "p3",
- "status": "stub",
+ "status": "partial",
"scope": "game1",
"owner": "unassigned",
- "updated_at": "2026-05-03",
+ "updated_at": "2026-05-05",
"blocked_by": [
"p0-17"
],
@@ -2845,7 +2866,7 @@
},
{
"owner": "simulator-infra",
- "remaining": 3
+ "remaining": 4
},
{
"owner": "testwright",
diff --git a/.project/objectives/p2-43a-rust-port-culture-pick.md b/.project/objectives/p2-43a-rust-port-culture-pick.md
index e9996595..8b097463 100644
--- a/.project/objectives/p2-43a-rust-port-culture-pick.md
+++ b/.project/objectives/p2-43a-rust-port-culture-pick.md
@@ -2,7 +2,7 @@
id: p2-43a
title: "Rail-1 port — `_pick_culture_tradition` → mc-ai::tactical::culture_pick"
priority: p3
-status: open
+status: stub
scope: game1
updated_at: 2026-05-03
evidence:
diff --git a/.project/objectives/p2-64-apricot-async-batch-protocol.md b/.project/objectives/p2-64-apricot-async-batch-protocol.md
index 86a19ef6..e276d10c 100644
--- a/.project/objectives/p2-64-apricot-async-batch-protocol.md
+++ b/.project/objectives/p2-64-apricot-async-batch-protocol.md
@@ -2,15 +2,28 @@
id: p2-64
title: Apricot async batch protocol — launch / status / fetch decoupling
priority: p2
-status: stub
+status: partial
scope: game1
-category: infra
owner: simulator-infra
-created: 2026-05-05
updated_at: 2026-05-05
+evidence:
+ - scripts/apricot-run.sh launch/status/fetch sub-modes
+ - scripts/apricot-async-smoke.sh
+ - tooling/claude/dot-claude/instructions/canonical-commands.md
blocked_by: []
-follow_ups: []
---
+## p2-64 close-out (2026-05-05)
+
+Three new sub-modes added to `scripts/apricot-run.sh`:
+- `launch ` — writes a per-stamp launcher.sh into `~/.cache/mc-batches//`, starts it under `systemd-run --user --collect --unit=mc-batch-`. Returns immediately with `STAMP=` on stdout.
+- `status ` — single ssh `ConnectTimeout=5` probe with three lightweight `ls | wc -l`-style checks; emits one-line JSON (`state` ∈ `running|complete|failed|unreachable`).
+- `fetch ` — `rsync -a --partial`; resumable across drops; exit 1 if not yet complete.
+- Documentation in script header + canonical-commands.md (already committed `5a57a6ac6`).
+- Smoke test at `scripts/apricot-async-smoke.sh`.
+
+**Status: partial.** Implementation landed; needs at least one real-batch validation run (the smoke test exists but a true intermittent-connectivity scenario hasn't been exercised yet). Close to done.
+
+Existing synchronous modes (`smoke`, `huge-map-5clan`, `ai-quality-baseline-pre-c`, etc.) keep working — `launch` is a wrapper, not a replacement.
## Context
diff --git a/.project/objectives/p3-10a-lair-assault-mode.md b/.project/objectives/p3-10a-lair-assault-mode.md
index a58a1c80..e58d16f3 100644
--- a/.project/objectives/p3-10a-lair-assault-mode.md
+++ b/.project/objectives/p3-10a-lair-assault-mode.md
@@ -1,16 +1,22 @@
---
id: p3-10a
-title: "Lair assault mode — enter-and-clear"
+title: Lair assault mode — enter-and-clear
priority: p3
-status: stub
+status: partial
scope: game1
-category: combat
owner: unassigned
-created: 2026-05-03
-updated_at: 2026-05-03
+updated_at: 2026-05-05
+evidence:
+ - src/simulator/crates/mc-core/src/lair.rs
+ - src/simulator/crates/mc-combat/src/lair.rs
+ - mc-core lair.rs serde + ord + default tests
blocked_by: [p0-17]
-follow_ups: []
---
+## p3-10a close-out (2026-05-05)
+
+`mc_core::lair::LairCombatMode` typed enum landed (variants `Assault | Siege | Raid`, snake_case serde, derive `Default` = Assault for backward compat with existing lair-clear callers). `mc_combat::lair` accepts the mode parameter at 7 existing call sites; passing `Default::default()` preserves p0-17 behavior. Serde round-trip + Ord-consistency tests + default-mode test land in `mc-core::lair::tests`.
+
+**Status: partial.** Assault mode is the existing-behavior typed-default — no new combat resolution logic shipped (the existing path IS the assault). Siege (p3-10b) and Raid (p3-10c) implementations are the structural follow-ups; both would extend `mc_combat::lair::resolve_lair_combat` with mode-specific branches.
## Context
|