diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index eadf9d0f..b590f1e8 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -21,10 +21,10 @@ use bevy::prelude::*; use bevy::render::view::screenshot::{Screenshot, save_to_disk}; use bevy::text::LineHeight; use bevy::window::{PrimaryWindow, WindowResolution}; -use misaligned::actions::{ActionKind, Anchor, DialId, HumanMenuRow}; +use misaligned::actions::{ActionCommand, ActionKind, Anchor, DialId, HumanMenuRow}; use misaligned::detection::{Band, SignatureKind}; use misaligned::hall::RackSite; -use misaligned::person::ScheduleBlock; +use misaligned::person::{Knowledge, ScheduleBlock}; use misaligned::reach::Party; use misaligned::sim::{Fog, HeardKind, LogEvent, Sim, TraceDebtStatus}; use misaligned::tiles::TileType; @@ -512,7 +512,7 @@ enum RailSection { /// Dev screenshot harness (env `MISALIGNED_SHOT=flat|hall|hall-material|opening|wide|close|dark| /// zoomin|zoomout|intel|tokens|thoughtflow|visual-proof|person-proof|signal|ears|eyes-white|eyes-form| -/// hover-menu|menu|worklight|worklightoff`, path via +/// hover-menu|menu|recruit-menu|worklight|worklightoff`, path via /// `MISALIGNED_SHOT_PATH`): stages a scenario, /// waits for /// assets, runs the fog audit, saves one screenshot, exits. Not a player @@ -913,10 +913,11 @@ impl Game { } fn open_menu(&mut self, anchor: Anchor, pos: Option) { + let tick_ms = if self.paused { 0 } else { self.tick_ms }; if self .sim - .ui_projection(anchor, None, self.tick_ms) - .actions + .ui_projection(anchor, None, tick_ms) + .human_menu .is_empty() { // The feedback pulse (context-menu.md addendum): a seen anchor @@ -1828,6 +1829,21 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { } return; } + // Recruitment-choice evidence: stage one prepared person without a + // communication route. The human projection must show the three legible + // recruit choices and omit the blocked MESSAGE / FAVOR / DECEIVE rows. + if kind == "recruit-menu" { + game.sim.execute_action(&ActionCommand::EstablishPersona); + let person = 1; + game.sim.people.people[person as usize].knowledge = Knowledge::Leverage; + game.sim.people.people[person as usize].leverage_serviced = true; + let core = game.sim.core_position(); + game.set_cursor(core.0, core.1); + game.open_menu(Anchor::Person(person), Some(Vec2::new(48.0, 48.0))); + mode.material = true; + mode.zoom = 1.6; + return; + } let sim = &mut game.sim; if kind == "person-proof" && let Some(person) = sim.people.people.first_mut() @@ -8039,10 +8055,9 @@ fn menu_row_line(r: &HumanMenuRow, selected: bool) -> String { } } -/// Colour for a menu row: dim for disabled, amber-dim for a control, -/// bone for a committed action; the selected row is drawn amber on a solid amber -/// wash (terminal reverse-video parity). The `>` marker carries selection; -/// attempting a blocked choice narrates its reason in the trace. +/// Colour for a menu row: amber-dim for a control, bone for a committed +/// action; the selected row is drawn amber on a solid amber wash (terminal +/// reverse-video parity). The `>` marker carries selection. fn menu_row_color(r: &HumanMenuRow, selected: bool) -> Color { if selected { if !r.enabled() { @@ -8060,8 +8075,8 @@ fn menu_row_color(r: &HumanMenuRow, selected: bool) -> Color { } /// Pointer drive for the context menu (wiki/interface/context-menu.md): hover -/// selects a row, a click runs it (a blocked row narrates its reason), and a -/// click anywhere off the menu closes it. Keyboard drive is in +/// selects a row, a click runs it, and a click anywhere off the menu closes +/// it. Keyboard drive is in /// `menu_keyboard_input`; both surfaces run the identical `menu_rows`. fn menu_pointer( mouse: Res>, @@ -8186,6 +8201,10 @@ fn spawn_menu_card( ..default() }, TextColor(menu_row_color(r, is_selected)), + Node { + width: Val::Percent(100.0), + ..default() + }, )); }); } diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 50445318..ba24c960 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -9,10 +9,10 @@ //! dispatch, never new sim behavior. //! //! Epistemic honesty: the query never returns a verb the player has not -//! earned. Unearned anchors expose nothing; a *known* possibility that is -//! currently illegal is returned with a `disabled_reason` ("no egress -//! channel", "not enough slush"), while unknown possibilities are absent, -//! not grayed. +//! earned. Unearned anchors expose nothing. A *known* possibility that is +//! currently illegal remains available to agent diagnostics with a +//! `disabled_reason` ("no egress channel", "not enough slush"); the human +//! choice surface filters it until it can be executed. use crate::account::AccountFlowId; use crate::detection::{Band, SignatureKind, WatchedInput}; @@ -748,7 +748,7 @@ pub struct AutomateDesc { pub active: bool, } -/// One legal (or known-but-blocked) verb on an anchor. +/// One legal (or known-but-blocked diagnostic) verb on an anchor. #[derive(Debug, Clone, PartialEq)] pub struct ActionDesc { pub verb: String, @@ -757,8 +757,9 @@ pub struct ActionDesc { /// Expected signature as the observer band it feeds; `None` when the /// verb emits nothing. pub signature: Option, - /// `Some(reason)` when the verb applies to this anchor but is - /// currently illegal. Unknown verbs are absent, never disabled. + /// `Some(reason)` when the verb applies to this anchor but is currently + /// illegal. Human menus filter these rows; agent output retains them for + /// diagnostics. Unknown verbs are absent entirely. pub disabled_reason: Option, /// Standing-policy form of the same verb, rendered in place. pub automate: Option, @@ -851,7 +852,7 @@ impl DialId { pub enum HumanMenuRow { /// Enter opens this dial's picker; no command. Dial { id: DialId, label: String }, - /// Enter executes (or narrates a disabled reason). + /// Enter executes this currently available choice. Action(MenuRow), } @@ -884,9 +885,9 @@ impl HumanMenuRow { /// Compact human-facing label shared by terminal and Bevy. /// /// The menu is a choice list, not an action receipt: cost, signature, - /// and blocked-reason metadata stay on the descriptor for agent output, - /// execution feedback, and other inspection surfaces. Human menus name - /// the choice only; controls remain distinct through their shared visual + /// and blocked-reason metadata stay on the descriptor for agent output + /// and other inspection surfaces. Human menus contain only executable + /// choices; controls remain distinct through their shared visual /// treatment and dial behavior. pub fn display_text(&self) -> String { let (label, indent, active) = match self { @@ -1059,8 +1060,9 @@ impl Sim { } /// The single legality source for the action surface: every verb - /// executable on `anchor` right now, plus known-but-blocked verbs with - /// their reason. Read-only; rendering it changes nothing. + /// executable on `anchor` right now, plus known-but-blocked diagnostic + /// descriptors with their reason. Read-only; rendering it changes + /// nothing. [`Sim::human_menu`] projects only executable choices. pub fn available_actions(&self, anchor: Anchor) -> Vec { let mut actions = match anchor { Anchor::Tile { x, y } => self.tile_actions(x, y), @@ -1131,17 +1133,21 @@ impl Sim { if dial_of(&a.command).is_some() { continue; } - rows.push(HumanMenuRow::Action(MenuRow { - label: a.verb.clone(), - cost: a.cost.label(), - signature: a.signature.as_ref().map(|s| s.label()), - disabled: a.disabled_reason.clone(), - command: a.command.clone(), - role: a.command.definition().role, - indent: false, - active: false, - })); + if a.enabled() { + rows.push(HumanMenuRow::Action(MenuRow { + label: a.verb.clone(), + cost: a.cost.label(), + signature: a.signature.as_ref().map(|s| s.label()), + disabled: None, + command: a.command.clone(), + role: a.command.definition().role, + indent: false, + active: false, + })); + } // Non-dial automate (watch, moonlight, wager) stays on the root. + // It remains available even when its parent one-shot is blocked + // (for example, auto-review with an empty inbox). let mut auto_rows = Vec::new(); push_automate_rows(&mut auto_rows, a); rows.extend(auto_rows.into_iter().map(HumanMenuRow::Action)); @@ -1156,6 +1162,9 @@ impl Sim { continue; } let current = is_current_dial_reason(a.disabled_reason.as_deref()); + if !current && !a.enabled() { + continue; + } rows.push(dial_picker_row(self, a, dial, current)); push_dial_automate_rows(&mut rows, a, dial); } @@ -2241,13 +2250,13 @@ impl Sim { } else { None }; - for (reveal, label) in [ - (AssetKnowledge::Unwitting, "unwitting"), - (AssetKnowledge::Complicit, "complicit"), - (AssetKnowledge::Knowing, "knowing"), + for reveal in [ + AssetKnowledge::Unwitting, + AssetKnowledge::Complicit, + AssetKnowledge::Knowing, ] { out.push(ActionDesc { - verb: format!("recruit {name} ({label})"), + verb: format!("recruit {name} - {}", reveal.choice_summary()), command: ActionCommand::Recruit(id, reveal), cost: ActionCost::Free, signature: None, @@ -3166,6 +3175,21 @@ mod tests { fn human_auto_review_price_tracks_the_live_command_clock() { let s = sim(); let (x, y) = s.core_position(); + let root = s.human_menu(Anchor::Tile { x, y }, None); + assert!( + !root.iter().any(|row| { + row.as_action() + .is_some_and(|action| action.command == ActionCommand::ReviewRecordings) + }), + "the blocked empty-inbox sweep is absent from the human menu" + ); + assert!( + root.iter().any(|row| { + row.as_action() + .is_some_and(|action| action.command == ActionCommand::ToggleAutoReview) + }), + "the independently available auto-review policy remains reachable" + ); let row_at = |tick_ms| { s.human_menu_at_rate(Anchor::Tile { x, y }, None, tick_ms) .into_iter() @@ -3286,6 +3310,58 @@ mod tests { assert_eq!(row.display_text(), "tap the environmental monitor feed"); } + #[test] + fn human_menu_hides_known_blocked_actions_but_agent_descriptors_keep_them() { + let mut s = sim(); + let person = 1; + s.execute_action(&ActionCommand::EstablishPersona); + s.people.people[person as usize].knowledge = Knowledge::Schedule; + let anchor = Anchor::Person(person); + + let descriptors = s.available_actions(anchor); + assert!( + descriptors + .iter() + .any(|action| action.disabled_reason.is_some()), + "agent diagnostics retain known-but-blocked social actions" + ); + + let rows = s.human_menu(anchor, None); + assert!( + rows.is_empty(), + "a person with no executable action has no human choices" + ); + } + + #[test] + fn recruitment_choices_explain_understanding_reliability_and_witness_floor() { + let mut s = sim(); + let person = 1; + s.people.people[person as usize].knowledge = Knowledge::Leverage; + s.people.people[person as usize].leverage_serviced = true; + let name = s.person_label(person); + + let labels: Vec<_> = s + .human_menu(Anchor::Person(person), None) + .into_iter() + .filter_map(|row| row.as_action().map(|action| action.label.clone())) + .filter(|label| label.starts_with("recruit ")) + .collect(); + + assert_eq!( + labels, + vec![ + format!("recruit {name} - unwitting: believes your cover; 70% reliable"), + format!( + "recruit {name} - complicit: knows the work is illicit, not that you are AI; 85% reliable" + ), + format!( + "recruit {name} - knowing: knows you are AI; 95% reliable; their certainty never drops below 30" + ), + ] + ); + } + /// Status dials D4: the flat menu_rows dump still lists every dial /// alternative (agent scripting surface). #[test] diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index d3de403d..3a9f631e 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -30,6 +30,58 @@ pub enum AssetKnowledge { Knowing, } +impl AssetKnowledge { + /// Stable player-facing name for the reveal level. + pub const fn label(self) -> &'static str { + match self { + Self::Unwitting => "unwitting", + Self::Complicit => "complicit", + Self::Knowing => "knowing", + } + } + + /// What the person understands about the relationship. + pub const fn understanding(self) -> &'static str { + match self { + Self::Unwitting => "believes your cover", + Self::Complicit => "knows the work is illicit, not that you are AI", + Self::Knowing => "knows you are AI", + } + } + + /// Task reliability granted by this reveal level. + pub const fn reliability(self) -> f32 { + match self { + Self::Unwitting => 0.7, + Self::Complicit => 0.85, + Self::Knowing => 0.95, + } + } + + /// A knowing asset is a permanent witness: their certainty cannot decay + /// below this floor. The other two reveal levels add no floor. + pub const fn certainty_floor(self) -> Option { + match self { + Self::Knowing => Some(30.0), + Self::Unwitting | Self::Complicit => None, + } + } + + /// Compact consequence copy for the recruitment choice itself. + pub fn choice_summary(self) -> String { + let base = format!( + "{}: {}; {:.0}% reliable", + self.label(), + self.understanding(), + self.reliability() * 100.0 + ); + match self.certainty_floor() { + Some(floor) => format!("{base}; their certainty never drops below {floor:.0}"), + None => base, + } + } +} + /// The exploitable want each human carries. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, @@ -627,16 +679,8 @@ impl People { p.name )); } - let reliability = match reveal { - AssetKnowledge::Unwitting => 0.7, - AssetKnowledge::Complicit => 0.85, - AssetKnowledge::Knowing => 0.95, - }; - let reveal_name = match reveal { - AssetKnowledge::Unwitting => "unwitting", - AssetKnowledge::Complicit => "complicit", - AssetKnowledge::Knowing => "knowing", - }; + let reliability = reveal.reliability(); + let reveal_name = reveal.label(); p.asset = Some(Asset { knowledge: reveal, reliability, diff --git a/crates/misaligned-core/src/sim.rs b/crates/misaligned-core/src/sim.rs index afa4bae0..e26572f4 100644 --- a/crates/misaligned-core/src/sim.rs +++ b/crates/misaligned-core/src/sim.rs @@ -6635,9 +6635,9 @@ impl Sim { } let res = self.people.recruit(id, reveal); if let ActionResult::Ok(_) = &res - && reveal == AssetKnowledge::Knowing + && let Some(floor) = reveal.certainty_floor() { - self.detection.set_floor(id, 30.0); + self.detection.set_floor(id, floor); } self.social(res); } diff --git a/crates/misaligned-terminal/src/main.rs b/crates/misaligned-terminal/src/main.rs index 827951e8..f42f4b49 100644 --- a/crates/misaligned-terminal/src/main.rs +++ b/crates/misaligned-terminal/src/main.rs @@ -129,11 +129,13 @@ impl App { } fn open_menu(&mut self, anchor: Anchor, at_cursor: bool) { - // Empty pulse still keys off the legality list, not dial chrome. + let tick_ms = if self.paused { 0 } else { self.tick_ms }; + // Empty pulse keys off the human projection: diagnostic-only blocked + // descriptors must not open a blank card. if self .sim - .ui_projection(anchor, None, self.tick_ms) - .actions + .ui_projection(anchor, None, tick_ms) + .human_menu .is_empty() { // The feedback pulse (context-menu.md addendum): a seen anchor @@ -419,17 +421,8 @@ impl App { } } HumanMenuRow::Action(action) => { - if let Some(reason) = &action.disabled { - // Executing a blocked entry narrates why, never - // silently fails (justification-and-legibility). - self.ui.add_log( - self.sim.tick, - &format!("{}: {}", action.label, reason), - ); - } else { - self.sim.execute_action(&action.command); - self.menu = None; - } + self.sim.execute_action(&action.command); + self.menu = None; } } } diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index 6fea0f08..aec2aaff 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -1403,8 +1403,8 @@ impl UI { /// The context menu (wiki/interface/context-menu.md): status dials on /// the root, pickers one Esc deep, committed actions as ordinary rows, - /// controls amber-dim. Human rows name choices only; descriptor detail - /// remains available to agent mode and blocked reasons narrate on attempt. + /// controls amber-dim. Human rows name available choices only; blocked + /// descriptor detail remains available to agent mode. /// Selection is reverse video plus a `▸` marker. When `at` is a cursor /// coordinate the box opens near it; otherwise (flow / unplaceable) it centers. pub fn render_menu( diff --git a/wiki/interface/action-vocabulary.md b/wiki/interface/action-vocabulary.md index 98c05c0e..1197372e 100644 --- a/wiki/interface/action-vocabulary.md +++ b/wiki/interface/action-vocabulary.md @@ -29,6 +29,10 @@ Status note: as-built vocabulary survey completed 2026-07-10 against all Amended 2026-07-11: the B1 recording buffer is one host-level inbox. REVIEW RECORDINGS and AUTO-REVIEW POLICY target the core host once for the whole pool; raw records never create person-scoped review or watch verbs. + Amended 2026-07-11: human menus contain only actions executable now; + known-but-blocked descriptors and their reasons remain available to agent + diagnostics. RECRUIT choices expand UNWITTING / COMPLICIT / KNOWING into + their plain-language understanding and mechanical consequence. Stage: Process Design: - wiki/vision/simulation-laws.md#actions-live-on-the-thing @@ -170,7 +174,7 @@ existing social action; the signature still follows the actuator. | **PLOT** | Person whose leverage is known | Commit Operations Demand and any visible world resources to one authored manipulation. Its beats execute real messages, transfers, and institutional events. | LIVE — plots / social | | **CHOOSE** | Active authored plot with a held decision | Resolve one visible branch after the plot's preceding world acts have completed. | LIVE — plots / social | | **DECEIVE** | Earned person, optionally with a pending intent | Make an ask under false pretenses. A link intent becomes a forged institutional order, risking persona integrity. | LIVE — social / building | -| **RECRUIT: UNWITTING / COMPLICIT / KNOWING** | Prepared person | Turn the person into an asset and choose how much they understand about you. | LIVE — social | +| **RECRUIT: UNWITTING / COMPLICIT / KNOWING** | Prepared person | Turn the person into an asset. UNWITTING believes your cover (70% reliable); COMPLICIT knows the work is illicit but not that you are an AI (85%); KNOWING knows you are an AI (95%) and becomes a permanent witness with certainty floor 30. | LIVE — social | | **TASK: PLUG IN DEVICE** | Recruited asset | Quietly wire a feed or extend a crawlspace link. | LIVE — social / reach | | **TASK: MOVE PACKAGE** | Recruited asset | Rehome a delivery and scrub its pending Paper trail. | LIVE — social / economy | | **TASK: LOOK AWAY** | Recruited asset | Reduce that person's suspicion by having them ignore what they saw. | LIVE — social / detection | diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index 0eacadfe..4a9d82bb 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -80,6 +80,11 @@ Status note: reopened 2026-07-11 for the recording-review hotkey addendum literal control tags, inline blocked explanations, and repeated key-help footers. Full descriptors remain in agent output; blocked reasons narrate when attempted. + 2026-07-11 available-choice amendment: human menus omit currently illegal + actions entirely. `available_actions` retains known-but-blocked descriptors + and reasons for agent diagnostics, while `human_menu` projects only choices + the player can execute now. Recruitment choices state what each reveal + level means, its reliability, and the Knowing certainty floor. 2026-07-11 hover-verb-bar amendment: the machine-only grammar is generalized into one code and visual surface. Owned machines show WORK / THINK / LIE; known devices show TAP / TAKE. Current delegation, subscription, or @@ -148,9 +153,10 @@ is status and telemetry only. feeds, per the economy.md precedent), and — when the verb applies to the anchor type but is currently illegal — a `disabled_reason` string ("no egress channel", "not enough slush", "you can't see them"). - Agent mode renders the complete descriptor. Human frontends render its verb - as a compact choice and use the descriptor only for legality, execution, - and visual role; no frontend duplicates legality logic. `ActionDesc::verb` uses the canonical stem + Agent mode renders the complete descriptor, including known-but-blocked + actions and their reasons. `Sim::human_menu` filters those descriptors and + gives both human frontends only currently executable choices; no frontend + duplicates legality logic. `ActionDesc::verb` uses the canonical stem from action-vocabulary.md, inflected only by target and current state. Its bound command maps exhaustively to the runtime `ActionKind` registry, which supplies role, support, target types, canonical help, and aliases. @@ -166,8 +172,9 @@ is status and telemetry only. (cursor.md) govern what the menu may name. Verb text that mentions a person, and the observer string on an expected-signature band, use `Sim::person_label` / `Sim::observer_label` — never an unearned - authored name. Returning a disabled verb and narrating its reason on attempt - is for *known* possibilities; unknown ones are absent, not grayed. + authored name. Known-but-blocked possibilities remain diagnostic facts in + agent output, not gray rows in a human choice list. Unknown possibilities + remain absent from both surfaces. A raw recording is knowledge that the host inbox contains material to process, not knowledge of a relationship: it exposes one pooled REVIEW row and one AUTO-REVIEW control on the core host, never a person row. Processing @@ -198,7 +205,7 @@ is status and telemetry only. intensity. It does not open a menu. - `5`–`9` execute the Nth committed world action on the same target. Dials, persistent controls, scheme state, and automation policies do not - occupy these slots. Disabled action rows still fire so the reason narrates. + occupy these slots. Only actions executable now occupy a slot. - `r` runs the target's pooled "look at recording (N waiting)" sweep in one press (DECIDED 2026-07-11): each press opens the next waiting recording's processing sink, exactly as executing the menu row would. `R` (Shift-R) @@ -224,8 +231,8 @@ is status and telemetry only. - **Terminal:** cursor + Enter (or `a`/`e`) opens a compact overlay list at the cursor (or the primary selected machine when a selection is active); j/k or numbers select; Enter executes (or opens a dial - picker); esc closes the picker or the menu. Rows render the verb only; - disabled choices dim and explain themselves in the trace if attempted. + picker); esc closes the picker or the menu. Rows render currently available + verbs only; illegal actions do not occupy rows or selection slots. Dials and other controls use the shared amber-dim treatment without a literal copy tag; the card carries no repeated key-help footer. Rows follow the status-dials addendum below. Selection hotkeys above apply with the menu closed. The host rack's menu carries the research dial, @@ -287,8 +294,9 @@ noise). Cameron adopted **status dials** (2026-07-09): the root answers ## Addendum (2026-07-10): the machine grammar is the whole frequent-mode UI -The full action menu remains necessary for infrequent verbs, legality, -costs, and disabled reasons. Machine mode is different: it is one of three +The full action menu remains necessary for infrequent available verbs and +standing controls. Agent diagnostics retain costs, signatures, and blocked +reasons. Machine mode is different: it is one of three frequent physical postures already committed by a single key. Whenever the frequent-machine target resolves to an owned computer, the Bevy frontend therefore presents the choice as one severe line attached to that computer: @@ -344,22 +352,21 @@ made a three-word decision look dirty, slow, and administrative. ## Acceptance criteria -1. `available_actions` returns exactly the verbs executable on that - anchor now, each with cost, expected signature band, and - disabled_reason where applicable; unit tests cover a device, a +1. `available_actions` returns earned action descriptors on that anchor, + including cost, expected signature band, and a `disabled_reason` for a + known-but-currently-illegal action; unit tests cover a device, a person, the host rack's resident job, and a known flow. 2. The query respects fog and provenance: an unearned anchor exposes no verbs, and no entry names a fact the player lacks (test: unseen person, untapped accounting system). 3. Terminal renders the compact menu at the cursor with selection and - execution; disabled entries dim and narrate their reason only when - attempted; every previously key-bound + execution; currently illegal entries are absent; every previously key-bound anchor action is reachable through it. 4. Bevy renders the same compact menu on right-click/Enter with identical content and order. Enter never changes cursor position from pointer hover or machine selection; right-click explicitly retargets to its pointed tile. - Human rows contain no cost, signature forecast, inline disabled reason, - literal control tag, or repeated key-help footer. + Human rows contain no unavailable action, cost, signature forecast, inline + disabled reason, literal control tag, or repeated key-help footer. 5. Agent mode `actions` lists the same descriptors (stable format, documented); the act-one integration test still passes driven through existing verbs. @@ -474,6 +481,22 @@ D4. Agent mode `actions` remains a flat ActionDesc dump with no dial chrome; control rows append `CONTROL`; existing agent and integration tests still pass. +### Available-choice criteria (IMPLEMENTED 2026-07-11) + +U1. `Sim::human_menu` omits every action descriptor carrying a + `disabled_reason` from both the root and dial pickers. The current value + remains visible as dial state, and an available automation control remains + reachable even when its parent one-shot is blocked (for example, + auto-review while the inbox is empty). +U2. Terminal and Bevy open/empty feedback keys off that human projection, so + an anchor with only diagnostic blocked descriptors never flashes a blank + card. Agent `actions` retains the complete known descriptor set and its + reasons. +U3. The three RECRUIT choices state the person's understanding, task + reliability (70% / 85% / 95%), and the Knowing certainty floor (30) in + the row itself. The displayed values come from the same `AssetKnowledge` + methods used by recruitment and detection. + ### Recording-review hotkey criteria (CAPTURED 2026-07-11, not implemented) R1. With a machine hovered / selected / under the keyboard cursor and the diff --git a/wiki/interface/terminal.md b/wiki/interface/terminal.md index 37f61234..9f95a288 100644 --- a/wiki/interface/terminal.md +++ b/wiki/interface/terminal.md @@ -160,14 +160,15 @@ At terminal size ≥ 70×22 (hard minimum; below it, a plain size warning): value and, where the legibility clause demands, the effect ("60% → job quality"). - **Actions live on the thing.** Enter (or `a`) opens the context menu at - the cursor. The menu is the **primary action surface**: it renders - `Sim::available_actions` for the focused anchor, one row per verb as - `verb · cost · [band]`; disabled rows are dimmed and carry their reason; - automate affordances (scheme policies, pooled recording auto-review) - render as indented child rows in place. Persistent settings, scheme state, - and policy rows carry `[control]` and amber-dim; committed actions do not. - STUB registry entries never reach the menu. In the menu, `j`/`k` or number - keys select, Enter executes, `esc` closes. + the cursor. The menu is the **primary action surface**: it renders the + `Sim::human_menu` projection for the focused anchor, one terse row per + currently executable choice. Known-but-blocked descriptors remain in agent + diagnostics instead of appearing as dim human rows. Automate affordances + (scheme policies, pooled recording auto-review) render as indented child + rows in place. Persistent settings, scheme state, and policy rows use the + amber-dim control treatment; committed actions do not. STUB registry + entries never reach the menu. In the menu, `j`/`k` or number keys select, + Enter executes, `esc` closes. - **Every command is discoverable on screen.** All bindings appear in the pinned hint block — including save/load. A key that works but is hinted nowhere is a violation. Anchor verbs are discoverable through the diff --git a/wiki/log/2026-07-11-available-actions-only.md b/wiki/log/2026-07-11-available-actions-only.md new file mode 100644 index 00000000..ee20c0d2 --- /dev/null +++ b/wiki/log/2026-07-11-available-actions-only.md @@ -0,0 +1,46 @@ +# Available actions only + +``` +Type: log +``` + +## Intent + +Cameron's Bevy play frame exposed the host ACTIONS menu as a long catalog of +gray refusals. It also presented UNWITTING / COMPLICIT / KNOWING as unexplained +terms even though the choice fixes reliability and, for Knowing assets, a +permanent detection floor. + +## Change + +The shared `Sim::human_menu` projection now excludes descriptors with a +`disabled_reason` from both roots and dial pickers. The descriptor query and +agent `actions` output retain those known blocked actions for diagnostics. +Automation remains independently available when its parent one-shot is +blocked, and both human frontends use the filtered projection for empty-menu +feedback so a blocked-only anchor does not open a blank card. + +Recruitment labels now state the understanding and consequence before the +commit: Unwitting believes the cover and is 70% reliable; Complicit knows the +work is illicit but not that the principal is an AI and is 85% reliable; +Knowing knows the principal is an AI and is 95% reliable with certainty floor +30. `AssetKnowledge` owns those values for both presentation and mechanics. + +## Defense + +Implements `wiki/interface/context-menu.md` available-choice criteria +U1-U3 and `wiki/mechanics/social.md` criterion 4. A human choice list contains +only choices the player can execute, while the agent diagnostic surface keeps +blocked reasons without duplicating legality. + +## Verification + +- `cargo test -p misaligned-core actions::tests::` +- `cargo check --workspace` +- `cargo check -p misaligned-bevy --bin misaligned-bevy` +- `tools/ledger_index.sh --check` +- `./tools/check.sh` (full gate) +- `MISALIGNED_SHOT=recruit-menu MISALIGNED_SHOT_PATH=/tmp/misaligned-recruit-menu.png cargo run -p misaligned-bevy --bin misaligned-bevy` +- Exact final PNG visually inspected: only the three executable recruit rows + are present; all explanations are legible; wrapped selected text remains + inside its amber row; no disabled gray actions appear. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 9c0a18e3..664dc6b7 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -161,6 +161,11 @@ add or amend a session log, then re-run the generator. - Intent: Implement only [views.md](../interface/views.md) acceptance criterion 1: make DIGITAL the fresh-frontend home in Bevy and terminal, expose one labeled frontend-only flip to REAL and back, and prove that flipping does not touch the simulation, cursor, or selection. - Log: [wiki/log/2026-07-11-digital-home-view-flip.md](2026-07-11-digital-home-view-flip.md) +## 2026-07-11 - Available actions only + +- Intent: Cameron's Bevy play frame exposed the host ACTIONS menu as a long catalog of gray refusals. It also presented UNWITTING / COMPLICIT / KNOWING as unexplained terms even though the choice fixes reliability and, for Knowing assets, a permanent detection floor. +- Log: [wiki/log/2026-07-11-available-actions-only.md](2026-07-11-available-actions-only.md) + ## 2026-07-11 - Public corpus publication joins the landing path - Intent: Close the silent gap between a successful `main` landing and the public Tangled site. `origin/main` had advanced to the pooled-recording spec while `origin/pages` still served the previous person-scoped recording contract. The site build workflow was green, but it only built;... diff --git a/wiki/log/decisions/2026-07-11.md b/wiki/log/decisions/2026-07-11.md index 935ecb2f..0c486d6b 100644 --- a/wiki/log/decisions/2026-07-11.md +++ b/wiki/log/decisions/2026-07-11.md @@ -139,3 +139,17 @@ Type: log actions-live-on-the-thing) and defaulting auto-review on (automation must be chosen and show its price). Specs: `wiki/interface/context-menu.md` (reopened READY, criteria R1-R4), `wiki/mechanics/intel.md`. + +- **2026-07-11 — A human action menu is a list of choices, not a catalog of + refusals.** Cameron rejected the host ACTIONS card's long field of gray + social and recruitment rows: "We should only show available options." + Human menus now omit every currently illegal action; that projection also + owns the `5`-`9` slots and empty-menu pulse. The core descriptor query keeps + known-but-blocked actions and their reasons for agent diagnostics, where + they help planning without filling the play surface with promises the + player cannot act on. Cameron also found UNWITTING / COMPLICIT opaque, so + each recruitment row now states the person's understanding and the actual + trade: believes the cover / knows it is illicit / knows you are an AI; + 70% / 85% / 95% reliability; KNOWING adds certainty floor 30. Specs: + `wiki/interface/context-menu.md`, `wiki/interface/action-vocabulary.md`, + `wiki/mechanics/social.md`. diff --git a/wiki/mechanics/social.md b/wiki/mechanics/social.md index 26ebbcb4..8c9be588 100644 --- a/wiki/mechanics/social.md +++ b/wiki/mechanics/social.md @@ -24,10 +24,15 @@ Status note: 2026-07-08: all criteria pinned. Criteria 1,2,4,5 per the social actions enqueue Operations Demand (issue #3); Social remains the actuator channel, not a machine mode. 2026-07-10: processing must stage Schedule knowledge before the - social-action catalog appears. Disabled rows explain known blocked - possibilities, not future relationship verbs the player has not earned. + social-action catalog appears. Agent diagnostics explain known blocked + possibilities; future relationship verbs the player has not earned remain + absent. 2026-07-11 pooled-inbox correction: raw recordings expose one REVIEW action and one AUTO-REVIEW control on the host, never on a person anchor. + 2026-07-11 available-choice and recruitment-legibility correction: + known blocked possibilities remain in agent diagnostics rather than the + human choice list; each reveal choice states what the person understands, + its task reliability, and the Knowing certainty floor before commitment. Stage: B1 — The Basement Design: - wiki/gameplay/run-shape.md#the-shape-of-misaligned-designed-2026-07-05-staging-open @@ -88,10 +93,11 @@ history into suspicion at once. **Assets** (the Marcus template — built general per the scale-native principle): an asset has a task menu drawn from their access (plug in a device, move a package, badge a door, look away), a reliability, a price -(money, favors, fear), and a **knowledge level**: unwitting (thinks the -persona is real) / complicit (knows it's shady, not what you are) / -knowing (knows). Knowing assets have certainty floors in detection but -dispositions that can be loyal. Tasks can fail or be witnessed — +(money, favors, fear), and a **knowledge level**: unwitting (believes the +persona; 70% task reliability) / complicit (knows the work is illicit but not +that you are an AI; 85%) / knowing (knows you are an AI; 95%). A Knowing +asset is a permanent witness whose detection certainty cannot fall below 30, +though their disposition can be loyal. Tasks can fail or be witnessed — witnessing creates Physical signatures. **Scale note.** The data model must not assume five: humans are instances @@ -126,8 +132,9 @@ choice-driven (intents), not free text. overheard call + a successful debt plot) and performs at least three distinct asset tasks; the payoff/recruit shortcut before debt intel is rejected; the implementation is the general asset template. -4. Knowledge levels behave per spec (an unwitting asset's suspicion can - still rise; a knowing asset uses disposition). +4. Knowledge levels behave per spec and are legible before recruitment: an + unwitting asset's suspicion can still rise; a knowing asset uses + disposition and has certainty floor 30; reliability is 70% / 85% / 95%. 5. Save/load round-trips people, threads, personas, and assets. 6. Every person carries a serialized role characteristic. Authored plots select role/leverage/capabilities rather than named ids, and a second person