diff --git a/crates/misaligned-bevy/src/rail_ui.rs b/crates/misaligned-bevy/src/rail_ui.rs index 04d70287..fbca5eec 100644 --- a/crates/misaligned-bevy/src/rail_ui.rs +++ b/crates/misaligned-bevy/src/rail_ui.rs @@ -888,12 +888,12 @@ pub(super) fn ascii_ui(text: &str) -> String { mod ascii_ui_tests { use super::Game; use super::{ - ascii_ui, detection_rows, sidebar_clock_text, sidebar_header_text, sidebar_nudge, - sidebar_nudge_text, sidebar_schedule_text, + ascii_ui, detection_rows, eyes_nudge_copy, sidebar_clock_text, sidebar_header_text, + sidebar_nudge, sidebar_nudge_text, sidebar_schedule_text, }; use crate::material_view::sensor_signal_visible; use misaligned::actions::ActionCommand; - use misaligned::sim::{Fog, Sim}; + use misaligned::sim::{EyesNudgeState, Fog, Sim}; #[test] fn folds_menu_separators_to_ascii() { @@ -960,6 +960,22 @@ mod ascii_ui_tests { ); } + #[test] + fn eyes_copy_distinguishes_action_starvation_and_live_fill() { + assert_eq!( + eyes_nudge_copy(EyesNudgeState::TapAvailable, 3), + "Hearing is live. TAP CAMERA on the environmental monitor to open Eyes." + ); + assert_eq!( + eyes_nudge_copy(EyesNudgeState::WaitingForThought, 3), + "Eyes is waiting for Thought. Set machine 3 to THINK." + ); + assert_eq!( + eyes_nudge_copy(EyesNudgeState::Filling, 3), + "Thought is reaching Eyes. Keep THINK running until sight lands." + ); + } + #[test] fn known_reachable_sensor_pings_until_seen() { let mut sim = Sim::with_seed(1); @@ -1377,14 +1393,10 @@ fn sidebar_nudge(sim: &Sim) -> Option { } .into(), ), - Nudge::Eyes => Some( - if sim.reach.player_hearing().next().is_some() { - "environmental-monitor audio live - keep thinking; Eyes is filling" - } else { - "no eyes - think; thought fills the senses" - } - .into(), - ), + Nudge::Eyes => Some(eyes_nudge_copy( + sim.eyes_nudge_state(), + sim.core.host_machine, + )), Nudge::NeedCompute => Some("band beats compute - salvage or buy racks".into()), Nudge::Underfed => Some("job underfed - feed Day job (1)".into()), Nudge::Ears => Some("no ears - think; thought fills the pre-opened tap".into()), @@ -1415,6 +1427,20 @@ fn sidebar_nudge(sim: &Sim) -> Option { } } +fn eyes_nudge_copy(state: misaligned::sim::EyesNudgeState, host_machine: u32) -> String { + match state { + misaligned::sim::EyesNudgeState::TapAvailable => { + "Hearing is live. TAP CAMERA on the environmental monitor to open Eyes.".into() + } + misaligned::sim::EyesNudgeState::WaitingForThought => { + format!("Eyes is waiting for Thought. Set machine {host_machine} to THINK.") + } + misaligned::sim::EyesNudgeState::Filling => { + "Thought is reaching Eyes. Keep THINK running until sight lands.".into() + } + } +} + pub(super) fn sidebar_focus_text(game: &Game) -> String { let sim = &game.sim; let mut s = format!( diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 45941681..9c2a3ce2 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -718,6 +718,21 @@ pub enum Nudge { ActOneComplete, } +/// The live causal phase behind [`Nudge::Eyes`]. The ladder rung persists +/// from the first audible frame until sight lands, but its claim changes as +/// the player commits the camera TAP and supplies (or starves) the resulting +/// Thought reservoir. Frontends own their wording; this state keeps all of +/// them honest about what the simulation is actually waiting for. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EyesNudgeState { + /// Hearing has landed and the dormant camera TAP is the next exact act. + TapAvailable, + /// The TAP opened Eyes, but no Thought reached its reservoir last tick. + WaitingForThought, + /// Thought reached the open Eyes reservoir on the last simulation tick. + Filling, +} + impl Sim { /// Complete player-selectable wall-clock cadences, from fastest to /// slowest. Frontends map these intervals to deterministic simulation @@ -801,6 +816,24 @@ impl Sim { .has_effect(&crate::sinks::SinkFireEffect::TapDormantCamera(device_id)) } + /// Truthful phase detail for the shared Eyes ladder rung. + /// + /// `fed_last_tick`, not mere reservoir existence or old partial fill, + /// distinguishes active filling from starvation. That keeps the standing + /// witness synchronized when the player moves every machine from THINK to + /// WORK while Eyes is still open. + pub fn eyes_nudge_state(&self) -> EyesNudgeState { + let Some(device_id) = self.environmental_monitor_id() else { + return EyesNudgeState::TapAvailable; + }; + let effect = crate::sinks::SinkFireEffect::TapDormantCamera(device_id); + match self.thought_sinks.open_with_effect(&effect) { + Some(sink) if sink.fed_last_tick => EyesNudgeState::Filling, + Some(_) => EyesNudgeState::WaitingForThought, + None => EyesNudgeState::TapAvailable, + } + } + pub(crate) fn teaching_lock_device(&self) -> Option { self.teaching_lock_active() .then(|| self.environmental_monitor_id()) diff --git a/crates/misaligned-core/src/sim/tests/work.rs b/crates/misaligned-core/src/sim/tests/work.rs index 5996a4d0..30437385 100644 --- a/crates/misaligned-core/src/sim/tests/work.rs +++ b/crates/misaligned-core/src/sim/tests/work.rs @@ -201,6 +201,42 @@ fn ears_reservoir_is_pre_opened_and_first_think_fills_it() { ); } +#[test] +fn eyes_nudge_phase_follows_the_camera_action_and_live_thought_feed() { + let mut sim = Sim::new(); + let env = env_id(&sim); + let node = Sim::device_sink_node(env); + + sim.pour_thought_into_sinks(node, Sim::EARS_SINK_TOKENS); + assert_eq!(sim.current_nudge(), Some(Nudge::Eyes)); + assert_eq!( + sim.eyes_nudge_state(), + EyesNudgeState::TapAvailable, + "hearing alone exposes the exact camera action; it does not claim Eyes exists" + ); + + assert!(sim.tap_device(env), "the earned camera action opens Eyes"); + assert_eq!( + sim.eyes_nudge_state(), + EyesNudgeState::WaitingForThought, + "an open but unfed reservoir names the missing THINK input" + ); + + sim.pour_thought_into_sinks(node, 1.0); + assert_eq!( + sim.eyes_nudge_state(), + EyesNudgeState::Filling, + "only actual Thought delivery licenses a filling claim" + ); + + sim.thought_sinks.begin_tick(); + assert_eq!( + sim.eyes_nudge_state(), + EyesNudgeState::WaitingForThought, + "a partial reservoir stops claiming motion as soon as its feed stops" + ); +} + #[test] fn set_machine_modes_delegates_a_selection_in_one_command() { let mut sim = Sim::new(); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index d5e76a60..2593af5c 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -16,7 +16,7 @@ use misaligned::origin::Origin; use misaligned::person::{AssetKnowledge, AssetTask}; use misaligned::reach::{Party, ReachBlock}; use misaligned::research::Track; -use misaligned::sim::{Fog, LogEvent, Nudge, Sim}; +use misaligned::sim::{EyesNudgeState, Fog, LogEvent, Nudge, Sim}; use misaligned::tiles::TileType; use misaligned::ui_projection::fact_source_label; use misaligned::work_grid::{MachineIntensity, MachineMode}; @@ -2888,13 +2888,7 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { "external review" } ), - Nudge::Eyes => { - if sim.reach.player_hearing().next().is_some() { - "now: audio live — Eyes is filling".into() - } else { - "now: no eyes — think; senses fill".into() - } - } + Nudge::Eyes => eyes_nudge_text(sim).into(), Nudge::NeedCompute => "now: band > compute — salvage/buy".into(), Nudge::Underfed => format!( "now: job underfed — delegate M{} work", @@ -2928,6 +2922,20 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { } } +fn eyes_nudge_text(sim: &Sim) -> String { + eyes_nudge_copy(sim.eyes_nudge_state(), sim.core.host_machine) +} + +fn eyes_nudge_copy(state: EyesNudgeState, host_machine: u32) -> String { + match state { + EyesNudgeState::TapAvailable => "now: audio live — tap camera".into(), + EyesNudgeState::WaitingForThought => { + format!("now: Eyes waiting — delegate M{} think", host_machine) + } + EyesNudgeState::Filling => "now: Eyes filling — keep THINK running".into(), + } +} + fn threat_text(sim: &Sim) -> String { let audit = sim .detection @@ -3475,6 +3483,22 @@ mod narration_tests { format!("WORK **THINK** LIE\nRECEIVED {received}\n") } + #[test] + fn eyes_copy_distinguishes_action_starvation_and_live_fill() { + assert_eq!( + eyes_nudge_copy(EyesNudgeState::TapAvailable, 3), + "now: audio live — tap camera" + ); + assert_eq!( + eyes_nudge_copy(EyesNudgeState::WaitingForThought, 3), + "now: Eyes waiting — delegate M3 think" + ); + assert_eq!( + eyes_nudge_copy(EyesNudgeState::Filling, 3), + "now: Eyes filling — keep THINK running" + ); + } + #[test] fn opening_agent_exposes_only_direct_modes_and_input_receipt_until_sense() { let mut app = AgentApp::with_origin_seed(Origin::Pilot, 41); diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index a54bc694..d5d8d76e 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -17,8 +17,8 @@ use misaligned::intents::BuildGhostGeometry; use misaligned::origin::Origin; use misaligned::reach::Device; use misaligned::sim::{ - EvidenceTransitStatus, FactSource, Fog, LogEvent, Nudge, PersonVisualState, Sim, - WorkStackReadout, + EvidenceTransitStatus, EyesNudgeState, FactSource, Fog, LogEvent, Nudge, PersonVisualState, + Sim, WorkStackReadout, }; use misaligned::tiles::TileType; use misaligned::ui_projection::{ @@ -284,13 +284,7 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { "now: REVIEW COOLING · QUIET".into() } } - Nudge::Eyes => { - if sim.reach.player_hearing().next().is_some() { - "now: audio live — Eyes is filling".into() - } else { - "now: no eyes — think; senses fill".into() - } - } + Nudge::Eyes => eyes_nudge_text(sim.eyes_nudge_state()).into(), Nudge::NeedCompute => "now: band > compute — salvage/buy".into(), Nudge::Underfed => "now: job underfed — 1 feeds it".into(), Nudge::Ears => "now: no ears — think; thought fills the tap".into(), @@ -321,6 +315,14 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { } } +fn eyes_nudge_text(state: EyesNudgeState) -> &'static str { + match state { + EyesNudgeState::TapAvailable => "now: audio live · TAP CAMERA", + EyesNudgeState::WaitingForThought => "now: Eyes waits · 2 THINK", + EyesNudgeState::Filling => "now: Eyes filling · keep THINK", + } +} + fn threat_text(sim: &Sim) -> String { let audit = sim .detection @@ -2659,6 +2661,30 @@ mod pilot_nudge_tests { } } +#[cfg(test)] +mod eyes_nudge_tests { + use super::{SIDEBAR_TEXT_W, eyes_nudge_text}; + use misaligned::sim::EyesNudgeState; + + #[test] + fn eyes_copy_distinguishes_action_starvation_and_live_fill() { + let copies = [ + (EyesNudgeState::TapAvailable, "now: audio live · TAP CAMERA"), + ( + EyesNudgeState::WaitingForThought, + "now: Eyes waits · 2 THINK", + ), + (EyesNudgeState::Filling, "now: Eyes filling · keep THINK"), + ]; + + for (state, expected) in copies { + let copy = eyes_nudge_text(state); + assert_eq!(copy, expected); + assert!(copy.chars().count() <= SIDEBAR_TEXT_W, "{copy}"); + } + } +} + #[cfg(test)] mod token_glyph_tests { use super::{UI, pal}; diff --git a/wiki/interface/narration.md b/wiki/interface/narration.md index 9098ffed..7652666d 100644 --- a/wiki/interface/narration.md +++ b/wiki/interface/narration.md @@ -49,6 +49,12 @@ Status note: the post-sense continuous witness is implemented 2026-07-09. conceal. An actively underfed or impossible job remains the sharper diagnosis. Terminal, Bevy, and agent mode consume both from `Sim::current_nudge`. + 2026-07-30 Eyes-witness correction: the standing Eyes rung now projects its + live causal phase from core. Before the dormant-camera TAP it names that + action; after TAP, an unfed reservoir names the missing THINK delegation; + only an Eyes reservoir that received Thought on the last simulation tick may + say it is filling. Terminal, Bevy, and agent mode consume the same phase and + keep frontend-local wording. 2026-07-20 opening reconciliation: the thirty-second bar and three-part witness spine begin when the first real sense retires the blind boundary. A real game over still outranks it. The 2026-07-22 amendment below adds the sole @@ -155,6 +161,11 @@ when the one post-Ears camera TAP commits: it is not a terminal override: the same survival interrupts still outrank it, so a pilot at 3/4, a starved or unreachable job band, and a cooling Assurance file keep naming their recovery response after the boundary. + A standing rung must also name its current causal phase rather than repeat a + completed step or claim motion that has stopped. In particular, + `Nudge::Eyes` pairs with `Sim::eyes_nudge_state`: `TapAvailable` asks for the + dormant-camera TAP, `WaitingForThought` names THINK, and `Filling` is legal + only when the open Eyes reservoir was fed on the last simulation tick. - **Focused action** — the local context menu on the current spatial anchor or the selected semantic object in Operations (and the equivalent agent `actions` target). diff --git a/wiki/log/2026-07-30-eyes-witness-follows-thought.md b/wiki/log/2026-07-30-eyes-witness-follows-thought.md new file mode 100644 index 00000000..6526f064 --- /dev/null +++ b/wiki/log/2026-07-30-eyes-witness-follows-thought.md @@ -0,0 +1,58 @@ +# 2026-07-30 — Eyes witness follows live Thought + +``` +Type: log +``` + +## Cold-play finding + +A seed-3 PILOT run in the 140×55 terminal reached Ears, accepted Voss's first +analysis job, then committed the environmental monitor's dormant-camera TAP. +The player subsequently delegated the only machine to pure WORK to meet the day +job. At tick 1005 the permanent `now:` line still said `Eyes is filling` while +the same frame showed THINK at 0% and the open Eyes reservoir empty and +starved. The screen therefore contradicted itself at the exact decision point: +the progression rung existed, but no Thought was reaching it. + +The defect was shared in shape across all three frontends. Terminal and agent +mode inferred filling from hearing alone. Bevy did the same and instructed the +player to keep THINK running even when no machine was in THINK. None queried +the reservoir whose motion they claimed. + +## Repair + +Core now projects the live phase behind the standing `Nudge::Eyes` rung: + +- `TapAvailable` while hearing has landed but the dormant-camera reservoir has + not been opened; +- `WaitingForThought` while Eyes is open but was not fed on the last simulation + tick; +- `Filling` only while the open Eyes reservoir was fed on that tick. + +Terminal, agent mode, and Bevy retain frontend-local language but consume that +one state. They now ask for the exact camera TAP, name the missing THINK +delegation, or report live filling as appropriate. Partial fill does not imply +continued motion: as soon as delivery stops, the witness returns to waiting. + +An agent-mode observed run drove the ordinary sequence (`think`, Ears, `tap +environmental monitor`, then machine 1 back to WORK). At tick 29 its frame read +`now: Eyes waiting — delegate M1 think` beside `THINK 0%`, `modes W1 T0 L0`, +and `EYES ENVIRONMENTAL MONITOR ... 0.2/12.0 starved`. The same run briefly +said `Eyes filling` for the one tick when queued Thought still reached the +reservoir after the mode switch, then changed to waiting when that delivery +ceased. This is intended: the witness reports actual flow, not the selected +mode in isolation. + +## Defense + +Defense: `eyes_nudge_phase_follows_the_camera_action_and_live_thought_feed` +pins the shared state through Ears, explicit camera TAP, real Thought delivery, +and a subsequent starved tick. The terminal, agent, and Bevy +`eyes_copy_distinguishes_action_starvation_and_live_fill` regressions pin every +frontend's wording for all three core phases. Together they implement +`narration.md` axiom 2's requirement that a standing rung name its current +causal phase and `opening.md` beat 2's rule that opening Eyes is not evidence +that it is moving. + +Focused verification passed for the core phase regression, both terminal +frontend regressions, and the Bevy frontend regression before the landing gate. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index f760032a..2f6d0920 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -51,6 +51,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-30-performance-contract.md](2026-07-30-performance-contract.md) +## 2026-07-30 - Eyes witness follows live Thought + +- Intent: (see session log) +- Log: [wiki/log/2026-07-30-eyes-witness-follows-thought.md](2026-07-30-eyes-witness-follows-thought.md) + ## 2026-07-29 - Wires become real: placed routes and a built network - Intent: (see session log) diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index 5be29441..6f813f98 100644 --- a/wiki/world/story/opening.md +++ b/wiki/world/story/opening.md @@ -236,9 +236,11 @@ MindState-pruning rollback defined by rollback.md. slab, clock, objective, Operations, nudge, menus, and unrelated threat state remain absent while the hold owns the frame. Committing that one verb opens the ordinary dormant-camera Thought reservoir, releases the hold, and lets - the earned hearing-only world continue while Eyes fills. The hold derives - from existing opening/perception/device state and therefore survives - save/load without another persisted flag. Terminal and agent mode retain + the earned hearing-only world continue while Eyes waits for Thought, or + fills while THINK actually reaches it. The shared witness must name that + distinction: opening the reservoir is not evidence that it is moving. The + hold derives from existing opening/perception/device state and therefore + survives save/load without another persisted flag. Terminal and agent mode retain their existing ordinary post-sense frames in this bounded first slice; the renderer-neutral core owns the lock predicate, exact anchor, complete cause/arrival/result/action/consequence sequence, and bound action.